Skip to main content

proof_engine/fractal/
lsystem.rs

1//! L-systems with parametric rules and 3D turtle graphics.
2
3use glam::Vec3;
4use std::collections::HashMap;
5
6#[derive(Debug, Clone)]
7pub struct LSystemRule { pub from: char, pub to: String }
8
9#[derive(Debug, Clone)]
10pub struct LSystem {
11    pub axiom: String,
12    pub rules: Vec<LSystemRule>,
13    pub angle: f32,
14    pub step_length: f32,
15}
16
17#[derive(Debug, Clone)]
18pub struct TurtleState { pub position: Vec3, pub heading: Vec3, pub up: Vec3, pub right: Vec3 }
19
20impl Default for TurtleState {
21    fn default() -> Self {
22        Self { position: Vec3::ZERO, heading: Vec3::Y, up: Vec3::Z, right: Vec3::X }
23    }
24}
25
26impl LSystem {
27    pub fn new(axiom: &str, rules: Vec<(&str, &str)>, angle: f32, step: f32) -> Self {
28        Self {
29            axiom: axiom.to_string(),
30            rules: rules.into_iter().map(|(f, t)| LSystemRule { from: f.chars().next().unwrap(), to: t.to_string() }).collect(),
31            angle, step_length: step,
32        }
33    }
34
35    /// Generate the string after n iterations.
36    pub fn generate(&self, iterations: u32) -> String {
37        let mut current = self.axiom.clone();
38        let rule_map: HashMap<char, &str> = self.rules.iter().map(|r| (r.from, r.to.as_str())).collect();
39        for _ in 0..iterations {
40            let mut next = String::with_capacity(current.len() * 2);
41            for ch in current.chars() {
42                if let Some(&replacement) = rule_map.get(&ch) { next.push_str(replacement); }
43                else { next.push(ch); }
44            }
45            current = next;
46        }
47        current
48    }
49
50    /// Interpret the generated string as turtle graphics. Returns line segments.
51    pub fn interpret(&self, generated: &str) -> Vec<(Vec3, Vec3)> {
52        let mut segments = Vec::new();
53        let mut turtle = TurtleState::default();
54        let mut stack: Vec<TurtleState> = Vec::new();
55        let angle_rad = self.angle.to_radians();
56
57        for ch in generated.chars() {
58            match ch {
59                'F' | 'G' => {
60                    let start = turtle.position;
61                    turtle.position += turtle.heading * self.step_length;
62                    segments.push((start, turtle.position));
63                }
64                'f' | 'g' => { turtle.position += turtle.heading * self.step_length; }
65                '+' => rotate_yaw(&mut turtle, angle_rad),
66                '-' => rotate_yaw(&mut turtle, -angle_rad),
67                '&' => rotate_pitch(&mut turtle, angle_rad),
68                '^' => rotate_pitch(&mut turtle, -angle_rad),
69                '\\' => rotate_roll(&mut turtle, angle_rad),
70                '/' => rotate_roll(&mut turtle, -angle_rad),
71                '|' => rotate_yaw(&mut turtle, std::f32::consts::PI),
72                '[' => stack.push(turtle.clone()),
73                ']' => { if let Some(s) = stack.pop() { turtle = s; } }
74                _ => {}
75            }
76        }
77        segments
78    }
79
80    // ── Presets ──────────────────────────────────────────────────────────
81
82    pub fn koch_curve() -> Self { Self::new("F", vec![("F", "F+F-F-F+F")], 90.0, 1.0) }
83    pub fn sierpinski_triangle() -> Self { Self::new("F-G-G", vec![("F", "F-G+F+G-F"), ("G", "GG")], 120.0, 1.0) }
84    pub fn dragon_curve() -> Self { Self::new("FX", vec![("X", "X+YF+"), ("Y", "-FX-Y")], 90.0, 1.0) }
85    pub fn plant() -> Self { Self::new("X", vec![("X", "F+[[X]-X]-F[-FX]+X"), ("F", "FF")], 25.0, 1.0) }
86    pub fn hilbert_3d() -> Self { Self::new("A", vec![("A", "B-F+CFC+F-D&F^D-F+&&CFC+F+B//"), ("B", "A&F^CFB^F^D^^-F-D^|F^B|FC^F^A//"), ("C", "|D^|F^B-F+C^F^A&&FA&F^C+F+B^F^D//"), ("D", "|CFB-F+B|FA&F^A&&FB-F+B|FC//")], 90.0, 1.0) }
87}
88
89fn rotate_yaw(t: &mut TurtleState, angle: f32) {
90    let (s, c) = angle.sin_cos();
91    let new_h = t.heading * c + t.right * s;
92    let new_r = -t.heading * s + t.right * c;
93    t.heading = new_h.normalize_or_zero();
94    t.right = new_r.normalize_or_zero();
95}
96
97fn rotate_pitch(t: &mut TurtleState, angle: f32) {
98    let (s, c) = angle.sin_cos();
99    let new_h = t.heading * c + t.up * s;
100    let new_u = -t.heading * s + t.up * c;
101    t.heading = new_h.normalize_or_zero();
102    t.up = new_u.normalize_or_zero();
103}
104
105fn rotate_roll(t: &mut TurtleState, angle: f32) {
106    let (s, c) = angle.sin_cos();
107    let new_r = t.right * c + t.up * s;
108    let new_u = -t.right * s + t.up * c;
109    t.right = new_r.normalize_or_zero();
110    t.up = new_u.normalize_or_zero();
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    #[test]
117    fn koch_generates() { let s = LSystem::koch_curve().generate(2); assert!(s.len() > 10); }
118    #[test]
119    fn plant_produces_segments() {
120        let ls = LSystem::plant();
121        let gen = ls.generate(3);
122        let segs = ls.interpret(&gen);
123        assert!(!segs.is_empty());
124    }
125    #[test]
126    fn dragon_curve_grows() {
127        let ls = LSystem::dragon_curve();
128        let s1 = ls.generate(1); let s2 = ls.generate(2);
129        assert!(s2.len() > s1.len());
130    }
131}