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::{self, CameraParams};
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 DEFAULT_LINK_DISTANCE: f32 = default_of(PARAMS, "link_distance");
122const DEFAULT_LINK_ALPHA: f32 = default_of(PARAMS, "link_alpha");
123const DEFAULT_LINE_WIDTH: f32 = default_of(PARAMS, "line_width");
124const DEFAULT_DRIFT: f32 = default_of(PARAMS, "drift");
125const DEFAULT_WAVE: f32 = default_of(PARAMS, "wave");
126const DEFAULT_WAVE_SCALE: f32 = default_of(PARAMS, "wave_scale");
127const DEFAULT_NODE_SIZE: f32 = default_of(PARAMS, "node_size");
128const DEFAULT_NODE_GLOW: f32 = default_of(PARAMS, "node_glow");
129const DEFAULT_BRIGHTNESS: f32 = default_of(PARAMS, "brightness");
130const DEFAULT_HUE_CENTER: f32 = default_of(PARAMS, "hue_center");
131const DEFAULT_HUE_SPREAD: f32 = default_of(PARAMS, "hue_spread");
132const DEFAULT_ZOOM: f32 = default_of(PARAMS, "zoom");
133
134pub const PARAMS: &[ParamSpec] = &[
137 ParamSpec {
138 name: "link_distance",
139 default: 0.35,
140 range: Some([0.05, 1.0]),
141 doc: "How close two points must be to be joined, in the layout's own units; the cube is 2 across.",
142 kind: ParamKind::Modal,
143 group: ParamGroup::Shape,
144 main: true,
145 },
146 ParamSpec {
147 name: "link_alpha",
148 default: 0.7,
149 range: Some([0.0, 1.0]),
150 doc: "How strongly a link at its closest is drawn; a link always fades to nothing at link_distance.",
151 kind: ParamKind::Modal,
152 group: ParamGroup::Shape,
153 main: false,
154 },
155 ParamSpec {
156 name: "line_width",
157 default: 1.5,
158 range: Some([0.5, 8.0]),
159 doc: "Line width in pixels at the focal plane; nearer lines are wider and farther ones thinner.",
160 kind: ParamKind::Modal,
161 group: ParamGroup::Shape,
162 main: true,
163 },
164 ParamSpec {
165 name: "node_size",
166 default: 2.5,
167 range: Some([0.0, 12.0]),
168 doc: "Radius of the dot at every point, in pixels at the focal plane; 0 draws no dots.",
169 kind: ParamKind::Modal,
170 group: ParamGroup::Shape,
171 main: true,
172 },
173 ParamSpec {
174 name: "node_glow",
175 default: 1.0,
176 range: Some([0.0, 4.0]),
177 doc: "Brightness of the dots relative to the lines.",
178 kind: ParamKind::Modal,
179 group: ParamGroup::Light,
180 main: false,
181 },
182 ParamSpec {
183 name: "drift",
184 default: 0.15,
185 range: Some([0.0, 1.0]),
186 doc: "How fast the points drift on their flow; 0 holds the network still.",
187 kind: ParamKind::Modal,
188 group: ParamGroup::Motion,
189 main: true,
190 },
191 ParamSpec {
192 name: "wave",
193 default: 0.15,
194 range: Some([0.0, 0.6]),
195 doc: "How far a sheet ripples above and below its plane, in the layout's own units; 0 lies flat.",
196 kind: ParamKind::Modal,
197 group: ParamGroup::Shape,
198 main: true,
199 },
200 ParamSpec {
201 name: "wave_scale",
202 default: 1.0,
203 range: Some([0.3, 3.0]),
204 doc: "How broad a sheet's ripples are; larger is a slower swell, smaller a fine chop.",
205 kind: ParamKind::Modal,
206 group: ParamGroup::Shape,
207 main: false,
208 },
209 camera::YAW,
210 camera::PITCH,
211 camera::DISTANCE,
212 camera::FOV,
213 camera::FOCUS,
214 camera::APERTURE,
215 camera::FOG,
216 camera::SOLID,
217 crate::render::scenes::common::brightness(1.0),
218 ParamSpec {
219 name: "hue_center",
220 default: 0.5,
221 range: Some([0.0, 1.0]),
222 doc: "Where along the palette the middle of the volume's depth is coloured.",
223 kind: ParamKind::Modal,
224 group: ParamGroup::Colour,
225 main: false,
226 },
227 ParamSpec {
228 name: "hue_spread",
229 default: 0.5,
230 range: Some([0.0, 1.0]),
231 doc: "How far along the palette the colour travels from the nearest part of the volume to the farthest.",
232 kind: ParamKind::Modal,
233 group: ParamGroup::Colour,
234 main: false,
235 },
236 crate::render::scenes::common::SATURATION,
237 crate::render::scenes::common::PALETTE_MIX,
238 crate::render::scenes::common::PALETTE_STEPS,
239 crate::render::scenes::common::zoom(1.0),
240 crate::render::scenes::common::PAN_X,
241 crate::render::scenes::common::PAN_Y,
242];
243
244pub const FAMILY_PARAMS: &[FamilyParam] = &[
247 FamilyParam {
248 name: "wave",
249 ranges: &[
250 FamilyRange {
251 family: "cloud",
252 range: None,
253 },
254 FamilyRange {
255 family: "sheet",
256 range: Some([0.0, 0.6]),
257 },
258 ],
259 },
260 FamilyParam {
261 name: "wave_scale",
262 ranges: &[
263 FamilyRange {
264 family: "cloud",
265 range: None,
266 },
267 FamilyRange {
268 family: "sheet",
269 range: Some([0.3, 3.0]),
270 },
271 ],
272 },
273];
274
275pub struct PlexusScene {
278 lines: LineRenderer,
279 nodes: InstancedQuads3d,
280 points_cap: usize,
282 edges_cap: usize,
284 max_coc: f32,
286 clamp: Option<super::CapOverflow>,
289 points: sim::Points,
290 edges: Vec<sim::Edge>,
291 instances: Vec<Segment3dInstance>,
292 node_instances: Vec<Quad3dInstance>,
293 palette: Palette,
294 dt: f32,
295 target: (u32, u32),
297
298 link_distance: f32,
299 link_alpha: f32,
300 line_width: f32,
301 node_size: f32,
302 node_glow: f32,
303 drift: f32,
304 wave: f32,
305 wave_scale: f32,
306 hue_center: f32,
307 hue_spread: f32,
308 zoom: f32,
309 colour: PaletteParams,
310 pan: PanParams,
311 camera: CameraParams,
312}
313
314impl PlexusScene {
315 pub(crate) fn new(
318 device: &wgpu::Device,
319 surface_format: wgpu::TextureFormat,
320 points_cap: usize,
321 edges_cap: usize,
322 max_coc: f32,
323 ) -> Self {
324 let config = PlexusConfig::default();
325 let count = (config.points as usize).min(points_cap);
326 Self {
327 lines: LineRenderer::new_3d(device, surface_format, edges_cap, "plexus"),
328 nodes: InstancedQuads3d::new(device, "plexus", points_cap, surface_format),
329 points_cap,
330 edges_cap,
331 max_coc,
332 clamp: None,
333 points: sim::Points::seeded(config.layout, config.seed, count, points_cap),
334 edges: Vec::with_capacity(edges_cap),
335 instances: Vec::with_capacity(edges_cap),
336 node_instances: Vec::with_capacity(points_cap),
337 palette: Palette::default_spectrum(),
338 dt: super::FALLBACK_DT,
339 target: (1, 1),
340 link_distance: DEFAULT_LINK_DISTANCE,
341 link_alpha: DEFAULT_LINK_ALPHA,
342 line_width: DEFAULT_LINE_WIDTH,
343 node_size: DEFAULT_NODE_SIZE,
344 node_glow: DEFAULT_NODE_GLOW,
345 drift: DEFAULT_DRIFT,
346 wave: DEFAULT_WAVE,
347 wave_scale: DEFAULT_WAVE_SCALE,
348 hue_center: DEFAULT_HUE_CENTER,
349 hue_spread: DEFAULT_HUE_SPREAD,
350 zoom: DEFAULT_ZOOM,
351 colour: PaletteParams::new(0.0, DEFAULT_BRIGHTNESS),
352 pan: PanParams::default(),
353 camera: CameraParams::default(),
354 }
355 }
356
357 fn colour_at(&self, depth01: f32) -> [f32; 3] {
361 depth_colour(
362 &self.palette,
363 &self.colour,
364 self.hue_center,
365 self.hue_spread,
366 depth01,
367 )
368 }
369}
370
371impl Scene for PlexusScene {
372 fn name(&self) -> &'static str {
373 "plexus"
374 }
375
376 fn advance(&mut self, dt: f32) {
377 self.dt = dt;
378 }
379
380 fn set_target_size(&mut self, width: u32, height: u32) {
381 self.target = (width, height);
382 }
383
384 fn set_palette(&mut self, palette: &Palette) {
385 self.palette = palette.clone();
386 }
387
388 fn configure(&mut self, cfg: &GeneratorConfig) -> Option<super::CapOverflow> {
389 let GeneratorConfig::Plexus(config) = cfg else {
390 return None;
391 };
392 let (count, overflow) = points_clamp(config.points, self.points_cap);
396 self.points = sim::Points::seeded(config.layout, config.seed, count, self.points_cap);
397 self.edges.clear();
398 self.clamp = None;
400 overflow
401 }
402
403 fn mirror_overflow(&self) -> Option<&super::CapOverflow> {
404 self.clamp.as_ref()
405 }
406
407 fn reset_params(&mut self) {
408 self.link_distance = DEFAULT_LINK_DISTANCE;
409 self.link_alpha = DEFAULT_LINK_ALPHA;
410 self.line_width = DEFAULT_LINE_WIDTH;
411 self.node_size = DEFAULT_NODE_SIZE;
412 self.node_glow = DEFAULT_NODE_GLOW;
413 self.drift = DEFAULT_DRIFT;
414 self.wave = DEFAULT_WAVE;
415 self.wave_scale = DEFAULT_WAVE_SCALE;
416 self.hue_center = DEFAULT_HUE_CENTER;
417 self.hue_spread = DEFAULT_HUE_SPREAD;
418 self.zoom = DEFAULT_ZOOM;
419 self.colour.reset();
420 self.pan.reset();
421 self.camera.reset();
422 }
423
424 fn set_param(&mut self, name: &str, value: f32) {
425 if self.colour.set(name, value) || self.pan.set(name, value) || self.camera.set(name, value)
428 {
429 return;
430 }
431 match name {
432 "link_distance" => self.link_distance = value,
433 "link_alpha" => self.link_alpha = value,
434 "line_width" => self.line_width = value,
435 "node_size" => self.node_size = value,
436 "node_glow" => self.node_glow = value,
437 "drift" => self.drift = value,
438 "wave" => self.wave = value,
439 "wave_scale" => self.wave_scale = value,
440 "hue_center" => self.hue_center = value,
441 "hue_spread" => self.hue_spread = value,
442 "zoom" => self.zoom = value,
443 _ => {}
444 }
445 }
446
447 fn update(&mut self, _frame: &AnalysisFrame) {
448 self.points
449 .step(self.dt, self.drift, self.wave, self.wave_scale);
450 let linked = sim::link(
451 self.points.pos(),
452 self.points.fade(),
453 self.link_distance,
454 self.edges_cap,
455 &mut self.edges,
456 );
457 self.clamp = (linked > self.edges.len()).then(|| super::CapOverflow {
458 dropped: linked - self.edges.len(),
459 context: super::OverflowContext::Edges(linked as u32),
460 cap: self.edges_cap,
461 });
462 }
463
464 fn render(
465 &mut self,
466 queue: &wgpu::Queue,
467 encoder: &mut wgpu::CommandEncoder,
468 view: &wgpu::TextureView,
469 aspect: f32,
470 ) {
471 let frame = self.camera.frame(
473 aspect,
474 self.zoom,
475 [self.pan.x, self.pan.y],
476 self.target,
477 self.points.bounding_radius(),
478 self.max_coc,
479 );
480 let (cam, margin) = (frame.view, frame.margin);
481 let (near_extent, span) = (frame.near_extent, frame.span);
482 if self.clamp.is_none() {
484 self.clamp = frame.blur;
485 }
486 let link_alpha = self.link_alpha.clamp(0.0, 1.0);
487 let width = if self.line_width.is_finite() {
488 self.line_width.max(0.0)
489 } else {
490 DEFAULT_LINE_WIDTH
491 };
492
493 let mut instances = std::mem::take(&mut self.instances);
494 instances.clear();
495 for edge in &self.edges {
496 let (Some(&pa), Some(&pb)) = (
497 self.points.pos().get(edge.a as usize),
498 self.points.pos().get(edge.b as usize),
499 ) else {
500 continue;
501 };
502 let alpha = edge.presence * link_alpha;
503 if alpha <= 0.0 {
504 continue;
505 }
506 let Some((a, b)) = cam.clip_near(pa, pb) else {
507 continue;
508 };
509 if cam.outside(a, b, margin) {
510 continue;
511 }
512 let mid = [
513 0.5 * (pa[0] + pb[0]),
514 0.5 * (pa[1] + pb[1]),
515 0.5 * (pa[2] + pb[2]),
516 ];
517 let depth01 = ((cam.depth(mid) - near_extent) / span).clamp(0.0, 1.0);
518 instances.push(Segment3dInstance {
519 a,
520 b,
521 color: self.colour_at(depth01),
522 width,
523 alpha,
524 prev: a,
526 next: b,
527 skirt: 0.0,
528 });
529 }
530
531 let uniform = frame.uniform;
532 self.lines
533 .draw_3d(queue, encoder, view, &frame, 1.0, SOFTNESS, &instances);
534 self.instances = instances;
535
536 let mut nodes = std::mem::take(&mut self.node_instances);
537 node_instances(
538 &mut nodes,
539 self.points.pos(),
540 self.points.fade(),
541 &cam,
542 margin,
543 self.node_size,
544 |depth| {
545 let colour = self.colour_at(((depth - near_extent) / span).clamp(0.0, 1.0));
546 if frame.fog > 0.0 {
550 let light = frame.fog_light(depth);
551 colour.map(|c| c * light)
552 } else {
553 colour
554 }
555 },
556 );
557 self.nodes.draw(
558 queue,
559 encoder,
560 view,
561 &uniform,
562 if self.node_glow.is_finite() {
563 self.node_glow.max(0.0)
564 } else {
565 DEFAULT_NODE_GLOW
566 },
567 &nodes,
568 );
569 self.node_instances = nodes;
570 }
571}
572
573pub(crate) fn points_clamp(asked: u32, cap: usize) -> (usize, Option<super::CapOverflow>) {
576 let asked_n = asked as usize;
577 if asked_n <= cap {
578 return (asked_n, None);
579 }
580 (
581 cap,
582 Some(super::CapOverflow {
583 dropped: asked_n - cap,
584 context: super::OverflowContext::Points(asked),
585 cap,
586 }),
587 )
588}
589
590pub(crate) fn depth_colour(
600 palette: &Palette,
601 colour: &PaletteParams,
602 hue_center: f32,
603 hue_spread: f32,
604 depth01: f32,
605) -> [f32; 3] {
606 let coord = hue_center + (depth01 - 0.5) * hue_spread;
607 let rgb = palette::desaturate(
608 palette.sample(palette::band_coord(coord, colour.steps), colour.mix),
609 colour.saturation,
610 );
611 let b = colour.brightness;
612 [rgb[0] * b, rgb[1] * b, rgb[2] * b]
613}
614
615pub(crate) fn node_instances(
623 out: &mut Vec<Quad3dInstance>,
624 pos: &[[f32; 3]],
625 fade: &[f32],
626 cam: &crate::render::camera::CameraView,
627 margin: f32,
628 node_size: f32,
629 colour: impl Fn(f32) -> [f32; 3],
630) {
631 out.clear();
632 if !node_size.is_finite() || node_size <= 0.0 {
633 return;
634 }
635 for (p, f) in pos.iter().zip(fade) {
636 if *f <= 0.0 {
637 continue;
638 }
639 let depth = cam.depth(*p);
640 if depth < crate::render::camera::NEAR || cam.outside(*p, *p, margin) {
641 continue;
642 }
643 let [r, g, b] = colour(depth);
644 out.push(Quad3dInstance {
645 center: *p,
646 radius: node_size,
647 color: [r * f, g * f, b * f],
648 });
649 }
650}
651
652#[cfg(test)]
653mod tests;