1#![deny(
28 clippy::unwrap_used,
29 clippy::expect_used,
30 clippy::indexing_slicing,
31 clippy::panic,
32 clippy::unreachable
33)]
34
35use std::cell::RefCell;
36use std::rc::Rc;
37
38use super::super::common;
39use super::super::{FALLBACK_DT, Phase, Scene};
40use super::biarc::Piece;
41use super::renderer::{
42 ArcInstance, LineRenderer, Segment3dInstance, SegmentInstance, StrokeMetric, joined_chord,
43};
44use super::{
45 CapOverflow, ColorRamp, CurveFamily, GeneratorConfig, MirrorSpec, OverflowContext,
46 ViewTransform, curves, replicate_mirror,
47};
48use crate::dsp::AnalysisFrame;
49use crate::render::camera::{self, CameraParams};
50use crate::render::palette::Palette;
51use crate::render::scenes::{
52 FamilyParam, FamilyRange, ParamGroup, ParamKind, ParamSpec, default_of,
53};
54
55const DEFAULT_N: f32 = default_of(PARAMS, "n");
57const DEFAULT_D: f32 = default_of(PARAMS, "d");
58const DEFAULT_PHASE: f32 = default_of(PARAMS, "phase");
62const DEFAULT_RADIAL_OFFSET: f32 = default_of(PARAMS, "radial_offset");
63const DEFAULT_PEN: f32 = default_of(PARAMS, "pen");
65const DEFAULT_SYM: f32 = default_of(PARAMS, "sym");
66const DEFAULT_SHARPNESS: f32 = default_of(PARAMS, "sharpness");
67const DEFAULT_LOBE: f32 = default_of(PARAMS, "lobe");
68const DEFAULT_DECAY: f32 = default_of(PARAMS, "decay");
69const DEFAULT_TUBE: f32 = default_of(PARAMS, "tube");
70const DEFAULT_M: f32 = default_of(PARAMS, "m");
71const DEFAULT_PHASE_Z: f32 = default_of(PARAMS, "phase_z");
72const DEFAULT_SAMPLES: f32 = default_of(PARAMS, "samples");
73const DEFAULT_THICKNESS: f32 = 2.0;
74const DEFAULT_HUE: f32 = 0.6;
75const DEFAULT_HUE_SPREAD: f32 = 0.0;
80const DEFAULT_SPIN: f32 = default_of(PARAMS, "spin");
81const DEFAULT_SCALE: f32 = 0.9;
82const DEFAULT_BRIGHTNESS: f32 = 1.0;
83const DEFAULT_GLOW: f32 = 1.0;
87const DEFAULT_DRAW_PROGRESS: f32 = 1.0;
88const DEFAULT_ZOOM: f32 = 1.0;
91const DEFAULT_MIRROR_ORDER: f32 = 1.0;
93const DEFAULT_MIRROR_REFLECT: f32 = 0.0;
94
95pub struct ParametricCurveScene {
98 renderer: Rc<RefCell<LineRenderer>>,
102 segments: Vec<SegmentInstance>,
105 single_buf: Vec<SegmentInstance>,
108 arcs: Vec<ArcInstance>,
113 single_arcs: Vec<ArcInstance>,
114 points: Vec<[f32; 2]>,
119 pieces: Vec<Piece>,
120 walk: Vec<f32>,
121 max_segments: usize,
127 mirror_overflow: Option<CapOverflow>,
130 family: CurveFamily,
132 dt: f32,
136 spin_phase: Phase,
141 palette: Palette,
145 n: f32,
146 d: f32,
147 phase: f32,
148 radial_offset: f32,
149 pen: f32,
150 sym: f32,
151 sharpness: f32,
152 lobe: f32,
153 decay: f32,
154 samples: f32,
155 thickness: f32,
156 colour: common::PaletteParams,
158 pan: common::PanParams,
160 hue_spread: f32,
161 spin: f32,
162 scale: f32,
163 glow: f32,
164 softness: f32,
165 stroke_blend: f32,
174 draw_progress: f32,
175 zoom: f32,
176 mirror_order: f32,
177 mirror_reflect: f32,
178 tube: f32,
179 m: f32,
180 phase_z: f32,
181
182 lines3d: LineRenderer,
188 seg3d_cap: usize,
190 max_coc: f32,
192 target: (u32, u32),
194 camera: CameraParams,
196 hue_axis: f32,
198 points3d: Vec<[f32; 3]>,
201 closed3d: bool,
203 extent3d: f32,
206 samples3d: usize,
209 instances3d: Vec<Segment3dInstance>,
211}
212
213impl ParametricCurveScene {
214 pub fn new(
219 renderer: Rc<RefCell<LineRenderer>>,
220 max_segments: usize,
221 device: &wgpu::Device,
222 surface_format: wgpu::TextureFormat,
223 seg3d_cap: usize,
224 max_coc: f32,
225 ) -> Self {
226 Self {
227 lines3d: LineRenderer::new_3d(device, surface_format, seg3d_cap, "parametric-3d"),
228 seg3d_cap,
229 max_coc,
230 target: (1, 1),
231 camera: CameraParams::default(),
232 hue_axis: DEFAULT_HUE_AXIS,
233 points3d: Vec::with_capacity(seg3d_cap + 1),
234 closed3d: false,
235 extent3d: 1.0,
236 samples3d: 0,
237 instances3d: Vec::with_capacity(seg3d_cap),
238 tube: DEFAULT_TUBE,
239 m: DEFAULT_M,
240 phase_z: DEFAULT_PHASE_Z,
241 renderer,
242 segments: Vec::with_capacity(max_segments),
243 single_buf: Vec::with_capacity(max_segments),
244 arcs: Vec::new(),
251 single_arcs: Vec::new(),
252 pieces: Vec::new(),
253 walk: Vec::new(),
254 points: Vec::with_capacity(max_segments + 1),
264 max_segments,
265 mirror_overflow: None,
266 family: CurveFamily::MaurerRose,
267 dt: FALLBACK_DT,
268 spin_phase: Phase::default(),
269 palette: Palette::default_spectrum(),
272 n: DEFAULT_N,
273 d: DEFAULT_D,
274 phase: DEFAULT_PHASE,
275 radial_offset: DEFAULT_RADIAL_OFFSET,
276 pen: DEFAULT_PEN,
277 sym: DEFAULT_SYM,
278 sharpness: DEFAULT_SHARPNESS,
279 lobe: DEFAULT_LOBE,
280 decay: DEFAULT_DECAY,
281 samples: DEFAULT_SAMPLES,
282 thickness: DEFAULT_THICKNESS,
283 colour: common::PaletteParams::new(DEFAULT_HUE, DEFAULT_BRIGHTNESS),
284 pan: common::PanParams::default(),
285 hue_spread: DEFAULT_HUE_SPREAD,
286 spin: DEFAULT_SPIN,
287 scale: DEFAULT_SCALE,
288 glow: DEFAULT_GLOW,
289 softness: super::DEFAULT_SOFTNESS,
290 stroke_blend: super::ADDITIVE_BLEND,
291 draw_progress: DEFAULT_DRAW_PROGRESS,
292 zoom: DEFAULT_ZOOM,
293 mirror_order: DEFAULT_MIRROR_ORDER,
294 mirror_reflect: DEFAULT_MIRROR_REFLECT,
295 }
296 }
297}
298
299impl ParametricCurveScene {
300 fn reserve_fit_buffers(&mut self) {
318 let cap = self.max_segments;
319 if self.pieces.capacity() < cap {
320 let extra = cap.saturating_sub(self.pieces.len());
321 self.pieces.reserve_exact(extra);
322 }
323 if self.walk.capacity() < cap {
324 let extra = cap.saturating_sub(self.walk.len());
325 self.walk.reserve_exact(extra);
326 }
327 if self.single_arcs.capacity() < cap {
328 let extra = cap.saturating_sub(self.single_arcs.len());
329 self.single_arcs.reserve_exact(extra);
330 }
331 if self.arcs.capacity() < cap {
332 let extra = cap.saturating_sub(self.arcs.len());
333 self.arcs.reserve_exact(extra);
334 }
335 }
336
337 fn split_pieces(
349 &mut self,
350 samples: usize,
351 ramp: ColorRamp,
352 color: [f32; 3],
353 width: f32,
354 closed: bool,
355 ) {
356 self.single_buf.clear();
357 self.single_arcs.clear();
358 let span = samples.saturating_sub(1).max(1) as f32;
359 for (k, piece) in self.pieces.iter().enumerate() {
360 let color = self
361 .walk
362 .get(k)
363 .map_or(color, |at| ramp.at(&self.palette, at / span));
364 match *piece {
365 Piece::Arc {
366 centre,
367 radius,
368 start,
369 sweep,
370 } => self.single_arcs.push(ArcInstance {
371 centre,
372 radius,
373 angle_start: start,
374 angle_sweep: sweep,
375 color,
376 width,
377 }),
378 Piece::Line { a, b } => {
379 let (ext_a, ext_b) = Piece::chain_extensions(&self.pieces, k, width, closed);
387 self.single_buf.push(SegmentInstance {
388 a,
389 b,
390 color,
391 width,
392 alpha: 1.0,
393 ext_a,
394 ext_b,
395 });
396 }
397 }
398 }
399 }
400
401 fn ramp(&self) -> ColorRamp {
403 ColorRamp {
404 hue: self.colour.hue,
405 hue_spread: self.hue_spread,
406 palette_mix: self.colour.mix,
407 palette_steps: self.colour.steps,
408 saturation: self.colour.saturation,
409 brightness: self.colour.brightness,
410 }
411 }
412
413 fn curve_params(
416 &self,
417 samples: usize,
418 rotation: f32,
419 color: [f32; 3],
420 width: f32,
421 ) -> curves::CurveParams {
422 curves::CurveParams {
423 n: self.n,
424 d: self.d,
425 phase: self.phase,
426 radial_offset: self.radial_offset,
427 samples,
428 scale: self.scale,
429 rotation,
430 draw_progress: self.draw_progress,
431 color,
432 width,
433 levers: curves::Levers {
434 pen: self.pen,
435 sym: self.sym,
436 sharpness: self.sharpness,
437 lobe: self.lobe,
438 decay: self.decay,
439 tube: self.tube,
440 m: self.m,
441 phase_z: self.phase_z,
442 },
443 }
444 }
445
446 fn update_space(&mut self, arm: curves::FamilyArm3d) {
454 let (samples, overflow) = space_samples(self.samples, self.seg3d_cap);
455 let params = self.curve_params(samples, 0.0, [0.0; 3], 0.0);
456 self.closed3d = (arm.sample)(¶ms, &mut self.points3d);
457 self.extent3d = (arm.extent)(¶ms);
458 self.samples3d = samples;
459 self.segments.clear();
460 self.arcs.clear();
461 self.mirror_overflow = overflow;
462 }
463
464 fn render_space(
470 &mut self,
471 queue: &wgpu::Queue,
472 encoder: &mut wgpu::CommandEncoder,
473 view: &wgpu::TextureView,
474 aspect: f32,
475 ) {
476 let frame = self.camera.frame(
478 aspect,
479 self.zoom,
480 [self.pan.x, self.pan.y],
481 self.target,
482 self.extent3d,
483 self.max_coc,
484 );
485 if self.mirror_overflow.is_none() {
486 self.mirror_overflow = frame.blur;
487 }
488 let ramp = self.ramp();
489 let width = if self.thickness.is_finite() {
490 self.thickness.max(0.0)
491 } else {
492 DEFAULT_THICKNESS
493 };
494 let span = self.samples3d.saturating_sub(1).max(1) as f32;
497
498 let mut instances = std::mem::take(&mut self.instances3d);
499 instances.clear();
500 let palette = &self.palette;
501 space_chords(
502 &frame,
503 &self.points3d,
504 self.closed3d,
505 span,
506 self.hue_axis,
507 |chord| {
508 instances.push(Segment3dInstance {
509 a: chord.a,
510 b: chord.b,
511 color: ramp.at(palette, chord.u),
512 width,
513 alpha: 1.0,
514 prev: chord.prev,
515 next: chord.next,
516 skirt: 0.0,
517 });
518 },
519 );
520 self.lines3d.draw_3d(
521 queue,
522 encoder,
523 view,
524 &frame,
525 self.glow,
526 self.softness,
527 &instances,
528 );
529 self.instances3d = instances;
530 }
531}
532
533#[derive(Clone, Copy, Debug)]
537pub(crate) struct SpaceChord {
538 #[cfg(test)]
539 pub(crate) k: usize,
540 pub(crate) prev: [f32; 3],
541 pub(crate) a: [f32; 3],
542 pub(crate) b: [f32; 3],
543 pub(crate) next: [f32; 3],
544 pub(crate) u: f32,
545}
546
547pub(crate) fn space_chords(
555 frame: &camera::CameraFrame,
556 points: &[[f32; 3]],
557 closed: bool,
558 span: f32,
559 hue_axis: f32,
560 mut emit: impl FnMut(SpaceChord),
561) {
562 let n = points.len();
563 let chords = if closed { n } else { n.saturating_sub(1) };
564 for k in 0..chords {
565 let (Some(&pa), Some(&pb)) = (points.get(k), points.get((k + 1) % n)) else {
566 continue;
567 };
568 let before = if k > 0 {
572 points.get(k - 1).copied()
573 } else if closed {
574 points.last().copied()
575 } else {
576 None
577 };
578 let after = if k + 2 < n || closed {
579 points.get((k + 2) % n).copied()
580 } else {
581 None
582 };
583 let Some([prev, a, b, next]) = joined_chord(&frame.view, before, pa, pb, after) else {
584 continue;
585 };
586 if frame.view.outside(a, b, frame.margin) {
587 continue;
588 }
589 let along = k as f32 / span;
590 let u = if hue_axis > 0.0 {
591 let mid = [
592 0.5 * (a[0] + b[0]),
593 0.5 * (a[1] + b[1]),
594 0.5 * (a[2] + b[2]),
595 ];
596 space_coordinate(along, frame.volume_depth(frame.view.depth(mid)), hue_axis)
597 } else {
598 along
599 };
600 emit(SpaceChord {
601 #[cfg(test)]
602 k,
603 prev,
604 a,
605 b,
606 next,
607 u,
608 });
609 }
610}
611
612pub(crate) fn space_coordinate(along: f32, depth01: f32, hue_axis: f32) -> f32 {
618 let h = if hue_axis.is_finite() {
619 hue_axis.clamp(0.0, 1.0)
620 } else {
621 0.0
622 };
623 if h >= 1.0 {
624 return depth01;
625 }
626 along + (depth01 - along) * h
627}
628
629pub(crate) fn space_samples(samples: f32, cap: usize) -> (usize, Option<CapOverflow>) {
632 let asked = samples.max(0.0) as usize;
633 if asked <= cap {
634 return (asked, None);
635 }
636 (
637 cap,
638 Some(CapOverflow {
639 dropped: asked - cap,
640 context: OverflowContext::Samples(u32::try_from(asked).unwrap_or(u32::MAX)),
641 cap,
642 }),
643 )
644}
645
646pub(crate) fn color_along_path(
657 segs: &mut [SegmentInstance],
658 palette: &Palette,
659 ramp: ColorRamp,
660 samples: usize,
661) {
662 let span = samples.saturating_sub(1).max(1) as f32;
663 for (i, seg) in segs.iter_mut().enumerate() {
664 seg.color = ramp.at(palette, i as f32 / span);
665 }
666}
667
668pub const PARAMS: &[ParamSpec] = &[
671 ParamSpec {
672 name: "n",
673 default: 6.0,
674 range: Some([1.0, 24.0]),
675 doc: "The figure's first number, read as a real value per family: the rose's petal \
676 number, the two Lissajous figures' and the harmonograph's x frequency, the \
677 hypotrochoid's signed radius ratio, the knot's whole turns round its axis.",
678 kind: ParamKind::Modal,
679 group: ParamGroup::Shape,
680 main: true,
681 },
682 ParamSpec {
683 name: "d",
684 default: 71.0,
685 range: Some([1.0, 360.0]),
686 doc: "The figure's second number, per family: the rose's sampling step in degrees, the \
687 two Lissajous figures' and the harmonograph's y frequency, the hypotrochoid's \
688 cusp count, the superformula's lobe skew, the knot's whole turns through its hole.",
689 kind: ParamKind::Modal,
690 group: ParamGroup::Shape,
691 main: true,
692 },
693 ParamSpec {
694 name: "phase",
695 default: 0.0,
696 range: Some([0.0, 1.0]),
697 doc: "Offsets where the figure starts: inside the rose's sine, between the two Lissajous \
698 figures' and the harmonograph's x and y axes, and at the hypotrochoid's pen.",
699 kind: ParamKind::Modal,
700 group: ParamGroup::Motion,
701 main: false,
702 },
703 ParamSpec {
704 name: "radial_offset",
705 default: 0.0,
706 range: Some([-1.0, 1.0]),
707 doc: "Pushes every point out from the centre, opening the figure into a ring.",
708 kind: ParamKind::Modal,
709 group: ParamGroup::Shape,
710 main: false,
711 },
712 ParamSpec {
713 name: "pen",
714 default: 1.0,
715 range: Some([0.0, 2.0]),
716 doc: "How far the tracing point sits from the rolling circle's centre, in rolling radii: \
717 1 draws cusps, less rounds them off, more throws them into loops.",
718 kind: ParamKind::Modal,
719 group: ParamGroup::Shape,
720 main: false,
721 },
722 ParamSpec {
723 name: "sym",
724 default: 5.0,
725 range: Some([1.0, 24.0]),
726 doc: "How many lobes the figure repeats around its centre, as a whole number.",
727 kind: ParamKind::Structural,
728 group: ParamGroup::Shape,
729 main: false,
730 },
731 ParamSpec {
732 name: "sharpness",
733 default: 1.0,
734 range: Some([0.1, 20.0]),
735 doc: "How pointed the lobes are: low draws a spiky star, high rounds the figure toward a \
736 circle.",
737 kind: ParamKind::Modal,
738 group: ParamGroup::Shape,
739 main: false,
740 },
741 ParamSpec {
742 name: "lobe",
743 default: 1.0,
744 range: Some([0.1, 10.0]),
745 doc: "How the lobes swell between their tips: low pinches them thin, high fills them \
746 into a polygon.",
747 kind: ParamKind::Modal,
748 group: ParamGroup::Shape,
749 main: false,
750 },
751 ParamSpec {
752 name: "decay",
753 default: 0.1,
754 range: Some([0.0, 0.5]),
755 doc: "How fast the pendulums die away along the trace: 0 closes the figure, more spirals \
756 it inward.",
757 kind: ParamKind::Modal,
758 group: ParamGroup::Light,
759 main: false,
760 },
761 ParamSpec {
762 name: "tube",
763 default: 0.4,
764 range: Some([0.05, 0.9]),
765 doc: "How far a knot's strand winds out from the torus's core circle, as a fraction of \
766 that circle's radius.",
767 kind: ParamKind::Modal,
768 group: ParamGroup::Shape,
769 main: false,
770 },
771 ParamSpec {
772 name: "m",
773 default: 1.0,
774 range: Some([0.0, 12.0]),
775 doc: "The depth axis's frequency on a 3D Lissajous figure; 0 with phase_z at 0 lays it \
776 flat.",
777 kind: ParamKind::Modal,
778 group: ParamGroup::Shape,
779 main: false,
780 },
781 ParamSpec {
782 name: "phase_z",
783 default: 0.25,
784 range: Some([0.0, 1.0]),
785 doc: "Offsets the depth axis of a 3D Lissajous figure against the other two, as a \
786 fraction of a turn.",
787 kind: ParamKind::Modal,
788 group: ParamGroup::Motion,
789 main: false,
790 },
791 ParamSpec {
792 name: "samples",
793 default: 361.0,
794 range: Some([16.0, 2048.0]),
795 doc: "How many points the curve is drawn from; fewer reads as a polygon. Truncated, so \
796 a rise adds its next point on arrival.",
797 kind: ParamKind::Modal,
798 group: ParamGroup::Shape,
799 main: false,
800 },
801 crate::render::scenes::lines::thickness(DEFAULT_THICKNESS),
802 crate::render::scenes::common::hue(DEFAULT_HUE),
803 crate::render::scenes::lines::hue_spread(DEFAULT_HUE_SPREAD),
804 crate::render::scenes::common::SATURATION,
805 crate::render::scenes::common::PALETTE_MIX,
806 crate::render::scenes::common::PALETTE_STEPS,
807 crate::render::scenes::common::PALETTE_CONTOUR,
808 ParamSpec {
809 name: "spin",
810 default: 0.1,
811 range: Some([-2.0, 2.0]),
812 doc: "Turns per second the whole figure rotates by.",
813 kind: ParamKind::Modal,
814 group: ParamGroup::Motion,
815 main: true,
816 },
817 crate::render::scenes::lines::scale(DEFAULT_SCALE),
818 crate::render::scenes::common::brightness(DEFAULT_BRIGHTNESS),
819 crate::render::scenes::lines::GLOW,
820 crate::render::scenes::lines::SOFTNESS,
821 STROKE_BLEND,
822 crate::render::scenes::lines::DRAW_PROGRESS,
823 crate::render::scenes::common::zoom(DEFAULT_ZOOM),
824 crate::render::scenes::common::PAN_X,
825 crate::render::scenes::common::PAN_Y,
826 MIRROR_ORDER,
827 MIRROR_REFLECT,
828 YAW,
829 PITCH,
830 DISTANCE,
831 FOV,
832 FOCUS,
833 APERTURE,
834 FOG,
835 SOLID,
836 HUE_AXIS,
837];
838
839const STROKE_BLEND: ParamSpec = ParamSpec {
845 doc: "Moves a flat figure's stroke from additive light toward opaque paint, so crossings \
846 stop brightening.",
847 ..crate::render::scenes::lines::STROKE_BLEND
848};
849const MIRROR_ORDER: ParamSpec = ParamSpec {
850 doc: "Repeats a flat figure this many times around the centre; 1 draws it once.",
851 ..crate::render::scenes::lines::MIRROR_ORDER
852};
853const MIRROR_REFLECT: ParamSpec = ParamSpec {
854 doc: "Alternates a flat figure's repeats into mirror images rather than plain rotations.",
855 ..crate::render::scenes::lines::MIRROR_REFLECT
856};
857const YAW: ParamSpec = ParamSpec {
858 doc: "Turns the camera around a space curve, in radians; bind it to a slow clock to orbit.",
859 ..camera::YAW
860};
861const PITCH: ParamSpec = ParamSpec {
862 doc: "Raises the camera above a space curve, in radians; negative looks up from below.",
863 ..camera::PITCH
864};
865const DISTANCE: ParamSpec = ParamSpec {
866 doc: "How far the camera sits from a space curve's centre; nearer exaggerates the \
867 perspective.",
868 ..camera::DISTANCE
869};
870const FOV: ParamSpec = ParamSpec {
871 doc: "The camera's vertical field of view onto a space curve, in radians; zoom divides it.",
872 ..camera::FOV
873};
874const FOCUS: ParamSpec = ParamSpec {
875 doc: "Where the focal plane sits in a space curve's depth: 0 at its nearest point, 1 at its \
876 farthest.",
877 ..camera::FOCUS
878};
879const APERTURE: ParamSpec = ParamSpec {
880 doc: "The blur of a space curve's far side, in pixels; strokes nearer than the focal plane \
881 blur more, up to the tier's cap. 0 keeps every stroke sharp, and wider costs fill.",
882 ..camera::APERTURE
883};
884const FOG: ParamSpec = ParamSpec {
885 doc: "Fades a space curve toward black with depth: at 1 its farthest point is black and its \
886 nearest keeps its light. 0 is off.",
887 ..camera::FOG
888};
889const SOLID: ParamSpec = ParamSpec {
890 doc: "1 paints a space curve's near strands over its far ones, so it reads as an object and \
891 crossings stop brightening; 0 is the additive glow. Solid sorts every chord by depth \
892 each frame.",
893 ..camera::SOLID
894};
895
896const HUE_AXIS: ParamSpec = ParamSpec {
899 name: "hue_axis",
900 default: 0.0,
901 range: Some([0.0, 1.0]),
902 doc: "Moves a space curve's colour from running along its path (0) to running with depth \
903 (1), nearest first; between mixes the two.",
904 kind: ParamKind::Modal,
905 group: ParamGroup::Colour,
906 main: false,
907};
908const DEFAULT_HUE_AXIS: f32 = default_of(PARAMS, "hue_axis");
909
910macro_rules! per_family {
915 ($name:expr; $rose:expr, $lissajous:expr, $hypotrochoid:expr, $superformula:expr, $harmonograph:expr, $torus_knot:expr, $lissajous_3d:expr $(,)?) => {
916 FamilyParam {
917 name: $name,
918 ranges: &[
919 FamilyRange {
920 family: "maurer_rose",
921 range: $rose,
922 },
923 FamilyRange {
924 family: "lissajous",
925 range: $lissajous,
926 },
927 FamilyRange {
928 family: "hypotrochoid",
929 range: $hypotrochoid,
930 },
931 FamilyRange {
932 family: "superformula",
933 range: $superformula,
934 },
935 FamilyRange {
936 family: "harmonograph",
937 range: $harmonograph,
938 },
939 FamilyRange {
940 family: "torus_knot",
941 range: $torus_knot,
942 },
943 FamilyRange {
944 family: "lissajous_3d",
945 range: $lissajous_3d,
946 },
947 ],
948 }
949 };
950}
951
952macro_rules! space_only {
955 ($spec:expr) => {
956 per_family!($spec.name; None, None, None, None, None, $spec.range, $spec.range)
957 };
958}
959
960macro_rules! flat_only {
965 ($spec:expr) => {
966 per_family!($spec.name; $spec.range, $spec.range, $spec.range, $spec.range, $spec.range, None, None)
967 };
968}
969
970pub const FAMILY_PARAMS: &[FamilyParam] = &[
979 per_family!("n";
980 Some([1.0, 24.0]), Some([1.0, 12.0]), Some([-8.0, 8.0]), None, Some([1.0, 12.0]),
981 Some([1.0, 12.0]), Some([1.0, 12.0])),
982 per_family!("d";
983 Some([1.0, 360.0]), Some([1.0, 12.0]), Some([1.0, 24.0]), Some([0.25, 4.0]), Some([1.0, 12.0]),
984 Some([1.0, 12.0]), Some([1.0, 12.0])),
985 per_family!("phase";
986 Some([0.0, 1.0]), Some([0.0, 1.0]), Some([0.0, 1.0]), None, Some([0.0, 1.0]), None,
987 Some([0.0, 1.0])),
988 per_family!("radial_offset"; Some([-1.0, 1.0]), None, None, None, None, None, None),
989 per_family!("pen"; None, None, Some([0.0, 2.0]), None, None, None, None),
990 per_family!("sym"; None, None, None, Some([1.0, 24.0]), None, None, None),
991 per_family!("sharpness"; None, None, None, Some([0.1, 20.0]), None, None, None),
992 per_family!("lobe"; None, None, None, Some([0.1, 10.0]), None, None, None),
993 per_family!("decay"; None, None, None, None, Some([0.0, 0.5]), None, None),
994 per_family!("tube"; None, None, None, None, None, Some([0.05, 0.9]), None),
995 per_family!("m"; None, None, None, None, None, None, Some([0.0, 12.0])),
996 per_family!("phase_z"; None, None, None, None, None, None, Some([0.0, 1.0])),
997 per_family!("spin";
998 Some([-2.0, 2.0]), Some([-2.0, 2.0]), Some([-2.0, 2.0]), Some([-2.0, 2.0]),
999 Some([-2.0, 2.0]), None, None),
1000 flat_only!(STROKE_BLEND),
1001 flat_only!(MIRROR_ORDER),
1002 flat_only!(MIRROR_REFLECT),
1003 space_only!(YAW),
1004 space_only!(PITCH),
1005 space_only!(DISTANCE),
1006 space_only!(FOV),
1007 space_only!(FOCUS),
1008 space_only!(APERTURE),
1009 space_only!(FOG),
1010 space_only!(SOLID),
1011 space_only!(HUE_AXIS),
1012];
1013
1014impl Scene for ParametricCurveScene {
1015 fn name(&self) -> &'static str {
1016 "parametric curve"
1017 }
1018
1019 fn advance(&mut self, dt: f32) {
1020 self.dt = dt;
1023 }
1024
1025 fn reset_params(&mut self) {
1026 self.n = DEFAULT_N;
1027 self.d = DEFAULT_D;
1028 self.phase = DEFAULT_PHASE;
1029 self.radial_offset = DEFAULT_RADIAL_OFFSET;
1030 self.pen = DEFAULT_PEN;
1031 self.sym = DEFAULT_SYM;
1032 self.sharpness = DEFAULT_SHARPNESS;
1033 self.lobe = DEFAULT_LOBE;
1034 self.decay = DEFAULT_DECAY;
1035 self.samples = DEFAULT_SAMPLES;
1036 self.thickness = DEFAULT_THICKNESS;
1037 self.colour.reset();
1038 self.pan.reset();
1039 self.hue_spread = DEFAULT_HUE_SPREAD;
1040 self.spin = DEFAULT_SPIN;
1041 self.scale = DEFAULT_SCALE;
1042 self.glow = DEFAULT_GLOW;
1043 self.softness = super::DEFAULT_SOFTNESS;
1044 self.stroke_blend = super::ADDITIVE_BLEND;
1045 self.draw_progress = DEFAULT_DRAW_PROGRESS;
1046 self.zoom = DEFAULT_ZOOM;
1047 self.mirror_order = DEFAULT_MIRROR_ORDER;
1048 self.mirror_reflect = DEFAULT_MIRROR_REFLECT;
1049 self.tube = DEFAULT_TUBE;
1050 self.m = DEFAULT_M;
1051 self.phase_z = DEFAULT_PHASE_Z;
1052 self.hue_axis = DEFAULT_HUE_AXIS;
1053 self.camera.reset();
1054 }
1055
1056 fn set_target_size(&mut self, width: u32, height: u32) {
1057 self.target = (width, height);
1058 }
1059
1060 fn set_param(&mut self, name: &str, value: f32) {
1061 if self.colour.set(name, value) || self.pan.set(name, value) || self.camera.set(name, value)
1064 {
1065 return;
1066 }
1067 match name {
1068 "n" => self.n = value,
1069 "d" => self.d = value,
1070 "phase" => self.phase = value,
1071 "radial_offset" => self.radial_offset = value,
1072 "pen" => self.pen = value,
1073 "sym" => self.sym = value,
1074 "sharpness" => self.sharpness = value,
1075 "lobe" => self.lobe = value,
1076 "decay" => self.decay = value,
1077 "tube" => self.tube = value,
1078 "m" => self.m = value,
1079 "phase_z" => self.phase_z = value,
1080 "hue_axis" => self.hue_axis = value,
1081 "samples" => self.samples = value,
1082 "thickness" => self.thickness = value,
1083 "hue_spread" => self.hue_spread = value,
1084 "spin" => self.spin = value,
1085 "scale" => self.scale = value,
1086 "glow" => self.glow = value,
1087 "softness" => self.softness = value,
1088 "stroke_blend" => self.stroke_blend = value,
1089 "draw_progress" => self.draw_progress = value,
1090 "zoom" => self.zoom = value,
1091 "mirror_order" => self.mirror_order = value,
1092 "mirror_reflect" => self.mirror_reflect = value,
1093 _ => {}
1094 }
1095 }
1096
1097 fn set_palette(&mut self, palette: &Palette) {
1098 self.palette = palette.clone();
1099 }
1100
1101 fn configure(&mut self, cfg: &GeneratorConfig) -> Option<CapOverflow> {
1102 if let GeneratorConfig::Curve { family } = cfg {
1108 self.family = *family;
1109 }
1110 None
1113 }
1114
1115 fn mirror_overflow(&self) -> Option<&CapOverflow> {
1116 self.mirror_overflow.as_ref()
1117 }
1118
1119 fn update(&mut self, _frame: &AnalysisFrame) {
1120 if let Some(arm) = curves::arm3d(self.family) {
1121 self.update_space(arm);
1122 return;
1123 }
1124 let samples = (self.samples.max(0.0) as usize).min(self.max_segments);
1131 self.spin_phase.step(self.spin, self.dt);
1132 let rotation = self.spin_phase.get();
1133 let ramp = self.ramp();
1134 let color = ramp.at(&self.palette, 0.0);
1138 let width = super::half_width(self.thickness);
1139
1140 let params = self.curve_params(samples, rotation, color, width);
1141
1142 let fit = curves::fit_walk(
1154 self.family,
1155 params,
1156 &mut self.points,
1157 &mut self.pieces,
1158 &mut self.walk,
1159 );
1160 if fit.fitted {
1161 self.reserve_fit_buffers();
1162 self.split_pieces(samples, ramp, color, width, fit.closed);
1163 } else {
1164 (curves::arm(self.family).polyline)(
1165 ¶ms,
1166 &self.points,
1167 fit.closed,
1168 &mut self.single_buf,
1169 );
1170 self.single_arcs.clear();
1171 color_along_path(&mut self.single_buf, &self.palette, ramp, samples);
1172 }
1173 let mirror = MirrorSpec::from_params(self.mirror_order, self.mirror_reflect);
1174 if mirror.is_identity() {
1175 debug_assert!(
1181 self.single_buf.len() <= self.max_segments,
1182 "the sampler already clamps to the cap, so identity cannot truncate"
1183 );
1184 std::mem::swap(&mut self.single_buf, &mut self.segments);
1185 std::mem::swap(&mut self.single_arcs, &mut self.arcs);
1186 self.mirror_overflow = None;
1187 return;
1188 }
1189 let dropped = replicate_mirror(
1190 &self.single_buf,
1191 mirror,
1192 self.max_segments,
1193 &mut self.segments,
1194 );
1195 let arc_cap = self.max_segments.saturating_sub(self.segments.len());
1199 let arc_dropped = replicate_mirror(&self.single_arcs, mirror, arc_cap, &mut self.arcs);
1200 let dropped = dropped + arc_dropped;
1201 self.mirror_overflow = (dropped > 0).then_some(CapOverflow {
1202 dropped,
1203 context: OverflowContext::Mirror(mirror.order),
1204 cap: self.max_segments,
1205 });
1206 }
1207
1208 fn render(
1209 &mut self,
1210 queue: &wgpu::Queue,
1211 encoder: &mut wgpu::CommandEncoder,
1212 view: &wgpu::TextureView,
1213 aspect: f32,
1214 ) {
1215 if self.family.is_space() {
1216 self.render_space(queue, encoder, view, aspect);
1217 return;
1218 }
1219 let xform = ViewTransform {
1222 zoom: self.zoom,
1223 pan: [self.pan.x, self.pan.y],
1224 _pad: 0.0,
1225 };
1226 let mut renderer = self.renderer.borrow_mut();
1227 if self.stroke_blend >= super::OPAQUE_BLEND {
1228 renderer.draw_opaque(
1229 queue,
1230 encoder,
1231 view,
1232 aspect,
1233 self.glow,
1234 self.softness,
1235 StrokeMetric::World,
1236 xform,
1237 &self.segments,
1238 &self.arcs,
1239 );
1240 } else {
1241 renderer.draw_arcs(
1242 queue,
1243 encoder,
1244 view,
1245 aspect,
1246 self.glow,
1247 self.softness,
1248 StrokeMetric::World,
1249 xform,
1250 &self.segments,
1251 &self.arcs,
1252 );
1253 }
1254 }
1255}
1256
1257#[cfg(test)]
1258mod tests {
1259 #![allow(clippy::indexing_slicing)]
1260
1261 use super::*;
1262
1263 const SAMPLES: usize = 240;
1264
1265 #[test]
1282 fn the_walk_writes_one_more_point_than_it_has_chords_and_a_web_fits_nothing() {
1283 let web = curves::CurveParams {
1284 n: 6.0,
1285 d: 71.0,
1287 phase: 0.0,
1288 radial_offset: 0.0,
1289 samples: SAMPLES,
1290 scale: 0.9,
1291 rotation: 0.0,
1292 draw_progress: 1.0,
1293 color: [1.0, 1.0, 1.0],
1294 width: 0.01,
1295 levers: curves::Levers::default(),
1296 };
1297
1298 let mut points = Vec::with_capacity(SAMPLES + 1);
1299 let mut pieces = Vec::new();
1300 let mut walk = Vec::new();
1301
1302 let fitted = curves::maurer_rose_pieces(web, &mut points, &mut pieces, &mut walk);
1303
1304 assert!(!fitted, "d = 71 is a chord web and declines the fit");
1305 assert!(
1306 pieces.is_empty() && walk.is_empty(),
1307 "a declined fit writes neither buffer, so reserving for them commits \
1308 heap nothing ever touches"
1309 );
1310 assert_eq!(
1311 pieces.capacity(),
1312 0,
1313 "and it does not even grow them: reserving nothing costs nothing"
1314 );
1315 assert_eq!(walk.capacity(), 0);
1316
1317 assert_eq!(
1320 points.len(),
1321 SAMPLES + 1,
1322 "the walk has one more point than it has chords"
1323 );
1324 assert_eq!(
1325 points.capacity(),
1326 SAMPLES + 1,
1327 "so a capacity of `samples` exactly would have reallocated on the \
1328 final push"
1329 );
1330 }
1331
1332 #[test]
1352 fn a_fitted_chains_line_pieces_reach_their_corners_and_its_ends_stay_free() {
1353 use crate::render::scenes::lines::MITER_SLACK;
1354 use std::f32::consts::PI;
1355
1356 const W: f32 = 0.01;
1357
1358 let rose = curves::CurveParams {
1359 n: 5.0,
1360 d: 2.0,
1362 phase: 0.0,
1363 radial_offset: 0.0,
1364 samples: SAMPLES,
1365 scale: 0.9,
1366 rotation: 0.0,
1367 draw_progress: 1.0,
1368 color: [1.0, 1.0, 1.0],
1369 width: W,
1370 levers: curves::Levers::default(),
1371 };
1372 let (mut points, mut pieces, mut at) = (Vec::new(), Vec::new(), Vec::new());
1373 assert!(
1374 curves::maurer_rose_pieces(rose, &mut points, &mut pieces, &mut at),
1375 "a d = 2 rose must be fitted, or this fixture tests nothing"
1376 );
1377
1378 let last = pieces.len() - 1;
1380 assert_eq!(
1381 Piece::chain_extensions(&pieces, 0, W, false).0,
1382 0.0,
1383 "the walk's first end has no neighbour to join"
1384 );
1385 assert_eq!(
1386 Piece::chain_extensions(&pieces, last, W, false).1,
1387 0.0,
1388 "nor its last"
1389 );
1390
1391 let mut straight = 0usize;
1392 let mut cornered = 0usize;
1393 for k in 0..pieces.len() {
1394 let (ext_a, ext_b) = Piece::chain_extensions(&pieces, k, W, false);
1395 for (side, got, incoming, outgoing) in [
1396 (
1397 "a",
1398 ext_a,
1399 k.checked_sub(1).map(|j| pieces[j].end_tangent()),
1400 Some(pieces[k].start_tangent()),
1401 ),
1402 (
1403 "b",
1404 ext_b,
1405 Some(pieces[k].end_tangent()),
1406 pieces.get(k + 1).map(|p| p.start_tangent()),
1407 ),
1408 ] {
1409 let (Some(d1), Some(d2)) = (incoming, outgoing) else {
1410 continue; };
1412 let turn = (d1[0] * d2[0] + d1[1] * d2[1]).clamp(-1.0, 1.0).acos();
1415 let want = W / ((PI - turn) * 0.5).sin();
1416 assert!(
1417 (got - want).abs() <= want * MITER_SLACK,
1418 "piece {k}'s `{side}` joint carries {got} against the {want} \
1419 its {}-degree turn asks for",
1420 turn.to_degrees()
1421 );
1422 if turn < 1e-4 {
1423 straight += 1;
1424 assert!(
1425 (got - W).abs() <= W * MITER_SLACK,
1426 "piece {k}'s `{side}` joint is G1, so its miter must be \
1427 exactly the flat half-width {W}, got {got}"
1428 );
1429 } else {
1430 cornered += 1;
1431 }
1432 }
1433 }
1434 assert!(
1435 straight > 0 && cornered > 0,
1436 "this chain holds {straight} tangent-continuous joints and \
1437 {cornered} corners — it must hold some of each, or one of the two \
1438 halves above was never exercised"
1439 );
1440 }
1441
1442 #[test]
1454 fn the_family_table_is_the_roster_and_its_inert_cells_are_inert() {
1455 let families: Vec<&str> = CurveFamily::ALL.iter().map(|f| f.as_str()).collect();
1456 assert_eq!(
1457 families,
1458 [
1459 "maurer_rose",
1460 "lissajous",
1461 "hypotrochoid",
1462 "superformula",
1463 "harmonograph",
1464 "torus_knot",
1465 "lissajous_3d"
1466 ],
1467 "the roster the macro's seven columns are written against"
1468 );
1469 let mut seen = Vec::new();
1470 for row in FAMILY_PARAMS {
1471 assert!(!seen.contains(&row.name), "`{}` has two rows", row.name);
1472 seen.push(row.name);
1473 let spec = PARAMS
1474 .iter()
1475 .find(|spec| spec.name == row.name)
1476 .unwrap_or_else(|| panic!("`{}` is not a declared parameter", row.name));
1477 let listed: Vec<&str> = row.ranges.iter().map(|r| r.family).collect();
1478 assert_eq!(listed, families, "`{}` must list every family", row.name);
1479 assert!(
1480 row.ranges.iter().any(|r| r.range == spec.range),
1481 "`{}`'s spec range {:?} is no family's range",
1482 row.name,
1483 spec.range
1484 );
1485 }
1486
1487 let levers = curves::Levers::default();
1488 let moved = |name: &str| -> curves::Levers {
1489 let mut l = levers;
1490 match name {
1491 "pen" => l.pen = 1.7,
1492 "sym" => l.sym = 9.0,
1493 "sharpness" => l.sharpness = 7.0,
1494 "lobe" => l.lobe = 4.0,
1495 "decay" => l.decay = 0.3,
1496 "tube" => l.tube = 0.7,
1497 "m" => l.m = 3.0,
1498 "phase_z" => l.phase_z = 0.6,
1499 other => panic!("no lever called `{other}`"),
1500 }
1501 l
1502 };
1503 let walk = |family: CurveFamily, levers: curves::Levers| -> Vec<[f32; 3]> {
1505 let p = curves::CurveParams {
1506 n: 3.0,
1507 d: 2.0,
1508 phase: 0.0,
1509 radial_offset: 0.0,
1510 samples: 200,
1511 scale: 0.9,
1512 rotation: 0.0,
1513 draw_progress: 1.0,
1514 color: [1.0; 3],
1515 width: 0.01,
1516 levers,
1517 };
1518 if let Some(arm) = curves::arm3d(family) {
1519 let mut points = Vec::new();
1520 (arm.sample)(&p, &mut points);
1521 return points;
1522 }
1523 let (mut points, mut pieces, mut at) = (Vec::new(), Vec::new(), Vec::new());
1524 curves::fit_walk(family, p, &mut points, &mut pieces, &mut at);
1525 points.iter().map(|&[x, y]| [x, y, 0.0]).collect()
1526 };
1527 let mut inert_checked = 0;
1528 const LEVERS: [&str; 8] = [
1529 "pen",
1530 "sym",
1531 "sharpness",
1532 "lobe",
1533 "decay",
1534 "tube",
1535 "m",
1536 "phase_z",
1537 ];
1538 for name in LEVERS {
1539 let row = FAMILY_PARAMS
1540 .iter()
1541 .find(|row| row.name == name)
1542 .unwrap_or_else(|| panic!("`{name}` has no family row"));
1543 for (cell, family) in row.ranges.iter().zip(CurveFamily::ALL) {
1544 let (before, after) = (walk(family, levers), walk(family, moved(name)));
1545 if cell.range.is_none() {
1546 assert_eq!(
1547 before, after,
1548 "`{name}` moved the {} it is inert on",
1549 cell.family
1550 );
1551 inert_checked += 1;
1552 } else {
1553 assert_ne!(
1554 before, after,
1555 "`{name}` did not move the {} it reads on",
1556 cell.family
1557 );
1558 }
1559 }
1560 }
1561 assert_eq!(
1562 inert_checked,
1563 LEVERS.len() * (CurveFamily::ALL.len() - 1),
1564 "each lever is inert on every family but its own"
1565 );
1566 }
1567
1568 #[test]
1573 fn a_constant_spin_integrates_to_the_multiply_it_replaced() {
1574 let dt = FALLBACK_DT;
1575 for rate in [DEFAULT_SPIN, 0.0, 0.4, -0.25] {
1576 let mut phase = Phase::default();
1577 let mut time = 0.0f32;
1578 for _ in 0..600 {
1579 phase.step(rate, dt);
1580 time += dt;
1581 }
1582 assert!(
1583 (phase.get() - rate * time).abs() < 1e-3,
1584 "rate {rate}: integrated {} against the multiply's {}",
1585 phase.get(),
1586 rate * time
1587 );
1588 }
1589 }
1590
1591 #[test]
1596 fn a_spin_change_bends_the_rotation_instead_of_teleporting_it() {
1597 let dt = FALLBACK_DT;
1598 let mut phase = Phase::default();
1599 let mut time = 0.0f32;
1600 for _ in 0..6_000 {
1601 phase.step(DEFAULT_SPIN, dt);
1602 time += dt;
1603 }
1604 assert!(time > 99.0, "the fixture must be far from t = 0: {time}");
1605
1606 let before = phase.get();
1607 phase.step(1.5, dt);
1608 let step = phase.get() - before;
1609 assert!(
1610 (step - 1.5 * dt).abs() < 1e-4,
1611 "the rotation advanced {step}, not {}",
1612 1.5 * dt
1613 );
1614 let teleport = (1.5 - DEFAULT_SPIN) * time;
1616 assert!(
1617 teleport > 100.0,
1618 "the multiply's one-frame jump at this elapsed time was {teleport} rad"
1619 );
1620 }
1621
1622 fn ramp(hue_spread: f32) -> ColorRamp {
1623 ColorRamp {
1624 hue: DEFAULT_HUE,
1625 hue_spread,
1626 palette_mix: common::DEFAULT_PALETTE_MIX,
1627 palette_steps: crate::render::palette::DEFAULT_PALETTE_STEPS,
1628 saturation: common::DEFAULT_SATURATION,
1629 brightness: DEFAULT_BRIGHTNESS,
1630 }
1631 }
1632
1633 fn curve(samples: usize, draw_progress: f32, hue_spread: f32) -> Vec<SegmentInstance> {
1634 let mut out = Vec::with_capacity(samples + 1);
1635 curves::maurer_rose(
1636 curves::CurveParams {
1637 n: DEFAULT_N,
1638 d: DEFAULT_D,
1639 phase: DEFAULT_PHASE,
1640 radial_offset: DEFAULT_RADIAL_OFFSET,
1641 samples,
1642 scale: DEFAULT_SCALE,
1643 rotation: 0.0,
1644 draw_progress,
1645 color: [0.0; 3],
1646 width: 0.01,
1647 levers: curves::Levers::default(),
1648 },
1649 &mut out,
1650 );
1651 color_along_path(
1652 &mut out,
1653 &Palette::default_spectrum(),
1654 ramp(hue_spread),
1655 samples,
1656 );
1657 out
1658 }
1659
1660 #[test]
1665 fn the_spread_colours_the_curve_along_its_direction_of_travel() {
1666 let swept = curve(SAMPLES, 1.0, 0.5);
1667 assert_eq!(swept.len(), SAMPLES, "one chord per sample");
1668 assert_ne!(
1669 swept[0].color,
1670 swept[SAMPLES - 1].color,
1671 "the path's start and end must differ — that is the whole claim"
1672 );
1673
1674 for k in 1..swept.len() {
1678 assert_ne!(
1679 swept[k].color,
1680 swept[k - 1].color,
1681 "chord {k} repeated chord {}'s colour, so the ramp is not \
1682 advancing along the path",
1683 k - 1
1684 );
1685 }
1686 }
1687
1688 #[test]
1691 fn zero_spread_is_one_flat_colour_along_the_whole_path() {
1692 let flat = curve(SAMPLES, 1.0, 0.0);
1693 for (k, seg) in flat.iter().enumerate() {
1694 assert_eq!(seg.color, flat[0].color, "chord {k} must carry the one hue");
1695 }
1696 }
1697
1698 #[test]
1703 fn the_reveal_draws_the_gradient_on_rather_than_re_tinting_it() {
1704 let full = curve(SAMPLES, 1.0, 0.5);
1705 let half = curve(SAMPLES, 0.5, 0.5);
1706 assert!(
1707 !half.is_empty() && half.len() < full.len(),
1708 "the probe must actually reveal a prefix"
1709 );
1710 for (k, seg) in half.iter().enumerate() {
1711 assert_eq!(
1712 seg.color, full[k].color,
1713 "chord {k} changed colour when the reveal shortened"
1714 );
1715 }
1716 assert_ne!(
1718 half[half.len() - 1].color,
1719 full[full.len() - 1].color,
1720 "a half-drawn curve must not already show the ramp's far end"
1721 );
1722 }
1723
1724 #[test]
1733 fn a_full_hue_axis_colours_a_knot_by_depth_alone() {
1734 let arm = curves::arm3d(CurveFamily::TorusKnot).unwrap_or_else(|| panic!("a space family"));
1735 let samples = 480;
1736 let params = curves::CurveParams {
1737 n: 2.0,
1738 d: 3.0,
1739 phase: 0.0,
1740 radial_offset: 0.0,
1741 samples,
1742 scale: 0.9,
1743 rotation: 0.0,
1744 draw_progress: 1.0,
1745 color: [0.0; 3],
1746 width: 0.0,
1747 levers: curves::Levers {
1748 tube: 0.35,
1749 ..curves::Levers::default()
1750 },
1751 };
1752 let mut points = Vec::new();
1753 let closed = (arm.sample)(¶ms, &mut points);
1754 assert!(closed && points.len() > 100, "a closed knot walk");
1755 let span = samples.saturating_sub(1).max(1) as f32;
1756 let frame_at = |distance: f32| {
1757 CameraParams {
1758 yaw: 0.4,
1759 pitch: 0.7,
1760 distance,
1761 ..CameraParams::default()
1762 }
1763 .frame(
1764 16.0 / 9.0,
1765 1.0,
1766 [0.0, 0.0],
1767 (1280, 720),
1768 (arm.extent)(¶ms),
1769 16.0,
1770 )
1771 };
1772 let emitted = |frame: &camera::CameraFrame, hue_axis: f32| {
1773 let mut out = Vec::new();
1774 space_chords(frame, &points, closed, span, hue_axis, |c| out.push(c));
1775 out
1776 };
1777 let depth_of = |frame: &camera::CameraFrame, c: &SpaceChord| {
1779 let mid = [
1780 0.5 * (c.a[0] + c.b[0]),
1781 0.5 * (c.a[1] + c.b[1]),
1782 0.5 * (c.a[2] + c.b[2]),
1783 ];
1784 frame.volume_depth(frame.view.depth(mid))
1785 };
1786
1787 let frame = frame_at(3.2);
1788 let by_path = emitted(&frame, 0.0);
1789 let by_depth = emitted(&frame, 1.0);
1790 assert_eq!(by_path.len(), points.len(), "every chord is drawn");
1791 for c in &by_path {
1792 assert_eq!(
1793 c.u.to_bits(),
1794 (c.k as f32 / span).to_bits(),
1795 "chord {}",
1796 c.k
1797 );
1798 }
1799 for c in &by_depth {
1800 assert_eq!(
1801 c.u.to_bits(),
1802 depth_of(&frame, c).to_bits(),
1803 "chord {} is coloured by its own depth",
1804 c.k
1805 );
1806 }
1807 let nearest = by_depth
1810 .iter()
1811 .min_by(|x, y| frame.view.depth(x.a).total_cmp(&frame.view.depth(y.a)))
1812 .unwrap_or_else(|| panic!("chords"));
1813 let farthest = by_depth
1814 .iter()
1815 .max_by(|x, y| frame.view.depth(x.a).total_cmp(&frame.view.depth(y.a)))
1816 .unwrap_or_else(|| panic!("chords"));
1817 assert!(
1818 nearest.u < 0.2 && farthest.u > 0.8,
1819 "near reads low and far reads high: {} .. {}",
1820 nearest.u,
1821 farthest.u
1822 );
1823 for (i, ci) in by_depth.iter().enumerate() {
1824 for cj in &by_depth[i + 1..] {
1825 let (di, dj) = (depth_of(&frame, ci), depth_of(&frame, cj));
1826 assert_eq!(
1827 di == dj,
1828 ci.u == cj.u,
1829 "depths {di} and {dj} took {} and {}",
1830 ci.u,
1831 cj.u
1832 );
1833 }
1834 }
1835 let (lo, hi) = by_depth
1839 .iter()
1840 .fold((1.0f32, 0.0f32), |(lo, hi), c| (lo.min(c.u), hi.max(c.u)));
1841 assert!(
1842 hi - lo > 0.5,
1843 "the knot spans the volume's depth: {lo}..{hi}"
1844 );
1845 let n = by_depth.len();
1846 let mut pair = (0, 0, f32::INFINITY);
1847 for i in 0..n {
1848 for j in (i + n / 4)..(i + 3 * n / 4).min(n) {
1850 let gap = (by_depth[i].u - by_depth[j].u).abs();
1851 if gap < pair.2 {
1852 pair = (i, j, gap);
1853 }
1854 }
1855 }
1856 let (i, j, gap) = pair;
1857 assert!(
1858 gap < 1e-3,
1859 "two chords a quarter-walk apart share a depth: {gap}"
1860 );
1861 assert!(
1862 (by_path[i].u - by_path[j].u).abs() > 0.2,
1863 "the path axis colours them apart"
1864 );
1865
1866 let (sy, cy) = 0.4f32.sin_cos();
1872 let (sp, cp) = 0.7f32.sin_cos();
1873 let toward_eye = [cp * sy, sp, cp * cy];
1874 let along_axis = |t: f32| toward_eye.map(|v| v * t);
1875 let (pa, pb) = (along_axis(-1.0), along_axis(frame.view.distance + 1.0));
1876 let mut cut = Vec::new();
1877 space_chords(&frame, &[pa, pb], false, 1.0, 1.0, |c| cut.push(c));
1878 let [c] = cut.as_slice() else {
1879 panic!("the axial chord is drawn: {cut:?}");
1880 };
1881 assert!(
1882 c.a == pa && c.b != pb,
1883 "the near plane cuts the chord's far end"
1884 );
1885 assert_eq!(c.u.to_bits(), depth_of(&frame, c).to_bits());
1886 let unclipped = frame.volume_depth(frame.view.depth([
1887 0.5 * (pa[0] + pb[0]),
1888 0.5 * (pa[1] + pb[1]),
1889 0.5 * (pa[2] + pb[2]),
1890 ]));
1891 assert!(
1892 (c.u - unclipped).abs() > 0.05,
1893 "the clipped chord reads a different depth from the unclipped one: {} vs {unclipped}",
1894 c.u
1895 );
1896 }
1897
1898 #[test]
1902 fn a_degenerate_sample_count_leaves_the_figure_flat() {
1903 for samples in [0usize, 1, 2] {
1904 let out = curve(samples, 1.0, 0.9);
1905 for seg in &out {
1906 assert!(
1907 seg.color.iter().all(|c| c.is_finite()),
1908 "samples = {samples} produced a non-finite colour"
1909 );
1910 }
1911 }
1912 }
1913}