1#![deny(
53 clippy::unwrap_used,
54 clippy::expect_used,
55 clippy::indexing_slicing,
56 clippy::panic,
57 clippy::unreachable
58)]
59
60use std::cell::RefCell;
61use std::rc::Rc;
62
63use super::super::Scene;
64use super::super::common;
65use super::renderer::{
66 LineRenderer, Segment3dInstance, SegmentInstance, StrokeMetric, joined_chord,
67};
68use super::turtle::TurtleMode;
69use super::{
70 CapOverflow, ColorRamp, GeneratorConfig, LineInstance, MAX_LSYSTEM_DEPTH, MirrorSpec,
71 OverflowContext, ViewTransform, grammar, replicate_mirror, transform_cached, turtle,
72};
73use crate::dsp::AnalysisFrame;
74use crate::render::camera::{self, CameraParams};
75use crate::render::palette::Palette;
76use crate::render::scenes::{
77 FamilyParam, FamilyRange, ParamGroup, ParamKind, ParamSpec, default_of,
78};
79
80const SPACE_RADIUS: f32 = 1.0;
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
91pub enum Growth {
92 #[default]
94 Fixed,
95 Endless,
97}
98
99impl Growth {
100 pub const ALL: [Growth; 2] = [Growth::Fixed, Growth::Endless];
102
103 pub fn as_str(self) -> &'static str {
105 match self {
106 Growth::Fixed => "fixed",
107 Growth::Endless => "endless",
108 }
109 }
110
111 pub fn from_name(name: &str) -> Option<Self> {
113 Self::ALL.into_iter().find(|mode| mode.as_str() == name)
114 }
115}
116
117pub const DEFAULT_TRAIL: u32 = 2000;
119
120const EMIT_CEILING: u64 = 1024;
124
125const SYMBOL_BUDGET: usize = 65_536;
129
130const ENDLESS_STEP: f64 = 0.025;
135
136pub const DEFAULT_FOLLOW_WINDOW: u32 = 32;
139
140const REBASE_RADIUS: f64 = 1024.0;
146
147#[derive(Debug, Clone, Copy, PartialEq)]
149struct RingSeg {
150 a: [f64; 3],
151 b: [f64; 3],
152 generation: u32,
154 joined: bool,
156 ext_a: f32,
159 ext_b: f32,
160}
161
162#[derive(Debug, Clone)]
176pub(crate) struct Endless {
177 stream: grammar::Stream,
178 pen: turtle::Pen,
179 ring: Vec<RingSeg>,
180 trail: usize,
181 head: usize,
183 phase: f64,
186 emitted: u64,
188 window: usize,
190 follow: [f64; 3],
192 follow_velocity: [f64; 3],
194 reached: u32,
197 #[cfg(test)]
200 origin: [f64; 3],
201}
202
203impl Endless {
204 pub(crate) fn new(
208 axiom: &str,
209 rules: &[(char, String)],
210 angle: f32,
211 mode: TurtleMode,
212 trail: usize,
213 window: usize,
214 ) -> Self {
215 let trail = trail.max(1);
216 let depth = grammar::STREAM_DEPTH;
217 Self {
218 stream: grammar::Stream::new(axiom, rules, depth),
219 pen: turtle::Pen::new(mode, angle, grammar::bracket_bound(axiom, rules, depth)),
220 ring: Vec::with_capacity(trail),
221 trail,
222 head: 0,
223 phase: 0.0,
224 emitted: 0,
225 window: window.max(1),
226 follow: [0.0; 3],
227 follow_velocity: [0.0; 3],
228 reached: 0,
229 #[cfg(test)]
230 origin: [0.0; 3],
231 }
232 }
233
234 pub(crate) fn follow_step(&mut self, tau: f32, dt: f32) {
246 let Some(target) = self.follow_target() else {
247 return;
248 };
249 if !(tau.is_finite() && tau > 0.0) {
250 return;
251 }
252 let w = 1.0 / f64::from(tau);
253 let dt = f64::from(dt);
254 let decay = (-w * dt).exp();
255 let mut point = self.follow;
256 for k in 0..3 {
257 let (Some(x), Some(v), Some(t)) = (
258 point.get_mut(k),
259 self.follow_velocity.get_mut(k),
260 target.get(k),
261 ) else {
262 continue;
263 };
264 let e = *x - t;
265 let c = *v + w * e;
266 *x = t + (e + c * dt) * decay;
267 *v = (*v - w * c * dt) * decay;
268 }
269 self.follow = turtle::on_grid(point);
270 }
271
272 fn follow_target(&self) -> Option<[f64; 3]> {
275 let n = self.window.min(self.ring.len());
276 if n == 0 {
277 return None;
278 }
279 let mut target = [0.0f64; 3];
280 for age in 0..n {
281 if let Some(seg) = self.at(age) {
282 for (t, (a, b)) in target.iter_mut().zip(seg.a.iter().zip(seg.b)) {
283 *t += 0.5 * (a + b);
284 }
285 }
286 }
287 for t in &mut target {
288 *t /= n as f64;
289 }
290 Some(target)
291 }
292
293 fn relative(&self, p: [f64; 3]) -> [f32; 3] {
296 let f = self.follow;
297 [
298 ((p[0] - f[0]) * ENDLESS_STEP) as f32,
299 ((p[1] - f[1]) * ENDLESS_STEP) as f32,
300 ((p[2] - f[2]) * ENDLESS_STEP) as f32,
301 ]
302 }
303
304 fn rebase(&mut self) {
308 let p = self.pen.position();
309 let origin = [p[0].round(), p[1].round(), p[2].round()];
310 let back = |q: [f64; 3]| [q[0] - origin[0], q[1] - origin[1], q[2] - origin[2]];
311 self.pen.shift(origin);
312 for seg in &mut self.ring {
313 seg.a = back(seg.a);
314 seg.b = back(seg.b);
315 }
316 self.follow = back(self.follow);
317 #[cfg(test)]
318 for (total, step) in self.origin.iter_mut().zip(origin) {
319 *total += step;
320 }
321 }
322
323 pub(crate) fn generations(&self) -> u32 {
326 u32::try_from(self.pen.stack_capacity()).unwrap_or(u32::MAX)
327 }
328
329 pub(crate) fn ramp_divisor(&self) -> u32 {
336 self.reached.max(1)
337 }
338
339 #[cfg(test)]
341 fn emitted(&self) -> u64 {
342 self.emitted
343 }
344
345 pub(crate) fn len(&self) -> usize {
347 self.ring.len()
348 }
349
350 pub(crate) fn advance(&mut self, grow: f32, dt: f32) {
355 let rate = if grow.is_finite() { grow.max(0.0) } else { 0.0 };
356 self.phase += f64::from(rate) * f64::from(dt);
357 let owed = (self.phase.floor() as u64).saturating_sub(self.emitted);
358 let mut budget = SYMBOL_BUDGET;
359 for _ in 0..owed.min(EMIT_CEILING) {
360 if !self.step(&mut budget) {
361 break;
362 }
363 }
364 if (self.phase.floor() as u64) > self.emitted {
367 self.phase = self.emitted as f64;
368 }
369 }
370
371 fn step(&mut self, budget: &mut usize) -> bool {
375 while *budget > 0 {
376 *budget -= 1;
377 let Some(ch) = self.stream.next_symbol() else {
378 self.stream.restart();
379 self.pen.clear_branches();
380 continue;
381 };
382 if let Some(drawn) = self.pen.read(ch) {
383 self.push(drawn);
384 self.emitted += 1;
385 let p = self.pen.position();
386 if p[0] * p[0] + p[1] * p[1] + p[2] * p[2] > REBASE_RADIUS * REBASE_RADIUS {
387 self.rebase();
388 }
389 return true;
390 }
391 }
392 false
393 }
394
395 fn push(&mut self, drawn: turtle::Drawn) {
397 self.reached = self.reached.max(drawn.generation);
398 let mut seg = RingSeg {
399 a: drawn.a,
400 b: drawn.b,
401 generation: drawn.generation,
402 joined: drawn.joined,
403 ext_a: 0.0,
404 ext_b: 0.0,
405 };
406 if drawn.joined
407 && let Some(prev) = self.newest_mut()
408 {
409 let at = |p: [f64; 3]| [(p[0] - prev.b[0]) as f32, (p[1] - prev.b[1]) as f32];
414 let ext = super::renderer::miter_extension(
415 super::PLACEHOLDER_WIDTH,
416 at(prev.a),
417 at(prev.b),
418 at(drawn.b),
419 );
420 prev.ext_b = ext;
421 seg.ext_a = ext;
422 }
423 if self.ring.len() < self.trail {
424 self.ring.push(seg);
425 self.head = self.ring.len() % self.trail;
426 } else if let Some(slot) = self.ring.get_mut(self.head) {
427 *slot = seg;
428 self.head = (self.head + 1) % self.trail;
429 }
430 }
431
432 fn index(&self, age: usize) -> Option<usize> {
434 (age < self.ring.len()).then(|| (self.head + self.trail - 1 - age) % self.trail)
435 }
436
437 fn at(&self, age: usize) -> Option<&RingSeg> {
439 self.index(age).and_then(|i| self.ring.get(i))
440 }
441
442 fn newest_mut(&mut self) -> Option<&mut RingSeg> {
443 self.index(0).and_then(|i| self.ring.get_mut(i))
444 }
445
446 #[cfg(test)]
449 fn capacities(&self) -> [usize; 2] {
450 [self.ring.capacity(), self.pen.stack_capacity()]
451 }
452}
453
454pub(crate) fn fade(age: usize, trail: usize, tail: f32) -> f32 {
458 let tail = if tail.is_finite() {
459 tail.clamp(0.0, 1.0)
460 } else {
461 0.0
462 };
463 let zone = tail * trail as f32;
464 if zone <= 0.0 {
465 return 1.0;
466 }
467 (trail.saturating_sub(age) as f32 / zone).min(1.0)
468}
469
470const DEFAULT_VISIBLE_DEPTH: f32 = default_of(PARAMS, "visible_depth");
471const DEFAULT_ROTATION: f32 = default_of(PARAMS, "rotation");
472const DEFAULT_HUE: f32 = 0.3;
473const DEFAULT_HUE_SPREAD: f32 = 0.0;
478const DEFAULT_DRAW_PROGRESS: f32 = 1.0;
479const DEFAULT_THICKNESS: f32 = 1.8;
480const DEFAULT_SCALE: f32 = 1.0;
481const DEFAULT_BRIGHTNESS: f32 = 1.0;
482const DEFAULT_GLOW: f32 = 1.0;
486const DEFAULT_ZOOM: f32 = 1.0;
488const DEFAULT_MIRROR_ORDER: f32 = 1.0;
490const DEFAULT_MIRROR_REFLECT: f32 = 0.0;
491const DEFAULT_GROW: f32 = default_of(PARAMS, "grow");
492const DEFAULT_TAIL: f32 = default_of(PARAMS, "tail");
493const DEFAULT_FOLLOW: f32 = default_of(PARAMS, "follow");
494
495pub struct LSystemScene {
497 renderer: Rc<RefCell<LineRenderer>>,
499 cached: Vec<Vec<SegmentInstance>>,
502 cached_depths: Vec<Vec<u32>>,
506 cached_max_depth: Vec<u32>,
510 depth_colors: Vec<[f32; 3]>,
515 draw_buf: Vec<SegmentInstance>,
518 single_buf: Vec<SegmentInstance>,
521 max_segments: usize,
527 mirror_overflow: Option<CapOverflow>,
530 overflow: Option<(u32, usize)>,
534 time: f32,
536 palette: Palette,
540 visible_depth: f32,
541 rotation: f32,
542 colour: common::PaletteParams,
544 pan: common::PanParams,
546 hue_spread: f32,
547 draw_progress: f32,
548 thickness: f32,
549 scale: f32,
550 glow: f32,
551 softness: f32,
552 stroke_blend: f32,
561 zoom: f32,
562 mirror_order: f32,
563 mirror_reflect: f32,
564
565 turtle: TurtleMode,
567 lines3d: Option<LineRenderer>,
575 device: wgpu::Device,
577 surface_format: wgpu::TextureFormat,
578 seg3d_cap: usize,
580 max_coc: f32,
582 target: (u32, u32),
584 camera: CameraParams,
586 cached3d: Vec<Vec<Segment3dInstance>>,
591 space_depth: usize,
593 space_keep: usize,
595 instances3d: Vec<Segment3dInstance>,
598
599 endless: Option<Endless>,
602 trail_overflow: Option<CapOverflow>,
604 dt: f32,
606 grow: f32,
608 tail: f32,
610 follow: f32,
612 endless_radius: f32,
615}
616
617impl LSystemScene {
618 pub fn new(
624 renderer: Rc<RefCell<LineRenderer>>,
625 max_segments: usize,
626 device: &wgpu::Device,
627 surface_format: wgpu::TextureFormat,
628 seg3d_cap: usize,
629 max_coc: f32,
630 ) -> Self {
631 Self {
632 turtle: TurtleMode::Flat,
633 lines3d: None,
634 device: device.clone(),
635 surface_format,
636 seg3d_cap,
637 max_coc,
638 target: (1, 1),
639 camera: CameraParams::default(),
640 cached3d: Vec::new(),
641 space_depth: 0,
642 space_keep: 0,
643 instances3d: Vec::new(),
644 endless: None,
645 trail_overflow: None,
646 dt: super::super::FALLBACK_DT,
647 grow: DEFAULT_GROW,
648 tail: DEFAULT_TAIL,
649 follow: DEFAULT_FOLLOW,
650 endless_radius: SPACE_RADIUS,
651 renderer,
652 cached: Vec::new(),
653 cached_depths: Vec::new(),
654 cached_max_depth: Vec::new(),
655 depth_colors: Vec::new(),
656 draw_buf: Vec::with_capacity(max_segments),
657 single_buf: Vec::with_capacity(max_segments),
658 max_segments,
659 mirror_overflow: None,
660 overflow: None,
661 time: 0.0,
662 palette: Palette::default_spectrum(),
665 visible_depth: DEFAULT_VISIBLE_DEPTH,
666 rotation: DEFAULT_ROTATION,
667 colour: common::PaletteParams::new(DEFAULT_HUE, DEFAULT_BRIGHTNESS),
668 pan: common::PanParams::default(),
669 hue_spread: DEFAULT_HUE_SPREAD,
670 draw_progress: DEFAULT_DRAW_PROGRESS,
671 thickness: DEFAULT_THICKNESS,
672 scale: DEFAULT_SCALE,
673 glow: DEFAULT_GLOW,
674 softness: super::DEFAULT_SOFTNESS,
675 stroke_blend: super::ADDITIVE_BLEND,
676 zoom: DEFAULT_ZOOM,
677 mirror_order: DEFAULT_MIRROR_ORDER,
678 mirror_reflect: DEFAULT_MIRROR_REFLECT,
679 }
680 }
681
682 fn ensure_space_renderer(&mut self) {
686 if self.lines3d.is_none() {
687 self.lines3d = Some(LineRenderer::new_3d(
688 &self.device,
689 self.surface_format,
690 self.seg3d_cap,
691 "lsystem-3d",
692 ));
693 self.instances3d.reserve_exact(self.seg3d_cap);
694 }
695 }
696
697 fn build(
700 &mut self,
701 axiom: &str,
702 rules: &[(char, String)],
703 angle_deg: f32,
704 max_depth: u32,
705 mode: TurtleMode,
706 ) {
707 self.cached.clear();
708 self.cached3d.clear();
709 self.cached_depths.clear();
710 self.cached_max_depth.clear();
711 self.overflow = None;
712 self.turtle = mode;
713 let depth = max_depth.clamp(1, MAX_LSYSTEM_DEPTH);
714 let angle = angle_deg.to_radians();
715
716 for d in 1..=depth {
717 let string = grammar::expand(axiom, rules, d);
718 let mut generations = Vec::new();
719 let dropped = match mode {
720 TurtleMode::Flat => {
721 let mut segs = Vec::new();
722 let dropped = turtle::walk_with_depths(
723 &string,
724 angle,
725 self.max_segments,
726 &mut segs,
727 &mut generations,
728 );
729 turtle::normalize_fit(&mut segs, 0.9);
730 self.cached.push(segs);
731 dropped
732 }
733 TurtleMode::Space => {
734 let mut segs = Vec::new();
735 let dropped = turtle::walk_3d_with_depths(
736 &string,
737 angle,
738 self.seg3d_cap,
739 &mut segs,
740 &mut generations,
741 );
742 turtle::sphere_fit(&mut segs, SPACE_RADIUS);
743 self.cached3d.push(segs);
744 dropped
745 }
746 };
747 if dropped > 0 && self.overflow.is_none() {
748 self.overflow = Some((d, dropped));
749 }
750 self.cached_max_depth
751 .push(generations.iter().copied().max().unwrap_or(0));
752 self.cached_depths.push(generations);
753 }
754 let generations = self
757 .cached_max_depth
758 .iter()
759 .copied()
760 .max()
761 .unwrap_or(0)
762 .saturating_add(1) as usize;
763 self.depth_colors.clear();
764 self.depth_colors.resize(generations, [0.0; 3]);
765 }
766
767 fn depth_index(&self, depths: usize) -> Option<usize> {
770 if depths == 0 {
771 return None;
772 }
773 let want = self.visible_depth.max(1.0) as usize;
774 Some(want.min(depths).saturating_sub(1))
775 }
776
777 fn ramp(&self) -> ColorRamp {
779 ColorRamp {
780 hue: self.colour.hue,
781 hue_spread: self.hue_spread,
782 palette_mix: self.colour.mix,
783 palette_steps: self.colour.steps,
784 saturation: self.colour.saturation,
785 brightness: self.colour.brightness,
786 }
787 }
788
789 fn update_space(&mut self) {
794 self.draw_buf.clear();
795 self.mirror_overflow = None;
796 let Some(idx) = self.depth_index(self.cached3d.len()) else {
797 self.space_keep = 0;
798 return;
799 };
800 let ramp = self.ramp();
801 fill_depth_colors(
802 &mut self.depth_colors,
803 &self.palette,
804 ramp,
805 self.cached_max_depth.get(idx).copied().unwrap_or(0),
806 );
807 let len = self.cached3d.get(idx).map_or(0, Vec::len);
808 self.space_keep = ((len as f32) * self.draw_progress.clamp(0.0, 1.0)).round() as usize;
810 self.space_depth = idx;
811 }
812
813 fn render_space(
818 &mut self,
819 queue: &wgpu::Queue,
820 encoder: &mut wgpu::CommandEncoder,
821 view: &wgpu::TextureView,
822 aspect: f32,
823 ) {
824 let frame = self.camera.frame(
826 aspect,
827 self.zoom,
828 [self.pan.x, self.pan.y],
829 self.target,
830 SPACE_RADIUS,
831 self.max_coc,
832 );
833 if self.mirror_overflow.is_none() {
834 self.mirror_overflow = frame.blur;
835 }
836 let width = if self.thickness.is_finite() {
837 self.thickness.max(0.0)
838 } else {
839 DEFAULT_THICKNESS
840 };
841 let trunk = self.depth_colors.first().copied().unwrap_or([1.0; 3]);
842 let base = self
843 .cached3d
844 .get(self.space_depth)
845 .map_or(&[][..], Vec::as_slice);
846 let generations = self
847 .cached_depths
848 .get(self.space_depth)
849 .map_or(&[][..], Vec::as_slice);
850 let keep = self.space_keep.min(base.len());
851
852 let mut instances = std::mem::take(&mut self.instances3d);
853 instances.clear();
854 for (i, seg) in base.iter().take(keep).enumerate() {
855 let before = (seg.prev != seg.a).then_some(seg.prev);
859 let after = (seg.next != seg.b && i + 1 < keep).then_some(seg.next);
860 let Some([prev, a, b, next]) = joined_chord(&frame.view, before, seg.a, seg.b, after)
861 else {
862 continue;
863 };
864 if frame.view.outside(a, b, frame.margin) {
865 continue;
866 }
867 let color = generations
868 .get(i)
869 .and_then(|&g| self.depth_colors.get(g as usize))
870 .copied()
871 .unwrap_or(trunk);
872 instances.push(Segment3dInstance {
873 a,
874 b,
875 color,
876 width,
877 alpha: 1.0,
878 prev,
879 next,
880 skirt: 0.0,
881 });
882 }
883 if let Some(lines3d) = self.lines3d.as_mut() {
884 lines3d.draw_3d(
885 queue,
886 encoder,
887 view,
888 &frame,
889 self.glow,
890 self.softness,
891 &instances,
892 );
893 }
894 self.instances3d = instances;
895 }
896
897 fn configure_endless(
901 &mut self,
902 axiom: &str,
903 rules: &[(char, String)],
904 angle_deg: f32,
905 mode: TurtleMode,
906 trail: u32,
907 window: u32,
908 ) {
909 self.cached.clear();
910 self.cached3d.clear();
911 self.cached_depths.clear();
912 self.cached_max_depth.clear();
913 self.overflow = None;
914 self.turtle = mode;
915 let cap = match mode {
916 TurtleMode::Flat => self.max_segments,
917 TurtleMode::Space => self.seg3d_cap,
918 };
919 let asked = trail as usize;
920 self.trail_overflow = (asked > cap).then(|| CapOverflow {
921 dropped: asked - cap,
922 context: OverflowContext::Trail {
923 asked: trail,
924 space: mode == TurtleMode::Space,
925 },
926 cap,
927 });
928 let endless = Endless::new(
929 axiom,
930 rules,
931 angle_deg.to_radians(),
932 mode,
933 asked.min(cap),
934 window as usize,
935 );
936 self.depth_colors.clear();
939 self.depth_colors
940 .resize(endless.generations() as usize + 1, [0.0; 3]);
941 self.endless = Some(endless);
942 }
943
944 fn update_endless(&mut self) {
949 self.mirror_overflow = None;
950 let ramp = self.ramp();
951 let Some(endless) = self.endless.as_mut() else {
952 self.draw_buf.clear();
953 return;
954 };
955 endless.advance(self.grow, self.dt);
956 endless.follow_step(self.follow, self.dt);
957 fill_depth_colors(
958 &mut self.depth_colors,
959 &self.palette,
960 ramp,
961 endless.ramp_divisor(),
962 );
963 self.lay_endless();
964 }
965
966 fn lay_endless(&mut self) {
972 self.draw_buf.clear();
973 let Some(endless) = self.endless.as_ref() else {
974 return;
975 };
976 let len = endless.len();
977 let trunk = self.depth_colors.first().copied().unwrap_or([1.0; 3]);
978
979 if self.turtle == TurtleMode::Space {
980 let mut reach = 0.0f32;
981 for age in 0..len {
982 if let Some(seg) = endless.at(age) {
983 for p in [seg.a, seg.b] {
984 let w = endless.relative(p);
985 reach = reach.max((w[0] * w[0] + w[1] * w[1] + w[2] * w[2]).sqrt());
986 }
987 }
988 }
989 self.endless_radius = reach.max(ENDLESS_STEP as f32);
990 return;
991 }
992
993 let width = super::half_width(self.thickness);
994 let (sin, cos) = self.rotation.sin_cos();
995 for age in (0..len).rev() {
996 let Some(seg) = endless.at(age) else {
997 continue;
998 };
999 let ext_a = if age + 1 < len { seg.ext_a } else { 0.0 };
1002 let (a, b) = (endless.relative(seg.a), endless.relative(seg.b));
1003 let flat = SegmentInstance {
1004 a: [a[0], a[1]],
1005 b: [b[0], b[1]],
1006 color: [1.0; 3],
1007 width: super::PLACEHOLDER_WIDTH,
1008 alpha: 1.0,
1009 ext_a,
1010 ext_b: seg.ext_b,
1011 };
1012 let color = self
1013 .depth_colors
1014 .get(seg.generation as usize)
1015 .copied()
1016 .unwrap_or(trunk);
1017 let mut instance = flat
1018 .rotate_scale(sin, cos, self.rotation, self.scale)
1019 .styled(color, width);
1020 instance.alpha = fade(age, endless.trail, self.tail);
1021 self.draw_buf.push(instance);
1022 }
1023 }
1024
1025 fn render_endless_space(
1029 &mut self,
1030 queue: &wgpu::Queue,
1031 encoder: &mut wgpu::CommandEncoder,
1032 view: &wgpu::TextureView,
1033 aspect: f32,
1034 ) {
1035 let frame = self.camera.frame(
1036 aspect,
1037 self.zoom,
1038 [self.pan.x, self.pan.y],
1039 self.target,
1040 self.endless_radius,
1041 self.max_coc,
1042 );
1043 if self.mirror_overflow.is_none() {
1044 self.mirror_overflow = frame.blur;
1045 }
1046 self.lay_endless_space(&frame);
1047 if let Some(lines3d) = self.lines3d.as_mut() {
1048 lines3d.draw_3d(
1049 queue,
1050 encoder,
1051 view,
1052 &frame,
1053 self.glow,
1054 self.softness,
1055 &self.instances3d,
1056 );
1057 }
1058 }
1059
1060 fn lay_endless_space(&mut self, frame: &camera::CameraFrame) {
1065 let width = if self.thickness.is_finite() {
1066 self.thickness.max(0.0)
1067 } else {
1068 DEFAULT_THICKNESS
1069 };
1070 let trunk = self.depth_colors.first().copied().unwrap_or([1.0; 3]);
1071 let mut instances = std::mem::take(&mut self.instances3d);
1072 instances.clear();
1073 if let Some(endless) = self.endless.as_ref() {
1074 let len = endless.len();
1075 for age in (0..len).rev() {
1076 let Some(seg) = endless.at(age) else {
1077 continue;
1078 };
1079 let before = if seg.joined && age + 1 < len {
1080 endless.at(age + 1).map(|p| endless.relative(p.a))
1081 } else {
1082 None
1083 };
1084 let after = match age.checked_sub(1).and_then(|n| endless.at(n)) {
1085 Some(next) if next.joined => Some(endless.relative(next.b)),
1086 _ => None,
1087 };
1088 let Some([prev, a, b, next]) = joined_chord(
1089 &frame.view,
1090 before,
1091 endless.relative(seg.a),
1092 endless.relative(seg.b),
1093 after,
1094 ) else {
1095 continue;
1096 };
1097 if frame.view.outside(a, b, frame.margin) {
1098 continue;
1099 }
1100 instances.push(Segment3dInstance {
1101 a,
1102 b,
1103 color: self
1104 .depth_colors
1105 .get(seg.generation as usize)
1106 .copied()
1107 .unwrap_or(trunk),
1108 width,
1109 alpha: fade(age, endless.trail, self.tail),
1110 prev,
1111 next,
1112 skirt: 0.0,
1113 });
1114 }
1115 }
1116 self.instances3d = instances;
1117 }
1118}
1119
1120pub(crate) fn fill_depth_colors(
1131 out: &mut [[f32; 3]],
1132 palette: &Palette,
1133 ramp: ColorRamp,
1134 generations: u32,
1135) {
1136 let span = generations.max(1) as f32;
1137 for (generation, slot) in out.iter_mut().enumerate() {
1138 *slot = ramp.at(palette, generation as f32 / span);
1139 }
1140}
1141
1142pub(crate) fn apply_depth_colors(
1150 segs: &mut [SegmentInstance],
1151 generations: &[u32],
1152 colors: &[[f32; 3]],
1153) {
1154 for (seg, &generation) in segs.iter_mut().zip(generations) {
1155 if let Some(&color) = colors.get(generation as usize) {
1156 seg.color = color;
1157 }
1158 }
1159}
1160
1161pub const PARAMS: &[ParamSpec] = &[
1164 VISIBLE_DEPTH,
1165 ROTATION,
1166 crate::render::scenes::common::hue(DEFAULT_HUE),
1167 crate::render::scenes::lines::hue_spread(DEFAULT_HUE_SPREAD),
1168 crate::render::scenes::common::SATURATION,
1169 crate::render::scenes::common::PALETTE_MIX,
1170 crate::render::scenes::common::PALETTE_STEPS,
1171 crate::render::scenes::common::PALETTE_CONTOUR,
1172 DRAW_PROGRESS,
1173 THICKNESS,
1174 SCALE,
1175 crate::render::scenes::common::brightness(DEFAULT_BRIGHTNESS),
1176 crate::render::scenes::lines::GLOW,
1177 crate::render::scenes::lines::SOFTNESS,
1178 crate::render::scenes::common::zoom(DEFAULT_ZOOM),
1179 crate::render::scenes::common::PAN_X,
1180 crate::render::scenes::common::PAN_Y,
1181 STROKE_BLEND,
1182 MIRROR_ORDER,
1183 MIRROR_REFLECT,
1184 YAW,
1185 PITCH,
1186 DISTANCE,
1187 FOV,
1188 FOCUS,
1189 APERTURE,
1190 FOG,
1191 SOLID,
1192 GROW,
1193 TAIL,
1194 FOLLOW,
1195];
1196
1197const VISIBLE_DEPTH: ParamSpec = ParamSpec {
1198 name: "visible_depth",
1199 default: 1.0,
1200 range: Some([1.0, 7.0]),
1201 doc: "Which recursion generation is drawn, counted from 1 and capped at `max_depth`; a \
1202 fraction floors to the generation below it, and anything under 2 draws the first.",
1203 kind: ParamKind::Modal,
1204 group: ParamGroup::Shape,
1205 main: true,
1206};
1207const GROW: ParamSpec = ParamSpec {
1208 name: "grow",
1209 default: 20.0,
1210 range: Some([0.0, 120.0]),
1211 doc: "How fast an endless figure grows, in draw steps a second; bind it to onset to make \
1212 it surge.",
1213 kind: ParamKind::Modal,
1214 group: ParamGroup::Motion,
1215 main: true,
1216};
1217const TAIL: ParamSpec = ParamSpec {
1218 name: "tail",
1219 default: 0.5,
1220 range: Some([0.0, 1.0]),
1221 doc: "The oldest fraction of an endless figure's trail that fades out; 0 keeps every \
1222 segment at full light until it is dropped.",
1223 kind: ParamKind::Modal,
1224 group: ParamGroup::Light,
1225 main: false,
1226};
1227const FOLLOW: ParamSpec = ParamSpec {
1228 name: "follow",
1229 default: 0.3,
1230 range: Some([0.05, 3.0]),
1231 doc: "How slowly the view follows an endless figure's growing tip, in seconds: short \
1232 keeps the tip near the centre, long lets it lead and glides after a branch's jump.",
1233 kind: ParamKind::Modal,
1234 group: ParamGroup::Motion,
1235 main: false,
1236};
1237
1238const DRAW_PROGRESS: ParamSpec = ParamSpec {
1244 doc: "How much of a fixed L-system is drawn, from its first segment: 0 none, 1 all of it.",
1245 ..crate::render::scenes::lines::DRAW_PROGRESS
1246};
1247const ROTATION: ParamSpec = ParamSpec {
1248 name: "rotation",
1249 default: 0.0,
1250 range: Some([0.0, std::f32::consts::TAU]),
1251 doc: "Turns a flat figure in its plane, in radians.",
1252 kind: ParamKind::Modal,
1253 group: ParamGroup::Motion,
1254 main: false,
1255};
1256const THICKNESS: ParamSpec = ParamSpec {
1257 doc: "Stroke width: on a flat figure in the shared line units, on a space figure in pixels \
1258 at the focal plane, wider nearer and narrower farther.",
1259 ..crate::render::scenes::lines::thickness(DEFAULT_THICKNESS)
1260};
1261const SCALE: ParamSpec = ParamSpec {
1262 doc: "Scales a flat figure about its centre; a space figure is sized by the camera's \
1263 distance instead.",
1264 ..crate::render::scenes::lines::scale(DEFAULT_SCALE)
1265};
1266const STROKE_BLEND: ParamSpec = ParamSpec {
1267 doc: "Moves a flat L-system's stroke from additive light toward opaque paint, so \
1268 crossing branches stop brightening.",
1269 ..crate::render::scenes::lines::STROKE_BLEND
1270};
1271const MIRROR_ORDER: ParamSpec = ParamSpec {
1272 doc: "Repeats a flat L-system this many times around the centre; 1 draws it once.",
1273 ..crate::render::scenes::lines::MIRROR_ORDER
1274};
1275const MIRROR_REFLECT: ParamSpec = ParamSpec {
1276 doc: "Alternates a flat L-system's repeats into mirror images rather than plain rotations.",
1277 ..crate::render::scenes::lines::MIRROR_REFLECT
1278};
1279const YAW: ParamSpec = ParamSpec {
1280 doc: "Turns the camera around a space tree, in radians; bind it to a slow clock to orbit.",
1281 ..camera::YAW
1282};
1283const PITCH: ParamSpec = ParamSpec {
1284 doc: "Raises the camera above a space tree, in radians; negative looks up from below.",
1285 ..camera::PITCH
1286};
1287const DISTANCE: ParamSpec = ParamSpec {
1288 doc: "How far the camera sits from a space tree's centre; nearer makes the tree larger \
1289 and exaggerates the perspective.",
1290 ..camera::DISTANCE
1291};
1292const FOV: ParamSpec = ParamSpec {
1293 doc: "The camera's vertical field of view onto a space tree, in radians; zoom divides it.",
1294 ..camera::FOV
1295};
1296const FOCUS: ParamSpec = ParamSpec {
1297 doc: "Where the focal plane sits in a space tree's depth: 0 at its nearest point, 1 at its \
1298 farthest.",
1299 ..camera::FOCUS
1300};
1301const APERTURE: ParamSpec = ParamSpec {
1302 doc: "The blur of a space tree's far side, in pixels; branches nearer than the focal plane \
1303 blur more, up to the tier's cap. 0 keeps every branch sharp, and wider costs fill.",
1304 ..camera::APERTURE
1305};
1306const FOG: ParamSpec = ParamSpec {
1307 doc: "Fades a space tree toward black with depth: at 1 its farthest point is black and its \
1308 nearest keeps its light. 0 is off.",
1309 ..camera::FOG
1310};
1311const SOLID: ParamSpec = ParamSpec {
1312 doc: "1 paints a space tree's near branches over its far ones, so it reads as an object and \
1313 crossings stop brightening; 0 is the additive glow. Solid sorts every segment by \
1314 depth each frame.",
1315 ..camera::SOLID
1316};
1317
1318pub const MODES: [&str; 4] = ["flat", "space", "flat endless", "space endless"];
1321
1322macro_rules! per_mode {
1325 ($name:expr; $flat:expr, $space:expr, $flat_endless:expr, $space_endless:expr $(,)?) => {
1326 FamilyParam {
1327 name: $name,
1328 ranges: &[
1329 FamilyRange {
1330 family: MODES[0],
1331 range: $flat,
1332 },
1333 FamilyRange {
1334 family: MODES[1],
1335 range: $space,
1336 },
1337 FamilyRange {
1338 family: MODES[2],
1339 range: $flat_endless,
1340 },
1341 FamilyRange {
1342 family: MODES[3],
1343 range: $space_endless,
1344 },
1345 ],
1346 }
1347 };
1348}
1349
1350macro_rules! plane {
1353 ($spec:expr) => {
1354 per_mode!($spec.name; $spec.range, None, $spec.range, None)
1355 };
1356}
1357
1358macro_rules! flat_fixed {
1361 ($spec:expr) => {
1362 per_mode!($spec.name; $spec.range, None, None, None)
1363 };
1364}
1365
1366macro_rules! in_space {
1368 ($spec:expr) => {
1369 per_mode!($spec.name; None, $spec.range, None, $spec.range)
1370 };
1371}
1372
1373macro_rules! fixed {
1375 ($spec:expr) => {
1376 per_mode!($spec.name; $spec.range, $spec.range, None, None)
1377 };
1378}
1379
1380macro_rules! endless {
1382 ($spec:expr) => {
1383 per_mode!($spec.name; None, None, $spec.range, $spec.range)
1384 };
1385}
1386
1387pub const FAMILY_PARAMS: &[FamilyParam] = &[
1391 fixed!(VISIBLE_DEPTH),
1392 fixed!(DRAW_PROGRESS),
1393 plane!(ROTATION),
1394 plane!(SCALE),
1395 plane!(STROKE_BLEND),
1396 flat_fixed!(MIRROR_ORDER),
1397 flat_fixed!(MIRROR_REFLECT),
1398 in_space!(YAW),
1399 in_space!(PITCH),
1400 in_space!(DISTANCE),
1401 in_space!(FOV),
1402 in_space!(FOCUS),
1403 in_space!(APERTURE),
1404 in_space!(FOG),
1405 in_space!(SOLID),
1406 endless!(GROW),
1407 endless!(TAIL),
1408 endless!(FOLLOW),
1409];
1410
1411impl Scene for LSystemScene {
1412 fn name(&self) -> &'static str {
1413 "l-system"
1414 }
1415
1416 fn set_time(&mut self, time: f32) {
1417 self.time = time;
1418 }
1419
1420 fn advance(&mut self, dt: f32) {
1421 self.dt = dt;
1424 }
1425
1426 fn reset_params(&mut self) {
1427 self.visible_depth = DEFAULT_VISIBLE_DEPTH;
1428 self.rotation = DEFAULT_ROTATION;
1429 self.colour.reset();
1430 self.pan.reset();
1431 self.hue_spread = DEFAULT_HUE_SPREAD;
1432 self.draw_progress = DEFAULT_DRAW_PROGRESS;
1433 self.thickness = DEFAULT_THICKNESS;
1434 self.scale = DEFAULT_SCALE;
1435 self.glow = DEFAULT_GLOW;
1436 self.softness = super::DEFAULT_SOFTNESS;
1437 self.stroke_blend = super::ADDITIVE_BLEND;
1438 self.zoom = DEFAULT_ZOOM;
1439 self.mirror_order = DEFAULT_MIRROR_ORDER;
1440 self.mirror_reflect = DEFAULT_MIRROR_REFLECT;
1441 self.camera.reset();
1442 self.grow = DEFAULT_GROW;
1443 self.tail = DEFAULT_TAIL;
1444 self.follow = DEFAULT_FOLLOW;
1445 }
1446
1447 fn set_target_size(&mut self, width: u32, height: u32) {
1448 self.target = (width, height);
1449 }
1450
1451 fn set_param(&mut self, name: &str, value: f32) {
1452 if self.colour.set(name, value) || self.pan.set(name, value) || self.camera.set(name, value)
1455 {
1456 return;
1457 }
1458 match name {
1459 "visible_depth" => self.visible_depth = value,
1460 "rotation" => self.rotation = value,
1461 "hue_spread" => self.hue_spread = value,
1462 "draw_progress" => self.draw_progress = value,
1463 "thickness" => self.thickness = value,
1464 "scale" => self.scale = value,
1465 "glow" => self.glow = value,
1466 "softness" => self.softness = value,
1467 "zoom" => self.zoom = value,
1468 "stroke_blend" => self.stroke_blend = value,
1469 "mirror_order" => self.mirror_order = value,
1470 "mirror_reflect" => self.mirror_reflect = value,
1471 "grow" => self.grow = value,
1472 "tail" => self.tail = value,
1473 "follow" => self.follow = value,
1474 _ => {}
1475 }
1476 }
1477
1478 fn set_palette(&mut self, palette: &Palette) {
1479 self.palette = palette.clone();
1480 }
1481
1482 fn configure(&mut self, cfg: &GeneratorConfig) -> Option<CapOverflow> {
1483 if let GeneratorConfig::LSystem {
1489 axiom,
1490 rules,
1491 angle_deg,
1492 max_depth,
1493 seed: _,
1494 turtle,
1495 growth,
1496 trail,
1497 follow_window,
1498 } = cfg
1499 {
1500 self.endless = None;
1501 self.trail_overflow = None;
1502 if *turtle == TurtleMode::Space {
1503 self.ensure_space_renderer();
1504 }
1505 match growth {
1506 Growth::Fixed => self.build(axiom, rules, *angle_deg, *max_depth, *turtle),
1507 Growth::Endless => {
1508 self.configure_endless(
1509 axiom,
1510 rules,
1511 *angle_deg,
1512 *turtle,
1513 *trail,
1514 *follow_window,
1515 );
1516 return self.trail_overflow;
1517 }
1518 }
1519 }
1520 let cap = match self.turtle {
1524 TurtleMode::Flat => self.max_segments,
1525 TurtleMode::Space => self.seg3d_cap,
1526 };
1527 self.overflow.map(|(depth, dropped)| CapOverflow {
1528 dropped,
1529 context: OverflowContext::Depth(depth),
1530 cap,
1531 })
1532 }
1533
1534 fn mirror_overflow(&self) -> Option<&CapOverflow> {
1535 self.mirror_overflow.as_ref()
1536 }
1537
1538 fn update(&mut self, _frame: &AnalysisFrame) {
1539 if self.endless.is_some() {
1540 self.update_endless();
1541 return;
1542 }
1543 if self.turtle == TurtleMode::Space {
1544 self.update_space();
1545 return;
1546 }
1547 let Some(idx) = self.depth_index(self.cached.len()) else {
1549 self.draw_buf.clear();
1550 return;
1551 };
1552 let Some(base) = self.cached.get(idx) else {
1553 self.draw_buf.clear();
1554 return;
1555 };
1556
1557 fill_depth_colors(
1565 &mut self.depth_colors,
1566 &self.palette,
1567 ColorRamp {
1568 hue: self.colour.hue,
1569 hue_spread: self.hue_spread,
1570 palette_mix: self.colour.mix,
1571 palette_steps: self.colour.steps,
1572 saturation: self.colour.saturation,
1573 brightness: self.colour.brightness,
1574 },
1575 self.cached_max_depth.get(idx).copied().unwrap_or(0),
1576 );
1577 let trunk = self.depth_colors.first().copied().unwrap_or([1.0; 3]);
1578
1579 let width = super::half_width(self.thickness);
1580 transform_cached(
1581 base,
1582 self.rotation,
1583 self.scale,
1584 trunk,
1585 width,
1586 self.draw_progress,
1587 &mut self.single_buf,
1588 );
1589 if let Some(generations) = self.cached_depths.get(idx) {
1590 apply_depth_colors(&mut self.single_buf, generations, &self.depth_colors);
1591 }
1592 let mirror = MirrorSpec::from_params(self.mirror_order, self.mirror_reflect);
1599 if mirror.is_identity() {
1600 debug_assert!(
1601 self.single_buf.len() <= self.max_segments,
1602 "the cached base is capped at load, so identity cannot truncate"
1603 );
1604 std::mem::swap(&mut self.single_buf, &mut self.draw_buf);
1605 self.mirror_overflow = None;
1606 return;
1607 }
1608 let dropped = replicate_mirror(
1609 &self.single_buf,
1610 mirror,
1611 self.max_segments,
1612 &mut self.draw_buf,
1613 );
1614 self.mirror_overflow = (dropped > 0).then_some(CapOverflow {
1615 dropped,
1616 context: OverflowContext::Mirror(mirror.order),
1617 cap: self.max_segments,
1618 });
1619 }
1620
1621 fn render(
1622 &mut self,
1623 queue: &wgpu::Queue,
1624 encoder: &mut wgpu::CommandEncoder,
1625 view: &wgpu::TextureView,
1626 aspect: f32,
1627 ) {
1628 if self.turtle == TurtleMode::Space {
1629 if self.endless.is_some() {
1630 self.render_endless_space(queue, encoder, view, aspect);
1631 } else {
1632 self.render_space(queue, encoder, view, aspect);
1633 }
1634 return;
1635 }
1636 let xform = ViewTransform {
1637 zoom: self.zoom,
1638 pan: [self.pan.x, self.pan.y],
1639 _pad: 0.0,
1640 };
1641 let mut renderer = self.renderer.borrow_mut();
1642 if self.stroke_blend >= super::OPAQUE_BLEND {
1643 renderer.draw_opaque(
1644 queue,
1645 encoder,
1646 view,
1647 aspect,
1648 self.glow,
1649 self.softness,
1650 StrokeMetric::World,
1651 xform,
1652 &self.draw_buf,
1653 &[],
1654 );
1655 } else {
1656 renderer.draw(
1657 queue,
1658 encoder,
1659 view,
1660 aspect,
1661 self.glow,
1662 self.softness,
1663 StrokeMetric::World,
1664 xform,
1665 &self.draw_buf,
1666 );
1667 }
1668 }
1669}
1670
1671#[cfg(test)]
1672mod tests {
1673 #![allow(clippy::indexing_slicing)]
1674
1675 use super::*;
1676
1677 const CAP: usize = crate::render::TierConfig::FLOOR.max_segments;
1681
1682 #[test]
1686 fn per_frame_transform_does_not_allocate() {
1687 let mut base = Vec::with_capacity(64);
1688 turtle::walk("F+F+F+F+F[-F]F", 0.5, CAP, &mut base);
1689 turtle::normalize_fit(&mut base, 0.9);
1690
1691 let mut out = Vec::with_capacity(CAP);
1692 let cap = out.capacity();
1693 for frame in 0..16 {
1694 let rotation = frame as f32 * 0.05;
1695 transform_cached(&base, rotation, 1.0, [0.5; 3], 0.01, 1.0, &mut out);
1696 }
1697 assert_eq!(out.capacity(), cap, "per-frame transform reused the buffer");
1698 assert_eq!(out.len(), base.len(), "full progress draws every segment");
1699 }
1700
1701 fn figure(string: &str) -> (Vec<SegmentInstance>, Vec<u32>, u32) {
1704 let mut segs = Vec::new();
1705 let mut generations = Vec::new();
1706 turtle::walk_with_depths(string, 0.4, CAP, &mut segs, &mut generations);
1707 let deepest = generations.iter().copied().max().unwrap_or(0);
1708 (segs, generations, deepest)
1709 }
1710
1711 fn coloured(string: &str, hue_spread: f32) -> (Vec<SegmentInstance>, Vec<u32>) {
1713 let (mut segs, generations, deepest) = figure(string);
1714 let mut colors = vec![[0.0; 3]; deepest as usize + 1];
1715 fill_depth_colors(
1716 &mut colors,
1717 &Palette::default_spectrum(),
1718 ColorRamp {
1719 hue: DEFAULT_HUE,
1720 hue_spread,
1721 palette_mix: common::DEFAULT_PALETTE_MIX,
1722 palette_steps: crate::render::palette::DEFAULT_PALETTE_STEPS,
1723 saturation: common::DEFAULT_SATURATION,
1724 brightness: DEFAULT_BRIGHTNESS,
1725 },
1726 deepest,
1727 );
1728 apply_depth_colors(&mut segs, &generations, &colors);
1729 (segs, generations)
1730 }
1731
1732 #[test]
1737 fn the_spread_colours_by_generation_and_not_by_traversal_order() {
1738 let string = "F[+F]FF[+F[-F]F]F";
1741 let (segs, generations) = coloured(string, 0.6);
1742 assert!(
1743 generations.iter().copied().max().unwrap_or(0) >= 2,
1744 "the probe must actually branch twice, or this proves nothing"
1745 );
1746
1747 for (i, a) in segs.iter().enumerate() {
1749 for (j, b) in segs.iter().enumerate() {
1750 if generations[i] == generations[j] {
1751 assert_eq!(
1752 a.color, b.color,
1753 "segments {i} and {j} share generation {} and must share \
1754 a colour — a traversal-order ramp would give them two",
1755 generations[i]
1756 );
1757 } else {
1758 assert_ne!(
1759 a.color, b.color,
1760 "segments {i} and {j} sit at generations {} and {} and \
1761 must differ",
1762 generations[i], generations[j]
1763 );
1764 }
1765 }
1766 }
1767
1768 let first_trunk = generations.iter().position(|&g| g == 0).unwrap_or(0);
1771 let last_trunk = generations.len() - 1;
1772 assert_eq!(
1773 segs[first_trunk].color, segs[last_trunk].color,
1774 "the trunk keeps one colour on both sides of the branches"
1775 );
1776 }
1777
1778 #[test]
1782 fn zero_spread_is_one_flat_colour_over_every_generation() {
1783 let (flat, _) = coloured("F[+F]F[+F[-F]]F", 0.0);
1784 let first = flat.first().map(|s| s.color).unwrap_or([0.0; 3]);
1785 for (i, seg) in flat.iter().enumerate() {
1786 assert_eq!(seg.color, first, "segment {i} must carry the single hue");
1787 }
1788 }
1789
1790 #[test]
1795 fn a_grammar_without_branches_has_one_generation() {
1796 let (_, _, deepest) = figure("F+G-F-G+F");
1797 assert_eq!(deepest, 0, "no brackets, no second generation");
1798
1799 let (segs, _) = coloured("F+G-F-G+F", 1.0);
1800 let first = segs.first().map(|s| s.color).unwrap_or([0.0; 3]);
1801 for seg in &segs {
1802 assert_eq!(
1803 seg.color, first,
1804 "one generation colours flat at any spread"
1805 );
1806 }
1807 }
1808
1809 #[test]
1813 fn the_draw_progress_reveal_keeps_each_segment_on_its_own_generation() {
1814 let string = "F[+F]F[+F[-F]]F";
1815 let (full, generations) = coloured(string, 0.6);
1816
1817 let (base, _, deepest) = figure(string);
1818 let mut colors = vec![[0.0; 3]; deepest as usize + 1];
1819 fill_depth_colors(
1820 &mut colors,
1821 &Palette::default_spectrum(),
1822 ColorRamp {
1823 hue: DEFAULT_HUE,
1824 hue_spread: 0.6,
1825 palette_mix: common::DEFAULT_PALETTE_MIX,
1826 palette_steps: crate::render::palette::DEFAULT_PALETTE_STEPS,
1827 saturation: common::DEFAULT_SATURATION,
1828 brightness: DEFAULT_BRIGHTNESS,
1829 },
1830 deepest,
1831 );
1832 let mut half = Vec::new();
1834 transform_cached(&base, 0.0, 1.0, [0.0; 3], 0.01, 0.5, &mut half);
1835 apply_depth_colors(&mut half, &generations, &colors);
1836
1837 assert!(!half.is_empty() && half.len() < full.len(), "a real prefix");
1838 for (i, seg) in half.iter().enumerate() {
1839 assert_eq!(
1840 seg.color, full[i].color,
1841 "revealed segment {i} must keep generation {}'s colour",
1842 generations[i]
1843 );
1844 }
1845 }
1846
1847 #[test]
1854 fn the_turtle_table_is_the_roster_and_names_every_inert_parameter() {
1855 let turtles: Vec<&str> = TurtleMode::ALL.iter().map(|m| m.as_str()).collect();
1856 let expected: Vec<String> = Growth::ALL
1857 .iter()
1858 .flat_map(|g| {
1859 turtles.iter().map(move |t| match g {
1860 Growth::Fixed => (*t).to_string(),
1861 Growth::Endless => format!("{t} {}", g.as_str()),
1862 })
1863 })
1864 .collect();
1865 assert_eq!(MODES.to_vec(), expected, "the modes are the roster's");
1866 let modes = MODES.to_vec();
1867 let mut seen = Vec::new();
1868 for row in FAMILY_PARAMS {
1869 assert!(!seen.contains(&row.name), "`{}` has two rows", row.name);
1870 seen.push(row.name);
1871 let spec = PARAMS
1872 .iter()
1873 .find(|spec| spec.name == row.name)
1874 .unwrap_or_else(|| panic!("`{}` is not a declared parameter", row.name));
1875 let listed: Vec<&str> = row.ranges.iter().map(|r| r.family).collect();
1876 assert_eq!(listed, modes, "`{}` must list every mode", row.name);
1877 assert!(
1878 row.ranges.iter().any(|r| r.range == spec.range),
1879 "`{}`'s spec range is no mode's range",
1880 row.name
1881 );
1882 }
1883 let inert_on = |mode: &str| -> Vec<&str> {
1884 FAMILY_PARAMS
1885 .iter()
1886 .filter(|row| {
1887 row.ranges
1888 .iter()
1889 .any(|r| r.family == mode && r.range.is_none())
1890 })
1891 .map(|row| row.name)
1892 .collect()
1893 };
1894 let camera: Vec<&str> = CameraParams::SPECS.iter().map(|s| s.name).collect();
1895 let plane = ["rotation", "scale", "stroke_blend"];
1896 let mirror = ["mirror_order", "mirror_reflect"];
1897 let fixed = ["visible_depth", "draw_progress"];
1898 let endless = ["grow", "tail", "follow"];
1899 fn joined(parts: &[&[&'static str]]) -> Vec<&'static str> {
1900 let mut all: Vec<&str> = parts.iter().flat_map(|p| p.iter().copied()).collect();
1901 all.sort_unstable();
1902 all
1903 }
1904 let sorted = |mut v: Vec<&'static str>| {
1905 v.sort_unstable();
1906 v
1907 };
1908 assert_eq!(
1909 sorted(inert_on("flat")),
1910 joined(&[&camera, &endless]),
1911 "flat"
1912 );
1913 assert_eq!(
1914 sorted(inert_on("space")),
1915 joined(&[&plane, &mirror, &endless]),
1916 "space"
1917 );
1918 assert_eq!(
1919 sorted(inert_on("flat endless")),
1920 joined(&[&camera, &mirror, &fixed]),
1921 "flat endless"
1922 );
1923 assert_eq!(
1924 sorted(inert_on("space endless")),
1925 joined(&[&plane, &mirror, &fixed]),
1926 "space endless"
1927 );
1928 }
1929
1930 fn vine(trail: usize) -> Endless {
1933 let rules = [('F', "F[+F]F[-F]F".to_string())];
1934 Endless::new(
1935 "F",
1936 &rules,
1937 25f32.to_radians(),
1938 TurtleMode::Flat,
1939 trail,
1940 DEFAULT_FOLLOW_WINDOW as usize,
1941 )
1942 }
1943
1944 #[test]
1948 fn the_endless_rate_is_the_same_at_any_display_rate() {
1949 let mut slow = vine(1000);
1950 for _ in 0..600 {
1951 slow.advance(20.0, 1.0 / 60.0);
1952 }
1953 let mut fast = vine(1000);
1954 for _ in 0..1440 {
1955 fast.advance(20.0, 1.0 / 144.0);
1956 }
1957 let (a, b) = (slow.emitted(), fast.emitted());
1958 assert!(a.abs_diff(b) <= 1, "60 Hz emitted {a}, 144 Hz {b}");
1959 assert!(
1960 a.abs_diff(200) <= 1,
1961 "ten seconds at 20 a second is 200, not {a}"
1962 );
1963 }
1964
1965 #[test]
1969 fn the_ring_fills_to_its_trail_and_nothing_grows() {
1970 let mut endless = vine(1000);
1971 let built = endless.capacities();
1972 for _ in 0..6000 {
1973 endless.advance(20.0, 1.0 / 60.0);
1974 }
1975 assert_eq!(endless.emitted(), 2000, "100 seconds at 20 a second");
1976 assert_eq!(endless.len(), 1000, "the ring holds exactly its trail");
1977 assert_eq!(endless.capacities(), built, "a buffer grew");
1978 let newest = endless.at(0).map(|s| s.b);
1981 assert_eq!(newest, Some(endless.pen.position()));
1982 }
1983
1984 #[test]
1987 fn a_huge_grow_is_held_to_the_ceiling_without_a_backlog() {
1988 let mut endless = vine(4000);
1989 endless.advance(1.0e9, 1.0 / 60.0);
1990 assert_eq!(endless.emitted(), EMIT_CEILING);
1991 endless.advance(60.0, 1.0 / 60.0);
1992 assert_eq!(
1993 endless.emitted(),
1994 EMIT_CEILING + 1,
1995 "one step owed, one taken"
1996 );
1997 endless.advance(f32::NAN, 1.0 / 60.0);
1998 endless.advance(-5.0, 1.0 / 60.0);
1999 assert_eq!(
2000 endless.emitted(),
2001 EMIT_CEILING + 1,
2002 "nothing grows backwards"
2003 );
2004 }
2005
2006 #[test]
2010 fn a_short_stream_restarts_without_a_jump() {
2011 let rules = [('F', "F+".to_string())];
2013 let mut endless = Endless::new("FF", &rules, 0.3, TurtleMode::Flat, 50, 8);
2014 for _ in 0..120 {
2015 endless.advance(20.0, 1.0 / 60.0);
2016 }
2017 assert_eq!(endless.emitted(), 40);
2018 for age in 0..endless.len() - 1 {
2019 let (newer, older) = (endless.at(age), endless.at(age + 1));
2020 assert_eq!(
2021 newer.map(|s| s.a),
2022 older.map(|s| s.b),
2023 "segment {age} does not begin where the one before it ended"
2024 );
2025 }
2026 }
2027
2028 fn absolute(endless: &Endless, p: [f64; 3]) -> [f64; 3] {
2030 [
2031 p[0] + endless.origin[0],
2032 p[1] + endless.origin[1],
2033 p[2] + endless.origin[2],
2034 ]
2035 }
2036
2037 fn distance(a: [f64; 3], b: [f64; 3]) -> f64 {
2038 ((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2) + (a[2] - b[2]).powi(2)).sqrt()
2039 }
2040
2041 #[test]
2046 fn the_follow_point_glides_across_a_branchs_jump() {
2047 let dt = 1.0 / 60.0;
2048 let mut endless = vine(2000);
2049 let w = 1.0 / f64::from(DEFAULT_FOLLOW);
2050 let mut farthest = 0.0f64;
2051 let mut biggest_jump = 0.0f64;
2052 let mut follow_at_jump = 0.0f64;
2053 for frame in 0..6000 {
2054 let tip = absolute(&endless, endless.pen.position());
2055 let before = absolute(&endless, endless.follow);
2056 endless.advance(DEFAULT_GROW, dt);
2057 if let Some(target) = endless.follow_target() {
2058 farthest = farthest.max(distance(target, endless.follow));
2059 }
2060 endless.follow_step(DEFAULT_FOLLOW, dt);
2061 let moved = distance(absolute(&endless, endless.follow), before);
2062 let allowed = w * f64::from(dt) * farthest;
2063 assert!(
2064 moved <= allowed + 1e-9,
2065 "frame {frame}: the follow point moved {moved} steps, past the \
2066 spring's {allowed}"
2067 );
2068 let jump = distance(absolute(&endless, endless.pen.position()), tip);
2069 if jump > biggest_jump {
2070 biggest_jump = jump;
2071 follow_at_jump = moved;
2072 }
2073 }
2074 println!("largest tip jump {biggest_jump:.1} steps, follow moved {follow_at_jump:.3}");
2075 assert!(
2076 biggest_jump >= 10.0,
2077 "the vine never jumped back (largest {biggest_jump}), so this proves nothing"
2078 );
2079 assert!(
2080 follow_at_jump * 10.0 < biggest_jump,
2081 "on the frame the tip jumped {biggest_jump} steps the follow point moved \
2082 {follow_at_jump}"
2083 );
2084 }
2085
2086 #[test]
2091 fn the_tip_of_a_vine_stays_in_frame() {
2092 let dt = 1.0 / 60.0;
2093 for (grammar, turtle) in [
2094 ("F+F-F-F+F", TurtleMode::Flat),
2095 ("F+F-F&F^F", TurtleMode::Space),
2096 ] {
2097 let rules = [('F', grammar.to_string())];
2098 let mut endless = Endless::new(
2099 "F",
2100 &rules,
2101 std::f32::consts::FRAC_PI_2,
2102 turtle,
2103 2000,
2104 DEFAULT_FOLLOW_WINDOW as usize,
2105 );
2106 let camera = CameraParams::default();
2107 let mut worst = 0.0f32;
2108 for frame in 0..6000 {
2109 endless.advance(DEFAULT_GROW, dt);
2110 endless.follow_step(DEFAULT_FOLLOW, dt);
2111 let tip = endless.relative(endless.pen.position());
2112 let ndc = match turtle {
2113 TurtleMode::Flat => [tip[0] / (1280.0 / 800.0), tip[1]],
2114 TurtleMode::Space => {
2115 let view = camera
2116 .frame(1920.0 / 1080.0, 1.0, [0.0, 0.0], (1920, 1080), 1.0, 12.0)
2117 .view;
2118 let c = view.clip(tip);
2119 assert!(c[3] > 0.0, "frame {frame}: the tip is behind the eye");
2120 [c[0] / c[3], c[1] / c[3]]
2121 }
2122 };
2123 let reach = ndc[0].abs().max(ndc[1].abs());
2124 worst = worst.max(reach);
2125 assert!(
2126 reach <= 1.0,
2127 "{turtle:?} frame {frame}: the tip left the frame at {ndc:?}"
2128 );
2129 }
2130 println!("{turtle:?}: the tip reached {worst:.3} of the frame's half-extent");
2131 }
2132 }
2133
2134 #[test]
2138 fn a_million_steps_on_the_newest_segment_is_still_one_step_long() {
2139 let rules = [('F', "FF".to_string())];
2140 let mut endless = Endless::new("F", &rules, 0.4, TurtleMode::Space, 1000, 32);
2141 let first = {
2142 endless.advance(60.0, 1.0 / 60.0);
2143 endless.at(0).map(|s| distance(s.a, s.b))
2144 };
2145 assert_eq!(first, Some(1.0), "step 0");
2146 while endless.emitted() < 1_000_000 {
2147 endless.advance(1.0e9, 1.0 / 60.0);
2148 }
2149 let newest = endless.at(0).map(|s| distance(s.a, s.b)).unwrap_or(0.0);
2150 assert!(
2151 (newest - 1.0).abs() <= 1e-5,
2152 "the newest segment is {newest} steps long"
2153 );
2154 let pen = endless.pen.position();
2155 assert!(
2156 distance(pen, [0.0; 3]) <= REBASE_RADIUS + 1.0,
2157 "the pen stands {} steps from the origin after {} steps",
2158 distance(pen, [0.0; 3]),
2159 endless.emitted()
2160 );
2161 assert!(
2162 distance(endless.origin, [0.0; 3]) > 999_000.0,
2163 "the origin moved with the vine"
2164 );
2165 }
2166
2167 #[test]
2172 fn a_rebase_leaves_every_drawn_position_bit_for_bit() {
2173 let mut endless = vine(2000);
2174 for _ in 0..3000 {
2175 endless.advance(DEFAULT_GROW, 1.0 / 60.0);
2176 endless.follow_step(DEFAULT_FOLLOW, 1.0 / 60.0);
2177 }
2178 let drawn = |e: &Endless| -> Vec<[f32; 3]> {
2179 (0..e.len())
2180 .filter_map(|age| e.at(age))
2181 .flat_map(|s| [e.relative(s.a), e.relative(s.b)])
2182 .chain([e.relative(e.pen.position())])
2183 .collect()
2184 };
2185 let before = drawn(&endless);
2186 let origin = endless.origin;
2187 endless.rebase();
2188 assert_ne!(
2189 endless.origin, origin,
2190 "the probe must actually move the origin"
2191 );
2192 assert_eq!(drawn(&endless), before, "a re-base moved something drawn");
2193 }
2194
2195 #[test]
2198 fn the_fade_falls_over_the_oldest_tail() {
2199 assert_eq!(fade(0, 100, 0.5), 1.0);
2200 assert_eq!(fade(49, 100, 0.5), 1.0);
2201 assert!((fade(75, 100, 0.5) - 0.5).abs() < 1e-6);
2202 assert!(fade(99, 100, 0.5) <= 0.02 + 1e-6);
2203 assert_eq!(fade(99, 100, 0.0), 1.0);
2204 assert_eq!(fade(99, 100, f32::NAN), 1.0);
2205 }
2206
2207 #[test]
2211 fn an_endless_scene_grows_no_buffer_it_owns() {
2212 use crate::render::context::{RenderContext, RenderError};
2213
2214 let ctx = match RenderContext::new_headless(64, 64, true) {
2215 Ok(ctx) => ctx,
2216 Err(RenderError::RequestAdapter(_)) => {
2217 eprintln!("skipped: no GPU adapter on this runner (ADR-0016)");
2218 return;
2219 }
2220 Err(e) => panic!("headless context build failed: {e}"),
2221 };
2222 let tier = crate::render::TierConfig::FLOOR;
2223 let format = crate::render::COMPOSITE_FORMAT;
2224 let shared = Rc::new(RefCell::new(LineRenderer::new(
2225 &ctx.device,
2226 format,
2227 tier.max_segments,
2228 "lsystem-test",
2229 )));
2230 let target = ctx
2231 .device
2232 .create_texture(&wgpu::TextureDescriptor {
2233 label: Some("lsystem-endless-target"),
2234 size: wgpu::Extent3d {
2235 width: 64,
2236 height: 64,
2237 depth_or_array_layers: 1,
2238 },
2239 mip_level_count: 1,
2240 sample_count: 1,
2241 dimension: wgpu::TextureDimension::D2,
2242 format,
2243 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2244 view_formats: &[],
2245 })
2246 .create_view(&wgpu::TextureViewDescriptor::default());
2247 for turtle in TurtleMode::ALL {
2248 let mut scene = LSystemScene::new(
2249 Rc::clone(&shared),
2250 tier.max_segments,
2251 &ctx.device,
2252 format,
2253 tier.seg3d_segments as usize,
2254 tier.max_coc_px as f32,
2255 );
2256 scene.set_target_size(64, 64);
2257 let overflow = scene.configure(&GeneratorConfig::LSystem {
2258 axiom: "F".into(),
2259 rules: vec![('F', "F[+F]F[-F]F".into())],
2260 angle_deg: 25.0,
2261 max_depth: 4,
2262 seed: 0,
2263 turtle,
2264 growth: Growth::Endless,
2265 trail: 1000,
2266 follow_window: DEFAULT_FOLLOW_WINDOW,
2267 });
2268 assert_eq!(overflow, None, "{turtle:?}: a trail inside the cap");
2269 let capacities = |s: &LSystemScene| {
2270 (
2271 [
2272 s.draw_buf.capacity(),
2273 s.single_buf.capacity(),
2274 s.instances3d.capacity(),
2275 s.depth_colors.capacity(),
2276 ],
2277 s.endless.as_ref().map(Endless::capacities),
2278 )
2279 };
2280 let built = capacities(&scene);
2281 let frame = AnalysisFrame::default();
2282 for i in 0..6000 {
2283 scene.reset_params();
2284 scene.set_param("grow", 20.0);
2285 scene.advance(1.0 / 60.0);
2286 scene.update(&frame);
2287 if i % 500 == 0 || i == 5999 {
2288 let mut encoder = ctx
2289 .device
2290 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
2291 scene.render(&ctx.queue, &mut encoder, &target, 1.0);
2292 ctx.queue.submit([encoder.finish()]);
2293 }
2294 }
2295 assert_eq!(
2296 scene.endless.as_ref().map(Endless::len),
2297 Some(1000),
2298 "{turtle:?}: the ring holds exactly its trail"
2299 );
2300 if turtle == TurtleMode::Flat {
2301 assert_eq!(scene.draw_buf.len(), 1000, "the whole ring is drawn");
2302 }
2303 assert_eq!(capacities(&scene), built, "{turtle:?}: a buffer grew");
2304
2305 let frame =
2308 scene
2309 .camera
2310 .frame(1.0, 1.0, [0.0, 0.0], (64, 64), scene.endless_radius, 12.0);
2311 let laid = |s: &mut LSystemScene| -> (Vec<u8>, Vec<u8>, u32) {
2312 s.lay_endless();
2313 s.lay_endless_space(&frame);
2314 (
2315 bytemuck::cast_slice(&s.draw_buf).to_vec(),
2316 bytemuck::cast_slice(&s.instances3d).to_vec(),
2317 s.endless_radius.to_bits(),
2318 )
2319 };
2320 let before = laid(&mut scene);
2321 assert!(
2322 !before.0.is_empty() || !before.1.is_empty(),
2323 "something is laid out"
2324 );
2325 if let Some(endless) = scene.endless.as_mut() {
2326 endless.rebase();
2327 }
2328 assert_eq!(
2329 laid(&mut scene),
2330 before,
2331 "{turtle:?}: a re-base moved a byte"
2332 );
2333 }
2334 }
2335
2336 #[test]
2342 fn the_endless_ramp_divides_by_the_deepest_generation_reached() {
2343 let trail = 40;
2344 let mut endless = vine(trail);
2345 let capacity = endless.generations();
2346 assert_eq!(
2347 capacity, 32,
2348 "the vine's bracket_bound over the stream depth"
2349 );
2350 let mut seen = 0u32;
2351 let mut last = endless.ramp_divisor();
2352 let mut ring_fell_below = false;
2353 for _ in 0..(trail * 20) {
2356 endless.advance(60.0, 1.0 / 60.0);
2357 let ring_max = (0..endless.len())
2358 .filter_map(|age| endless.at(age))
2359 .map(|seg| seg.generation)
2360 .max()
2361 .unwrap_or(0);
2362 seen = seen.max(ring_max);
2363 let divisor = endless.ramp_divisor();
2364 assert_eq!(divisor, seen.max(1), "the divisor is the deepest reached");
2365 assert!(divisor >= last, "the divisor fell: {last} -> {divisor}");
2366 ring_fell_below |= ring_max < divisor;
2367 last = divisor;
2368 }
2369 assert!(
2370 endless.emitted() > (trail as u64) * 10,
2371 "the ring turned over many times"
2372 );
2373 assert!(
2374 last > 1 && last * 4 <= capacity,
2375 "the vine nests well under its bound: reached {last} of {capacity}"
2376 );
2377 assert!(
2378 ring_fell_below,
2379 "the deepest segments left the ring and the divisor held"
2380 );
2381 }
2382
2383 #[test]
2387 fn the_space_renderer_is_built_by_the_first_space_grammar() {
2388 use crate::render::context::{RenderContext, RenderError};
2389
2390 let ctx = match RenderContext::new_headless(64, 64, true) {
2391 Ok(ctx) => ctx,
2392 Err(RenderError::RequestAdapter(_)) => {
2393 eprintln!("skipped: no GPU adapter on this runner (ADR-0016)");
2394 return;
2395 }
2396 Err(e) => panic!("headless context build failed: {e}"),
2397 };
2398 let tier = crate::render::TierConfig::FLOOR;
2399 let format = crate::render::COMPOSITE_FORMAT;
2400 let shared = Rc::new(RefCell::new(LineRenderer::new(
2401 &ctx.device,
2402 format,
2403 tier.max_segments,
2404 "lsystem-test",
2405 )));
2406 let mut scene = LSystemScene::new(
2407 shared,
2408 tier.max_segments,
2409 &ctx.device,
2410 format,
2411 tier.seg3d_segments as usize,
2412 tier.max_coc_px as f32,
2413 );
2414 let grammar = |turtle| GeneratorConfig::LSystem {
2415 axiom: "F".into(),
2416 rules: vec![('F', "F[+F]F[-F]F".into())],
2417 angle_deg: 25.0,
2418 max_depth: 3,
2419 seed: 0,
2420 turtle,
2421 growth: Growth::Fixed,
2422 trail: 1000,
2423 follow_window: DEFAULT_FOLLOW_WINDOW,
2424 };
2425 assert!(scene.lines3d.is_none(), "nothing built before a grammar");
2426 scene.configure(&grammar(TurtleMode::Flat));
2427 assert!(
2428 scene.lines3d.is_none(),
2429 "a flat grammar builds no seg3d renderer"
2430 );
2431 assert_eq!(scene.instances3d.capacity(), 0, "nor a 3D instance buffer");
2432
2433 scene.configure(&grammar(TurtleMode::Space));
2434 assert!(
2435 scene.lines3d.is_some(),
2436 "a space grammar builds the renderer"
2437 );
2438 assert!(
2439 scene.instances3d.capacity() >= tier.seg3d_segments as usize,
2440 "and reserves the instance buffer at the tier's cap"
2441 );
2442
2443 scene.configure(&grammar(TurtleMode::Flat));
2444 assert!(scene.lines3d.is_some(), "a switch back keeps it");
2445 }
2446
2447 #[test]
2448 fn draw_progress_reveals_a_prefix() {
2449 let mut base = Vec::with_capacity(64);
2450 turtle::walk("FFFFFFFF", 0.0, CAP, &mut base);
2451 let mut out = Vec::with_capacity(64);
2452 transform_cached(&base, 0.0, 1.0, [1.0; 3], 0.01, 0.5, &mut out);
2453 assert_eq!(out.len(), 4, "half of eight segments");
2454 }
2455}