1#![deny(
33 clippy::unwrap_used,
34 clippy::expect_used,
35 clippy::indexing_slicing,
36 clippy::panic,
37 clippy::unreachable
38)]
39
40pub(crate) mod sim;
41
42use crate::dsp::AnalysisFrame;
43use crate::render::camera::{Camera3d, CameraUniform, Lens};
44use crate::render::palette::{self, Palette};
45use crate::render::scenes::common::{PaletteParams, PanParams};
46use crate::render::scenes::lines::{GeneratorConfig, LineRenderer, Segment3dInstance};
47use crate::render::scenes::marks::{InstancedQuads3d, Quad3dInstance};
48use crate::render::scenes::{
49 FamilyParam, FamilyRange, ParamGroup, ParamKind, ParamSpec, Scene, default_of,
50};
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
54pub enum PlexusLayout {
55 #[default]
57 Cloud,
58 Sheet,
61}
62
63impl PlexusLayout {
64 pub const ALL: [PlexusLayout; 2] = [PlexusLayout::Cloud, PlexusLayout::Sheet];
67
68 pub fn from_name(name: &str) -> Option<Self> {
70 Self::ALL.into_iter().find(|layout| layout.as_str() == name)
71 }
72
73 pub fn as_str(self) -> &'static str {
75 match self {
76 PlexusLayout::Cloud => "cloud",
77 PlexusLayout::Sheet => "sheet",
78 }
79 }
80}
81
82pub const MIN_POINTS: u32 = 16;
85
86pub const MAX_POINTS: u32 = 4096;
90
91pub const DEFAULT_POINTS: u32 = 300;
93
94#[derive(Debug, Clone, Copy, PartialEq)]
96pub struct PlexusConfig {
97 pub layout: PlexusLayout,
99 pub points: u32,
102 pub seed: u64,
104}
105
106impl Default for PlexusConfig {
107 fn default() -> Self {
108 Self {
109 layout: PlexusLayout::default(),
110 points: DEFAULT_POINTS,
111 seed: 0,
112 }
113 }
114}
115
116const SOFTNESS: f32 = 0.5;
120
121const CULL_MARGIN: f32 = 0.05;
125
126const DEFAULT_LINK_DISTANCE: f32 = default_of(PARAMS, "link_distance");
127const DEFAULT_LINK_ALPHA: f32 = default_of(PARAMS, "link_alpha");
128const DEFAULT_LINE_WIDTH: f32 = default_of(PARAMS, "line_width");
129const DEFAULT_DRIFT: f32 = default_of(PARAMS, "drift");
130const DEFAULT_WAVE: f32 = default_of(PARAMS, "wave");
131const DEFAULT_WAVE_SCALE: f32 = default_of(PARAMS, "wave_scale");
132const DEFAULT_YAW: f32 = default_of(PARAMS, "yaw");
133const DEFAULT_PITCH: f32 = default_of(PARAMS, "pitch");
134const DEFAULT_DISTANCE: f32 = default_of(PARAMS, "distance");
135const DEFAULT_FOV: f32 = default_of(PARAMS, "fov");
136const DEFAULT_NODE_SIZE: f32 = default_of(PARAMS, "node_size");
137const DEFAULT_NODE_GLOW: f32 = default_of(PARAMS, "node_glow");
138const DEFAULT_FOCUS: f32 = default_of(PARAMS, "focus");
139const DEFAULT_APERTURE: f32 = default_of(PARAMS, "aperture");
140const DEFAULT_BRIGHTNESS: f32 = default_of(PARAMS, "brightness");
141const DEFAULT_HUE_CENTER: f32 = default_of(PARAMS, "hue_center");
142const DEFAULT_HUE_SPREAD: f32 = default_of(PARAMS, "hue_spread");
143const DEFAULT_ZOOM: f32 = default_of(PARAMS, "zoom");
144
145pub const PARAMS: &[ParamSpec] = &[
148 ParamSpec {
149 name: "link_distance",
150 default: 0.35,
151 range: Some([0.05, 1.0]),
152 doc: "How close two points must be to be joined, in the layout's own units; the cube is 2 across.",
153 kind: ParamKind::Modal,
154 group: ParamGroup::Shape,
155 main: true,
156 },
157 ParamSpec {
158 name: "link_alpha",
159 default: 0.7,
160 range: Some([0.0, 1.0]),
161 doc: "How strongly a link at its closest is drawn; a link always fades to nothing at link_distance.",
162 kind: ParamKind::Modal,
163 group: ParamGroup::Shape,
164 main: false,
165 },
166 ParamSpec {
167 name: "line_width",
168 default: 1.5,
169 range: Some([0.5, 8.0]),
170 doc: "Line width in pixels at the focal plane; nearer lines are wider and farther ones thinner.",
171 kind: ParamKind::Modal,
172 group: ParamGroup::Shape,
173 main: true,
174 },
175 ParamSpec {
176 name: "node_size",
177 default: 2.5,
178 range: Some([0.0, 12.0]),
179 doc: "Radius of the dot at every point, in pixels at the focal plane; 0 draws no dots.",
180 kind: ParamKind::Modal,
181 group: ParamGroup::Shape,
182 main: true,
183 },
184 ParamSpec {
185 name: "node_glow",
186 default: 1.0,
187 range: Some([0.0, 4.0]),
188 doc: "Brightness of the dots relative to the lines.",
189 kind: ParamKind::Modal,
190 group: ParamGroup::Light,
191 main: false,
192 },
193 ParamSpec {
194 name: "drift",
195 default: 0.15,
196 range: Some([0.0, 1.0]),
197 doc: "How fast the points drift on their flow; 0 holds the network still.",
198 kind: ParamKind::Modal,
199 group: ParamGroup::Motion,
200 main: true,
201 },
202 ParamSpec {
203 name: "wave",
204 default: 0.15,
205 range: Some([0.0, 0.6]),
206 doc: "How far a sheet ripples above and below its plane, in the layout's own units; 0 lies flat.",
207 kind: ParamKind::Modal,
208 group: ParamGroup::Shape,
209 main: true,
210 },
211 ParamSpec {
212 name: "wave_scale",
213 default: 1.0,
214 range: Some([0.3, 3.0]),
215 doc: "How broad a sheet's ripples are; larger is a slower swell, smaller a fine chop.",
216 kind: ParamKind::Modal,
217 group: ParamGroup::Shape,
218 main: false,
219 },
220 ParamSpec {
221 name: "yaw",
222 default: 0.0,
223 range: Some([-std::f32::consts::PI, std::f32::consts::PI]),
224 doc: "Turns the camera around the network, in radians; bind it to a slow clock to orbit.",
225 kind: ParamKind::Modal,
226 group: ParamGroup::Motion,
227 main: false,
228 },
229 ParamSpec {
230 name: "pitch",
231 default: 0.25,
232 range: Some([-1.55, 1.55]),
233 doc: "Raises the camera above the network, in radians; negative looks up from below.",
234 kind: ParamKind::Modal,
235 group: ParamGroup::Motion,
236 main: false,
237 },
238 ParamSpec {
239 name: "distance",
240 default: 3.5,
241 range: Some([1.5, 8.0]),
242 doc: "How far the camera sits from the network's centre; nearer exaggerates the perspective.",
243 kind: ParamKind::Modal,
244 group: ParamGroup::Motion,
245 main: false,
246 },
247 ParamSpec {
248 name: "fov",
249 default: 0.8,
250 range: Some([0.2, 2.0]),
251 doc: "The camera's vertical field of view in radians; zoom divides it.",
252 kind: ParamKind::Modal,
253 group: ParamGroup::Motion,
254 main: false,
255 },
256 ParamSpec {
257 name: "focus",
258 default: 0.5,
259 range: Some([0.0, 1.0]),
260 doc: "Where the focal plane sits in the network's depth: 0 at its nearest point, 1 at its farthest.",
261 kind: ParamKind::Modal,
262 group: ParamGroup::Light,
263 main: true,
264 },
265 ParamSpec {
266 name: "aperture",
267 default: 0.0,
268 range: Some([0.0, crate::render::TierConfig::RICH.max_coc_px as f32]),
269 doc: "The blur of the far background, in pixels; lines nearer than the focal plane blur more, up to the tier's cap. 0 keeps every line sharp, and wider costs fill.",
270 kind: ParamKind::Modal,
271 group: ParamGroup::Light,
272 main: true,
273 },
274 crate::render::scenes::common::brightness(1.0),
275 ParamSpec {
276 name: "hue_center",
277 default: 0.5,
278 range: Some([0.0, 1.0]),
279 doc: "Where along the palette the middle of the volume's depth is coloured.",
280 kind: ParamKind::Modal,
281 group: ParamGroup::Colour,
282 main: false,
283 },
284 ParamSpec {
285 name: "hue_spread",
286 default: 0.5,
287 range: Some([0.0, 1.0]),
288 doc: "How far along the palette the colour travels from the nearest part of the volume to the farthest.",
289 kind: ParamKind::Modal,
290 group: ParamGroup::Colour,
291 main: false,
292 },
293 crate::render::scenes::common::SATURATION,
294 crate::render::scenes::common::PALETTE_MIX,
295 crate::render::scenes::common::PALETTE_STEPS,
296 crate::render::scenes::common::zoom(1.0),
297 crate::render::scenes::common::PAN_X,
298 crate::render::scenes::common::PAN_Y,
299];
300
301pub const FAMILY_PARAMS: &[FamilyParam] = &[
304 FamilyParam {
305 name: "wave",
306 ranges: &[
307 FamilyRange {
308 family: "cloud",
309 range: None,
310 },
311 FamilyRange {
312 family: "sheet",
313 range: Some([0.0, 0.6]),
314 },
315 ],
316 },
317 FamilyParam {
318 name: "wave_scale",
319 ranges: &[
320 FamilyRange {
321 family: "cloud",
322 range: None,
323 },
324 FamilyRange {
325 family: "sheet",
326 range: Some([0.3, 3.0]),
327 },
328 ],
329 },
330];
331
332pub struct PlexusScene {
335 lines: LineRenderer,
336 nodes: InstancedQuads3d,
337 points_cap: usize,
339 edges_cap: usize,
341 max_coc: f32,
343 clamp: Option<super::CapOverflow>,
346 points: sim::Points,
347 edges: Vec<sim::Edge>,
348 instances: Vec<Segment3dInstance>,
349 node_instances: Vec<Quad3dInstance>,
350 palette: Palette,
351 dt: f32,
352 target: (u32, u32),
354
355 link_distance: f32,
356 link_alpha: f32,
357 line_width: f32,
358 node_size: f32,
359 node_glow: f32,
360 drift: f32,
361 wave: f32,
362 wave_scale: f32,
363 yaw: f32,
364 pitch: f32,
365 distance: f32,
366 fov: f32,
367 focus: f32,
368 aperture: f32,
369 hue_center: f32,
370 hue_spread: f32,
371 zoom: f32,
372 colour: PaletteParams,
373 pan: PanParams,
374}
375
376impl PlexusScene {
377 pub(crate) fn new(
380 device: &wgpu::Device,
381 surface_format: wgpu::TextureFormat,
382 points_cap: usize,
383 edges_cap: usize,
384 max_coc: f32,
385 ) -> Self {
386 let config = PlexusConfig::default();
387 let count = (config.points as usize).min(points_cap);
388 Self {
389 lines: LineRenderer::new_3d(device, surface_format, edges_cap, "plexus"),
390 nodes: InstancedQuads3d::new(device, "plexus", points_cap, surface_format),
391 points_cap,
392 edges_cap,
393 max_coc,
394 clamp: None,
395 points: sim::Points::seeded(config.layout, config.seed, count, points_cap),
396 edges: Vec::with_capacity(edges_cap),
397 instances: Vec::with_capacity(edges_cap),
398 node_instances: Vec::with_capacity(points_cap),
399 palette: Palette::default_spectrum(),
400 dt: super::FALLBACK_DT,
401 target: (1, 1),
402 link_distance: DEFAULT_LINK_DISTANCE,
403 link_alpha: DEFAULT_LINK_ALPHA,
404 line_width: DEFAULT_LINE_WIDTH,
405 node_size: DEFAULT_NODE_SIZE,
406 node_glow: DEFAULT_NODE_GLOW,
407 drift: DEFAULT_DRIFT,
408 wave: DEFAULT_WAVE,
409 wave_scale: DEFAULT_WAVE_SCALE,
410 yaw: DEFAULT_YAW,
411 pitch: DEFAULT_PITCH,
412 distance: DEFAULT_DISTANCE,
413 fov: DEFAULT_FOV,
414 focus: DEFAULT_FOCUS,
415 aperture: DEFAULT_APERTURE,
416 hue_center: DEFAULT_HUE_CENTER,
417 hue_spread: DEFAULT_HUE_SPREAD,
418 zoom: DEFAULT_ZOOM,
419 colour: PaletteParams::new(0.0, DEFAULT_BRIGHTNESS),
420 pan: PanParams::default(),
421 }
422 }
423
424 fn camera(&self) -> Camera3d {
426 Camera3d {
427 yaw: self.yaw,
428 pitch: self.pitch,
429 distance: self.distance,
430 fov: self.fov,
431 focus: self.focus,
432 aperture: self.aperture,
433 }
434 }
435
436 fn colour_at(&self, depth01: f32) -> [f32; 3] {
440 depth_colour(
441 &self.palette,
442 &self.colour,
443 self.hue_center,
444 self.hue_spread,
445 depth01,
446 )
447 }
448}
449
450impl Scene for PlexusScene {
451 fn name(&self) -> &'static str {
452 "plexus"
453 }
454
455 fn advance(&mut self, dt: f32) {
456 self.dt = dt;
457 }
458
459 fn set_target_size(&mut self, width: u32, height: u32) {
460 self.target = (width, height);
461 }
462
463 fn set_palette(&mut self, palette: &Palette) {
464 self.palette = palette.clone();
465 }
466
467 fn configure(&mut self, cfg: &GeneratorConfig) -> Option<super::CapOverflow> {
468 let GeneratorConfig::Plexus(config) = cfg else {
469 return None;
470 };
471 let (count, overflow) = points_clamp(config.points, self.points_cap);
475 self.points = sim::Points::seeded(config.layout, config.seed, count, self.points_cap);
476 self.edges.clear();
477 self.clamp = None;
479 overflow
480 }
481
482 fn mirror_overflow(&self) -> Option<&super::CapOverflow> {
483 self.clamp.as_ref()
484 }
485
486 fn reset_params(&mut self) {
487 self.link_distance = DEFAULT_LINK_DISTANCE;
488 self.link_alpha = DEFAULT_LINK_ALPHA;
489 self.line_width = DEFAULT_LINE_WIDTH;
490 self.node_size = DEFAULT_NODE_SIZE;
491 self.node_glow = DEFAULT_NODE_GLOW;
492 self.drift = DEFAULT_DRIFT;
493 self.wave = DEFAULT_WAVE;
494 self.wave_scale = DEFAULT_WAVE_SCALE;
495 self.yaw = DEFAULT_YAW;
496 self.pitch = DEFAULT_PITCH;
497 self.distance = DEFAULT_DISTANCE;
498 self.fov = DEFAULT_FOV;
499 self.focus = DEFAULT_FOCUS;
500 self.aperture = DEFAULT_APERTURE;
501 self.hue_center = DEFAULT_HUE_CENTER;
502 self.hue_spread = DEFAULT_HUE_SPREAD;
503 self.zoom = DEFAULT_ZOOM;
504 self.colour.reset();
505 self.pan.reset();
506 }
507
508 fn set_param(&mut self, name: &str, value: f32) {
509 if self.colour.set(name, value) || self.pan.set(name, value) {
512 return;
513 }
514 match name {
515 "link_distance" => self.link_distance = value,
516 "link_alpha" => self.link_alpha = value,
517 "line_width" => self.line_width = value,
518 "node_size" => self.node_size = value,
519 "node_glow" => self.node_glow = value,
520 "drift" => self.drift = value,
521 "wave" => self.wave = value,
522 "wave_scale" => self.wave_scale = value,
523 "yaw" => self.yaw = value,
524 "pitch" => self.pitch = value,
525 "distance" => self.distance = value,
526 "fov" => self.fov = value,
527 "focus" => self.focus = value,
528 "aperture" => self.aperture = value,
529 "hue_center" => self.hue_center = value,
530 "hue_spread" => self.hue_spread = value,
531 "zoom" => self.zoom = value,
532 _ => {}
533 }
534 }
535
536 fn update(&mut self, _frame: &AnalysisFrame) {
537 self.points
538 .step(self.dt, self.drift, self.wave, self.wave_scale);
539 let linked = sim::link(
540 self.points.pos(),
541 self.points.fade(),
542 self.link_distance,
543 self.edges_cap,
544 &mut self.edges,
545 );
546 self.clamp = (linked > self.edges.len()).then(|| super::CapOverflow {
547 dropped: linked - self.edges.len(),
548 context: super::OverflowContext::Edges(linked as u32),
549 cap: self.edges_cap,
550 });
551 }
552
553 fn render(
554 &mut self,
555 queue: &wgpu::Queue,
556 encoder: &mut wgpu::CommandEncoder,
557 view: &wgpu::TextureView,
558 aspect: f32,
559 ) {
560 let camera = self.camera();
562 let cam = camera.view(aspect, self.zoom, [self.pan.x, self.pan.y]);
563 let radius = self.points.bounding_radius();
564 let near_extent = cam.distance - radius;
565 let far_extent = cam.distance + radius;
566 let span = far_extent - near_extent;
567 let lens = Lens::new(
568 camera.aperture,
569 cam.focal_depth(camera.focus, radius),
570 self.max_coc,
571 );
572 if self.clamp.is_none() && lens.aperture > lens.max_coc {
578 self.clamp = Some(super::CapOverflow {
579 dropped: 0,
580 context: super::OverflowContext::Blur(lens.aperture.ceil() as u32),
581 cap: lens.max_coc as usize,
582 });
583 }
584 let margin = if lens.aperture > 0.0 {
587 CULL_MARGIN + 2.0 * lens.max_coc / self.target.1.max(1) as f32
588 } else {
589 CULL_MARGIN
590 };
591 let link_alpha = self.link_alpha.clamp(0.0, 1.0);
592 let width = if self.line_width.is_finite() {
593 self.line_width.max(0.0)
594 } else {
595 DEFAULT_LINE_WIDTH
596 };
597
598 let mut instances = std::mem::take(&mut self.instances);
599 instances.clear();
600 for edge in &self.edges {
601 let (Some(&pa), Some(&pb)) = (
602 self.points.pos().get(edge.a as usize),
603 self.points.pos().get(edge.b as usize),
604 ) else {
605 continue;
606 };
607 let alpha = edge.presence * link_alpha;
608 if alpha <= 0.0 {
609 continue;
610 }
611 let Some((a, b)) = cam.clip_near(pa, pb) else {
612 continue;
613 };
614 if cam.outside(a, b, margin) {
615 continue;
616 }
617 let mid = [
618 0.5 * (pa[0] + pb[0]),
619 0.5 * (pa[1] + pb[1]),
620 0.5 * (pa[2] + pb[2]),
621 ];
622 let depth01 = ((cam.depth(mid) - near_extent) / span).clamp(0.0, 1.0);
623 instances.push(Segment3dInstance {
624 a,
625 b,
626 color: self.colour_at(depth01),
627 width,
628 alpha,
629 });
630 }
631
632 let uniform = CameraUniform::new(&cam, self.target.0, self.target.1, lens);
633 self.lines
634 .draw_3d(queue, encoder, view, &uniform, 1.0, SOFTNESS, &instances);
635 self.instances = instances;
636
637 let mut nodes = std::mem::take(&mut self.node_instances);
638 node_instances(
639 &mut nodes,
640 self.points.pos(),
641 self.points.fade(),
642 &cam,
643 margin,
644 self.node_size,
645 |depth| self.colour_at(((depth - near_extent) / span).clamp(0.0, 1.0)),
646 );
647 self.nodes.draw(
648 queue,
649 encoder,
650 view,
651 &uniform,
652 if self.node_glow.is_finite() {
653 self.node_glow.max(0.0)
654 } else {
655 DEFAULT_NODE_GLOW
656 },
657 &nodes,
658 );
659 self.node_instances = nodes;
660 }
661}
662
663pub(crate) fn points_clamp(asked: u32, cap: usize) -> (usize, Option<super::CapOverflow>) {
666 let asked_n = asked as usize;
667 if asked_n <= cap {
668 return (asked_n, None);
669 }
670 (
671 cap,
672 Some(super::CapOverflow {
673 dropped: asked_n - cap,
674 context: super::OverflowContext::Points(asked),
675 cap,
676 }),
677 )
678}
679
680pub(crate) fn depth_colour(
690 palette: &Palette,
691 colour: &PaletteParams,
692 hue_center: f32,
693 hue_spread: f32,
694 depth01: f32,
695) -> [f32; 3] {
696 let coord = hue_center + (depth01 - 0.5) * hue_spread;
697 let rgb = palette::desaturate(
698 palette.sample(palette::band_coord(coord, colour.steps), colour.mix),
699 colour.saturation,
700 );
701 let b = colour.brightness;
702 [rgb[0] * b, rgb[1] * b, rgb[2] * b]
703}
704
705pub(crate) fn node_instances(
713 out: &mut Vec<Quad3dInstance>,
714 pos: &[[f32; 3]],
715 fade: &[f32],
716 cam: &crate::render::camera::CameraView,
717 margin: f32,
718 node_size: f32,
719 colour: impl Fn(f32) -> [f32; 3],
720) {
721 out.clear();
722 if !node_size.is_finite() || node_size <= 0.0 {
723 return;
724 }
725 for (p, f) in pos.iter().zip(fade) {
726 if *f <= 0.0 {
727 continue;
728 }
729 let depth = cam.depth(*p);
730 if depth < crate::render::camera::NEAR || cam.outside(*p, *p, margin) {
731 continue;
732 }
733 let [r, g, b] = colour(depth);
734 out.push(Quad3dInstance {
735 center: *p,
736 radius: node_size,
737 color: [r * f, g * f, b * f],
738 });
739 }
740}
741
742#[cfg(test)]
743mod tests;