1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
//! # L-System (Lindenmayer System) Turtle Graphics
//!
//! Generates fractal line patterns via string-rewriting production rules and
//! renders them via turtle-graphics into line-segment lists or rasterised
//! pixel buffers.
use std::collections::{HashMap, HashSet};
// ── LSystem ───────────────────────────────────────────────────────────────────
/// An L-system defined by an axiom and a set of character production rules.
#[derive(Debug, Clone)]
pub struct LSystem {
/// Starting string (seed).
pub axiom: String,
/// Production rules: each variable character maps to its replacement.
pub rules: HashMap<char, String>,
/// Characters that are rewritten by production rules.
pub variables: HashSet<char>,
/// Characters that pass through unchanged.
pub constants: HashSet<char>,
}
impl LSystem {
/// Create an L-system with just an axiom and no rules yet.
pub fn new(axiom: &str) -> Self {
Self {
axiom: axiom.to_string(),
rules: HashMap::new(),
variables: HashSet::new(),
constants: HashSet::new(),
}
}
/// Builder: add a production rule `from → to` and register `from` as a
/// variable.
pub fn rule(mut self, from: char, to: &str) -> Self {
self.rules.insert(from, to.to_string());
self.variables.insert(from);
self
}
/// Expand the axiom by applying production rules `iterations` times.
pub fn expand(&self, iterations: usize) -> String {
let mut current = self.axiom.clone();
for _ in 0..iterations {
let mut next = String::new();
for c in current.chars() {
if let Some(replacement) = self.rules.get(&c) {
next.push_str(replacement);
} else {
next.push(c);
}
}
current = next;
}
current
}
// ── Named L-systems ───────────────────────────────────────────────────────
/// Dragon curve: axiom "F", rules F→F+G, G→F-G; angle=90°.
pub fn dragon_curve() -> Self {
Self::new("F").rule('F', "F+G").rule('G', "F-G")
}
/// Sierpinski triangle: axiom "F-G-G", rules F→F-G+F+G-F, G→GG; angle=120°.
pub fn sierpinski_triangle() -> Self {
Self::new("F-G-G").rule('F', "F-G+F+G-F").rule('G', "GG")
}
/// Fractal plant: axiom `X`, rules `X -> F+[[X]-X]-F[-FX]+X` and `F -> FF`, angle 25 degrees.
pub fn plant() -> Self {
Self::new("X")
.rule('X', "F+[[X]-X]-F[-FX]+X")
.rule('F', "FF")
}
/// Hilbert curve: axiom "A", rules A→+BF-AFA-FB+, B→-AF+BFB+FA-; angle=90°.
pub fn hilbert_curve() -> Self {
Self::new("A")
.rule('A', "+BF-AFA-FB+")
.rule('B', "-AF+BFB+FA-")
}
}
// ── TurtleState ───────────────────────────────────────────────────────────────
/// The turtle's current drawing state.
#[derive(Debug, Clone)]
pub struct TurtleState {
/// X position.
pub x: f64,
/// Y position.
pub y: f64,
/// Current heading in degrees (0° = right, 90° = up).
pub angle_deg: f64,
}
// ── TurtleRenderer ────────────────────────────────────────────────────────────
/// Renders an L-system instruction string into a list of line segments.
pub struct TurtleRenderer {
/// Length of a single forward step in world units.
pub step_size: f64,
/// Angle turned on `+` / `-` commands in degrees.
pub angle_delta_deg: f64,
/// Canvas width (for normalisation).
pub canvas_w: usize,
/// Canvas height (for normalisation).
pub canvas_h: usize,
}
impl TurtleRenderer {
/// Execute turtle-graphics commands and return `(x1, y1, x2, y2)` line
/// segments for every forward-draw step.
///
/// Commands:
/// - `F` / `G` — move forward, emit segment.
/// - `+` — turn left by `angle_delta_deg`.
/// - `-` — turn right by `angle_delta_deg`.
/// - `[` — push state.
/// - `]` — pop state.
/// - anything else — ignored.
pub fn render(&self, instructions: &str, start: TurtleState) -> Vec<(f64, f64, f64, f64)> {
let mut state = start;
let mut stack: Vec<TurtleState> = Vec::new();
let mut segments: Vec<(f64, f64, f64, f64)> = Vec::new();
for ch in instructions.chars() {
match ch {
'F' | 'G' => {
let rad = state.angle_deg.to_radians();
let x2 = state.x + self.step_size * rad.cos();
let y2 = state.y + self.step_size * rad.sin();
segments.push((state.x, state.y, x2, y2));
state.x = x2;
state.y = y2;
}
'+' => state.angle_deg += self.angle_delta_deg,
'-' => state.angle_deg -= self.angle_delta_deg,
'[' => stack.push(state.clone()),
']' => {
if let Some(saved) = stack.pop() {
state = saved;
}
}
_ => {}
}
}
segments
}
/// Translate and uniformly scale `segments` to fit within `(w, h)`.
pub fn normalize_segments(segments: &mut Vec<(f64, f64, f64, f64)>, w: usize, h: usize) {
if segments.is_empty() {
return;
}
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut max_y = f64::NEG_INFINITY;
for &(x1, y1, x2, y2) in segments.iter() {
min_x = min_x.min(x1).min(x2);
min_y = min_y.min(y1).min(y2);
max_x = max_x.max(x1).max(x2);
max_y = max_y.max(y1).max(y2);
}
let range_x = (max_x - min_x).max(1e-12);
let range_y = (max_y - min_y).max(1e-12);
let margin = 4.0;
let scale = ((w as f64 - margin * 2.0) / range_x).min((h as f64 - margin * 2.0) / range_y);
for seg in segments.iter_mut() {
seg.0 = (seg.0 - min_x) * scale + margin;
seg.1 = (seg.1 - min_y) * scale + margin;
seg.2 = (seg.2 - min_x) * scale + margin;
seg.3 = (seg.3 - min_y) * scale + margin;
}
}
}
// ── Rasterisation ─────────────────────────────────────────────────────────────
/// Rasterise `segments` onto an RGB byte buffer of `w × h` pixels using
/// Bresenham's line algorithm.
///
/// Returns a `w * h * 3` byte buffer (R, G, B interleaved), initialised to
/// black with segments drawn in `color`.
pub fn to_pixels(
segments: &[(f64, f64, f64, f64)],
w: usize,
h: usize,
color: (u8, u8, u8),
) -> Vec<u8> {
let mut buf = vec![0u8; w * h * 3];
let set_pixel = |buf: &mut Vec<u8>, x: i64, y: i64| {
if x >= 0 && y >= 0 && (x as usize) < w && (y as usize) < h {
let idx = ((y as usize) * w + (x as usize)) * 3;
buf[idx] = color.0;
buf[idx + 1] = color.1;
buf[idx + 2] = color.2;
}
};
for &(fx1, fy1, fx2, fy2) in segments {
let mut x0 = fx1.round() as i64;
let mut y0 = fy1.round() as i64;
let x1 = fx2.round() as i64;
let y1 = fy2.round() as i64;
let dx = (x1 - x0).abs();
let dy = (y1 - y0).abs();
let sx: i64 = if x0 < x1 { 1 } else { -1 };
let sy: i64 = if y0 < y1 { 1 } else { -1 };
let mut err = dx - dy;
loop {
set_pixel(&mut buf, x0, y0);
if x0 == x1 && y0 == y1 {
break;
}
let e2 = 2 * err;
if e2 > -dy {
err -= dy;
x0 += sx;
}
if e2 < dx {
err += dx;
y0 += sy;
}
}
}
buf
}
// ── Tests ─────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dragon_curve_expand_one_iteration() {
let ls = LSystem::dragon_curve();
let expanded = ls.expand(1);
// F → F+G
assert_eq!(expanded, "F+G");
}
#[test]
fn dragon_curve_expand_two_iterations() {
let ls = LSystem::dragon_curve();
let expanded = ls.expand(2);
// F+G → (F+G)+(F-G) = F+G+F-G
assert_eq!(expanded, "F+G+F-G");
}
#[test]
fn plant_segments_nonempty() {
let ls = LSystem::plant();
let instructions = ls.expand(3);
let renderer = TurtleRenderer {
step_size: 5.0,
angle_delta_deg: 25.0,
canvas_w: 512,
canvas_h: 512,
};
let start = TurtleState {
x: 256.0,
y: 10.0,
angle_deg: 90.0,
};
let segments = renderer.render(&instructions, start);
assert!(!segments.is_empty(), "plant should produce segments");
}
#[test]
fn normalize_keeps_within_bounds() {
let mut segments = vec![
(0.0_f64, 0.0_f64, 1000.0_f64, 1000.0_f64),
(-500.0, -500.0, 500.0, 500.0),
];
TurtleRenderer::normalize_segments(&mut segments, 800, 600);
for &(x1, y1, x2, y2) in &segments {
assert!(x1 >= 0.0 && x1 <= 800.0, "x1 out of bounds: {x1}");
assert!(y1 >= 0.0 && y1 <= 600.0, "y1 out of bounds: {y1}");
assert!(x2 >= 0.0 && x2 <= 800.0, "x2 out of bounds: {x2}");
assert!(y2 >= 0.0 && y2 <= 600.0, "y2 out of bounds: {y2}");
}
}
#[test]
fn to_pixels_correct_buffer_size() {
let segs = vec![(0.0_f64, 0.0_f64, 10.0_f64, 10.0_f64)];
let buf = to_pixels(&segs, 64, 64, (255, 0, 0));
assert_eq!(buf.len(), 64 * 64 * 3);
}
#[test]
fn to_pixels_draws_something() {
let segs = vec![(10.0_f64, 10.0_f64, 50.0_f64, 10.0_f64)];
let buf = to_pixels(&segs, 64, 64, (255, 255, 255));
let lit = buf.chunks(3).any(|px| px[0] > 0);
assert!(lit, "expected at least one non-black pixel");
}
#[test]
fn hilbert_curve_expands_without_panic() {
let ls = LSystem::hilbert_curve();
let s = ls.expand(4);
assert!(!s.is_empty());
}
}