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 ¶ms.fill,
1351 ¶ms.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}