Skip to main content

oxiphysics_geometry/fractal_geometry/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5#[allow(unused_imports)]
6use super::functions::*;
7use rand::RngExt;
8use std::collections::HashMap;
9
10/// Lacunarity computation for fractal point sets.
11///
12/// Lacunarity measures the "gappiness" or texture of a fractal. A higher
13/// lacunarity indicates more heterogeneity (more gaps).
14pub struct Lacunarity;
15impl Lacunarity {
16    /// Compute the gliding-box lacunarity of a 2D binary image (grid).
17    ///
18    /// `grid` is a 2D boolean array where `true` indicates the fractal is present.
19    /// `box_size` is the side length of the gliding box.
20    ///
21    /// Returns the lacunarity value Lambda = Var(mass) / Mean(mass)^2 + 1.
22    #[allow(dead_code)]
23    pub fn gliding_box(grid: &[Vec<bool>], box_size: usize) -> f64 {
24        let rows = grid.len();
25        if rows == 0 || box_size == 0 {
26            return 0.0;
27        }
28        let cols = grid[0].len();
29        if box_size > rows || box_size > cols {
30            return 0.0;
31        }
32        let mut masses = Vec::new();
33        for r in 0..=(rows - box_size) {
34            for c in 0..=(cols - box_size) {
35                let mut mass = 0u64;
36                for dr in 0..box_size {
37                    for dc in 0..box_size {
38                        if grid[r + dr][c + dc] {
39                            mass += 1;
40                        }
41                    }
42                }
43                masses.push(mass as f64);
44            }
45        }
46        if masses.is_empty() {
47            return 0.0;
48        }
49        let n = masses.len() as f64;
50        let mean = masses.iter().sum::<f64>() / n;
51        if mean.abs() < 1e-15 {
52            return 0.0;
53        }
54        let variance = masses.iter().map(|&m| (m - mean).powi(2)).sum::<f64>() / n;
55        variance / (mean * mean) + 1.0
56    }
57    /// Compute lacunarity for a point set by first rasterizing onto a grid.
58    ///
59    /// `resolution` is the grid side length. Points are mapped to grid cells.
60    #[allow(dead_code)]
61    pub fn from_points(points: &[Point2], resolution: usize, box_size: usize) -> f64 {
62        if points.is_empty() || resolution == 0 {
63            return 0.0;
64        }
65        let (min_x, max_x, min_y, max_y) = FractalDimension::bounding_box(points);
66        let dx = (max_x - min_x).max(1e-15);
67        let dy = (max_y - min_y).max(1e-15);
68        let mut grid = vec![vec![false; resolution]; resolution];
69        for p in points {
70            let ix = (((p.x - min_x) / dx) * (resolution - 1) as f64)
71                .round()
72                .clamp(0.0, (resolution - 1) as f64) as usize;
73            let iy = (((p.y - min_y) / dy) * (resolution - 1) as f64)
74                .round()
75                .clamp(0.0, (resolution - 1) as f64) as usize;
76            grid[iy][ix] = true;
77        }
78        Self::gliding_box(&grid, box_size)
79    }
80    /// Multi-scale lacunarity: compute lacunarity for a range of box sizes.
81    ///
82    /// Returns `(box_size, lacunarity)` pairs.
83    #[allow(dead_code)]
84    pub fn multiscale(grid: &[Vec<bool>], box_sizes: &[usize]) -> Vec<(usize, f64)> {
85        box_sizes
86            .iter()
87            .map(|&bs| (bs, Self::gliding_box(grid, bs)))
88            .collect()
89    }
90}
91/// Sierpinski arrowhead curve L-system preset.
92pub struct SierpinskiArrowhead;
93impl SierpinskiArrowhead {
94    /// Create the L-system for a Sierpinski arrowhead curve.
95    pub fn lsystem() -> LSystem {
96        LSystem::new(
97            vec!['F', 'G', '+', '-'],
98            vec![
99                ProductionRule::new('F', "G-F-G"),
100                ProductionRule::new('G', "F+G+F"),
101            ],
102            "F",
103        )
104    }
105    /// Theoretical dimension: log(3)/log(2) ≈ 1.585.
106    pub fn theoretical_dimension() -> f64 {
107        (3.0_f64).ln() / (2.0_f64).ln()
108    }
109    /// Generate Sierpinski arrowhead segments.
110    pub fn generate(generations: usize, step_size: f64) -> Vec<(Point2, Point2)> {
111        let ls = Self::lsystem();
112        ls.turtle_interpret(generations, step_size, 60.0)
113    }
114}
115/// An Iterated Function System consisting of affine transforms with associated probabilities.
116#[derive(Debug, Clone)]
117pub struct IteratedFunctionSystem {
118    /// The affine transforms.
119    pub transforms: Vec<AffineTransform2D>,
120    /// Probability weights for each transform (must sum to ~1.0).
121    pub probabilities: Vec<f64>,
122}
123impl IteratedFunctionSystem {
124    /// Create a new IFS with given transforms and probabilities.
125    ///
126    /// Handles empty transforms gracefully (returns empty results).
127    pub fn new(transforms: Vec<AffineTransform2D>, probabilities: Vec<f64>) -> Self {
128        if transforms.is_empty() {
129            return Self {
130                transforms: Vec::new(),
131                probabilities: Vec::new(),
132            };
133        }
134        assert_eq!(
135            transforms.len(),
136            probabilities.len(),
137            "transforms and probabilities must have equal length"
138        );
139        Self {
140            transforms,
141            probabilities,
142        }
143    }
144    /// Create an IFS with equal probabilities for all transforms.
145    pub fn with_equal_probabilities(transforms: Vec<AffineTransform2D>) -> Self {
146        let n = transforms.len();
147        assert!(n > 0, "IFS must have at least one transform");
148        let prob = 1.0 / n as f64;
149        let probabilities = vec![prob; n];
150        Self {
151            transforms,
152            probabilities,
153        }
154    }
155    /// Run the chaos game algorithm to generate attractor points.
156    ///
157    /// Starting from `start`, iterates `iterations` times selecting transforms
158    /// according to their probabilities. Returns the generated points after
159    /// discarding the first `skip` transient points.
160    pub fn chaos_game(&self, start: Point2, iterations: usize, skip: usize) -> Vec<Point2> {
161        use rand::RngExt;
162        let mut rng = rand::rng();
163        let mut point = start;
164        let mut points = Vec::with_capacity(iterations.saturating_sub(skip));
165        let cumulative = self.cumulative_distribution();
166        for i in 0..iterations {
167            let r: f64 = rng.random_range(0.0..1.0);
168            let idx = cumulative
169                .iter()
170                .position(|&c| r < c)
171                .unwrap_or(self.transforms.len() - 1);
172            point = self.transforms[idx].apply(&point);
173            if i >= skip {
174                points.push(point);
175            }
176        }
177        points
178    }
179    /// Deterministic iteration: apply all transforms to every point in the set.
180    ///
181    /// Starting from `initial_points`, applies all transforms to produce
182    /// the next generation. Repeats for `generations` steps.
183    pub fn deterministic_iterate(
184        &self,
185        initial_points: &[Point2],
186        generations: usize,
187    ) -> Vec<Point2> {
188        let mut current = initial_points.to_vec();
189        for _ in 0..generations {
190            let mut next = Vec::with_capacity(current.len() * self.transforms.len());
191            for t in &self.transforms {
192                for p in &current {
193                    next.push(t.apply(p));
194                }
195            }
196            current = next;
197        }
198        current
199    }
200    /// Number of transforms in this IFS.
201    pub fn num_transforms(&self) -> usize {
202        self.transforms.len()
203    }
204    /// Create the Barnsley Fern IFS.
205    pub fn barnsley_fern() -> Self {
206        let transforms = vec![
207            AffineTransform2D::new(0.0, 0.0, 0.0, 0.16, 0.0, 0.0),
208            AffineTransform2D::new(0.85, 0.04, -0.04, 0.85, 0.0, 1.6),
209            AffineTransform2D::new(0.20, -0.26, 0.23, 0.22, 0.0, 1.6),
210            AffineTransform2D::new(-0.15, 0.28, 0.26, 0.24, 0.0, 0.44),
211        ];
212        let probabilities = vec![0.01, 0.85, 0.07, 0.07];
213        Self::new(transforms, probabilities)
214    }
215    /// Generate `n` points using a deterministic seeded chaos game.
216    ///
217    /// Returns points as `[x, y]` arrays.
218    pub fn generate(&self, n: usize, seed: u64) -> Vec<[f64; 2]> {
219        if self.transforms.is_empty() {
220            return Vec::new();
221        }
222        let cumulative = self.cumulative_distribution();
223        let mut x = 0.0_f64;
224        let mut y = 0.0_f64;
225        let mut rng_state = seed;
226        let mut points = Vec::with_capacity(n);
227        // Simple LCG pseudo-random for reproducibility
228        for _ in 0..n {
229            rng_state = rng_state
230                .wrapping_mul(6364136223846793005)
231                .wrapping_add(1442695040888963407);
232            let r = ((rng_state >> 33) as f64) / (u32::MAX as f64);
233            let idx = cumulative
234                .iter()
235                .position(|&c| r < c)
236                .unwrap_or(self.transforms.len() - 1);
237            let t = &self.transforms[idx];
238            let nx = t.a * x + t.b * y + t.e;
239            let ny = t.c * x + t.d * y + t.f;
240            x = nx;
241            y = ny;
242            points.push([x, y]);
243        }
244        points
245    }
246    /// Build cumulative distribution from probabilities.
247    fn cumulative_distribution(&self) -> Vec<f64> {
248        let mut cum = Vec::with_capacity(self.probabilities.len());
249        let mut acc = 0.0;
250        for &p in &self.probabilities {
251            acc += p;
252            cum.push(acc);
253        }
254        cum
255    }
256}
257/// A production rule for an L-system.
258#[derive(Debug, Clone)]
259pub struct ProductionRule {
260    /// The predecessor character.
261    pub predecessor: char,
262    /// The successor string.
263    pub successor: String,
264}
265impl ProductionRule {
266    /// Create a new production rule.
267    pub fn new(predecessor: char, successor: impl Into<String>) -> Self {
268        Self {
269            predecessor,
270            successor: successor.into(),
271        }
272    }
273}
274/// The Mandelbrot set: z_{n+1} = z_n^2 + c, starting from z_0 = 0.
275///
276/// Points `c` in the complex plane are classified by the escape time
277/// of the orbit {z_n}.
278pub struct MandelbrotSet {
279    /// Maximum number of iterations before declaring a point in the set.
280    pub max_iter: u32,
281    /// Escape radius squared.
282    pub escape_radius_sq: f64,
283}
284impl MandelbrotSet {
285    /// Create a new Mandelbrot set with given parameters.
286    pub fn new(max_iter: u32, escape_radius: f64) -> Self {
287        Self {
288            max_iter,
289            escape_radius_sq: escape_radius * escape_radius,
290        }
291    }
292    /// Default Mandelbrot set with max_iter=256, escape_radius=2.
293    pub fn default_set() -> Self {
294        Self::new(256, 2.0)
295    }
296    /// Compute escape time for a complex number c = (cr, ci).
297    ///
298    /// Returns `None` if the orbit does not escape within `max_iter` iterations,
299    /// or `Some(iterations)` with the escape iteration count.
300    pub fn escape_time(&self, cr: f64, ci: f64) -> Option<u32> {
301        let mut zr = 0.0;
302        let mut zi = 0.0;
303        for i in 0..self.max_iter {
304            let zr2 = zr * zr;
305            let zi2 = zi * zi;
306            if zr2 + zi2 > self.escape_radius_sq {
307                return Some(i);
308            }
309            zi = 2.0 * zr * zi + ci;
310            zr = zr2 - zi2 + cr;
311        }
312        None
313    }
314    /// Smooth escape time using renormalization.
315    ///
316    /// Returns a continuous (smooth) iteration count for coloring.
317    pub fn smooth_escape_time(&self, cr: f64, ci: f64) -> f64 {
318        let mut zr = 0.0;
319        let mut zi = 0.0;
320        for i in 0..self.max_iter {
321            let zr2 = zr * zr;
322            let zi2 = zi * zi;
323            if zr2 + zi2 > self.escape_radius_sq {
324                let modulus = (zr2 + zi2).sqrt();
325                let mu = i as f64 - modulus.ln().ln() / (2.0_f64).ln();
326                return mu;
327            }
328            zi = 2.0 * zr * zi + ci;
329            zr = zr2 - zi2 + cr;
330        }
331        self.max_iter as f64
332    }
333    /// Check if a point is in the Mandelbrot set.
334    pub fn is_in_set(&self, cr: f64, ci: f64) -> bool {
335        self.escape_time(cr, ci).is_none()
336    }
337    /// Detect if a point is near the boundary of the set.
338    ///
339    /// A point is near the boundary if it is in the set but has a neighbor
340    /// that is not (or vice versa), within the given `delta` distance.
341    pub fn is_boundary(&self, cr: f64, ci: f64, delta: f64) -> bool {
342        let center = self.is_in_set(cr, ci);
343        let offsets = [(delta, 0.0), (-delta, 0.0), (0.0, delta), (0.0, -delta)];
344        for (dr, di) in offsets {
345            if self.is_in_set(cr + dr, ci + di) != center {
346                return true;
347            }
348        }
349        false
350    }
351    /// Compute the area of the Mandelbrot set using Monte Carlo sampling.
352    ///
353    /// Samples random points in the rectangle \[x_min, x_max\] x \[y_min, y_max\]
354    /// and estimates the area as the fraction of points in the set times the
355    /// rectangle area.
356    #[allow(dead_code)]
357    pub fn area_monte_carlo(
358        &self,
359        x_min: f64,
360        x_max: f64,
361        y_min: f64,
362        y_max: f64,
363        num_samples: usize,
364    ) -> f64 {
365        let mut rng = rand::rng();
366        let mut count = 0usize;
367        for _ in 0..num_samples {
368            let cr = rng.random_range(x_min..x_max);
369            let ci = rng.random_range(y_min..y_max);
370            if self.is_in_set(cr, ci) {
371                count += 1;
372            }
373        }
374        let rect_area = (x_max - x_min) * (y_max - y_min);
375        rect_area * count as f64 / num_samples as f64
376    }
377    /// Generate a grid of escape times for rendering.
378    ///
379    /// Returns a row-major `width x height` grid of escape times.
380    pub fn render_grid(
381        &self,
382        x_min: f64,
383        x_max: f64,
384        y_min: f64,
385        y_max: f64,
386        width: usize,
387        height: usize,
388    ) -> Vec<Vec<Option<u32>>> {
389        let dx = (x_max - x_min) / width as f64;
390        let dy = (y_max - y_min) / height as f64;
391        (0..height)
392            .map(|j| {
393                let ci = y_min + (j as f64 + 0.5) * dy;
394                (0..width)
395                    .map(|i| {
396                        let cr = x_min + (i as f64 + 0.5) * dx;
397                        self.escape_time(cr, ci)
398                    })
399                    .collect()
400            })
401            .collect()
402    }
403}
404/// Hilbert curve coordinate mapping (non-L-system approach).
405///
406/// Provides index-to-coordinate and coordinate-to-index conversions
407/// for Hilbert curves of order `n` (grid size `2^n x 2^n`).
408pub struct HilbertCurveMap;
409impl HilbertCurveMap {
410    /// Convert a Hilbert curve index `d` to (x, y) coordinates for a curve of order `n`.
411    #[allow(dead_code)]
412    pub fn d2xy(n: u32, d: u32) -> (u32, u32) {
413        let mut rx: u32;
414        let mut ry: u32;
415        let mut x = 0_u32;
416        let mut y = 0_u32;
417        let mut d = d;
418        let mut s = 1_u32;
419        while s < n {
420            rx = if (d & 2) != 0 { 1 } else { 0 };
421            ry = if (d & 1) != 0 {
422                if rx == 0 { 1 } else { 0 }
423            } else {
424                0
425            };
426            Self::rot(s, &mut x, &mut y, rx, ry);
427            x += s * rx;
428            y += s * ry;
429            d /= 4;
430            s *= 2;
431        }
432        (x, y)
433    }
434    /// Convert (x, y) coordinates to a Hilbert curve index for a curve of order `n`.
435    #[allow(dead_code)]
436    pub fn xy2d(n: u32, x: u32, y: u32) -> u32 {
437        let mut rx: u32;
438        let mut ry: u32;
439        let mut d = 0_u32;
440        let mut x = x;
441        let mut y = y;
442        let mut s = n / 2;
443        while s > 0 {
444            rx = if (x & s) > 0 { 1 } else { 0 };
445            ry = if (y & s) > 0 { 1 } else { 0 };
446            d += s * s * ((3 * rx) ^ ry);
447            Self::rot(s, &mut x, &mut y, rx, ry);
448            s /= 2;
449        }
450        d
451    }
452    /// Rotate/flip a quadrant.
453    #[allow(dead_code)]
454    fn rot(n: u32, x: &mut u32, y: &mut u32, rx: u32, ry: u32) {
455        if ry == 0 {
456            if rx == 1 {
457                *x = n.wrapping_sub(1).wrapping_sub(*x);
458                *y = n.wrapping_sub(1).wrapping_sub(*y);
459            }
460            std::mem::swap(x, y);
461        }
462    }
463    /// Generate the full Hilbert curve path as a sequence of (x, y) for order `n`.
464    ///
465    /// `n` must be a power of 2. Returns `n*n` points.
466    #[allow(dead_code)]
467    pub fn generate_path(n: u32) -> Vec<(u32, u32)> {
468        let total = n * n;
469        (0..total).map(|d| Self::d2xy(n, d)).collect()
470    }
471}
472/// Fractal terrain generator using the diamond-square algorithm.
473///
474/// Produces a heightmap on a (2^n + 1) x (2^n + 1) grid.
475pub struct FractalTerrain;
476impl FractalTerrain {
477    /// Generate a terrain heightmap using the diamond-square algorithm.
478    ///
479    /// `power` determines the grid size: side = 2^power + 1.
480    /// `roughness` controls how quickly the random displacement decreases (0..1 typical).
481    /// `seed_corners` sets the four corner heights `[top_left, top_right, bot_left, bot_right]`.
482    #[allow(dead_code)]
483    pub fn diamond_square(power: u32, roughness: f64, seed_corners: [f64; 4]) -> Vec<Vec<f64>> {
484        let size = (1 << power) + 1;
485        let mut grid = vec![vec![0.0_f64; size]; size];
486        let last = size - 1;
487        grid[0][0] = seed_corners[0];
488        grid[0][last] = seed_corners[1];
489        grid[last][0] = seed_corners[2];
490        grid[last][last] = seed_corners[3];
491        let mut rng = rand::rng();
492        let mut step = last;
493        let mut scale = 1.0_f64;
494        while step > 1 {
495            let half = step / 2;
496            let mut y = 0;
497            while y < last {
498                let mut x = 0;
499                while x < last {
500                    let avg = (grid[y][x]
501                        + grid[y][x + step]
502                        + grid[y + step][x]
503                        + grid[y + step][x + step])
504                        / 4.0;
505                    grid[y + half][x + half] = avg + rng.random_range(-scale..scale);
506                    x += step;
507                }
508                y += step;
509            }
510            let mut y = 0;
511            while y <= last {
512                let x_start = if (y / half).is_multiple_of(2) {
513                    half
514                } else {
515                    0
516                };
517                let mut x = x_start;
518                while x <= last {
519                    let mut sum = 0.0;
520                    let mut count = 0.0;
521                    if y >= half {
522                        sum += grid[y - half][x];
523                        count += 1.0;
524                    }
525                    if y + half <= last {
526                        sum += grid[y + half][x];
527                        count += 1.0;
528                    }
529                    if x >= half {
530                        sum += grid[y][x - half];
531                        count += 1.0;
532                    }
533                    if x + half <= last {
534                        sum += grid[y][x + half];
535                        count += 1.0;
536                    }
537                    grid[y][x] = sum / count + rng.random_range(-scale..scale);
538                    x += step;
539                }
540                y += half;
541            }
542            step = half;
543            scale *= roughness;
544        }
545        grid
546    }
547    /// Generate terrain using midpoint displacement (1D variant).
548    ///
549    /// Produces a heightmap profile (1D array) of size 2^power + 1.
550    /// `roughness` is the Hurst exponent factor (0 < roughness < 1).
551    #[allow(dead_code)]
552    pub fn midpoint_displacement_1d(
553        power: u32,
554        roughness: f64,
555        h_left: f64,
556        h_right: f64,
557    ) -> Vec<f64> {
558        let size = (1 << power) + 1;
559        let mut heights = vec![0.0_f64; size];
560        heights[0] = h_left;
561        heights[size - 1] = h_right;
562        let mut rng = rand::rng();
563        let mut step = size - 1;
564        let mut scale = 1.0_f64;
565        while step > 1 {
566            let half = step / 2;
567            let mut i = half;
568            while i < size {
569                let left = heights[i - half];
570                let right = if i + half < size {
571                    heights[i + half]
572                } else {
573                    heights[size - 1]
574                };
575                heights[i] = (left + right) / 2.0 + rng.random_range(-scale..scale);
576                i += step;
577            }
578            step = half;
579            scale *= roughness;
580        }
581        heights
582    }
583    /// Compute the RMS (root mean square) roughness of a heightmap.
584    #[allow(dead_code)]
585    pub fn rms_roughness(grid: &[Vec<f64>]) -> f64 {
586        let mut sum = 0.0;
587        let mut count = 0usize;
588        let mut mean = 0.0;
589        for row in grid {
590            for &h in row {
591                mean += h;
592                count += 1;
593            }
594        }
595        if count == 0 {
596            return 0.0;
597        }
598        mean /= count as f64;
599        for row in grid {
600            for &h in row {
601                sum += (h - mean).powi(2);
602            }
603        }
604        (sum / count as f64).sqrt()
605    }
606    /// Find the min and max height in a grid.
607    #[allow(dead_code)]
608    pub fn height_range(grid: &[Vec<f64>]) -> (f64, f64) {
609        let mut lo = f64::MAX;
610        let mut hi = f64::MIN;
611        for row in grid {
612            for &h in row {
613                if h < lo {
614                    lo = h;
615                }
616                if h > hi {
617                    hi = h;
618                }
619            }
620        }
621        (lo, hi)
622    }
623}
624/// Simple fractal mesh: triangle mesh from a fractal terrain heightmap.
625///
626/// Converts a 2D heightmap grid into a triangle mesh with vertices and triangle
627/// index lists suitable for rendering.
628pub struct FractalMesh;
629impl FractalMesh {
630    /// Convert a heightmap to a triangle mesh.
631    ///
632    /// Returns `(vertices, triangles)` where vertices are `[f64; 3]` positions
633    /// and triangles are index triples into the vertex array.
634    #[allow(dead_code)]
635    pub fn from_heightmap(grid: &[Vec<f64>], spacing: f64) -> (Vec<[f64; 3]>, Vec<[usize; 3]>) {
636        let rows = grid.len();
637        if rows == 0 {
638            return (Vec::new(), Vec::new());
639        }
640        let cols = grid[0].len();
641        let mut vertices = Vec::with_capacity(rows * cols);
642        for (r, row) in grid.iter().enumerate() {
643            for (c, &h) in row.iter().enumerate() {
644                vertices.push([c as f64 * spacing, h, r as f64 * spacing]);
645            }
646        }
647        let mut triangles = Vec::new();
648        for r in 0..(rows - 1) {
649            for c in 0..(cols - 1) {
650                let i0 = r * cols + c;
651                let i1 = i0 + 1;
652                let i2 = i0 + cols;
653                let i3 = i2 + 1;
654                triangles.push([i0, i2, i1]);
655                triangles.push([i1, i2, i3]);
656            }
657        }
658        (vertices, triangles)
659    }
660    /// Compute the total surface area of a triangle mesh.
661    #[allow(dead_code)]
662    pub fn surface_area(vertices: &[[f64; 3]], triangles: &[[usize; 3]]) -> f64 {
663        let mut area = 0.0;
664        for tri in triangles {
665            let a = vertices[tri[0]];
666            let b = vertices[tri[1]];
667            let c = vertices[tri[2]];
668            let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
669            let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
670            let cross = [
671                ab[1] * ac[2] - ab[2] * ac[1],
672                ab[2] * ac[0] - ab[0] * ac[2],
673                ab[0] * ac[1] - ab[1] * ac[0],
674            ];
675            area += (cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]).sqrt() / 2.0;
676        }
677        area
678    }
679    /// Number of vertices and triangles expected from a heightmap of given size.
680    #[allow(dead_code)]
681    pub fn expected_counts(rows: usize, cols: usize) -> (usize, usize) {
682        let verts = rows * cols;
683        let tris = if rows > 1 && cols > 1 {
684            2 * (rows - 1) * (cols - 1)
685        } else {
686            0
687        };
688        (verts, tris)
689    }
690}
691/// Hilbert curve L-system preset.
692pub struct HilbertCurve;
693impl HilbertCurve {
694    /// Create the L-system for a Hilbert curve.
695    pub fn lsystem() -> LSystem {
696        LSystem::new(
697            vec!['A', 'B', 'F', '+', '-'],
698            vec![
699                ProductionRule::new('A', "+BF-AFA-FB+"),
700                ProductionRule::new('B', "-AF+BFB+FA-"),
701            ],
702            "A",
703        )
704    }
705    /// Generate Hilbert curve segments.
706    pub fn generate(generations: usize, step_size: f64) -> Vec<(Point2, Point2)> {
707        let ls = Self::lsystem();
708        ls.turtle_interpret(generations, step_size, 90.0)
709    }
710}
711/// Methods for estimating the fractal dimension of a point set.
712pub struct FractalDimension;
713impl FractalDimension {
714    /// Estimate the box-counting (Minkowski) dimension of a 2D point set.
715    ///
716    /// Counts the number of boxes of side `epsilon` needed to cover the point set,
717    /// for geometrically decreasing `epsilon` values. Returns `(dimension, r_squared)`
718    /// where `r_squared` is the coefficient of determination of the linear fit.
719    pub fn box_counting(points: &[Point2], num_scales: usize) -> (f64, f64) {
720        if points.is_empty() || num_scales < 2 {
721            return (0.0, 0.0);
722        }
723        let (min_x, max_x, min_y, max_y) = Self::bounding_box(points);
724        let extent = (max_x - min_x).max(max_y - min_y).max(1e-15);
725        let mut log_eps = Vec::with_capacity(num_scales);
726        let mut log_n = Vec::with_capacity(num_scales);
727        for i in 0..num_scales {
728            let epsilon = extent / (2.0_f64).powi(i as i32 + 1);
729            if epsilon < 1e-15 {
730                break;
731            }
732            let mut occupied = std::collections::HashSet::new();
733            for p in points {
734                let ix = ((p.x - min_x) / epsilon).floor() as i64;
735                let iy = ((p.y - min_y) / epsilon).floor() as i64;
736                occupied.insert((ix, iy));
737            }
738            let count = occupied.len() as f64;
739            if count > 0.0 {
740                log_eps.push(epsilon.ln());
741                log_n.push(count.ln());
742            }
743        }
744        if log_eps.len() < 2 {
745            return (0.0, 0.0);
746        }
747        let (slope, r_sq) = Self::linear_regression(&log_eps, &log_n);
748        (-slope, r_sq)
749    }
750    /// Estimate the correlation dimension of a point set.
751    ///
752    /// Uses the Grassberger-Procaccia algorithm. For various radii `r`,
753    /// counts pairs of points within distance `r`, then fits `log C(r)` vs `log r`.
754    pub fn correlation_dimension(points: &[Point2], num_scales: usize) -> (f64, f64) {
755        let n = points.len();
756        if n < 10 || num_scales < 2 {
757            return (0.0, 0.0);
758        }
759        let mut max_dist = 0.0_f64;
760        let mut min_dist = f64::MAX;
761        let num_pairs = n * (n - 1) / 2;
762        let mut distances = Vec::with_capacity(num_pairs);
763        for i in 0..n {
764            for j in (i + 1)..n {
765                let d = points[i].distance(&points[j]);
766                distances.push(d);
767                if d > max_dist {
768                    max_dist = d;
769                }
770                if d > 0.0 && d < min_dist {
771                    min_dist = d;
772                }
773            }
774        }
775        if max_dist < 1e-15 {
776            return (0.0, 0.0);
777        }
778        distances.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
779        let mut log_r = Vec::with_capacity(num_scales);
780        let mut log_c = Vec::with_capacity(num_scales);
781        let r_min = min_dist.max(max_dist * 0.001);
782        let r_max = max_dist * 0.5;
783        let ratio = (r_max / r_min).powf(1.0 / (num_scales as f64 - 1.0));
784        for i in 0..num_scales {
785            let r = r_min * ratio.powi(i as i32);
786            let count = distances.partition_point(|&d| d <= r);
787            if count > 0 {
788                let c = count as f64 / num_pairs as f64;
789                log_r.push(r.ln());
790                log_c.push(c.ln());
791            }
792        }
793        if log_r.len() < 2 {
794            return (0.0, 0.0);
795        }
796        let (slope, r_sq) = Self::linear_regression(&log_r, &log_c);
797        (slope, r_sq)
798    }
799    /// Estimate Hausdorff dimension using box-counting with Richardson extrapolation.
800    ///
801    /// This provides a more accurate estimate by using multiple scale ranges.
802    pub fn hausdorff_estimate(points: &[Point2], num_scales: usize) -> f64 {
803        if points.is_empty() || num_scales < 4 {
804            return 0.0;
805        }
806        let (dim1, _) = Self::box_counting(points, num_scales);
807        let half = num_scales / 2;
808        let (dim2, _) = Self::box_counting(points, half);
809        0.6 * dim1 + 0.4 * dim2
810    }
811    /// Compute bounding box of a point set.
812    pub(super) fn bounding_box(points: &[Point2]) -> (f64, f64, f64, f64) {
813        let mut min_x = f64::MAX;
814        let mut max_x = f64::MIN;
815        let mut min_y = f64::MAX;
816        let mut max_y = f64::MIN;
817        for p in points {
818            if p.x < min_x {
819                min_x = p.x;
820            }
821            if p.x > max_x {
822                max_x = p.x;
823            }
824            if p.y < min_y {
825                min_y = p.y;
826            }
827            if p.y > max_y {
828                max_y = p.y;
829            }
830        }
831        (min_x, max_x, min_y, max_y)
832    }
833    /// Simple linear regression returning (slope, r_squared).
834    fn linear_regression(x: &[f64], y: &[f64]) -> (f64, f64) {
835        let n = x.len() as f64;
836        let sum_x: f64 = x.iter().sum();
837        let sum_y: f64 = y.iter().sum();
838        let sum_xy: f64 = x.iter().zip(y.iter()).map(|(xi, yi)| xi * yi).sum();
839        let sum_x2: f64 = x.iter().map(|xi| xi * xi).sum();
840        let sum_y2: f64 = y.iter().map(|yi| yi * yi).sum();
841        let denom = n * sum_x2 - sum_x * sum_x;
842        if denom.abs() < 1e-30 {
843            return (0.0, 0.0);
844        }
845        let slope = (n * sum_xy - sum_x * sum_y) / denom;
846        let ss_tot = sum_y2 - sum_y * sum_y / n;
847        let ss_res = sum_y2 - slope * sum_xy - (sum_y - slope * sum_x) * sum_y / n;
848        let r_sq = if ss_tot.abs() > 1e-30 {
849            1.0 - ss_res / ss_tot
850        } else {
851            0.0
852        };
853        (slope, r_sq.clamp(0.0, 1.0))
854    }
855}
856/// Sierpinski triangle IFS preset.
857///
858/// The Sierpinski triangle is constructed from 3 affine transforms,
859/// each scaling by 1/2 and translating to a corner of the triangle.
860pub struct SierpinskiTriangle;
861impl SierpinskiTriangle {
862    /// Create the IFS for a Sierpinski triangle.
863    pub fn ifs() -> IteratedFunctionSystem {
864        let transforms = vec![
865            AffineTransform2D::new(0.5, 0.0, 0.0, 0.5, 0.0, 0.0),
866            AffineTransform2D::new(0.5, 0.0, 0.0, 0.5, 0.5, 0.0),
867            AffineTransform2D::new(0.5, 0.0, 0.0, 0.5, 0.25, 0.433),
868        ];
869        IteratedFunctionSystem::with_equal_probabilities(transforms)
870    }
871    /// Theoretical Hausdorff dimension: log(3)/log(2) ≈ 1.585.
872    pub fn theoretical_dimension() -> f64 {
873        (3.0_f64).ln() / (2.0_f64).ln()
874    }
875    /// Generate Sierpinski triangle points via chaos game.
876    pub fn generate(num_points: usize) -> Vec<Point2> {
877        let ifs = Self::ifs();
878        ifs.chaos_game(Point2::new(0.0, 0.0), num_points + 100, 100)
879    }
880}
881/// A 2D affine transformation: 2x2 matrix \[\[a, b\\], \[c, d\]] + translation (e, f).
882///
883/// Applies as: x' = a*x + b*y + e, y' = c*x + d*y + f
884#[derive(Debug, Clone, Copy, PartialEq)]
885pub struct AffineTransform2D {
886    /// Matrix element (0,0).
887    pub a: f64,
888    /// Matrix element (0,1).
889    pub b: f64,
890    /// Matrix element (1,0).
891    pub c: f64,
892    /// Matrix element (1,1).
893    pub d: f64,
894    /// Translation x.
895    pub e: f64,
896    /// Translation y.
897    pub f: f64,
898}
899impl AffineTransform2D {
900    /// Create a new affine transform.
901    pub fn new(a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> Self {
902        Self { a, b, c, d, e, f }
903    }
904    /// Identity transform.
905    pub fn identity() -> Self {
906        Self::new(1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
907    }
908    /// Create a rotation transform.
909    pub fn rotation(angle_rad: f64) -> Self {
910        let cos = angle_rad.cos();
911        let sin = angle_rad.sin();
912        Self::new(cos, -sin, sin, cos, 0.0, 0.0)
913    }
914    /// Create a scaling transform.
915    pub fn scaling(sx: f64, sy: f64) -> Self {
916        Self::new(sx, 0.0, 0.0, sy, 0.0, 0.0)
917    }
918    /// Create a translation transform.
919    pub fn translation(tx: f64, ty: f64) -> Self {
920        Self::new(1.0, 0.0, 0.0, 1.0, tx, ty)
921    }
922    /// Apply this transform to a point.
923    pub fn apply(&self, p: &Point2) -> Point2 {
924        Point2 {
925            x: self.a * p.x + self.b * p.y + self.e,
926            y: self.c * p.x + self.d * p.y + self.f,
927        }
928    }
929    /// Compose two transforms: self followed by other.
930    ///
931    /// The result transform applies self first, then other.
932    pub fn compose(&self, other: &AffineTransform2D) -> AffineTransform2D {
933        AffineTransform2D {
934            a: other.a * self.a + other.b * self.c,
935            b: other.a * self.b + other.b * self.d,
936            c: other.c * self.a + other.d * self.c,
937            d: other.c * self.b + other.d * self.d,
938            e: other.a * self.e + other.b * self.f + other.e,
939            f: other.c * self.e + other.d * self.f + other.f,
940        }
941    }
942    /// Determinant of the linear part.
943    pub fn determinant(&self) -> f64 {
944        self.a * self.d - self.b * self.c
945    }
946    /// Inverse transform, if the determinant is nonzero.
947    pub fn inverse(&self) -> Option<AffineTransform2D> {
948        let det = self.determinant();
949        if det.abs() < 1e-15 {
950            return None;
951        }
952        let inv_det = 1.0 / det;
953        let a = self.d * inv_det;
954        let b = -self.b * inv_det;
955        let c = -self.c * inv_det;
956        let d = self.a * inv_det;
957        let e = -(a * self.e + b * self.f);
958        let f = -(c * self.e + d * self.f);
959        Some(AffineTransform2D { a, b, c, d, e, f })
960    }
961    /// Contractivity factor (spectral norm estimate of the linear part).
962    pub fn contractivity(&self) -> f64 {
963        (self.a * self.a + self.b * self.b + self.c * self.c + self.d * self.d).sqrt()
964    }
965}
966/// Classification of an orbit in the Julia/Mandelbrot iteration.
967#[derive(Debug, Clone, PartialEq, Eq)]
968pub enum OrbitType {
969    /// Orbit escapes to infinity.
970    Escaping,
971    /// Orbit converges to a fixed point.
972    FixedPoint,
973    /// Orbit is periodic with the given period.
974    Periodic(usize),
975    /// Orbit is bounded but no periodicity detected.
976    Bounded,
977}
978/// Koch snowflake: three Koch curves forming a closed triangle.
979///
980/// The Koch snowflake starts from an equilateral triangle and applies the Koch
981/// curve construction to each side, producing a fractal with dimension log(4)/log(3).
982pub struct KochSnowflake;
983impl KochSnowflake {
984    /// Theoretical Hausdorff dimension: same as Koch curve, log(4)/log(3).
985    #[allow(dead_code)]
986    pub fn theoretical_dimension() -> f64 {
987        (4.0_f64).ln() / (3.0_f64).ln()
988    }
989    /// Generate the Koch snowflake as line segments by recursively subdividing
990    /// each side of an equilateral triangle.
991    #[allow(dead_code)]
992    pub fn generate(generations: usize, side_length: f64) -> Vec<(Point2, Point2)> {
993        let h = side_length * (3.0_f64).sqrt() / 2.0;
994        let vertices = [
995            Point2::new(0.0, 0.0),
996            Point2::new(side_length, 0.0),
997            Point2::new(side_length / 2.0, h),
998        ];
999        let mut segments = Vec::new();
1000        for i in 0..3 {
1001            let a = vertices[i];
1002            let b = vertices[(i + 1) % 3];
1003            Self::subdivide(a, b, generations, &mut segments);
1004        }
1005        segments
1006    }
1007    /// Recursively subdivide one edge of the snowflake.
1008    #[allow(dead_code)]
1009    fn subdivide(a: Point2, b: Point2, depth: usize, out: &mut Vec<(Point2, Point2)>) {
1010        if depth == 0 {
1011            out.push((a, b));
1012            return;
1013        }
1014        let dx = b.x - a.x;
1015        let dy = b.y - a.y;
1016        let p1 = Point2::new(a.x + dx / 3.0, a.y + dy / 3.0);
1017        let p3 = Point2::new(a.x + 2.0 * dx / 3.0, a.y + 2.0 * dy / 3.0);
1018        let cos60 = 0.5_f64;
1019        let sin60 = (3.0_f64).sqrt() / 2.0;
1020        let mx = dx / 3.0;
1021        let my = dy / 3.0;
1022        let apex = Point2::new(
1023            p1.x + mx * cos60 - my * sin60,
1024            p1.y + mx * sin60 + my * cos60,
1025        );
1026        Self::subdivide(a, p1, depth - 1, out);
1027        Self::subdivide(p1, apex, depth - 1, out);
1028        Self::subdivide(apex, p3, depth - 1, out);
1029        Self::subdivide(p3, b, depth - 1, out);
1030    }
1031    /// Count the number of segments after `n` generations.
1032    #[allow(dead_code)]
1033    pub fn segment_count(n: usize) -> usize {
1034        3 * 4_usize.pow(n as u32)
1035    }
1036    /// Perimeter of the snowflake after `n` generations with initial side `s`.
1037    #[allow(dead_code)]
1038    pub fn perimeter(n: usize, side_length: f64) -> f64 {
1039        3.0 * side_length * (4.0 / 3.0_f64).powi(n as i32)
1040    }
1041    /// Area of the Koch snowflake after `n` generations.
1042    #[allow(dead_code)]
1043    pub fn area(n: usize, side_length: f64) -> f64 {
1044        let a0 = (3.0_f64).sqrt() / 4.0 * side_length * side_length;
1045        let mut area = a0;
1046        for k in 1..=n {
1047            let num_new = 3 * 4_usize.pow((k - 1) as u32);
1048            let triangle_side = side_length / 3.0_f64.powi(k as i32);
1049            let triangle_area = (3.0_f64).sqrt() / 4.0 * triangle_side * triangle_side;
1050            area += num_new as f64 * triangle_area;
1051        }
1052        area
1053    }
1054}
1055/// Multifractal analysis tools.
1056///
1057/// Multifractal analysis examines the distribution of a measure over a fractal
1058/// set, computing the spectrum of generalized dimensions D(q) and the
1059/// singularity spectrum f(alpha).
1060pub struct Multifractal;
1061impl Multifractal {
1062    /// Compute the generalized fractal dimension D(q) for a 2D point set
1063    /// using the box-counting approach.
1064    ///
1065    /// `q` is the moment order (q=0 gives box-counting dim, q=1 info dim, q=2 correlation dim).
1066    /// Returns `(dimension, r_squared)`.
1067    #[allow(dead_code)]
1068    pub fn generalized_dimension(points: &[Point2], q: f64, num_scales: usize) -> (f64, f64) {
1069        if points.is_empty() || num_scales < 2 {
1070            return (0.0, 0.0);
1071        }
1072        let (min_x, max_x, min_y, max_y) = FractalDimension::bounding_box(points);
1073        let extent = (max_x - min_x).max(max_y - min_y).max(1e-15);
1074        let n_total = points.len() as f64;
1075        let mut log_eps = Vec::with_capacity(num_scales);
1076        let mut log_partition = Vec::with_capacity(num_scales);
1077        for i in 0..num_scales {
1078            let epsilon = extent / (2.0_f64).powi(i as i32 + 1);
1079            if epsilon < 1e-15 {
1080                break;
1081            }
1082            let mut box_counts: HashMap<(i64, i64), usize> = HashMap::new();
1083            for p in points {
1084                let ix = ((p.x - min_x) / epsilon).floor() as i64;
1085                let iy = ((p.y - min_y) / epsilon).floor() as i64;
1086                *box_counts.entry((ix, iy)).or_insert(0) += 1;
1087            }
1088            if (q - 1.0).abs() < 1e-10 {
1089                let mut entropy = 0.0;
1090                for &count in box_counts.values() {
1091                    let pi = count as f64 / n_total;
1092                    if pi > 0.0 {
1093                        entropy -= pi * pi.ln();
1094                    }
1095                }
1096                if entropy > 0.0 {
1097                    log_eps.push(epsilon.ln());
1098                    log_partition.push(entropy);
1099                }
1100            } else {
1101                let partition: f64 = box_counts
1102                    .values()
1103                    .map(|&count| (count as f64 / n_total).powf(q))
1104                    .sum();
1105                if partition > 0.0 {
1106                    log_eps.push(epsilon.ln());
1107                    log_partition.push(partition.ln());
1108                }
1109            }
1110        }
1111        if log_eps.len() < 2 {
1112            return (0.0, 0.0);
1113        }
1114        let (slope, r_sq) = FractalDimension::linear_regression(&log_eps, &log_partition);
1115        if (q - 1.0).abs() < 1e-10 {
1116            (-slope, r_sq)
1117        } else {
1118            (slope / (q - 1.0), r_sq)
1119        }
1120    }
1121    /// Compute the multifractal spectrum D(q) for a range of q values.
1122    ///
1123    /// Returns a vector of `(q, D_q)` pairs.
1124    #[allow(dead_code)]
1125    pub fn spectrum(points: &[Point2], q_values: &[f64], num_scales: usize) -> Vec<(f64, f64)> {
1126        q_values
1127            .iter()
1128            .map(|&q| {
1129                let (dq, _) = Self::generalized_dimension(points, q, num_scales);
1130                (q, dq)
1131            })
1132            .collect()
1133    }
1134    /// Compute the singularity spectrum f(alpha) from the D(q) spectrum.
1135    ///
1136    /// Uses the Legendre transform: alpha(q) = d(tau(q))/dq, f(alpha) = q*alpha - tau(q),
1137    /// where tau(q) = (q-1)*D(q).
1138    ///
1139    /// Returns a vector of `(alpha, f_alpha)` pairs.
1140    #[allow(dead_code)]
1141    pub fn singularity_spectrum(dq_spectrum: &[(f64, f64)]) -> Vec<(f64, f64)> {
1142        if dq_spectrum.len() < 3 {
1143            return Vec::new();
1144        }
1145        let tau: Vec<(f64, f64)> = dq_spectrum
1146            .iter()
1147            .map(|&(q, dq)| (q, (q - 1.0) * dq))
1148            .collect();
1149        let mut result = Vec::new();
1150        for i in 1..(tau.len() - 1) {
1151            let dq = tau[i + 1].0 - tau[i - 1].0;
1152            if dq.abs() < 1e-15 {
1153                continue;
1154            }
1155            let alpha = (tau[i + 1].1 - tau[i - 1].1) / dq;
1156            let f_alpha = tau[i].0 * alpha - tau[i].1;
1157            result.push((alpha, f_alpha));
1158        }
1159        result
1160    }
1161}
1162/// Dragon curve L-system preset.
1163pub struct DragonCurve;
1164impl DragonCurve {
1165    /// Create the L-system for a Dragon curve.
1166    pub fn lsystem() -> LSystem {
1167        LSystem::new(
1168            vec!['F', 'G', '+', '-'],
1169            vec![
1170                ProductionRule::new('F', "F+G"),
1171                ProductionRule::new('G', "F-G"),
1172            ],
1173            "F",
1174        )
1175    }
1176    /// Generate Dragon curve segments via turtle graphics.
1177    pub fn generate(generations: usize, step_size: f64) -> Vec<(Point2, Point2)> {
1178        let ls = Self::lsystem();
1179        ls.turtle_interpret(generations, step_size, 90.0)
1180    }
1181}
1182/// Barnsley fern IFS preset.
1183///
1184/// The classic Barnsley fern uses 4 affine transforms with specific probabilities
1185/// to generate a realistic fern shape.
1186pub struct BarnsleyFern;
1187impl BarnsleyFern {
1188    /// Create the IFS for a Barnsley fern.
1189    pub fn ifs() -> IteratedFunctionSystem {
1190        let transforms = vec![
1191            AffineTransform2D::new(0.0, 0.0, 0.0, 0.16, 0.0, 0.0),
1192            AffineTransform2D::new(0.85, 0.04, -0.04, 0.85, 0.0, 1.6),
1193            AffineTransform2D::new(0.20, -0.26, 0.23, 0.22, 0.0, 1.6),
1194            AffineTransform2D::new(-0.15, 0.28, 0.26, 0.24, 0.0, 0.44),
1195        ];
1196        let probabilities = vec![0.01, 0.85, 0.07, 0.07];
1197        IteratedFunctionSystem::new(transforms, probabilities)
1198    }
1199    /// Generate Barnsley fern points.
1200    pub fn generate(num_points: usize) -> Vec<Point2> {
1201        let ifs = Self::ifs();
1202        ifs.chaos_game(Point2::new(0.0, 0.0), num_points + 200, 200)
1203    }
1204}
1205/// A 3D point for fractal geometry (no nalgebra).
1206#[derive(Debug, Clone, Copy, PartialEq)]
1207pub struct Point3 {
1208    /// X coordinate.
1209    pub x: f64,
1210    /// Y coordinate.
1211    pub y: f64,
1212    /// Z coordinate.
1213    pub z: f64,
1214}
1215impl Point3 {
1216    /// Create a new 3D point.
1217    pub fn new(x: f64, y: f64, z: f64) -> Self {
1218        Self { x, y, z }
1219    }
1220    /// Euclidean distance to another point.
1221    #[allow(dead_code)]
1222    pub fn distance(&self, other: &Point3) -> f64 {
1223        ((self.x - other.x).powi(2) + (self.y - other.y).powi(2) + (self.z - other.z).powi(2))
1224            .sqrt()
1225    }
1226}
1227/// A deterministic context-free L-system (D0L-system).
1228///
1229/// An L-system generates strings by parallel rewriting: at each step,
1230/// every character in the current string is replaced according to the
1231/// production rules simultaneously.
1232#[derive(Debug, Clone)]
1233pub struct LSystem {
1234    /// The alphabet of valid symbols.
1235    pub alphabet: Vec<char>,
1236    /// Production rules mapping symbols to replacement strings.
1237    pub rules: HashMap<char, String>,
1238    /// The starting string (axiom).
1239    pub axiom: String,
1240    /// Default turning angle in degrees (for turtle interpretation).
1241    pub angle_deg: f64,
1242    /// Default step size (for turtle interpretation).
1243    pub step_size: f64,
1244}
1245/// State of an L-system after evolution.
1246#[derive(Debug, Clone)]
1247pub struct LSystemEvolution {
1248    /// The generated string.
1249    pub string: String,
1250    /// The generation number.
1251    pub generation: usize,
1252}
1253impl LSystem {
1254    /// Create a new L-system.
1255    pub fn new(alphabet: Vec<char>, rules: Vec<ProductionRule>, axiom: impl Into<String>) -> Self {
1256        let mut rule_map = HashMap::new();
1257        for rule in rules {
1258            rule_map.insert(rule.predecessor, rule.successor);
1259        }
1260        Self {
1261            alphabet,
1262            rules: rule_map,
1263            axiom: axiom.into(),
1264            angle_deg: 90.0,
1265            step_size: 1.0,
1266        }
1267    }
1268    /// Koch curve L-system (angle = 60°).
1269    pub fn koch_curve() -> Self {
1270        let mut ls = Self::new(vec!['F'], vec![ProductionRule::new('F', "F+F-F-F+F")], "F");
1271        ls.angle_deg = 90.0;
1272        ls
1273    }
1274    /// Sierpinski triangle L-system (angle = 120°).
1275    pub fn sierpinski() -> Self {
1276        let mut ls = Self::new(
1277            vec!['A', 'B'],
1278            vec![
1279                ProductionRule::new('A', "B-A-B"),
1280                ProductionRule::new('B', "A+B+A"),
1281            ],
1282            "A",
1283        );
1284        ls.angle_deg = 60.0;
1285        ls
1286    }
1287    /// Dragon curve L-system (angle = 90°).
1288    pub fn dragon_curve() -> Self {
1289        let mut ls = Self::new(
1290            vec!['X', 'Y'],
1291            vec![
1292                ProductionRule::new('X', "X+YF+"),
1293                ProductionRule::new('Y', "-FX-Y"),
1294            ],
1295            "FX",
1296        );
1297        ls.angle_deg = 90.0;
1298        ls
1299    }
1300    /// Plant branching L-system (angle = 25°).
1301    pub fn plant_branching() -> Self {
1302        let mut ls = Self::new(
1303            vec!['X', 'F'],
1304            vec![
1305                ProductionRule::new('X', "F+[[X]-X]-F[-FX]+X"),
1306                ProductionRule::new('F', "FF"),
1307            ],
1308            "X",
1309        );
1310        ls.angle_deg = 25.0;
1311        ls
1312    }
1313    /// Evolve the L-system for `n` generations, returning the state.
1314    pub fn evolve(&self, n: usize) -> LSystemEvolution {
1315        LSystemEvolution {
1316            string: self.iterate(n),
1317            generation: n,
1318        }
1319    }
1320    /// Interpret a previously evolved state as turtle graphics segments.
1321    pub fn interpret(&self, state: &LSystemEvolution) -> Vec<(Point2, Point2)> {
1322        let angle_rad = self.angle_deg * std::f64::consts::PI / 180.0;
1323        let step = self.step_size;
1324        let mut x = 0.0_f64;
1325        let mut y = 0.0_f64;
1326        let mut heading = 0.0_f64;
1327        let mut stack: Vec<(f64, f64, f64)> = Vec::new();
1328        let mut segments = Vec::new();
1329        for ch in state.string.chars() {
1330            match ch {
1331                'F' | 'G' | 'A' | 'B' => {
1332                    let nx = x + step * heading.cos();
1333                    let ny = y + step * heading.sin();
1334                    segments.push((Point2::new(x, y), Point2::new(nx, ny)));
1335                    x = nx;
1336                    y = ny;
1337                }
1338                '+' => heading += angle_rad,
1339                '-' => heading -= angle_rad,
1340                '[' => stack.push((x, y, heading)),
1341                ']' => {
1342                    if let Some((sx, sy, sh)) = stack.pop() {
1343                        x = sx;
1344                        y = sy;
1345                        heading = sh;
1346                    }
1347                }
1348                _ => {}
1349            }
1350        }
1351        segments
1352    }
1353    /// Generate the string after `n` iterations.
1354    pub fn iterate(&self, n: usize) -> String {
1355        let mut current = self.axiom.clone();
1356        for _ in 0..n {
1357            let mut next = String::with_capacity(current.len() * 2);
1358            for ch in current.chars() {
1359                if let Some(replacement) = self.rules.get(&ch) {
1360                    next.push_str(replacement);
1361                } else {
1362                    next.push(ch);
1363                }
1364            }
1365            current = next;
1366        }
1367        current
1368    }
1369    /// Count the length of the string after `n` iterations without building it.
1370    pub fn iterate_length(&self, n: usize) -> usize {
1371        let mut lengths: HashMap<char, usize> = HashMap::new();
1372        for &ch in &self.alphabet {
1373            lengths.insert(ch, 1);
1374        }
1375        for _ in 0..n {
1376            let mut new_lengths: HashMap<char, usize> = HashMap::new();
1377            for &ch in &self.alphabet {
1378                if let Some(replacement) = self.rules.get(&ch) {
1379                    let len: usize = replacement
1380                        .chars()
1381                        .map(|c| lengths.get(&c).copied().unwrap_or(1))
1382                        .sum();
1383                    new_lengths.insert(ch, len);
1384                } else {
1385                    new_lengths.insert(ch, *lengths.get(&ch).unwrap_or(&1));
1386                }
1387            }
1388            lengths = new_lengths;
1389        }
1390        self.axiom
1391            .chars()
1392            .map(|c| lengths.get(&c).copied().unwrap_or(1))
1393            .sum()
1394    }
1395    /// Interpret the generated string as turtle graphics commands.
1396    ///
1397    /// - `F`, `G`: move forward by `step_size`
1398    /// - `+`: turn left by `angle_deg` degrees
1399    /// - `-`: turn right by `angle_deg` degrees
1400    /// - `[`: push state
1401    /// - `]`: pop state
1402    ///
1403    /// Returns a list of line segments as (start, end) pairs.
1404    pub fn turtle_interpret(
1405        &self,
1406        n: usize,
1407        step_size: f64,
1408        angle_deg: f64,
1409    ) -> Vec<(Point2, Point2)> {
1410        let string = self.iterate(n);
1411        let angle_rad = angle_deg * std::f64::consts::PI / 180.0;
1412        let mut x = 0.0_f64;
1413        let mut y = 0.0_f64;
1414        let mut heading = 0.0_f64;
1415        let mut stack: Vec<(f64, f64, f64)> = Vec::new();
1416        let mut segments = Vec::new();
1417        for ch in string.chars() {
1418            match ch {
1419                'F' | 'G' => {
1420                    let nx = x + step_size * heading.cos();
1421                    let ny = y + step_size * heading.sin();
1422                    segments.push((Point2::new(x, y), Point2::new(nx, ny)));
1423                    x = nx;
1424                    y = ny;
1425                }
1426                '+' => {
1427                    heading += angle_rad;
1428                }
1429                '-' => {
1430                    heading -= angle_rad;
1431                }
1432                '[' => {
1433                    stack.push((x, y, heading));
1434                }
1435                ']' => {
1436                    if let Some((sx, sy, sh)) = stack.pop() {
1437                        x = sx;
1438                        y = sy;
1439                        heading = sh;
1440                    }
1441                }
1442                _ => {}
1443            }
1444        }
1445        segments
1446    }
1447    /// Add a production rule.
1448    pub fn add_rule(&mut self, predecessor: char, successor: impl Into<String>) {
1449        let succ: String = successor.into();
1450        self.rules.insert(predecessor, succ);
1451        if !self.alphabet.contains(&predecessor) {
1452            self.alphabet.push(predecessor);
1453        }
1454    }
1455}
1456/// A Julia set for the quadratic map z_{n+1} = z_n^2 + c with fixed c.
1457///
1458/// Points z_0 in the complex plane are classified by orbit behavior.
1459pub struct JuliaSet {
1460    /// Real part of the constant c.
1461    pub cr: f64,
1462    /// Imaginary part of the constant c.
1463    pub ci: f64,
1464    /// Maximum iterations.
1465    pub max_iter: u32,
1466    /// Escape radius squared.
1467    pub escape_radius_sq: f64,
1468}
1469impl JuliaSet {
1470    /// Create a new Julia set with given c parameter.
1471    pub fn new(cr: f64, ci: f64, max_iter: u32, escape_radius: f64) -> Self {
1472        Self {
1473            cr,
1474            ci,
1475            max_iter,
1476            escape_radius_sq: escape_radius * escape_radius,
1477        }
1478    }
1479    /// Classic Julia set c = -0.7 + 0.27015i.
1480    pub fn classic() -> Self {
1481        Self::new(-0.7, 0.27015, 256, 2.0)
1482    }
1483    /// Dendrite Julia set c = 0 + i.
1484    pub fn dendrite() -> Self {
1485        Self::new(0.0, 1.0, 256, 2.0)
1486    }
1487    /// Compute escape time for an initial point z0 = (zr, zi).
1488    pub fn escape_time(&self, zr: f64, zi: f64) -> Option<u32> {
1489        let mut zr = zr;
1490        let mut zi = zi;
1491        for i in 0..self.max_iter {
1492            let zr2 = zr * zr;
1493            let zi2 = zi * zi;
1494            if zr2 + zi2 > self.escape_radius_sq {
1495                return Some(i);
1496            }
1497            let new_zi = 2.0 * zr * zi + self.ci;
1498            zr = zr2 - zi2 + self.cr;
1499            zi = new_zi;
1500        }
1501        None
1502    }
1503    /// Check if a point is in the Julia set (orbit does not escape).
1504    pub fn is_in_set(&self, zr: f64, zi: f64) -> bool {
1505        self.escape_time(zr, zi).is_none()
1506    }
1507    /// Classify the orbit of z0: returns `OrbitType`.
1508    pub fn classify_orbit(&self, zr: f64, zi: f64) -> OrbitType {
1509        let mut zr = zr;
1510        let mut zi = zi;
1511        let mut history_r = Vec::with_capacity(self.max_iter as usize);
1512        let mut history_i = Vec::with_capacity(self.max_iter as usize);
1513        for _ in 0..self.max_iter {
1514            let zr2 = zr * zr;
1515            let zi2 = zi * zi;
1516            if zr2 + zi2 > self.escape_radius_sq {
1517                return OrbitType::Escaping;
1518            }
1519            history_r.push(zr);
1520            history_i.push(zi);
1521            let new_zi = 2.0 * zr * zi + self.ci;
1522            zr = zr2 - zi2 + self.cr;
1523            zi = new_zi;
1524        }
1525        let n = history_r.len();
1526        if n < 2 {
1527            return OrbitType::Bounded;
1528        }
1529        let last_r = history_r[n - 1];
1530        let last_i = history_i[n - 1];
1531        for period in 1..=16.min(n - 1) {
1532            let prev_r = history_r[n - 1 - period];
1533            let prev_i = history_i[n - 1 - period];
1534            let dr = (last_r - prev_r).abs();
1535            let di = (last_i - prev_i).abs();
1536            if dr < 1e-10 && di < 1e-10 {
1537                if period == 1 {
1538                    return OrbitType::FixedPoint;
1539                }
1540                return OrbitType::Periodic(period);
1541            }
1542        }
1543        OrbitType::Bounded
1544    }
1545    /// Smooth escape time for coloring.
1546    pub fn smooth_escape_time(&self, zr: f64, zi: f64) -> f64 {
1547        let mut zr = zr;
1548        let mut zi = zi;
1549        for i in 0..self.max_iter {
1550            let zr2 = zr * zr;
1551            let zi2 = zi * zi;
1552            if zr2 + zi2 > self.escape_radius_sq {
1553                let modulus = (zr2 + zi2).sqrt();
1554                let mu = i as f64 - modulus.ln().ln() / (2.0_f64).ln();
1555                return mu;
1556            }
1557            let new_zi = 2.0 * zr * zi + self.ci;
1558            zr = zr2 - zi2 + self.cr;
1559            zi = new_zi;
1560        }
1561        self.max_iter as f64
1562    }
1563    /// Render a grid of escape times.
1564    pub fn render_grid(
1565        &self,
1566        x_min: f64,
1567        x_max: f64,
1568        y_min: f64,
1569        y_max: f64,
1570        width: usize,
1571        height: usize,
1572    ) -> Vec<Vec<Option<u32>>> {
1573        let dx = (x_max - x_min) / width as f64;
1574        let dy = (y_max - y_min) / height as f64;
1575        (0..height)
1576            .map(|j| {
1577                let zi = y_min + (j as f64 + 0.5) * dy;
1578                (0..width)
1579                    .map(|i| {
1580                        let zr = x_min + (i as f64 + 0.5) * dx;
1581                        self.escape_time(zr, zi)
1582                    })
1583                    .collect()
1584            })
1585            .collect()
1586    }
1587}
1588/// An axis-aligned box in 3D, used for Menger sponge cubes.
1589#[derive(Debug, Clone, Copy, PartialEq)]
1590pub struct Aabb3 {
1591    /// Minimum corner.
1592    pub min: Point3,
1593    /// Maximum corner.
1594    pub max: Point3,
1595}
1596impl Aabb3 {
1597    /// Create a new AABB.
1598    pub fn new(min: Point3, max: Point3) -> Self {
1599        Self { min, max }
1600    }
1601    /// Side length (assumes cubic).
1602    #[allow(dead_code)]
1603    pub fn side(&self) -> f64 {
1604        self.max.x - self.min.x
1605    }
1606    /// Volume.
1607    #[allow(dead_code)]
1608    pub fn volume(&self) -> f64 {
1609        (self.max.x - self.min.x) * (self.max.y - self.min.y) * (self.max.z - self.min.z)
1610    }
1611    /// Center point.
1612    #[allow(dead_code)]
1613    pub fn center(&self) -> Point3 {
1614        Point3::new(
1615            (self.min.x + self.max.x) / 2.0,
1616            (self.min.y + self.max.y) / 2.0,
1617            (self.min.z + self.max.z) / 2.0,
1618        )
1619    }
1620}
1621/// Morton (Z-order) curve utilities.
1622///
1623/// Maps 2D coordinates to a 1D index using bit-interleaving, producing
1624/// a space-filling Z-shaped traversal pattern.
1625pub struct MortonCurve;
1626impl MortonCurve {
1627    /// Encode 2D coordinates into a Morton code.
1628    ///
1629    /// Interleaves the bits of `x` and `y` (each up to 16 bits).
1630    #[allow(dead_code)]
1631    pub fn encode(x: u32, y: u32) -> u64 {
1632        Self::spread_bits(x as u64) | (Self::spread_bits(y as u64) << 1)
1633    }
1634    /// Decode a Morton code back to 2D coordinates.
1635    #[allow(dead_code)]
1636    pub fn decode(code: u64) -> (u32, u32) {
1637        let x = Self::compact_bits(code) as u32;
1638        let y = Self::compact_bits(code >> 1) as u32;
1639        (x, y)
1640    }
1641    /// Spread bits: insert a zero between each bit.
1642    #[allow(dead_code)]
1643    fn spread_bits(mut v: u64) -> u64 {
1644        v &= 0x0000_0000_FFFF_FFFF;
1645        v = (v | (v << 16)) & 0x0000_FFFF_0000_FFFF;
1646        v = (v | (v << 8)) & 0x00FF_00FF_00FF_00FF;
1647        v = (v | (v << 4)) & 0x0F0F_0F0F_0F0F_0F0F;
1648        v = (v | (v << 2)) & 0x3333_3333_3333_3333;
1649        v = (v | (v << 1)) & 0x5555_5555_5555_5555;
1650        v
1651    }
1652    /// Compact bits: remove every other bit.
1653    #[allow(dead_code)]
1654    fn compact_bits(mut v: u64) -> u64 {
1655        v &= 0x5555_5555_5555_5555;
1656        v = (v | (v >> 1)) & 0x3333_3333_3333_3333;
1657        v = (v | (v >> 2)) & 0x0F0F_0F0F_0F0F_0F0F;
1658        v = (v | (v >> 4)) & 0x00FF_00FF_00FF_00FF;
1659        v = (v | (v >> 8)) & 0x0000_FFFF_0000_FFFF;
1660        v = (v | (v >> 16)) & 0x0000_0000_FFFF_FFFF;
1661        v
1662    }
1663    /// Generate the Z-order traversal as a list of 2D points for an `n x n` grid.
1664    ///
1665    /// `n` should be a power of 2.
1666    #[allow(dead_code)]
1667    pub fn generate_path(n: u32) -> Vec<(u32, u32)> {
1668        let total = n * n;
1669        let mut pairs: Vec<(u64, u32, u32)> = Vec::with_capacity(total as usize);
1670        for y in 0..n {
1671            for x in 0..n {
1672                pairs.push((Self::encode(x, y), x, y));
1673            }
1674        }
1675        pairs.sort_by_key(|&(code, _, _)| code);
1676        pairs.iter().map(|&(_, x, y)| (x, y)).collect()
1677    }
1678    /// 3D Morton code encoding.
1679    #[allow(dead_code)]
1680    pub fn encode_3d(x: u32, y: u32, z: u32) -> u64 {
1681        Self::spread_bits_3d(x as u64)
1682            | (Self::spread_bits_3d(y as u64) << 1)
1683            | (Self::spread_bits_3d(z as u64) << 2)
1684    }
1685    /// Spread bits for 3D: insert two zeros between each bit.
1686    #[allow(dead_code)]
1687    fn spread_bits_3d(mut v: u64) -> u64 {
1688        v &= 0x001F_FFFF;
1689        v = (v | (v << 32)) & 0x001F_0000_0000_FFFF;
1690        v = (v | (v << 16)) & 0x001F_0000_FF00_00FF;
1691        v = (v | (v << 8)) & 0x100F_00F0_0F00_F00F;
1692        v = (v | (v << 4)) & 0x10C3_0C30_C30C_30C3;
1693        v = (v | (v << 2)) & 0x1249_2492_4924_9249;
1694        v
1695    }
1696}
1697/// A 2D point used in fractal geometry computations.
1698#[derive(Debug, Clone, Copy, PartialEq)]
1699pub struct Point2 {
1700    /// X coordinate.
1701    pub x: f64,
1702    /// Y coordinate.
1703    pub y: f64,
1704}
1705impl Point2 {
1706    /// Create a new 2D point.
1707    pub fn new(x: f64, y: f64) -> Self {
1708        Self { x, y }
1709    }
1710    /// Euclidean distance to another point.
1711    pub fn distance(&self, other: &Point2) -> f64 {
1712        ((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
1713    }
1714    /// Squared distance to another point.
1715    pub fn distance_sq(&self, other: &Point2) -> f64 {
1716        (self.x - other.x).powi(2) + (self.y - other.y).powi(2)
1717    }
1718}
1719/// Menger sponge generator.
1720///
1721/// The Menger sponge is a 3D fractal formed by recursively removing the center
1722/// and face-center sub-cubes from a cube. Its Hausdorff dimension is
1723/// log(20)/log(3) ≈ 2.727.
1724pub struct MengerSponge;
1725impl MengerSponge {
1726    /// Theoretical Hausdorff dimension: log(20)/log(3).
1727    #[allow(dead_code)]
1728    pub fn theoretical_dimension() -> f64 {
1729        (20.0_f64).ln() / (3.0_f64).ln()
1730    }
1731    /// Generate the Menger sponge as a list of cubes (AABBs) at the given recursion level.
1732    ///
1733    /// `level` 0 returns a single unit cube, level 1 returns 20 sub-cubes, etc.
1734    #[allow(dead_code)]
1735    pub fn generate(level: usize, origin: Point3, size: f64) -> Vec<Aabb3> {
1736        if level == 0 {
1737            return vec![Aabb3::new(
1738                origin,
1739                Point3::new(origin.x + size, origin.y + size, origin.z + size),
1740            )];
1741        }
1742        let sub = size / 3.0;
1743        let mut cubes = Vec::new();
1744        for ix in 0..3_u8 {
1745            for iy in 0..3_u8 {
1746                for iz in 0..3_u8 {
1747                    let center_count = u8::from(ix == 1) + u8::from(iy == 1) + u8::from(iz == 1);
1748                    if center_count >= 2 {
1749                        continue;
1750                    }
1751                    let o = Point3::new(
1752                        origin.x + ix as f64 * sub,
1753                        origin.y + iy as f64 * sub,
1754                        origin.z + iz as f64 * sub,
1755                    );
1756                    cubes.extend(Self::generate(level - 1, o, sub));
1757                }
1758            }
1759        }
1760        cubes
1761    }
1762    /// Number of cubes at a given level.
1763    #[allow(dead_code)]
1764    pub fn cube_count(level: usize) -> usize {
1765        20_usize.pow(level as u32)
1766    }
1767    /// Total volume at a given level (unit cube start).
1768    #[allow(dead_code)]
1769    pub fn total_volume(level: usize) -> f64 {
1770        (20.0 / 27.0_f64).powi(level as i32)
1771    }
1772}
1773/// Koch curve L-system preset.
1774///
1775/// The Koch curve replaces each line segment with 4 segments arranged
1776/// in a triangular bump, creating a fractal curve of dimension log(4)/log(3).
1777pub struct KochCurve;
1778impl KochCurve {
1779    /// Create the L-system for a Koch curve.
1780    pub fn lsystem() -> LSystem {
1781        LSystem::new(
1782            vec!['F', '+', '-'],
1783            vec![ProductionRule::new('F', "F+F--F+F")],
1784            "F",
1785        )
1786    }
1787    /// Theoretical dimension: log(4)/log(3) ≈ 1.2619.
1788    pub fn theoretical_dimension() -> f64 {
1789        (4.0_f64).ln() / (3.0_f64).ln()
1790    }
1791    /// Generate Koch curve segments via turtle graphics.
1792    pub fn generate(generations: usize, step_size: f64) -> Vec<(Point2, Point2)> {
1793        let ls = Self::lsystem();
1794        ls.turtle_interpret(generations, step_size, 60.0)
1795    }
1796    /// Compute the number of segments after `n` generations.
1797    pub fn segment_count(n: usize) -> usize {
1798        4_usize.pow(n as u32)
1799    }
1800}