use crate::math::{Vec2, Vec3};
use crate::mesh::Mesh;
use crate::monte_carlo::Rng;
use crate::quaternion::Quaternion;
use crate::spatial::frame::Frame;
use crate::spatial::primitives::{Rect, Segment, Segment2};
#[derive(Debug, Clone)]
pub enum Rule {
Simple { from: char, to: String },
Stochastic { from: char, options: Vec<(f64, String)> },
Context { left: Option<String>, from: char, right: Option<String>, to: String },
}
#[derive(Debug, Clone)]
pub struct LSystem {
pub axiom: String,
pub rules: Vec<Rule>,
pub angle: f64,
pub ignore: Vec<char>,
}
impl LSystem {
#[must_use]
pub fn new(axiom: &str, angle_deg: f64) -> Self {
Self {
axiom: axiom.to_string(),
rules: Vec::new(),
angle: angle_deg.to_radians(),
ignore: Vec::new(),
}
}
#[must_use]
pub fn rule(mut self, from: char, to: &str) -> Self {
self.rules.push(Rule::Simple { from, to: to.to_string() });
self
}
#[must_use]
pub fn stochastic_rule(mut self, from: char, options: &[(f64, &str)]) -> Self {
assert!(
options.iter().any(|&(p, _)| p > 0.0),
"stochastic rule needs a positive probability"
);
self.rules.push(Rule::Stochastic {
from,
options: options.iter().map(|&(p, s)| (p, s.to_string())).collect(),
});
self
}
fn matches_left(&self, chars: &[char], i: usize, ctx: &str) -> bool {
let want: Vec<char> = ctx.chars().collect();
let mut w = want.len();
let mut j = i;
while w > 0 {
if j == 0 {
return false;
}
j -= 1;
let c = chars[j];
if c == ']' {
let mut depth = 1;
while depth > 0 {
if j == 0 {
return false;
}
j -= 1;
match chars[j] {
']' => depth += 1,
'[' => depth -= 1,
_ => {}
}
}
} else if c == '[' || self.ignore.contains(&c) {
} else {
w -= 1;
if want[w] != c {
return false;
}
}
}
true
}
fn matches_right(&self, chars: &[char], i: usize, ctx: &str) -> bool {
let want: Vec<char> = ctx.chars().collect();
let mut w = 0;
let mut j = i + 1;
while w < want.len() {
if j >= chars.len() {
return false;
}
let c = chars[j];
if c == '[' {
let mut depth = 1;
j += 1;
while depth > 0 {
if j >= chars.len() {
return false;
}
match chars[j] {
'[' => depth += 1,
']' => depth -= 1,
_ => {}
}
j += 1;
}
continue;
}
if c == ']' {
return false; }
if !self.ignore.contains(&c) {
if want[w] != c {
return false;
}
w += 1;
}
j += 1;
}
true
}
fn rewrite_char(&self, chars: &[char], i: usize, rng: &mut Option<&mut Rng>) -> Option<String> {
let c = chars[i];
for rule in &self.rules {
if let Rule::Context { left, from, right, to } = rule {
if *from == c
&& left.as_ref().is_none_or(|l| self.matches_left(chars, i, l))
&& right.as_ref().is_none_or(|r| self.matches_right(chars, i, r))
{
return Some(to.clone());
}
}
}
for rule in &self.rules {
match rule {
Rule::Simple { from, to } if *from == c => return Some(to.clone()),
Rule::Stochastic { from, options } if *from == c => {
let total: f64 = options.iter().map(|&(p, _)| p.max(0.0)).sum();
let mut pick = match rng {
Some(r) => r.next_f64() * total,
None => 0.0,
};
for (p, s) in options {
pick -= p.max(0.0);
if pick <= 0.0 {
return Some(s.clone());
}
}
return Some(options.last().expect("non-empty options").1.clone());
}
_ => {}
}
}
None
}
#[must_use]
pub fn generate(&self, iterations: usize, mut rng: Option<&mut Rng>) -> String {
let mut current: Vec<char> = self.axiom.chars().collect();
for _ in 0..iterations {
let mut next = String::with_capacity(current.len() * 2);
for i in 0..current.len() {
match self.rewrite_char(¤t, i, &mut rng) {
Some(s) => next.push_str(&s),
None => next.push(current[i]),
}
}
current = next.chars().collect();
}
current.into_iter().collect()
}
}
#[derive(Debug, Clone)]
pub struct Turtle2 {
pub pos: Vec2,
pub heading: f64,
pub step: f64,
pub angle: f64,
pub pen_down: bool,
pub line_width: f64,
pub width_factor: f64,
stack: Vec<(Vec2, f64, f64)>,
lo: Vec2,
hi: Vec2,
}
impl Turtle2 {
#[must_use]
pub fn new(step: f64, angle_deg: f64) -> Self {
assert!(step > 0.0, "turtle step must be positive");
Self {
pos: Vec2::ZERO,
heading: 0.0,
step,
angle: angle_deg.to_radians(),
pen_down: true,
line_width: 1.0,
width_factor: 0.7,
stack: Vec::new(),
lo: Vec2::ZERO,
hi: Vec2::ZERO,
}
}
fn touch(&mut self) {
self.lo = Vec2::new(self.lo.x.min(self.pos.x), self.lo.y.min(self.pos.y));
self.hi = Vec2::new(self.hi.x.max(self.pos.x), self.hi.y.max(self.pos.y));
}
pub fn interpret(&mut self, s: &str) -> Vec<Segment2> {
self.interpret_with_width(s).into_iter().map(|(seg, _)| seg).collect()
}
pub fn interpret_with_width(&mut self, s: &str) -> Vec<(Segment2, f64)> {
let mut out = Vec::new();
self.touch();
for c in s.chars() {
match c {
'F' | 'G' => {
let from = self.pos;
self.pos = self.pos
+ Vec2::new(self.heading.cos(), self.heading.sin()) * self.step;
self.touch();
if self.pen_down {
out.push((Segment2 { a: from, b: self.pos }, self.line_width));
}
}
'f' | 'g' => {
self.pos = self.pos
+ Vec2::new(self.heading.cos(), self.heading.sin()) * self.step;
self.touch();
}
'+' => self.heading += self.angle,
'-' => self.heading -= self.angle,
'|' => self.heading += std::f64::consts::PI,
'[' => self.stack.push((self.pos, self.heading, self.line_width)),
']' => {
if let Some((p, h, w)) = self.stack.pop() {
self.pos = p;
self.heading = h;
self.line_width = w;
}
}
'!' => self.line_width *= self.width_factor,
_ => {}
}
}
out
}
#[must_use]
pub fn bounds(&self) -> Rect {
Rect { min: self.lo, max: self.hi }
}
}
#[derive(Debug, Clone)]
pub struct Turtle3 {
pub frame: Frame,
pub step: f64,
pub angle: f64,
pub radius: f64,
pub taper: f64,
stack: Vec<(Frame, f64)>,
}
impl Turtle3 {
#[must_use]
pub fn new(step: f64, angle_deg: f64) -> Self {
assert!(step > 0.0, "turtle step must be positive");
Self {
frame: Frame::identity(),
step,
angle: angle_deg.to_radians(),
radius: 1.0,
taper: 0.7,
stack: Vec::new(),
}
}
fn turn(&mut self, local_axis: Vec3, angle: f64) {
let axis = self.frame.to_world_vector(local_axis);
let q = Quaternion::from_axis_angle(axis, angle);
self.frame =
Frame { origin: self.frame.origin, rotation: (q * self.frame.rotation).normalize() };
}
pub fn interpret(&mut self, s: &str) -> Vec<Segment> {
self.interpret_tree(s).into_iter().map(|(seg, _)| seg).collect()
}
pub fn interpret_tree(&mut self, s: &str) -> Vec<(Segment, f64)> {
let mut out = Vec::new();
for c in s.chars() {
match c {
'F' | 'G' => {
let from = self.frame.origin;
let dir = self.frame.to_world_vector(Vec3::new(1.0, 0.0, 0.0));
self.frame.origin = from + dir * self.step;
out.push((Segment { a: from, b: self.frame.origin }, self.radius));
}
'f' | 'g' => {
let dir = self.frame.to_world_vector(Vec3::new(1.0, 0.0, 0.0));
self.frame.origin = self.frame.origin + dir * self.step;
}
'+' => self.turn(Vec3::new(0.0, 0.0, 1.0), self.angle),
'-' => self.turn(Vec3::new(0.0, 0.0, 1.0), -self.angle),
'&' => self.turn(Vec3::new(0.0, 1.0, 0.0), self.angle),
'^' => self.turn(Vec3::new(0.0, 1.0, 0.0), -self.angle),
'\\' => self.turn(Vec3::new(1.0, 0.0, 0.0), self.angle),
'/' => self.turn(Vec3::new(1.0, 0.0, 0.0), -self.angle),
'|' => self.turn(Vec3::new(0.0, 0.0, 1.0), std::f64::consts::PI),
'[' => self.stack.push((self.frame, self.radius)),
']' => {
if let Some((f, r)) = self.stack.pop() {
self.frame = f;
self.radius = r;
}
}
'!' => self.radius *= self.taper,
_ => {}
}
}
out
}
pub fn to_mesh(&mut self, s: &str, base_radius: f64, taper: f64, segments: usize) -> Mesh {
assert!(base_radius > 0.0, "base radius must be positive");
assert!(segments >= 3, "cylinder needs >= 3 segments");
self.radius = base_radius;
self.taper = taper;
let branches = self.interpret_tree(s);
let mut mesh = Mesh { vertices: Vec::new(), indices: Vec::new(), normals: None, uvs: None };
for (seg, radius) in branches {
let axis = seg.b - seg.a;
let len = axis.magnitude();
if len <= 0.0 {
continue;
}
let mut cyl = crate::mesh::generate::cylinder(radius, len, segments, false);
let dir = axis * (1.0 / len);
let y = Vec3::new(0.0, 1.0, 0.0);
let d = y.dot(&dir).clamp(-1.0, 1.0);
let q = if d > 1.0 - 1e-12 {
Quaternion::identity()
} else if d < -1.0 + 1e-12 {
Quaternion::from_axis_angle(Vec3::new(1.0, 0.0, 0.0), std::f64::consts::PI)
} else {
Quaternion::from_axis_angle(y.cross(&dir).normalized(), d.acos())
};
cyl.rotate(&q);
cyl.translate((seg.a + seg.b) * 0.5);
mesh.merge(&cyl);
}
mesh
}
}
pub mod presets {
use super::LSystem;
#[must_use]
pub fn koch_curve() -> LSystem {
LSystem::new("F", 60.0).rule('F', "F+F--F+F")
}
#[must_use]
pub fn koch_snowflake() -> LSystem {
LSystem::new("F--F--F", 60.0).rule('F', "F+F--F+F")
}
#[must_use]
pub fn koch_island() -> LSystem {
LSystem::new("F+F+F+F", 90.0).rule('F', "F+F-F-FF+F+F-F")
}
#[must_use]
pub fn dragon() -> LSystem {
LSystem::new("FX", 90.0).rule('X', "X+YF+").rule('Y', "-FX-Y")
}
#[must_use]
pub fn hilbert() -> LSystem {
LSystem::new("A", 90.0).rule('A', "-BF+AFA+FB-").rule('B', "+AF-BFB-FA+")
}
#[must_use]
pub fn peano() -> LSystem {
LSystem::new("X", 90.0)
.rule('X', "XFYFX+F+YFXFY-F-XFYFX")
.rule('Y', "YFXFY-F-XFYFX+F+YFXFY")
}
#[must_use]
pub fn gosper() -> LSystem {
LSystem::new("F", 60.0)
.rule('F', "F-G--G+F++FF+G-")
.rule('G', "+F-GG--G-F++F+G")
}
#[must_use]
pub fn sierpinski_triangle() -> LSystem {
LSystem::new("F-G-G", 120.0).rule('F', "F-G+F+G-F").rule('G', "GG")
}
#[must_use]
pub fn sierpinski_arrowhead() -> LSystem {
LSystem::new("F", 60.0).rule('F', "G-F-G").rule('G', "F+G+F")
}
#[must_use]
pub fn levy_c() -> LSystem {
LSystem::new("F", 45.0).rule('F', "+F--F+")
}
#[must_use]
pub fn cantor() -> LSystem {
LSystem::new("F", 0.0).rule('F', "FfF").rule('f', "fff")
}
#[must_use]
pub fn plant_a() -> LSystem {
LSystem::new("F", 25.7).rule('F', "F[+F]F[-F]F")
}
#[must_use]
pub fn plant_b() -> LSystem {
LSystem::new("F", 20.0).rule('F', "F[+F]F[-F][F]")
}
#[must_use]
pub fn plant_c() -> LSystem {
LSystem::new("F", 22.5).rule('F', "FF-[-F+F+F]+[+F-F-F]")
}
#[must_use]
pub fn plant_d() -> LSystem {
LSystem::new("X", 20.0).rule('X', "F[+X]F[-X]+X").rule('F', "FF")
}
#[must_use]
pub fn plant_e() -> LSystem {
LSystem::new("X", 25.7).rule('X', "F[+X][-X]FX").rule('F', "FF")
}
#[must_use]
pub fn plant_f() -> LSystem {
LSystem::new("X", 22.5).rule('X', "F-[[X]+X]+F[+FX]-X").rule('F', "FF")
}
#[must_use]
pub fn tree_3d() -> LSystem {
LSystem::new("FA", 28.0)
.rule('A', "!F[&FA]/[&FA]/[&FA]")
.rule('/', "//") }
#[must_use]
pub fn bush_3d() -> LSystem {
LSystem::new("A", 22.5)
.rule('A', "[&FL!A]/////[&FL!A]///////[&FL!A]")
.rule('F', "S/////F")
.rule('S', "FL")
}
#[must_use]
pub fn cesaro() -> LSystem {
LSystem::new("F", 85.0).rule('F', "F+F--F+F")
}
#[must_use]
pub fn pentaplexity() -> LSystem {
LSystem::new("F++F++F++F++F", 36.0).rule('F', "F++F++F|F-F++F")
}
#[must_use]
pub fn penrose_lsystem() -> LSystem {
LSystem::new("[N]++[N]++[N]++[N]++[N]", 36.0)
.rule('M', "OF++PF----NF[-OF----MF]++")
.rule('N', "+OF--PF[---MF--NF]+")
.rule('O', "-MF++NF[+++OF++PF]-")
.rule('P', "--OF++++MF[+PF++++NF]--NF")
.rule('F', "")
}
#[must_use]
pub fn hexagonal_gosper() -> LSystem {
LSystem::new("XF", 60.0)
.rule('X', "X+YF++YF-FX--FXFX-YF+")
.rule('Y', "-FX+YFYF++YF+FX--FX-Y")
}
}
#[must_use]
pub fn lsystem_to_polylines(segments: &[Segment2]) -> Vec<Vec<Vec2>> {
let mut out: Vec<Vec<Vec2>> = Vec::new();
for seg in segments {
if let Some(last) = out.last_mut() {
let end = *last.last().expect("polylines are non-empty");
if end.distance_to(&seg.a) < 1e-9 {
last.push(seg.b);
continue;
}
}
out.push(vec![seg.a, seg.b]);
}
out
}
#[must_use]
pub fn fractal_dimension_lsystem(ls: &LSystem, iterations: usize) -> f64 {
let s = ls.generate(iterations, None);
let mut turtle = Turtle2::new(1.0, ls.angle.to_degrees());
let segments = turtle.interpret(&s);
assert!(!segments.is_empty(), "L-system draws nothing");
let b = turtle.bounds();
let extent = (b.max.x - b.min.x).max(b.max.y - b.min.y);
assert!(extent > 0.0, "degenerate drawing");
let bounds = (b.min.x, b.min.x + extent, b.min.y, b.min.y + extent);
let fine = usize::min(512, (extent.max(4.0) as usize).next_power_of_two());
let cell = extent / fine as f64;
let mut points = Vec::new();
for seg in &segments {
let len = seg.a.distance_to(&seg.b);
let steps = (len / (0.5 * cell)).ceil().max(1.0) as usize;
for i in 0..=steps {
let t = i as f64 / steps as f64;
let p = seg.a + (seg.b - seg.a) * t;
points.push((p.x, p.y));
}
}
let coarse = (fine / 4).max(2);
let n1 = crate::fractals::box_count_2d(&points, coarse, bounds) as f64;
let n2 = crate::fractals::box_count_2d(&points, fine, bounds) as f64;
(n2 / n1).ln() / (fine as f64 / coarse as f64).ln()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_koch_growth() {
let koch = presets::koch_curve();
for n in 1..=5 {
let s = koch.generate(n, None);
let mut t = Turtle2::new(1.0, 60.0);
let segs = t.interpret(&s);
assert_eq!(segs.len(), 4usize.pow(n as u32), "Koch has 4^n segments");
for seg in &segs {
assert!((seg.a.distance_to(&seg.b) - 1.0).abs() < 1e-9);
}
let b = t.bounds();
let width = b.max.x - b.min.x;
assert!((width - 3.0f64.powi(n as i32)).abs() < 1e-6, "Koch spans 3^n");
}
}
#[test]
fn test_hilbert_matches_space_filling() {
for order in 2..=4u32 {
let ls = presets::hilbert();
let s = ls.generate(order as usize, None);
let mut t = Turtle2::new(1.0, 90.0);
let segs = t.interpret(&s);
let n = 1u64 << order;
assert_eq!(segs.len() as u64, n * n - 1, "Hilbert visits n^2 cells");
for seg in &segs {
assert!((seg.a.distance_to(&seg.b) - 1.0).abs() < 1e-9);
}
let b = t.bounds();
let mut cells: Vec<(i64, i64)> = Vec::with_capacity((n * n) as usize);
let mut push = |p: crate::math::Vec2| {
cells.push(((p.x - b.min.x).round() as i64, (p.y - b.min.y).round() as i64));
};
push(segs[0].a);
for seg in &segs {
push(seg.b);
}
cells.sort_unstable();
cells.dedup();
let reference: Vec<(i64, i64)> = {
let mut v: Vec<(i64, i64)> =
crate::patterns::space_filling::hilbert_curve_2d(order)
.iter()
.map(|p| {
(
(p.x * n as f64 - 0.5).round() as i64,
(p.y * n as f64 - 0.5).round() as i64,
)
})
.collect();
v.sort_unstable();
v
};
assert_eq!(cells, reference, "same cell set as hilbert_curve_2d");
}
}
#[test]
fn test_dragon_no_self_intersection() {
let s = presets::dragon().generate(10, None);
let mut t = Turtle2::new(1.0, 90.0);
let segs = t.interpret(&s);
assert_eq!(segs.len(), 1024);
for i in 0..segs.len() {
for j in i + 1..segs.len() {
let (a, b) = (&segs[i], &segs[j]);
if let Some((s1, s2)) =
crate::spatial::intersect::segment_segment_2d_params(a, b)
{
let interior = s1 > 1e-9 && s1 < 1.0 - 1e-9 && s2 > 1e-9 && s2 < 1.0 - 1e-9;
assert!(!interior, "dragon self-intersects at pair ({i}, {j})");
}
}
}
}
#[test]
fn test_koch_box_dimension() {
let d = fractal_dimension_lsystem(&presets::koch_curve(), 5);
let expected = 4.0f64.ln() / 3.0f64.ln();
assert!((d - expected).abs() < 0.05, "Koch dimension {d} vs {expected}");
}
#[test]
fn test_stochastic_and_context_rules() {
let ls = LSystem::new("F", 25.0)
.stochastic_rule('F', &[(0.5, "F[+F]"), (0.5, "F[-F]")]);
let mut rng = Rng::new(42);
let mut saw_plus = false;
let mut saw_minus = false;
for _ in 0..40 {
let s = ls.generate(1, Some(&mut rng));
saw_plus |= s.contains("[+F]");
saw_minus |= s.contains("[-F]");
}
assert!(saw_plus && saw_minus, "both stochastic options occur");
assert_eq!(ls.generate(1, None), "F[+F]");
let mut prop = LSystem::new("BAA+A", 0.0);
prop.ignore.push('+');
prop.rules.push(Rule::Context {
left: Some("B".to_string()),
from: 'A',
right: None,
to: "B".to_string(),
});
prop.rules.push(Rule::Simple { from: 'B', to: "A".to_string() });
let s1 = prop.generate(1, None);
assert_eq!(s1, "ABA+A", "signal moved one cell");
let s2 = prop.generate(2, None);
assert_eq!(s2, "AAB+A", "signal crossed the second cell");
let s3 = prop.generate(3, None);
assert_eq!(s3, "AAA+B", "signal skips the ignored symbol");
}
#[test]
fn test_right_context_matching() {
let mut ls = LSystem::new("ABAC", 0.0);
ls.rules.push(Rule::Context {
left: None,
from: 'A',
right: Some("B".to_string()),
to: "X".to_string(),
});
assert_eq!(ls.generate(1, None), "XBAC", "only the A before B rewrites");
let mut two = LSystem::new("ABCABD", 0.0);
two.rules.push(Rule::Context {
left: None,
from: 'A',
right: Some("BC".to_string()),
to: "X".to_string(),
});
assert_eq!(two.generate(1, None), "XBCABD");
let mut edge = LSystem::new("A", 0.0);
edge.rules.push(Rule::Context {
left: None,
from: 'A',
right: Some("B".to_string()),
to: "X".to_string(),
});
assert_eq!(edge.generate(1, None), "A", "no right neighbour, no match");
let mut skip = LSystem::new("A+-BA+-C", 0.0);
skip.ignore.extend(['+', '-']);
skip.rules.push(Rule::Context {
left: None,
from: 'A',
right: Some("B".to_string()),
to: "X".to_string(),
});
assert_eq!(skip.generate(1, None), "X+-BA+-C");
let mut strict = skip.clone();
strict.ignore.clear();
assert_eq!(strict.generate(1, None), "A+-BA+-C");
let mut branch = LSystem::new("A[B]C", 0.0);
branch.rules.push(Rule::Context {
left: None,
from: 'A',
right: Some("C".to_string()),
to: "X".to_string(),
});
assert_eq!(branch.generate(1, None), "X[B]C", "branch skipped");
let mut into = LSystem::new("A[B]C", 0.0);
into.rules.push(Rule::Context {
left: None,
from: 'A',
right: Some("B".to_string()),
to: "X".to_string(),
});
assert_eq!(into.generate(1, None), "A[B]C", "branch contents are not context");
let mut nested = LSystem::new("A[B[C]D]E", 0.0);
nested.rules.push(Rule::Context {
left: None,
from: 'A',
right: Some("E".to_string()),
to: "X".to_string(),
});
assert_eq!(nested.generate(1, None), "X[B[C]D]E");
let mut open = LSystem::new("A[BE", 0.0);
open.rules.push(Rule::Context {
left: None,
from: 'A',
right: Some("E".to_string()),
to: "X".to_string(),
});
assert_eq!(open.generate(1, None), "A[BE");
let mut inside = LSystem::new("[A]B", 0.0);
inside.rules.push(Rule::Context {
left: None,
from: 'A',
right: Some("B".to_string()),
to: "X".to_string(),
});
assert_eq!(inside.generate(1, None), "[A]B", "context stops at ']'");
let mut both = LSystem::new("BACAAC", 0.0);
both.rules.push(Rule::Context {
left: Some("B".to_string()),
from: 'A',
right: Some("C".to_string()),
to: "X".to_string(),
});
assert_eq!(both.generate(1, None), "BXCAAC");
let mut prop = LSystem::new("AAAX", 0.0);
prop.rules.push(Rule::Context {
left: None,
from: 'A',
right: Some("X".to_string()),
to: "X".to_string(),
});
prop.rules.push(Rule::Simple { from: 'X', to: "A".to_string() });
assert_eq!(prop.generate(1, None), "AAXA");
assert_eq!(prop.generate(2, None), "AXAA");
assert_eq!(prop.generate(3, None), "XAAA");
}
#[test]
fn test_bush_3d_preset_expansion_and_drawing() {
let bush = presets::bush_3d();
assert_eq!(bush.axiom, "A");
assert!((bush.angle - 22.5f64.to_radians()).abs() < 1e-15);
let alphabet: Vec<char> = "AFLS&!/[]".chars().collect();
let mut previous = bush.axiom.len();
for n in 1..=5usize {
let s = bush.generate(n, None);
for c in s.chars() {
assert!(alphabet.contains(&c), "iteration {n}: stray symbol {c:?}");
}
assert!(s.len() > previous, "iteration {n}: {} vs {previous}", s.len());
previous = s.len();
assert_eq!(
s.chars().filter(|&c| c == 'A').count(),
3usize.pow(n as u32),
"iteration {n}: apex count"
);
let mut depth = 0i32;
let mut max_depth = 0i32;
for c in s.chars() {
match c {
'[' => {
depth += 1;
max_depth = max_depth.max(depth);
}
']' => {
depth -= 1;
assert!(depth >= 0, "iteration {n}: unbalanced ']'");
}
_ => {}
}
}
assert_eq!(depth, 0, "iteration {n}: unbalanced brackets");
assert_eq!(max_depth, n as i32, "iteration {n}: nesting depth");
let mut t = Turtle3::new(1.0, bush.angle.to_degrees());
let segs = t.interpret(&s);
assert_eq!(
segs.len(),
s.chars().filter(|&c| c == 'F').count(),
"iteration {n}: one segment per F"
);
assert!(!segs.is_empty(), "iteration {n} draws nothing");
for seg in &segs {
assert!(
((seg.b - seg.a).magnitude() - 1.0).abs() < 1e-12,
"iteration {n}: non-unit step"
);
assert!(seg.a.x.is_finite() && seg.b.z.is_finite());
}
assert!(
t.frame.origin.magnitude() < 1e-12,
"iteration {n}: turtle should return to the origin"
);
if n >= 2 {
let out_of_plane = segs.iter().any(|s| s.b.z.abs() > 0.1 || s.a.z.abs() > 0.1);
assert!(out_of_plane, "iteration {n}: bush stayed planar");
}
}
let mut t = Turtle3::new(1.0, 22.5);
let mesh = t.to_mesh(&bush.generate(3, None), 0.05, 0.6, 6);
assert!(!mesh.vertices.is_empty());
assert!(mesh.surface_area() > 0.0);
let radii: Vec<f64> = {
let mut t2 = Turtle3::new(1.0, 22.5);
t2.radius = 0.05;
t2.taper = 0.6;
t2.interpret_tree(&bush.generate(3, None)).into_iter().map(|(_, r)| r).collect()
};
let rmax = radii.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let rmin = radii.iter().cloned().fold(f64::INFINITY, f64::min);
assert!(rmin < rmax, "branches taper ({rmin} .. {rmax})");
assert!((rmax - 0.05).abs() < 1e-12, "trunk keeps the base radius");
}
#[test]
fn test_turtle3_and_mesh() {
let mut t = Turtle3::new(1.0, 90.0);
let segs = t.interpret("F+F");
assert_eq!(segs.len(), 2);
assert!((segs[1].b - Vec3::new(1.0, 1.0, 0.0)).magnitude() < 1e-12);
let mut t2 = Turtle3::new(1.0, 90.0);
let segs2 = t2.interpret("&F");
assert!(segs2[0].b.z.abs() > 0.99, "pitch moves out of the plane");
let mut t3 = Turtle3::new(1.0, 90.0);
let segs3 = t3.interpret("F[+F]F");
assert!((segs3[2].b - Vec3::new(2.0, 0.0, 0.0)).magnitude() < 1e-12);
let mut t4 = Turtle3::new(1.0, 28.0);
let mesh = t4.to_mesh(&presets::tree_3d().generate(2, None), 0.1, 0.7, 6);
assert!(!mesh.vertices.is_empty());
assert!(mesh.surface_area() > 0.0);
}
#[test]
fn test_polylines_and_presets_draw() {
let mut t = Turtle2::new(1.0, 90.0);
let segs = t.interpret("FF[+F]F");
let polys = lsystem_to_polylines(&segs);
assert_eq!(polys.len(), 2);
assert_eq!(polys[0].len(), 4);
for (name, ls, n) in [
("snowflake", presets::koch_snowflake(), 3),
("island", presets::koch_island(), 2),
("peano", presets::peano(), 2),
("gosper", presets::gosper(), 3),
("sierpinski", presets::sierpinski_triangle(), 4),
("arrowhead", presets::sierpinski_arrowhead(), 4),
("levy", presets::levy_c(), 6),
("cantor", presets::cantor(), 4),
("plant_a", presets::plant_a(), 3),
("plant_b", presets::plant_b(), 3),
("plant_c", presets::plant_c(), 3),
("plant_d", presets::plant_d(), 4),
("plant_e", presets::plant_e(), 4),
("plant_f", presets::plant_f(), 4),
("cesaro", presets::cesaro(), 4),
("pentaplexity", presets::pentaplexity(), 3),
("penrose", presets::penrose_lsystem(), 4),
("hex_gosper", presets::hexagonal_gosper(), 3),
] {
let s = ls.generate(n, None);
let mut turtle = Turtle2::new(1.0, ls.angle.to_degrees());
let segs = turtle.interpret(&s);
assert!(!segs.is_empty(), "{name} draws segments");
for seg in &segs {
assert!(seg.a.x.is_finite() && seg.b.y.is_finite(), "{name} finite");
}
}
}
#[test]
fn test_sierpinski_triangle_dimension() {
let d = fractal_dimension_lsystem(&presets::sierpinski_triangle(), 7);
let expected = 3.0f64.ln() / 2.0f64.ln();
assert!((d - expected).abs() < 0.1, "gasket dimension {d} vs {expected}");
}
}