l_system_fractals/
rules.rs

1// src/rules.rs
2//
3// This file is part of l-system-fractals
4//
5// Copyright 2024 Christopher Phan
6//
7// See README.md in repository root directory for copyright/license info
8//
9// SPDX-License-Identifier: MIT OR Apache-2.0
10
11//! Defines and generates L-systems.
12use std::collections::HashMap;
13use std::f64::consts::{PI, TAU};
14
15use crate::errors::LSystemError;
16use crate::num_validity;
17use crate::num_validity::AlmostEq;
18use crate::paths::{Path, PathCommand, PlotParameters, Point};
19
20const USED_CHARS: [char; 4] = ['+', '-', '[', ']'];
21
22/// Describes the rewriting rules that will be iterated.
23///
24/// # Example
25///
26/// ```
27/// use std::collections::HashMap;
28///
29/// use l_system_fractals::rules::RewriteRules;
30///
31/// let cantor = RewriteRules(
32///     HashMap::from([
33///         ('=', "=.=".into()),
34///         ('.', "...".into())
35///     ])
36/// );
37///
38/// let mut dust = cantor.iterate("=".to_string());
39///
40/// assert_eq!(&dust, "=.=");
41/// dust = cantor.iterate(dust);
42/// assert_eq!(&dust, "=.=...=.=");
43/// dust = cantor.iterate(dust);
44/// assert_eq!(&dust, "=.=...=.=.........=.=...=.=");
45/// ```
46#[derive(Clone, Debug, PartialEq, Eq)]
47pub struct RewriteRules(pub HashMap<char, String>);
48
49impl RewriteRules {
50    /// Applies the rules once to a string.
51    ///
52    /// # Example
53    ///
54    /// ```
55    /// use std::collections::HashMap;
56    ///
57    /// use l_system_fractals::rules::RewriteRules;
58    ///
59    /// let r = RewriteRules(
60    ///     HashMap::from([
61    ///         ('A', "AB".into()),
62    ///         ('B', "CA".into()),
63    ///         ('C', "AA".into())
64    ///     ])
65    /// );
66    ///
67    /// assert_eq!(r.iterate("A".to_string()), "AB".to_string());
68    /// assert_eq!(r.iterate("ABC".to_string()), "ABCAAA".to_string());
69    /// assert_eq!(r.iterate("ACX".to_string()), "ABAAX".to_string());
70    /// ```
71    pub fn iterate(&self, start: String) -> String {
72        let mut out_str: String = "".into();
73        for k in start.chars() {
74            match self.0.get(&k) {
75                Some(s) => {
76                    out_str += &s;
77                }
78                None => {
79                    out_str.push(k);
80                }
81            }
82        }
83        out_str
84    }
85
86    /// Applies the rule the specified number of times.
87    ///
88    /// # Example
89    ///
90    /// ```
91    /// use std::collections::HashMap;
92    ///
93    /// use l_system_fractals::rules::RewriteRules;
94    ///
95    /// let cantor = RewriteRules(
96    ///     HashMap::from([
97    ///         ('=', "=.=".into()),
98    ///         ('.', "...".into())
99    ///     ])
100    /// );
101    ///
102    /// assert_eq!(cantor.repeat_iterate("=".to_string(), 0), "=".to_string());
103    /// assert_eq!(cantor.repeat_iterate("=".to_string(), 1), "=.=".to_string());
104    /// assert_eq!(cantor.repeat_iterate("=".to_string(), 2), "=.=...=.=".to_string());
105    /// assert_eq!(
106    ///     cantor.repeat_iterate("=".to_string(), 3),
107    ///     "=.=...=.=.........=.=...=.=".to_string()
108    /// );
109    /// ```
110    pub fn repeat_iterate(&self, start: String, num_iter: usize) -> String {
111        let mut out_str: String = start;
112        for _ in 0..num_iter {
113            out_str = self.iterate(out_str);
114        }
115        out_str
116    }
117
118    /// Calculates the first `num_iter` iterations of the rule applied to a given string.
119    ///
120    /// # Example
121    ///
122    /// ```
123    /// use std::collections::HashMap;
124    ///
125    /// use l_system_fractals::rules::RewriteRules;
126    ///
127    /// let cantor = RewriteRules(
128    ///     HashMap::from([
129    ///         ('=', "=.=".into()),
130    ///         ('.', "...".into())
131    ///     ])
132    /// );
133    ///
134    /// let output = cantor.iterations_up_to("=".to_string(), 3);
135    ///
136    /// assert_eq!(output.get(&0).unwrap(), "=");
137    /// assert_eq!(output.get(&1).unwrap(), "=.=");
138    /// assert_eq!(output.get(&2).unwrap(), "=.=...=.=");
139    /// assert_eq!(output.get(&3).unwrap(), "=.=...=.=.........=.=...=.=");
140    /// ```
141    pub fn iterations_up_to(&self, start: String, num_iter: usize) -> HashMap<usize, String> {
142        let mut outmap: HashMap<usize, String> = HashMap::from([(0, start.clone())]);
143        let mut current: String = start;
144        for k in 1..(num_iter + 1) {
145            current = self.iterate(current);
146            outmap.insert(k, current.clone());
147        }
148        outmap
149    }
150}
151
152impl From<HashMap<char, String>> for RewriteRules {
153    fn from(hm: HashMap<char, String>) -> Self {
154        Self(hm)
155    }
156}
157
158/// Represents the various actions that can be assigned to characters in a string.
159#[derive(Copy, Clone, Debug, PartialEq)]
160pub enum DrawAction {
161    /// Draws forward the given distance.
162    ///
163    /// # Example
164    ///
165    /// ```
166    /// use std::f64::consts::PI;
167    ///
168    /// use l_system_fractals::rules::{DrawAction, ActionString};
169    /// use l_system_fractals::paths::{Path, PathCommand, Point};
170    /// use l_system_fractals::num_validity::AlmostEq;
171    ///
172    /// let action_string = ActionString(vec![DrawAction::DrawForward(20.0)]);
173    ///
174    /// let pth = action_string.make_path(Point::new(5.0, 5.0), 0.0).unwrap();
175    ///
176    /// let pc_vec: Vec<PathCommand> = vec![
177    ///     PathCommand::MoveTo(Point::new(5.0, 5.0)),
178    ///     PathCommand::LineTo(Point::new(25.0, 5.0))
179    /// ];
180    ///
181    /// let pth2: Path = pc_vec.into();
182    ///
183    /// assert!(pth.almost_eq(&pth2, 0.001));
184    /// ```
185    DrawForward(f64),
186    /// Moves forward the given distance.
187    ///
188    /// # Example
189    ///
190    /// ```
191    /// use std::f64::consts::PI;
192    ///
193    /// use l_system_fractals::rules::{DrawAction, ActionString};
194    /// use l_system_fractals::paths::{Path, PathCommand, Point};
195    /// use l_system_fractals::num_validity::AlmostEq;
196    ///
197    /// let action_string = ActionString(
198    ///     vec![
199    ///         DrawAction::DrawForward(20.0),
200    ///         DrawAction::MoveForward(10.0),
201    ///         DrawAction::DrawForward(5.0)
202    ///     ]
203    /// );
204    ///
205    /// let pth = action_string.make_path(Point::new(5.0, 5.0), 0.0).unwrap();
206    ///
207    /// let pc_vec: Vec<PathCommand> = vec![
208    ///     PathCommand::MoveTo(Point::new(5.0, 5.0)),
209    ///     PathCommand::LineTo(Point::new(25.0, 5.0)),
210    ///     PathCommand::MoveTo(Point::new(35.0, 5.0)),
211    ///     PathCommand::LineTo(Point::new(40.0, 5.0))
212    /// ];
213    ///
214    /// let pth2: Path = pc_vec.into();
215    ///
216    /// assert!(pth.almost_eq(&pth2, 0.001));
217    /// ```
218    MoveForward(f64),
219    /// Rotates the current angle the given amount clockwise.
220    ///
221    /// # Example
222    ///
223    /// ```
224    /// use std::f64::consts::PI;
225    ///
226    /// use l_system_fractals::rules::{DrawAction, ActionString};
227    /// use l_system_fractals::paths::{Path, PathCommand, Point};
228    /// use l_system_fractals::num_validity::AlmostEq;
229    ///
230    /// let action_string = ActionString(
231    ///     vec![
232    ///         DrawAction::DrawForward(20.0),
233    ///         DrawAction::RotateCW(PI/2.0),
234    ///         DrawAction::DrawForward(5.0)
235    ///     ]
236    /// );
237    ///
238    /// let pth = action_string.make_path(Point::new(5.0, 5.0), 0.0).unwrap();
239    ///
240    /// let pc_vec: Vec<PathCommand> = vec![
241    ///     PathCommand::MoveTo(Point::new(5.0, 5.0)),
242    ///     PathCommand::LineTo(Point::new(25.0, 5.0)),
243    ///     // Note that in SVG, the origin is in the top-left corner, and
244    ///     // the positive y-axis points downward.
245    ///     PathCommand::LineTo(Point::new(25.0, 10.0))
246    /// ];
247    ///
248    /// let pth2: Path = pc_vec.into();
249    ///
250    /// assert!(pth.almost_eq(&pth2, 0.001));
251    /// ```
252    RotateCW(f64),
253    /// Pushes the current location and angle onto the specified stack.
254    ///
255    /// # Example
256    ///
257    /// ```
258    /// use std::f64::consts::PI;
259    ///
260    /// use l_system_fractals::rules::{DrawAction, ActionString};
261    /// use l_system_fractals::paths::{Path, PathCommand, Point};
262    /// use l_system_fractals::num_validity::AlmostEq;
263    ///
264    /// let action_string = ActionString(
265    ///     vec![
266    ///         DrawAction::DrawForward(20.0),
267    ///         DrawAction::Push(0),
268    ///         DrawAction::RotateCW(PI/2.0),
269    ///         DrawAction::DrawForward(5.0),
270    ///         DrawAction::Pop(0),
271    ///         DrawAction::DrawForward(6.0)
272    ///     ]
273    /// );
274    ///
275    /// let pth = action_string.make_path(Point::new(5.0, 5.0), 0.0).unwrap();
276    ///
277    /// let pc_vec: Vec<PathCommand> = vec![
278    ///     PathCommand::MoveTo(Point::new(5.0, 5.0)),
279    ///     PathCommand::LineTo(Point::new(25.0, 5.0)),
280    ///     // Location and direction saved to stack
281    ///     //
282    ///     // Note that in SVG, the origin is in the top-left corner, and
283    ///     // the positive y-axis points downward.
284    ///     PathCommand::LineTo(Point::new(25.0, 10.0)),
285    ///     // Back to previous location and direction
286    ///     PathCommand::MoveTo(Point::new(25.0, 5.0)),
287    ///     PathCommand::LineTo(Point::new(31.0, 5.0))
288    /// ];
289    ///
290    /// let pth2: Path = pc_vec.into();
291    ///
292    /// assert!(pth.almost_eq(&pth2, 0.001));
293    /// ```
294    Push(usize),
295    /// Pops the top location/angle pair off the specified stack and adopts that as the current
296    /// location and angle.
297    ///
298    /// # Example
299    ///
300    /// ```
301    /// use std::f64::consts::PI;
302    ///
303    /// use l_system_fractals::rules::{DrawAction, ActionString};
304    /// use l_system_fractals::paths::{Path, PathCommand, Point};
305    /// use l_system_fractals::num_validity::AlmostEq;
306    ///
307    /// let action_string = ActionString(
308    ///     vec![
309    ///         DrawAction::DrawForward(20.0),
310    ///         DrawAction::Push(0),
311    ///         DrawAction::RotateCW(PI/2.0),
312    ///         DrawAction::DrawForward(5.0),
313    ///         DrawAction::Push(1),
314    ///         DrawAction::RotateCW(-PI/2.0),
315    ///         DrawAction::DrawForward(1.0),
316    ///         DrawAction::Pop(0),
317    ///         DrawAction::DrawForward(6.0),
318    ///         DrawAction::Pop(1),
319    ///         DrawAction::DrawForward(1.0)
320    ///     ]
321    /// );
322    ///
323    /// let pth = action_string.make_path(Point::new(5.0, 5.0), 0.0).unwrap();
324    ///
325    /// let pc_vec: Vec<PathCommand> = vec![
326    ///     PathCommand::MoveTo(Point::new(5.0, 5.0)),
327    ///     PathCommand::LineTo(Point::new(25.0, 5.0)),
328    ///     // Location and direction saved to stack 0
329    ///     //
330    ///     // Note that in SVG, the origin is in the top-left corner, and
331    ///     // the positive y-axis points downward.
332    ///     PathCommand::LineTo(Point::new(25.0, 10.0)),
333    ///     // Location and direction saved to stack 1
334    ///     PathCommand::LineTo(Point::new(26.0, 10.0)),
335    ///     // Back to previous location and direction from stack 0
336    ///     PathCommand::MoveTo(Point::new(25.0, 5.0)),
337    ///     PathCommand::LineTo(Point::new(31.0, 5.0)),
338    ///     // Back to previous location and direction from stack 1
339    ///     PathCommand::MoveTo(Point::new(25.0, 10.0)),
340    ///     PathCommand::LineTo(Point::new(25.0, 11.0))
341    /// ];
342    ///
343    /// let pth2: Path = pc_vec.into();
344    ///
345    /// assert!(pth.almost_eq(&pth2, 0.001));
346    /// ```
347    Pop(usize),
348    /// Does nothing.
349    Null,
350}
351
352impl DrawAction {
353    fn angle_abs_diff(a: f64, b: f64) -> f64 {
354        let diff = (a - b).abs().rem_euclid(2.0 * PI);
355        // e.g., if a = 1.5 * PI and b = 5.499 * PI, then diff = 1.999 * PI
356        diff.min(2.0 * PI - diff)
357    }
358}
359
360impl AlmostEq for DrawAction {
361    fn almost_eq(&self, other: &Self, epsilon: f64) -> bool {
362        match (self, other) {
363            (Self::DrawForward(x), Self::DrawForward(y)) => x.almost_eq(y, epsilon),
364            (Self::MoveForward(x), Self::MoveForward(y)) => x.almost_eq(y, epsilon),
365            (Self::RotateCW(x), Self::RotateCW(y)) => Self::angle_abs_diff(*x, *y) < epsilon,
366            (a, b) => a == b,
367        }
368    }
369}
370
371/// Represents an assignment of `char`s to `DrawAction`s.
372#[derive(Clone, Debug, PartialEq)]
373pub struct RuleAssignment(pub HashMap<char, DrawAction>);
374
375impl AlmostEq for RuleAssignment {
376    fn almost_eq(&self, other: &Self, epsilon: f64) -> bool {
377        self.0.almost_eq(&other.0, epsilon)
378    }
379}
380
381/// Represents a series of `DrawAction`s.
382#[derive(Clone, Debug, PartialEq)]
383pub struct ActionString(pub Vec<DrawAction>);
384
385impl AlmostEq for ActionString {
386    fn almost_eq(&self, other: &Self, epsilon: f64) -> bool {
387        self.0.almost_eq(&other.0, epsilon)
388    }
389}
390
391impl From<Vec<DrawAction>> for ActionString {
392    fn from(v: Vec<DrawAction>) -> Self {
393        Self(v)
394    }
395}
396
397impl ActionString {
398    /// Creates a `Path` from the action string, starting at a given location and angle.
399    ///
400    /// # Example
401    ///
402    /// ```
403    /// use std::f64::consts::PI;
404    ///
405    /// use l_system_fractals::rules::{DrawAction, ActionString};
406    /// use l_system_fractals::paths::{Path, PathCommand, Point};
407    /// use l_system_fractals::num_validity::AlmostEq;
408    ///
409    /// let action_string = ActionString(
410    ///     vec![
411    ///         DrawAction::DrawForward(20.0),
412    ///         DrawAction::RotateCW(PI/2.0),
413    ///         DrawAction::MoveForward(10.0),
414    ///         DrawAction::DrawForward(10.0)
415    ///     ]
416    /// );
417    ///
418    /// let pth = action_string.make_path(Point::new(5.0, 5.0), 0.0).unwrap();
419    ///
420    /// let pc_vec: Vec<PathCommand> = vec![
421    ///     PathCommand::MoveTo(Point::new(5.0, 5.0)),
422    ///     PathCommand::LineTo(Point::new(25.0, 5.0)),
423    ///     // Note that in SVG, the origin is in the top-left corner, and
424    ///     // the positive y-axis points downward.
425    ///     PathCommand::MoveTo(Point::new(25.0, 15.0)),
426    ///     PathCommand::LineTo(Point::new(25.0, 25.0))
427    /// ];
428    ///
429    /// let pth2: Path = pc_vec.into();
430    ///
431    /// assert!(pth.almost_eq(&pth2, 0.001));
432    /// ```
433    pub fn make_path(&self, start: Point, start_angle: f64) -> Result<Path, LSystemError> {
434        let mut current_angle: f64 = start_angle;
435        let mut path_points: Vec<PathCommand> = vec![PathCommand::MoveTo(start)];
436        let mut stacks: HashMap<usize, Vec<(Point, f64)>> = HashMap::new();
437
438        for k in self.0.iter() {
439            let last_point = path_points.last().unwrap().point();
440            match *k {
441                DrawAction::DrawForward(dist) => {
442                    path_points.push(PathCommand::LineTo(
443                        last_point
444                            + Point::from_polar(num_validity::err_if_invalid(dist)?, current_angle),
445                    ));
446                }
447                DrawAction::MoveForward(dist) => {
448                    path_points.push(PathCommand::MoveTo(
449                        last_point
450                            + Point::from_polar(num_validity::err_if_invalid(dist)?, current_angle),
451                    ));
452                }
453                DrawAction::RotateCW(angle) => {
454                    current_angle += num_validity::err_if_invalid(angle)?;
455                    current_angle = current_angle.rem_euclid(TAU);
456                }
457                DrawAction::Push(idx) => {
458                    if let Some(stck) = stacks.get_mut(&idx) {
459                        stck.push((last_point, current_angle));
460                    } else {
461                        stacks.insert(idx, vec![(last_point, current_angle)]);
462                    }
463                }
464                DrawAction::Pop(idx) => {
465                    if let Some(stck) = stacks.get_mut(&idx) {
466                        if let Some((pt, angle)) = stck.pop() {
467                            path_points.push(PathCommand::MoveTo(pt));
468                            current_angle = angle;
469                        }
470                    }
471                }
472                DrawAction::Null => { /* pass */ }
473            }
474        }
475        Ok(Path::from(path_points))
476    }
477
478    /// Creates a path with the default starting point and angle (`Point::new(0.0, 0.0)` and `0.0`,
479    /// respectively).
480    ///
481    /// The `TryFrom<ActionString>` trait for `Path` is identical.
482    ///
483    /// # Example
484    ///
485    /// ```
486    /// use std::f64::consts::PI;
487    ///
488    /// use l_system_fractals::rules::{DrawAction, ActionString};
489    /// use l_system_fractals::paths::{Path, Point};
490    /// use l_system_fractals::num_validity::AlmostEq;
491    ///
492    /// let action_string = ActionString(
493    ///     vec![
494    ///         DrawAction::DrawForward(20.0),
495    ///         DrawAction::RotateCW(PI/2.0),
496    ///         DrawAction::MoveForward(10.0),
497    ///         DrawAction::DrawForward(10.0)
498    ///     ]
499    /// );
500    ///
501    /// let pth1 = action_string.make_default_path().unwrap();
502    /// let pth2 = action_string.make_path(Point::new(0.0, 0.0), 0.0).unwrap();
503    /// let pth3: Path = action_string.try_into().unwrap();
504    ///
505    /// assert!(pth1.almost_eq(&pth2, 0.001));
506    /// assert!(pth2.almost_eq(&pth3, 0.001));
507    /// ```
508    pub fn make_default_path(&self) -> Result<Path, LSystemError> {
509        self.make_path(Point::new(0.0, 0.0), 0.0)
510    }
511}
512
513impl TryFrom<ActionString> for Path {
514    type Error = LSystemError;
515
516    fn try_from(s: ActionString) -> Result<Self, Self::Error> {
517        s.make_default_path()
518    }
519}
520
521impl From<HashMap<char, DrawAction>> for RuleAssignment {
522    fn from(hm: HashMap<char, DrawAction>) -> Self {
523        Self(hm)
524    }
525}
526
527impl RuleAssignment {
528    /// Applies the assignment to a string to produce an action string.
529    ///
530    /// # Example
531    ///
532    /// ```
533    /// use std::f64::consts::PI;
534    /// use std::collections::HashMap;
535    /// use l_system_fractals::num_validity::AlmostEq;
536    ///
537    /// use l_system_fractals::rules::{DrawAction, ActionString, RuleAssignment};
538    ///
539    /// let assignment = RuleAssignment(
540    ///     HashMap::from([
541    ///         ('A', DrawAction::DrawForward(1.0)),
542    ///         ('B', DrawAction::MoveForward(1.0))
543    ///     ])
544    /// );
545    ///
546    /// let action1 = assignment.translate("ABA".to_string());
547    /// let action2 = ActionString(
548    ///     vec![
549    ///         DrawAction::DrawForward(1.0),
550    ///         DrawAction::MoveForward(1.0),
551    ///         DrawAction::DrawForward(1.0)
552    ///     ]
553    /// );
554    ///
555    /// assert!(action1.almost_eq(&action2, 0.001));
556    /// ```
557    pub fn translate(&self, s: String) -> ActionString {
558        ActionString(
559            s.chars()
560                .map(|c| *(self.0.get(&c).unwrap_or(&DrawAction::Null)))
561                .collect(),
562        )
563    }
564
565    /// Translates the string using the assignment and then makes a path with the given starting
566    /// point and initial angle.
567    ///
568    /// # Example
569    ///
570    /// ```
571    /// use std::f64::consts::PI;
572    /// use std::collections::HashMap;
573    ///
574    /// use l_system_fractals::rules::{DrawAction, ActionString, RuleAssignment};
575    /// use l_system_fractals::paths::{Path, Point};
576    /// use l_system_fractals::num_validity::AlmostEq;
577    ///
578    /// let assignment = RuleAssignment(
579    ///     HashMap::from([
580    ///         ('A', DrawAction::DrawForward(1.0)),
581    ///         ('B', DrawAction::MoveForward(1.0))
582    ///     ])
583    /// );
584    ///
585    /// let pth1 = assignment
586    ///     .make_path("ABA".to_string(), Point::new(1.0, 1.0), 0.0)
587    ///     .unwrap();
588    /// let pth2 = assignment
589    ///     .translate("ABA".to_string())
590    ///     .make_path(Point::new(1.0, 1.0), 0.0)
591    ///     .unwrap();
592    ///
593    /// assert!(pth1.almost_eq(&pth2, 0.001));
594    /// ```
595    pub fn make_path(
596        &self,
597        s: String,
598        start: Point,
599        start_angle: f64,
600    ) -> Result<Path, LSystemError> {
601        self.translate(s).make_path(start, start_angle)
602    }
603
604    /// Translates the string using the assignment and then makes a path with the default starting
605    /// point and angle (`Point::new(0.0, 0.0)` and `0.0`, respectively).
606    ///
607    ///
608    /// # Example
609    ///
610    /// ```
611    /// use std::f64::consts::PI;
612    /// use std::collections::HashMap;
613    ///
614    /// use l_system_fractals::rules::{DrawAction, ActionString, RuleAssignment};
615    /// use l_system_fractals::paths::{Path, Point};
616    /// use l_system_fractals::num_validity::AlmostEq;
617    ///
618    /// let assignment = RuleAssignment(
619    ///     HashMap::from([
620    ///         ('A', DrawAction::DrawForward(1.0)),
621    ///         ('B', DrawAction::MoveForward(1.0))
622    ///     ])
623    /// );
624    ///
625    /// let pth1 = assignment
626    ///     .make_path("ABA".to_string(), Point::new(0.0, 0.0), 0.0)
627    ///     .unwrap();
628    /// let pth2 = assignment
629    ///     .make_default_path("ABA".to_string())
630    ///     .unwrap();
631    ///
632    /// assert!(pth1.almost_eq(&pth2, 0.001));
633    /// ```
634    pub fn make_default_path(&self, s: String) -> Result<Path, LSystemError> {
635        let p: Path = self.translate(s).try_into()?;
636        Ok(p)
637    }
638}
639
640/// Gives the specification for an L-system, excluding the initial string.
641#[derive(Clone, Debug, PartialEq)]
642pub struct DrawRules {
643    /// The rewriting rules.
644    pub rewrite_rules: RewriteRules,
645    /// The assignment of characters to draw actions.
646    pub assignment: RuleAssignment,
647}
648
649impl AlmostEq for DrawRules {
650    fn almost_eq(&self, other: &Self, epsilon: f64) -> bool {
651        self.rewrite_rules == other.rewrite_rules
652            && self.assignment.almost_eq(&other.assignment, epsilon)
653    }
654}
655
656impl DrawRules {
657    /// Creates `DrawRules` with the given rewrite rules and rule assignments given as
658    /// [`HashMap`]s.
659    ///
660    /// # Example
661    ///
662    /// ```
663    /// use std::collections::HashMap;
664    /// use std::f64::consts::PI;
665    ///
666    /// use l_system_fractals::rules::{DrawAction, DrawRules};
667    /// use l_system_fractals::num_validity::AlmostEq;
668    ///
669    /// // Koch snowflake
670    /// // from Benoit B. Mandelbrot (1983), _The Fractal Geometry of Nautre (Updated and
671    /// // Agumented)_, New York: W.H. Freeman and Company, p. 42
672    ///
673    /// let rwr_hm: HashMap<char, String> = HashMap::from([('A', "A+A--A+A".into())]);
674    /// let assignment_hm: HashMap<char, DrawAction> =  HashMap::from([
675    ///    ('A', DrawAction::DrawForward(1.0)),
676    ///    ('+', DrawAction::RotateCW(PI/3.0)),
677    ///    ('-', DrawAction::RotateCW(-PI/3.0))
678    /// ]);
679    ///
680    /// let dr1 = DrawRules::new(rwr_hm.clone(), assignment_hm.clone());
681    /// let dr2 = DrawRules { rewrite_rules: rwr_hm.into(), assignment: assignment_hm.into() };
682    ///
683    /// assert!(dr1.almost_eq(&dr2, 0.001));
684    /// ```
685    pub fn new(
686        rewrite_rules: HashMap<char, String>,
687        assignment: HashMap<char, DrawAction>,
688    ) -> Self {
689        Self {
690            rewrite_rules: RewriteRules(rewrite_rules),
691            assignment: RuleAssignment(assignment),
692        }
693    }
694
695    fn extract_chars_rules(rules: &HashMap<char, String>) -> Vec<char> {
696        let mut out_vec: Vec<char> = rules.keys().copied().collect();
697        for s in rules.values() {
698            out_vec.extend(s.chars().filter(|c| !USED_CHARS.contains(c)));
699        }
700        out_vec
701    }
702
703    /// Creates a new DrawRules with the specified replacement rules and angle.
704    ///
705    /// The resulting DrawRules will have the following properties:
706    ///
707    /// - The following commands are defined:
708    ///   - `+`: rotate clockwise by angle
709    ///   - `-`: rotate counterclockwise by angle
710    ///   - `[`: push current location/angle pair onto stack `0`
711    ///   - `]`: pop location/angle pair off of stack `0`
712    ///
713    /// - The angle is specified as `PI * angle_numerator / angle_denominator`.
714    ///
715    /// - Every other character represents drawing forward a distance of 1.0.
716    ///
717    /// # Example
718    ///
719    /// ```
720    /// use std::collections::HashMap;
721    /// use std::f64::consts::PI;
722    ///
723    /// use l_system_fractals::rules::{DrawAction, DrawRules, RuleAssignment, RewriteRules};
724    /// use l_system_fractals::num_validity::AlmostEq;
725    ///
726    /// // Koch snowflake
727    /// // from Benoit B. Mandelbrot (1983), _The Fractal Geometry of Nautre (Updated and
728    /// // Agumented)_, New York: W.H. Freeman and Company, p. 42
729    ///
730    /// let rwr_hm = HashMap::from([('A', "A+A--A+A".into())]);
731    /// let angle_numerator: isize = 1;
732    /// let angle_denominator: usize = 3;
733    ///
734    /// let dr1 = DrawRules::new_simple(
735    ///     rwr_hm.clone(),
736    ///     angle_numerator,
737    ///     angle_denominator
738    /// ).unwrap();
739    ///
740    /// let rwr = RewriteRules(rwr_hm);
741    /// let assignment = RuleAssignment(
742    ///     HashMap::from([
743    ///         ('A', DrawAction::DrawForward(1.0)),
744    ///         ('+', DrawAction::RotateCW(PI/3.0)),
745    ///         ('-', DrawAction::RotateCW(-PI/3.0)),
746    ///         ('[', DrawAction::Push(0)),
747    ///         (']', DrawAction::Pop(0))
748    ///     ])
749    /// );
750    ///
751    /// let dr2 = DrawRules {
752    ///     rewrite_rules: rwr,
753    ///     assignment,
754    /// };
755    ///
756    /// assert!(dr1.almost_eq(&dr2, 0.001));
757    /// ```
758    pub fn new_simple(
759        replacement: HashMap<char, String>,
760        angle_numerator: isize,
761        angle_denominator: usize,
762    ) -> Result<Self, LSystemError> {
763        if angle_denominator == 0 {
764            Err(LSystemError::DivideByZero)
765        } else {
766            let angle = PI * angle_numerator as f64 / angle_denominator as f64;
767            let mut assignment_hm: HashMap<char, DrawAction> = HashMap::from([
768                ('+', DrawAction::RotateCW(angle)),
769                ('-', DrawAction::RotateCW(-angle)),
770                ('[', DrawAction::Push(0)),
771                (']', DrawAction::Pop(0)),
772            ]);
773            assignment_hm.extend(
774                Self::extract_chars_rules(&replacement)
775                    .iter()
776                    .map(|c| (*c, DrawAction::DrawForward(1.0))),
777            );
778            let rewrite_rules = RewriteRules(replacement);
779            let assignment = RuleAssignment(assignment_hm);
780            Ok(Self {
781                rewrite_rules,
782                assignment,
783            })
784        }
785    }
786
787    /// Creates a new DrawRules with the specified replacement rules, draw and move distances,
788    /// and angle.
789    ///
790    /// The resulting DrawRules will have the following properties:
791    ///
792    /// - The following commands are defined:
793    ///   - `+`: rotate clockwise by angle
794    ///   - `-`: rotate counterclockwise by angle
795    ///   - `[`: push current location/angle pair onto stack `0`
796    ///   - `]`: pop location/angle pair off of stack `0`
797    ///
798    /// - The angle is specified as `PI * angle_numerator / angle_denominator`.
799    ///
800    /// - Other characters are mapped to drawing or moving forward based on
801    ///   the fields `draw_step_sizes` and `move_step_sizes`.
802    ///
803    /// - Any other character is mapped to `Null`.
804    ///
805    /// # Example
806    ///
807    /// ```
808    /// use std::collections::HashMap;
809    /// use std::f64::consts::PI;
810    ///
811    /// use l_system_fractals::rules::{DrawAction, DrawRules, RuleAssignment, RewriteRules};
812    /// use l_system_fractals::num_validity::AlmostEq;
813    ///
814    /// // Islands
815    /// // from Benoit B. Mandelbrot (1983), _The Fractal Geometry of Nautre (Updated and
816    /// // Agumented)_, New York: W.H. Freeman and Company, p. 121
817    ///
818    /// let replacement = HashMap::from([
819    ///     ('A', "A+BA-AA-A-AA+B+AA-BA+AA+A+AA-B-AAA".into()),
820    ///     ('B', "BBBBBB".into())
821    /// ]);
822    /// let draw_step_sizes = HashMap::from([('A', 1.0)]);
823    /// let move_step_sizes = HashMap::from([('B', 1.0)]);
824    /// let angle_numerator: isize = 1;
825    /// let angle_denominator: usize = 2;
826    ///
827    /// let dr1 = DrawRules::new_advanced(
828    ///     replacement.clone(),
829    ///     draw_step_sizes,
830    ///     move_step_sizes,
831    ///     angle_numerator,
832    ///     angle_denominator,
833    /// ).unwrap();
834    ///
835    /// let rwr = RewriteRules(replacement);
836    /// let assignment = RuleAssignment(
837    ///     HashMap::from([
838    ///         ('A', DrawAction::DrawForward(1.0)),
839    ///         ('B', DrawAction::MoveForward(1.0)),
840    ///         ('+', DrawAction::RotateCW(PI/2.0)),
841    ///         ('-', DrawAction::RotateCW(-PI/2.0)),
842    ///         ('[', DrawAction::Push(0)),
843    ///         (']', DrawAction::Pop(0))
844    ///     ])
845    /// );
846    ///
847    /// let dr2 = DrawRules {
848    ///     rewrite_rules: rwr,
849    ///     assignment,
850    /// };
851    ///
852    /// assert!(dr1.almost_eq(&dr2, 0.001));
853    /// ```
854    pub fn new_advanced(
855        replacement: HashMap<char, String>,
856        draw_step_sizes: HashMap<char, f64>,
857        move_step_sizes: HashMap<char, f64>,
858        angle_numerator: isize,
859        angle_denominator: usize,
860    ) -> Result<Self, LSystemError> {
861        if angle_denominator == 0 {
862            Err(LSystemError::DivideByZero)
863        } else {
864            let angle = PI * angle_numerator as f64 / angle_denominator as f64;
865            let mut assignment_hm: HashMap<char, DrawAction> = HashMap::from([
866                ('+', DrawAction::RotateCW(angle)),
867                ('-', DrawAction::RotateCW(-angle)),
868                ('[', DrawAction::Push(0)),
869                (']', DrawAction::Pop(0)),
870            ]);
871            assignment_hm.extend(
872                draw_step_sizes
873                    .iter()
874                    .map(|(c, r)| (*c, DrawAction::DrawForward(*r))),
875            );
876            assignment_hm.extend(
877                move_step_sizes
878                    .iter()
879                    .map(|(c, r)| (*c, DrawAction::MoveForward(*r))),
880            );
881            let rewrite_rules = RewriteRules(replacement);
882            let assignment = RuleAssignment(assignment_hm);
883            Ok(Self {
884                rewrite_rules,
885                assignment,
886            })
887        }
888    }
889
890    /// Applies the the rules to the intial string `s` to create a path with given
891    /// starting point and angle.
892    ///
893    /// # Example
894    ///
895    /// ```
896    /// use std::collections::HashMap;
897    ///
898    /// use l_system_fractals::rules::DrawRules;
899    /// use l_system_fractals::paths::{Path, Point};
900    /// use l_system_fractals::num_validity::AlmostEq;
901    ///
902    /// let dw = DrawRules::new_simple(
903    ///     HashMap::from([
904    ///         ('A', "A-C+B+C-A".into()),
905    ///         ('B', "BBB".into()),
906    ///         ('C', "C".into())
907    ///     ]),
908    ///     1,
909    ///     2
910    /// ).unwrap();
911    ///
912    /// let pth1 = dw.make_path(
913    ///     "A-C+B+C-A".into(),
914    ///     Point::new(1.0, 5.0),
915    ///     0.0
916    /// ).unwrap();
917    ///
918    /// let pth2 = Path::from(
919    ///     vec![
920    ///         Point::new(1.0, 5.0),
921    ///         Point::new(2.0, 5.0),
922    ///         Point::new(2.0, 4.0),
923    ///         Point::new(3.0, 4.0),
924    ///         Point::new(3.0, 5.0),
925    ///         Point::new(4.0, 5.0)
926    ///     ]
927    /// );
928    ///
929    /// assert!(pth1.almost_eq(&pth2, 0.001));
930    /// ```
931    pub fn make_path(
932        &self,
933        s: String,
934        start: Point,
935        start_angle: f64,
936    ) -> Result<Path, LSystemError> {
937        self.assignment.translate(s).make_path(start, start_angle)
938    }
939
940    /// Applies the rules to the string `s` to create a path from the default starting point
941    /// (`Point::new(0.0, 0.0)`) and angle (`0`).
942    ///
943    /// # Example
944    ///
945    /// ```
946    /// use std::collections::HashMap;
947    ///
948    /// use l_system_fractals::rules::DrawRules;
949    /// use l_system_fractals::paths::{Path, Point};
950    /// use l_system_fractals::num_validity::AlmostEq;
951    ///
952    /// let dw = DrawRules::new_simple(
953    ///     HashMap::from([
954    ///         ('A', "A-C+B+C-A".into()),
955    ///         ('B', "BBB".into()),
956    ///         ('C', "C".into())
957    ///     ]),
958    ///     1,
959    ///     2
960    /// ).unwrap();
961    ///
962    /// let pth1 = dw.make_default_path(
963    ///     "A-C+B+C-A".into(),
964    /// ).unwrap();
965    ///
966    /// let pth2 = Path::from(
967    ///     vec![
968    ///         Point::new(0.0, 0.0),
969    ///         Point::new(1.0, 0.0),
970    ///         Point::new(1.0, -1.0),
971    ///         Point::new(2.0, -1.0),
972    ///         Point::new(2.0, 0.0),
973    ///         Point::new(3.0, 0.0)
974    ///     ]
975    /// );
976    ///
977    /// assert!(pth1.almost_eq(&pth2, 0.001));
978    /// ```
979    pub fn make_default_path(&self, s: String) -> Result<Path, LSystemError> {
980        self.make_path(s, Point::new(0.0, 0.0), 0.0)
981    }
982}
983
984/// Gives the specification for an L-System, including the intitial string.
985#[derive(Clone, Debug, PartialEq)]
986pub struct InitializedDrawRules {
987    /// The rewriting rules and assignment of characters to draw actions.
988    pub draw_rules: DrawRules,
989    /// The initial string.
990    pub start: String,
991}
992
993impl AlmostEq for InitializedDrawRules {
994    fn almost_eq(&self, other: &Self, epsilon: f64) -> bool {
995        self.draw_rules.almost_eq(&other.draw_rules, epsilon) && self.start == other.start
996    }
997}
998
999impl InitializedDrawRules {
1000    /// Creates `InitializedDrawRules` with the given rewrite rules and rule assignments given as
1001    /// [`HashMap`]s, and the specified start string.
1002    ///
1003    /// # Example
1004    ///
1005    /// ```
1006    /// use std::collections::HashMap;
1007    ///
1008    /// use l_system_fractals::rules::{
1009    ///     DrawRules,
1010    ///     InitializedDrawRules,
1011    ///     DrawAction,
1012    ///     RewriteRules,
1013    ///     RuleAssignment
1014    /// };
1015    /// use l_system_fractals::num_validity::AlmostEq;
1016    ///
1017    /// let rwr_hm = HashMap::from([
1018    ///     ('=', "=.=".to_string()),
1019    ///     ('.', "...".to_string())
1020    /// ]);
1021    ///
1022    /// let assignment_hm = HashMap::from([
1023    ///     ('=', DrawAction::DrawForward(1.0)),
1024    ///     ('.', DrawAction::MoveForward(1.0))
1025    /// ]);
1026    ///
1027    /// let start = String::from("=");
1028    ///
1029    /// let dr1 = InitializedDrawRules::new(rwr_hm.clone(), assignment_hm.clone(), start.clone());
1030    /// let dr2 = InitializedDrawRules {
1031    ///     draw_rules: DrawRules {
1032    ///         rewrite_rules: RewriteRules(rwr_hm),
1033    ///         assignment: RuleAssignment(assignment_hm)
1034    ///     },
1035    ///     start
1036    /// };
1037    ///
1038    /// assert!(dr1.almost_eq(&dr2, 0.001));
1039    /// ```
1040    pub fn new(
1041        rewrite_rules: HashMap<char, String>,
1042        assignment: HashMap<char, DrawAction>,
1043        start: String,
1044    ) -> Self {
1045        Self {
1046            draw_rules: DrawRules::new(rewrite_rules, assignment),
1047            start,
1048        }
1049    }
1050
1051    /// Creates a new `InitializedDrawRules` with the specified replacement rules, angle, and start
1052    /// string.
1053    ///
1054    /// The resulting `InitializedDrawRules` will have the following properties:
1055    ///
1056    /// - The following commands are defined:
1057    ///   - `+`: rotate clockwise by angle
1058    ///   - `-`: rotate counterclockwise by angle
1059    ///   - `[`: push current location/angle pair onto stack `0`
1060    ///   - `]`: pop location/angle pair off of stack `0`
1061    ///
1062    /// - The angle is specified as `PI * angle_numerator / angle_denominator`.
1063    ///
1064    /// - Every other character represents drawing forward a distance of 1.0.
1065    ///
1066    /// # Example
1067    ///
1068    /// ```
1069    /// use std::collections::HashMap;
1070    /// use std::f64::consts::PI;
1071    ///
1072    /// use l_system_fractals::rules::{
1073    ///     DrawAction,
1074    ///     DrawRules,
1075    ///     InitializedDrawRules,
1076    ///     RuleAssignment,
1077    ///     RewriteRules
1078    /// };
1079    /// use l_system_fractals::num_validity::AlmostEq;
1080    ///
1081    /// // Koch snowflake
1082    /// // from Benoit B. Mandelbrot (1983), _The Fractal Geometry of Nautre (Updated and
1083    /// // Agumented)_, New York: W.H. Freeman and Company, p. 42
1084    ///
1085    /// let rwr_hm = HashMap::from([('A', "A+A--A+A".into())]);
1086    /// let angle_numerator: isize = 1;
1087    /// let angle_denominator: usize = 3;
1088    /// let start = String::from("+A--A--A");
1089    ///
1090    /// let dr1 = InitializedDrawRules::new_simple(
1091    ///     rwr_hm.clone(),
1092    ///     angle_numerator,
1093    ///     angle_denominator,
1094    ///     start.clone()
1095    /// ).unwrap();
1096    ///
1097    /// let rwr = RewriteRules(rwr_hm);
1098    /// let assignment = RuleAssignment(
1099    ///     HashMap::from([
1100    ///         ('A', DrawAction::DrawForward(1.0)),
1101    ///         ('+', DrawAction::RotateCW(PI/3.0)),
1102    ///         ('-', DrawAction::RotateCW(-PI/3.0)),
1103    ///         ('[', DrawAction::Push(0)),
1104    ///         (']', DrawAction::Pop(0))
1105    ///     ])
1106    /// );
1107    ///
1108    /// let dr2 = InitializedDrawRules {
1109    ///     draw_rules: DrawRules {
1110    ///         rewrite_rules: rwr,
1111    ///         assignment
1112    ///     },
1113    ///     start
1114    /// };
1115    ///
1116    /// assert!(dr1.almost_eq(&dr2, 0.001));
1117    /// ```
1118    pub fn new_simple(
1119        replacement: HashMap<char, String>,
1120        angle_numerator: isize,
1121        angle_denominator: usize,
1122        start: String,
1123    ) -> Result<Self, LSystemError> {
1124        let draw_rules = DrawRules::new_simple(replacement, angle_numerator, angle_denominator)?;
1125        Ok(Self { draw_rules, start })
1126    }
1127
1128    /// Creates a new InitializedDrawRules with the specified replacement rules, draw and move
1129    /// distances, angle, and start string.
1130    ///
1131    /// The resulting InitializedDrawRules will have the following properties:
1132    ///
1133    /// - The following commands are defined:
1134    ///   - `+`: rotate clockwise by angle
1135    ///   - `-`: rotate counterclockwise by angle
1136    ///   - `[`: push current location/angle pair onto stack `0`
1137    ///   - `]`: pop location/angle pair off of stack `0`
1138    ///
1139    /// - The angle is specified as `PI * angle_numerator / angle_denominator`.
1140    ///
1141    /// - Other characters are mapped to drawing or moving forward based on
1142    ///   the fields `draw_step_sizes` and `move_step_sizes`.
1143    ///
1144    /// - Any other character is mapped to `Null`.
1145    ///
1146    /// # Example
1147    ///
1148    /// ```
1149    /// use std::collections::HashMap;
1150    /// use std::f64::consts::PI;
1151    ///
1152    /// use l_system_fractals::rules::{
1153    ///     DrawAction,
1154    ///     DrawRules,
1155    ///     InitializedDrawRules,
1156    ///     RuleAssignment,
1157    ///     RewriteRules
1158    /// };
1159    /// use l_system_fractals::num_validity::AlmostEq;
1160    ///
1161    /// // Islands
1162    /// // from Benoit B. Mandelbrot (1983), _The Fractal Geometry of Nautre (Updated and
1163    /// // Agumented)_, New York: W.H. Freeman and Company, p. 121
1164    ///
1165    /// let replacement = HashMap::from([
1166    ///     ('A', "A+BA-AA-A-AA+B+AA-BA+AA+A+AA-B-AAA".into()),
1167    ///     ('B', "BBBBBB".into())
1168    /// ]);
1169    /// let draw_step_sizes = HashMap::from([('A', 1.0)]);
1170    /// let move_step_sizes = HashMap::from([('B', 1.0)]);
1171    /// let angle_numerator: isize = 1;
1172    /// let angle_denominator: usize = 2;
1173    /// let start = "A-A-A-A-".to_string();
1174    ///
1175    /// let dr1 = InitializedDrawRules::new_advanced(
1176    ///     replacement.clone(),
1177    ///     draw_step_sizes,
1178    ///     move_step_sizes,
1179    ///     angle_numerator,
1180    ///     angle_denominator,
1181    ///     start.clone()
1182    /// ).unwrap();
1183    ///
1184    /// let rwr = RewriteRules(replacement);
1185    /// let assignment = RuleAssignment(
1186    ///     HashMap::from([
1187    ///         ('A', DrawAction::DrawForward(1.0)),
1188    ///         ('B', DrawAction::MoveForward(1.0)),
1189    ///         ('+', DrawAction::RotateCW(PI/2.0)),
1190    ///         ('-', DrawAction::RotateCW(-PI/2.0)),
1191    ///         ('[', DrawAction::Push(0)),
1192    ///         (']', DrawAction::Pop(0))
1193    ///     ])
1194    /// );
1195    ///
1196    /// let dr2 = InitializedDrawRules {
1197    ///     draw_rules: DrawRules {
1198    ///         rewrite_rules: rwr,
1199    ///         assignment
1200    ///     },
1201    ///     start
1202    /// };
1203    ///
1204    /// assert!(dr1.almost_eq(&dr2, 0.001));
1205    /// ```
1206    pub fn new_advanced(
1207        replacement: HashMap<char, String>,
1208        draw_step_sizes: HashMap<char, f64>,
1209        move_step_sizes: HashMap<char, f64>,
1210        angle_numerator: isize,
1211        angle_denominator: usize,
1212        start: String,
1213    ) -> Result<Self, LSystemError> {
1214        let draw_rules = DrawRules::new_advanced(
1215            replacement,
1216            draw_step_sizes,
1217            move_step_sizes,
1218            angle_numerator,
1219            angle_denominator,
1220        )?;
1221        Ok(Self { draw_rules, start })
1222    }
1223
1224    /// Produces paths for the specified number of iterations, returning the [`Path`]s as
1225    /// [`HashMap`]s.
1226    ///
1227    /// # Example
1228    ///
1229    /// ```
1230    /// use std::collections::HashMap;
1231    /// use std::f64::consts::PI;
1232    ///
1233    /// use l_system_fractals::rules::InitializedDrawRules;
1234    /// use l_system_fractals::paths::{Point, Path};
1235    /// use l_system_fractals::num_validity::AlmostEq;
1236    ///
1237    /// // Koch curve
1238    /// // from Benoit B. Mandelbrot (1983), _The Fractal Geometry of Nautre (Updated and
1239    /// // Agumented)_, New York: W.H. Freeman and Company, p. 42
1240    ///
1241    ///
1242    /// let dr = InitializedDrawRules::new_simple(
1243    ///     HashMap::from([('A', "A+A--A+A".into())]),
1244    ///     1,
1245    ///     3,
1246    ///     "A".into()
1247    /// ).unwrap();
1248    ///
1249    /// let ipaths = dr.get_iteration_paths(2, Point::new(1.0, 1.0), 0.0).unwrap();
1250    ///
1251    /// let p0: Path = vec![Point::new(1.0, 1.0), Point::new(2.0, 1.0)].into();
1252    /// assert!(ipaths.get(&0).unwrap().almost_eq(&p0, 0.001));
1253    ///
1254    /// let p1: Path = vec![
1255    ///     Point::new(1.0, 1.0),
1256    ///     Point::new(2.0, 1.0),
1257    ///     Point::new(2.5, 1.0 + 3.0_f64.sqrt() / 2.0),
1258    ///     Point::new(3.0, 1.0),
1259    ///     Point::new(4.0, 1.0)
1260    /// ].into();
1261    /// assert!(ipaths.get(&1).unwrap().almost_eq(&p1, 0.001));
1262    ///
1263    /// assert_eq!(ipaths.get(&2).unwrap().0.len(), 17);
1264    /// ```
1265    pub fn get_iteration_paths(
1266        &self,
1267        iterations: usize,
1268        start: Point,
1269        start_angle: f64,
1270    ) -> Result<HashMap<usize, Path>, LSystemError> {
1271        let replaced_strings = self
1272            .draw_rules
1273            .rewrite_rules
1274            .iterations_up_to(self.start.clone(), iterations);
1275        let mut outmap: HashMap<usize, Path> = HashMap::new();
1276        for (k, s) in replaced_strings.iter() {
1277            outmap.insert(
1278                *k,
1279                self.draw_rules
1280                    .make_path(s.to_string(), start, start_angle)?,
1281            );
1282        }
1283        Ok(outmap)
1284    }
1285
1286    /// Creates an SVG of the result of the specified iteration.
1287    ///
1288    /// The resulting SVG (returned as a [`String`] that can be saved into a file) will be resized
1289    /// so that its width and height are at most `max_width` and `max_height`, respectively, and
1290    /// will be styled with the given `fill`, `stroke` and `stroke_width`.
1291    pub fn make_iteration_svg(
1292        &self,
1293        iteration: usize,
1294        params: &PlotParameters,
1295    ) -> Result<String, LSystemError> {
1296        let output_string: String = self
1297            .draw_rules
1298            .rewrite_rules
1299            .repeat_iterate(self.start.clone(), iteration);
1300        let path: Path = self.draw_rules.make_default_path(output_string)?;
1301        path.svg_output(params)
1302    }
1303
1304    /// Creates an SVG showing all specified iterations.
1305    ///
1306    /// The resulting paths will be showing in a grid, where every row has length `row_len`.
1307    ///
1308    /// The resulting SVG (returned as a [`String`] that can be saved into a file) will be resized
1309    /// so that its width and height are at most `max_width` and `max_height`, respectively, and
1310    /// will be styled with the given `fill`, `stroke` and `stroke_width`.
1311    pub fn make_combined_svg(
1312        &self,
1313        params: &PlotParameters,
1314        row_len: usize,
1315        iterations: Vec<usize>,
1316    ) -> Result<String, LSystemError> {
1317        let mut inside = String::new();
1318        if !iterations.is_empty() {
1319            if row_len == 0 {
1320                return Err(LSystemError::DivideByZero);
1321            } else {
1322                let max_iter = iterations.iter().max().unwrap();
1323                let inner_width = num_validity::err_if_invalid(
1324                    (params.width - params.border * (row_len as f64 + 1.0)) / (row_len as f64),
1325                )?;
1326                let num_rows = iterations.len() / row_len
1327                    + if iterations.len() % row_len == 0 {
1328                        0
1329                    } else {
1330                        1
1331                    };
1332                let inner_height = num_validity::err_if_invalid(
1333                    (params.height - params.border * (num_rows as f64 + 1.0)) / (num_rows as f64),
1334                )?;
1335                let iter_paths = self.get_iteration_paths(*max_iter, Point::new(0.0, 0.0), 0.0)?;
1336                for (idx, k) in iterations.iter().enumerate() {
1337                    let vert_loc: usize = idx / row_len;
1338                    let horiz_loc: usize = idx % row_len;
1339                    inside += &iter_paths
1340                        .get(k)
1341                        .unwrap()
1342                        .clone()
1343                        .rescale(inner_width, inner_height, params.border)?
1344                        .0
1345                        .svg_path_output(
1346                            Point::new(
1347                                (params.border + inner_width) * horiz_loc as f64,
1348                                (params.border + inner_height) * vert_loc as f64,
1349                            ),
1350                            &params.fill,
1351                            &params.stroke,
1352                            params.stroke_width,
1353                        )?;
1354                }
1355            }
1356        }
1357        let mut out_str = format!(
1358            "<svg ViewBox=\"0 0 {:0.5} {:0.5}\" xmlns=\"http://www.w3.org/2000/svg\">",
1359            params.width, params.height
1360        );
1361        if let Some(color) = params.background.clone() {
1362            out_str +=
1363                &crate::paths::rectangle(Point::new(0.0, 0.0), params.width, params.height, &color);
1364        }
1365        out_str += &inside;
1366        out_str += "</svg>";
1367        Ok(out_str)
1368    }
1369}
1370
1371#[cfg(test)]
1372mod tests {
1373    use std::f64::consts::PI;
1374
1375    use super::DrawAction;
1376    use crate::misc::logical_xor;
1377    use crate::num_validity::AlmostEq;
1378
1379    fn generic_test_angle_abs_diff(a: f64, b: f64, epsilon: f64, result: bool) {
1380        let aad = DrawAction::angle_abs_diff(a * PI, b * PI);
1381        assert!(logical_xor(aad / PI < epsilon, !result));
1382    }
1383
1384    fn generic_test_draw_action_almost_eq(
1385        a: DrawAction,
1386        b: DrawAction,
1387        epsilon: f64,
1388        result: bool,
1389    ) {
1390        assert!(logical_xor(a.almost_eq(&b, epsilon), !result));
1391    }
1392
1393    #[test]
1394    fn test_angle_abs_diff() {
1395        generic_test_angle_abs_diff(1.5, 5.499, 0.01, true);
1396        generic_test_angle_abs_diff(1.5, 5.499, 0.000001, false);
1397        generic_test_angle_abs_diff(1.499, 5.5, 0.01, true);
1398        generic_test_angle_abs_diff(1.499, 5.5, 0.000001, false);
1399    }
1400
1401    #[test]
1402    fn test_draw_action_almost_eq() {
1403        generic_test_draw_action_almost_eq(
1404            DrawAction::DrawForward(1.0001),
1405            DrawAction::DrawForward(0.9999),
1406            0.001,
1407            true,
1408        );
1409        generic_test_draw_action_almost_eq(
1410            DrawAction::DrawForward(1.0001),
1411            DrawAction::DrawForward(0.9999),
1412            0.00001,
1413            false,
1414        );
1415        generic_test_draw_action_almost_eq(
1416            DrawAction::MoveForward(1.0001),
1417            DrawAction::MoveForward(0.9999),
1418            0.001,
1419            true,
1420        );
1421        generic_test_draw_action_almost_eq(
1422            DrawAction::MoveForward(1.0001),
1423            DrawAction::MoveForward(0.9999),
1424            0.00001,
1425            false,
1426        );
1427        generic_test_draw_action_almost_eq(
1428            DrawAction::RotateCW(0.25 * PI),
1429            DrawAction::RotateCW(0.2499999 * PI),
1430            0.0001,
1431            true,
1432        );
1433        generic_test_draw_action_almost_eq(
1434            DrawAction::RotateCW(0.25 * PI),
1435            DrawAction::RotateCW(8.2499999 * PI),
1436            0.0001,
1437            true,
1438        );
1439        generic_test_draw_action_almost_eq(
1440            DrawAction::RotateCW(8.25 * PI),
1441            DrawAction::RotateCW(4.2499999 * PI),
1442            0.0001,
1443            true,
1444        );
1445        generic_test_draw_action_almost_eq(
1446            DrawAction::RotateCW(-1.75 * PI),
1447            DrawAction::RotateCW(0.2499999 * PI),
1448            0.0001,
1449            true,
1450        );
1451        generic_test_draw_action_almost_eq(
1452            DrawAction::RotateCW(-1.75 * PI),
1453            DrawAction::RotateCW(0.24 * PI),
1454            0.0001,
1455            false,
1456        );
1457        // not the same angle!
1458        generic_test_draw_action_almost_eq(
1459            DrawAction::RotateCW(3.25 * PI),
1460            DrawAction::RotateCW(0.25 * PI),
1461            0.0001,
1462            false,
1463        );
1464        // test some cases that fall back to equality
1465        generic_test_draw_action_almost_eq(
1466            DrawAction::DrawForward(1.0001),
1467            DrawAction::MoveForward(0.9999),
1468            0.001,
1469            false,
1470        );
1471        generic_test_draw_action_almost_eq(
1472            DrawAction::DrawForward(1.0),
1473            DrawAction::MoveForward(1.0),
1474            0.00001,
1475            false,
1476        );
1477        generic_test_draw_action_almost_eq(DrawAction::Pop(0), DrawAction::Pop(0), 0.001, true);
1478        generic_test_draw_action_almost_eq(DrawAction::Push(0), DrawAction::Pop(0), 0.001, false);
1479    }
1480}