l_system_fractals/
paths.rs

1// src/paths.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//! Representation of basic SVG paths (made with moves and lines).
12use std::ops::{Add, Mul, Sub};
13
14use serde::Deserialize;
15
16use crate::errors::LSystemError;
17use crate::num_validity;
18use crate::num_validity::AlmostEq;
19
20/// Creates an SVG [`rect`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/rect).
21pub fn rectangle(upper_left: Point, width: f64, height: f64, fill: &str) -> String {
22    format!(
23        "<rect x=\"{:0.5}\" y=\"{:0.5}\" width=\"{:0.5}\" height=\"{:0.5}\" fill=\"{}\" />",
24        upper_left.x, upper_left.y, width, height, fill
25    )
26}
27
28/// Provides specifications for SVG output.
29///
30/// The SVG will be rescaled to fit within the maximum width and height.
31///
32/// # Example
33///
34/// ```
35/// use l_system_fractals::paths::PlotParameters;
36/// use l_system_fractals::num_validity::AlmostEq;
37///
38/// let json_string = r#"{
39///     "width": 700.0,
40///     "height": 700.0,
41///     "border": 50.0,
42///     "fill": "cyan",
43///     "stroke": "blue",
44///     "stroke_width": 0.5
45/// }"#;
46///
47/// let param1: PlotParameters = serde_json::from_str(json_string).unwrap();
48/// let param2 = PlotParameters {
49///     width: 700.0,
50///     height: 700.0,
51///     border: 50.0,
52///     fill: "cyan".into(),
53///     stroke: "blue".into(),
54///     stroke_width: 0.5,
55///     background: None
56/// };
57///
58/// assert!(param1.almost_eq(&param2, 0.001));
59/// ```
60#[derive(Clone, Debug, Deserialize, PartialEq)]
61pub struct PlotParameters {
62    /// Maximum width of the SVG.
63    pub width: f64,
64    /// Maximum height of the SVG.
65    pub height: f64,
66    /// Width of space around plots.
67    pub border: f64,
68    /// Value of the `fill` attribute in the SVG output (e.g. `"none"`, `"LimeGreen"`,
69    /// `"#32CD32"`).
70    pub fill: String,
71    /// Value of the `stroke` attribute in the SVG output (e.g. `"none"`, `"LimeGreen"`,
72    /// `"#32CD32"`).
73    pub stroke: String,
74    /// Value of the `stroke-width` attribute in the SVG output.
75    pub stroke_width: f64,
76    /// Value of the `fill` attribute in a background rectangle.
77    ///
78    /// No background rectangle is drawn if `None`.
79    pub background: Option<String>,
80}
81
82impl AlmostEq for PlotParameters {
83    fn almost_eq(&self, other: &Self, epsilon: f64) -> bool {
84        self.width.almost_eq(&other.width, epsilon)
85            && self.height.almost_eq(&other.height, epsilon)
86            && self.border.almost_eq(&other.border, epsilon)
87            && self.stroke_width.almost_eq(&other.stroke_width, epsilon)
88            && self.fill == other.fill
89            && self.stroke == other.stroke
90            && self.background == other.background
91    }
92}
93
94/// Your garden-variety pair of [`f64`]s.
95///
96/// Points can be [add](`Add`)ed, [sub](`Sub`)tracted, and rescaled in various ways.
97///
98/// # Examples
99///
100/// ```
101/// use l_system_fractals::paths::Point;
102/// use l_system_fractals::num_validity::AlmostEq;
103///
104/// let p1 = Point::new(1.5, 2.5);
105///
106/// assert!(p1.almost_eq(&Point::new(1.4999, 2.5001), 0.001));
107/// assert!(!p1.almost_eq(&Point::new(1.4999, 2.5001), 0.000001));
108///
109/// let p2 = Point::new(2.0, 3.0);
110///
111/// assert!((p1 + p2).almost_eq(&Point::new(3.5, 5.5), 0.001));
112/// assert!((p2 - p1).almost_eq(&Point::new(0.5, 0.5), 0.001));
113/// assert!((5.0 * p2).almost_eq(&Point::new(10.0, 15.0), 0.001));
114/// ```
115#[derive(Debug, Copy, Clone, PartialEq)]
116pub struct Point {
117    x: f64,
118    y: f64,
119}
120
121impl AlmostEq for Point {
122    fn almost_eq(&self, other: &Self, epsilon: f64) -> bool {
123        self.dist(other).almost_eq(&0.0, epsilon)
124    }
125}
126
127impl Point {
128    /// Creates a new point with the given coordinates.
129    ///
130    /// # Example
131    ///
132    /// ```
133    /// use l_system_fractals::paths::Point;
134    ///
135    /// let p1 = Point::new(1.5, 2.5);
136    /// ```
137    pub fn new(x: f64, y: f64) -> Self {
138        Self { x, y }
139    }
140
141    /// Returns `true` if each coordinate is [neither NaN or
142    /// &pm;&infin;](`num_validity::is_valid`).
143    ///
144    /// # Examples
145    ///
146    /// ```
147    /// use l_system_fractals::paths::Point;
148    ///
149    /// assert!(Point::new(2.5, -3.0).is_valid());
150    /// assert!(!Point::new(f64::NAN, -3.0).is_valid());
151    /// assert!(!Point::new(f64::INFINITY, -3.0).is_valid());
152    ///
153    /// ```
154    pub fn is_valid(&self) -> bool {
155        num_validity::is_valid(self.x) && num_validity::is_valid(self.y)
156    }
157
158    /// Raises an error if either coordinate is [NaN or &pm;&infin;](`num_validity::is_valid`).
159    ///
160    /// The error raised is [`LSystemError::InvalidFloat`].
161    ///
162    /// # Examples
163    ///
164    /// ```
165    /// use l_system_fractals::paths::Point;
166    ///
167    /// assert!(Point::new(2.5, -3.0).err_if_invalid().is_ok());
168    /// assert!(Point::new(f64::NAN, -3.0).err_if_invalid().is_err());
169    /// assert!(Point::new(f64::INFINITY, -3.0).err_if_invalid().is_err());
170    /// ```
171    pub fn err_if_invalid(&self) -> Result<Self, LSystemError> {
172        if self.is_valid() {
173            Ok(*self)
174        } else {
175            Err(LSystemError::InvalidFloat)
176        }
177    }
178
179    /// Creates a point with the specified polar coordinates.
180    ///
181    /// # Examples
182    ///
183    /// ```
184    /// use std::f64::consts::PI;
185    ///
186    /// use l_system_fractals::paths::Point;
187    /// use l_system_fractals::num_validity::AlmostEq;
188    ///
189    /// let p1 = Point::from_polar(5.0, PI / 4.0);
190    /// let p2 = Point::from_polar(-5.0, 5.0 * PI / 4.0);
191    /// let p3 = Point::new(5.0 / 2.0_f64.sqrt(), 5.0 / 2.0_f64.sqrt());
192    ///
193    /// assert!(p1.almost_eq(&p2, 0.001));
194    /// assert!(p1.almost_eq(&p3, 0.001));
195    /// ```
196    pub fn from_polar(r: f64, theta: f64) -> Self {
197        let (uy, ux) = theta.sin_cos();
198        Self::new(r * ux, r * uy)
199    }
200
201    /// Returns a string for the coordinates for use in SVG `path`s.
202    ///
203    /// # Example
204    ///
205    /// ```
206    /// use l_system_fractals::paths::Point;
207    ///
208    /// assert_eq!(Point::new(1.25, 1.25).svg_output(), "1.25000,1.25000".to_string());
209    /// ```
210    pub fn svg_output(&self) -> String {
211        format!("{:.5},{:.5}", self.x, self.y)
212    }
213
214    /// Returns the difference in horizontal coordinates.
215    ///
216    /// # Examples
217    ///
218    /// ```
219    /// use std::f64::consts::PI;
220    ///
221    /// use l_system_fractals::paths::Point;
222    /// use l_system_fractals::num_validity::AlmostEq;
223    ///
224    /// let p1 = Point::new(5.0, 4.0);
225    /// let p2 = Point::new(3.0, 9.0);
226    /// assert!(p1.delta_x(&p2).almost_eq(&-2.0, 0.001));
227    ///
228    /// let p3 = Point::from_polar(10.0, PI / 6.0);
229    /// assert!(p1.delta_x(&p3).almost_eq(&(5.0 * 3.0_f64.sqrt() - 5.0), 0.001));
230    /// ```
231    pub fn delta_x(&self, other: &Self) -> f64 {
232        other.x - self.x
233    }
234
235    /// Returns the difference in vertical coordinates.
236    ///
237    /// # Examples
238    ///
239    /// ```
240    /// use std::f64::consts::PI;
241    ///
242    /// use l_system_fractals::paths::Point;
243    /// use l_system_fractals::num_validity::AlmostEq;
244    ///
245    /// let p1 = Point::new(5.0, 4.0);
246    /// let p2 = Point::new(3.0, 9.0);
247    /// assert!(p1.delta_y(&p2).almost_eq(&5.0, 0.001));
248    ///
249    /// let p3 = Point::from_polar(10.0, PI / 3.0);
250    /// assert!(p1.delta_y(&p3).almost_eq(&(5.0 * 3.0_f64.sqrt() - 4.0), 0.001));
251    /// ```
252    pub fn delta_y(&self, other: &Self) -> f64 {
253        other.y - self.y
254    }
255
256    /// Returns a point that represents the vector between the two points.
257    ///
258    /// This is equivalent to `other - self`.
259    ///
260    /// # Examples
261    ///
262    /// ```
263    /// use std::f64::consts::PI;
264    ///
265    /// use l_system_fractals::paths::Point;
266    /// use l_system_fractals::num_validity::AlmostEq;
267    ///
268    /// let p1 = Point::new(5.0, 5.0);
269    /// let p2 = Point::new(3.0, 9.0);
270    ///
271    /// assert!(p1.delta(&p2).almost_eq(&Point::new(-2.0, 4.0), 0.001));
272    /// assert_eq!(p1.delta(&p2), p2 - p1);
273    ///
274    /// let p3 = Point::from_polar(10.0, PI / 3.0);
275    /// assert!(p1.delta(&p3).almost_eq(&Point::new(0.0, 5.0 * (3.0_f64.sqrt() - 1.0)), 0.001));
276    /// ```
277    pub fn delta(&self, other: &Self) -> Self {
278        Point::new(self.delta_x(other), self.delta_y(other))
279    }
280
281    /// Returns the angle between the two points.
282    ///
283    /// # Examples
284    ///
285    /// ```
286    /// use std::f64::consts::PI;
287    ///
288    /// use l_system_fractals::paths::Point;
289    /// use l_system_fractals::num_validity::AlmostEq;
290    ///
291    /// let p1 = Point::new(5.0, 5.0);
292    /// let p2 = Point::new(8.0, 8.0);
293    ///
294    /// assert!(p1.angle(&p2).almost_eq(&(PI / 4.0), 0.001));
295    ///
296    /// let p3 = Point::from_polar(1000.0, 1.23456);
297    /// let origin = Point::new(0.0, 0.0);
298    /// assert!(origin.angle(&p3).almost_eq(&1.23456, 0.0001));
299    /// ```
300    pub fn angle(&self, other: &Self) -> f64 {
301        self.delta_y(other).atan2(self.delta_x(other))
302    }
303
304    /// Returns the distance between two points.
305    ///
306    /// # Examples
307    ///
308    /// ```
309    /// use std::f64::consts::PI;
310    ///
311    /// use l_system_fractals::paths::Point;
312    /// use l_system_fractals::num_validity::AlmostEq;
313    ///
314    /// let p1 = Point::new(1.0, 2.0);
315    /// let p2 = Point::new(4.0, 6.0);
316    ///
317    /// assert!(p1.dist(&p2).almost_eq(&5.0, 0.001));
318    ///
319    /// let p3 = Point::from_polar(2024.0125, 1.23456);
320    /// let origin = Point::new(0.0, 0.0);
321    /// assert!(origin.dist(&p3).almost_eq(&2024.0125, 0.0001));
322    /// ```
323    pub fn dist(&self, other: &Self) -> f64 {
324        let dist_sq = self.delta_x(other).powi(2) + self.delta_y(other).powi(2);
325        dist_sq.sqrt()
326    }
327
328    /// Returns a pair of points one-third and two-third of the way between the given points.
329    ///
330    /// # Examples
331    ///
332    /// ```
333    /// use std::f64::consts::PI;
334    ///
335    /// use l_system_fractals::paths::Point;
336    /// use l_system_fractals::num_validity::AlmostEq;
337    ///
338    /// let p1 = Point::new(1.0, 1.0);
339    /// let p2 = Point::new(7.0, 16.0);
340    ///
341    /// let (q1, q2) = p1.third_points(&p2);
342    ///
343    /// assert!(q1.almost_eq(&Point::new(3.0, 6.0), 0.001));
344    /// assert!(q2.almost_eq(&Point::new(5.0, 11.0), 0.001));
345    ///
346    /// let p3 = Point::from_polar(3000.0, 1.23456);
347    /// let origin = Point::new(0.0, 0.0);
348    ///
349    /// let (q3, q4) = p3.third_points(&origin);
350    ///
351    /// assert!(q3.almost_eq(&Point::from_polar(2000.0, 1.23456), 0.001));
352    /// assert!(q4.almost_eq(&Point::from_polar(1000.0, 1.23456), 0.001));
353    /// ```
354    pub fn third_points(&self, other: &Self) -> (Self, Self) {
355        let delta = self.delta(other);
356        (*self + (1.0 / 3.0) * delta, *self + (2.0 / 3.0) * delta)
357    }
358
359    /// Multiply the vertical coordinate by the given factor.
360    ///
361    /// # Example
362    ///
363    /// ```
364    /// use l_system_fractals::paths::Point;
365    /// use l_system_fractals::num_validity::AlmostEq;
366    ///
367    /// let p1 = Point::new(1.0, 1.0);
368    ///
369    /// assert!(p1.rescale_vert(32.1).almost_eq(&Point::new(1.0, 32.1), 0.001));
370    /// ```
371    pub fn rescale_vert(&self, factor: f64) -> Self {
372        Self::new(self.x, factor * self.y)
373    }
374
375    /// Multiply the horizontal coordinate by the given factor.
376    ///
377    /// # Example
378    ///
379    /// ```
380    /// use l_system_fractals::paths::Point;
381    /// use l_system_fractals::num_validity::AlmostEq;
382    ///
383    /// let p1 = Point::new(1.0, 1.0);
384    ///
385    /// assert!(p1.rescale_horiz(32.1).almost_eq(&Point::new(32.1, 1.0), 0.001));
386    /// ```
387    pub fn rescale_horiz(&self, factor: f64) -> Self {
388        Self::new(factor * self.x, self.y)
389    }
390
391    /// Rotate the point about the origin by the given angle.
392    ///
393    /// # Examples
394    ///
395    /// ```
396    /// use std::f64::consts::PI;
397    ///
398    /// use l_system_fractals::paths::Point;
399    /// use l_system_fractals::num_validity::AlmostEq;
400    ///
401    /// let p1 = Point::new(3.0, 4.0);
402    /// assert!(p1.rotate_about_origin(PI / 2.0).almost_eq(&Point::new(-4.0, 3.0), 0.001));
403    ///
404    /// let p2 = Point::new(1.0, 1.0);
405    /// assert!(p2.rotate_about_origin(PI/4.0).almost_eq(
406    ///          &Point::new(0.0, 2.0_f64.sqrt()),
407    ///          0.001
408    /// ));
409    /// ```
410    pub fn rotate_about_origin(&self, angle: f64) -> Self {
411        let origin = Point::new(0.0, 0.0);
412        let r = self.dist(&origin);
413        let current_angle = origin.angle(self);
414        Self::from_polar(r, current_angle + angle)
415    }
416
417    /// Rotate the point about another by the given angle.
418    ///
419    /// # Examples
420    ///
421    /// ```
422    /// use std::f64::consts::PI;
423    ///
424    /// use l_system_fractals::paths::Point;
425    /// use l_system_fractals::num_validity::AlmostEq;
426    ///
427    /// let p1 = Point::new(4.0, 5.0);
428    /// let p2 = Point::new(1.0, 1.0);
429    /// assert!(p1.rotate(&p2, PI / 2.0).almost_eq(&Point::new(-3.0, 4.0), 0.001));
430    /// ```
431    pub fn rotate(&self, axis: &Point, angle: f64) -> Self {
432        (*self - *axis).rotate_about_origin(angle) + *axis
433    }
434}
435
436impl Add for Point {
437    type Output = Self;
438
439    fn add(self, other: Self) -> Self {
440        Self::new(self.x + other.x, self.y + other.y)
441    }
442}
443
444impl Sub for Point {
445    type Output = Self;
446
447    fn sub(self, other: Self) -> Self {
448        Self::new(self.x - other.x, self.y - other.y)
449    }
450}
451
452impl Mul<f64> for Point {
453    type Output = Self;
454
455    fn mul(self, other: f64) -> Self {
456        Self::new(self.x * other, self.y * other)
457    }
458}
459
460impl Mul<Point> for f64 {
461    type Output = Point;
462
463    fn mul(self, other: Point) -> Self::Output {
464        Self::Output::new(self * other.x, self * other.y)
465    }
466}
467
468/// Record of the region occupied by a [`Path`].
469///
470/// This will be a rectangle in which the points of a [`Path`] fit.
471///
472/// # Examples
473///
474/// ```
475/// use l_system_fractals::paths::{BoundingBox, Path, Point};
476/// use l_system_fractals::num_validity::AlmostEq;
477///
478/// let pth = Path::from(vec![
479///     Point::new(1.0, 5.0),
480///     Point::new(3.7, 4.5),
481///     Point::new(2.5, 3.0)
482/// ]);
483///
484/// let bb = pth.bounding_box().unwrap();
485///
486/// assert!(bb.lower_right.almost_eq(&Point::new(3.7, 5.0), 0.001));
487/// assert!(bb.upper_left.almost_eq(&Point::new(1.0, 3.0), 0.001));
488/// ```
489/// ```
490/// // AlmostEq is implemented for BoundingBox
491///
492/// use std::f64::consts::PI;
493///
494/// use l_system_fractals::paths::{BoundingBox, Path, Point};
495/// use l_system_fractals::num_validity::AlmostEq;
496///
497/// let pts: Vec<Point> = (0..15_839)
498///     .map(|x| (x as f64) * PI / 7919.0 + PI / 4.0)
499///     .map(
500///         |x| Point::new(5.0 * x.cos() + 6.0, 5.0 * x.sin() + 6.0)
501///     ).collect();
502///
503/// let pth: Path = pts.into();
504///
505/// let bb1: BoundingBox = pth.bounding_box().unwrap();
506///
507/// let bb2 = BoundingBox {
508///     upper_left: Point::new(1.0, 1.0),
509///     lower_right: Point::new(11.0, 11.0)
510/// };
511///
512/// assert!(bb1.almost_eq(&bb2, 0.00001));
513/// ```
514#[derive(Debug, Copy, Clone, PartialEq)]
515pub struct BoundingBox {
516    /// The upper-left corner of the region.
517    pub upper_left: Point,
518    /// The lower-right corner of the region.
519    pub lower_right: Point,
520}
521
522impl AlmostEq for BoundingBox {
523    fn almost_eq(&self, other: &Self, epsilon: f64) -> bool {
524        self.upper_left.almost_eq(&other.upper_left, epsilon)
525            && self.lower_right.almost_eq(&other.lower_right, epsilon)
526    }
527}
528
529impl BoundingBox {
530    /// The width of the bounding box.
531    ///
532    /// # Examples
533    ///
534    /// ```
535    /// use l_system_fractals::paths::{BoundingBox, Path, Point};
536    /// use l_system_fractals::num_validity::AlmostEq;
537    ///
538    /// let pth = Path::from(vec![
539    ///     Point::new(1.0, 5.0),
540    ///     Point::new(3.7, 4.5),
541    ///     Point::new(2.5, 3.0)
542    /// ]);
543    ///
544    /// let bb = pth.bounding_box().unwrap();
545    ///
546    /// assert!(bb.width().almost_eq(&2.7, 0.001));
547    /// ```
548    pub fn width(&self) -> f64 {
549        self.upper_left.delta_x(&self.lower_right)
550    }
551
552    /// The height of the bounding box.
553    ///
554    /// # Examples
555    ///
556    /// ```
557    /// use l_system_fractals::paths::{BoundingBox, Path, Point};
558    /// use l_system_fractals::num_validity::AlmostEq;
559    ///
560    /// let pth = Path::from(vec![
561    ///     Point::new(1.0, 5.0),
562    ///     Point::new(3.7, 4.5),
563    ///     Point::new(2.5, 3.0)
564    /// ]);
565    ///
566    /// let bb = pth.bounding_box().unwrap();
567    ///
568    /// assert!(bb.height().almost_eq(&2.0, 0.001));
569    /// ```
570    pub fn height(&self) -> f64 {
571        self.upper_left.delta_y(&self.lower_right)
572    }
573
574    /// Returns an SVG [`rect`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/rect) with
575    /// the coordinates of the bounding box (to be used as a background).
576    pub fn background_box(&self, fill: &str) -> String {
577        rectangle(self.upper_left, self.width(), self.height(), fill)
578    }
579}
580
581/// Represents one of the two SVG [`path`
582/// commands](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d) implemented by this
583/// crate (`L` and `M`).
584///
585/// [`Path`] is a wrapper of `Vec<PathCommand>`.
586///
587/// # Examples
588///
589/// ```
590/// use l_system_fractals::paths::{Path, PathCommand, Point};
591/// use l_system_fractals::num_validity::AlmostEq;
592///
593/// let pth: Path = vec![
594///     Point::new(1.0, 2.0),
595///     Point::new(5.0, 3.0),
596///     Point::new(2.0, 4.0)
597/// ].into();
598///
599/// let pcv: Vec<PathCommand> = pth.0;
600///
601/// assert!(pcv.get(0).unwrap().almost_eq(&PathCommand::MoveTo(Point::new(1.0, 2.0)), 0.001));
602/// assert!(pcv.get(1).unwrap().almost_eq(&PathCommand::LineTo(Point::new(5.0, 3.0)), 0.001));
603/// assert!(pcv.get(2).unwrap().almost_eq(&PathCommand::LineTo(Point::new(2.0, 4.0)), 0.001));
604/// ```
605///
606/// [Add](`Add`)ing a [`Point`] to a `PathCommand` will add the underlying points. Likewise
607/// [sub](`Sub`)tracting a [`Point`] from a `PathCommand` subtracts the underlying points.
608///
609/// ```
610/// use l_system_fractals::paths::{PathCommand, Point};
611/// use l_system_fractals::num_validity::AlmostEq;
612///
613/// let pc1 = PathCommand::LineTo(Point::new(1.0, 2.0));
614/// let pc2 = pc1 + Point::new(1.0, -1.0);
615/// assert!(pc2.point().almost_eq(&Point::new(2.0, 1.0), 0.001));
616///
617/// let pc3 = pc1 - Point::new(0.0, 0.5);
618/// assert!(pc3.point().almost_eq(&Point::new(1.0, 1.5), 0.001));
619///
620/// let pc4 = Point::new(5.0, 4.0) + pc1;
621/// assert!(pc4.point().almost_eq(&Point::new(6.0, 6.0), 0.001));
622/// ```
623///
624/// Likewise, [mul](`Mul`)tiplying a `PathCommand` by a [`f64`] factor will multiply the
625/// coordinates of the underlying point.
626///
627/// ```
628/// use l_system_fractals::paths::{PathCommand, Point};
629/// use l_system_fractals::num_validity::AlmostEq;
630///
631/// let pc1 = PathCommand::LineTo(Point::new(1.0, 2.0));
632/// let pc2 = pc1 * 2.0;
633/// assert!(pc2.point().almost_eq(&Point::new(2.0, 4.0), 0.001));
634/// ```
635#[derive(Debug, Copy, Clone, PartialEq)]
636pub enum PathCommand {
637    /// An instruction to draw a line segment to the specified point (corresponds to [`L` in SVG
638    /// `path`
639    /// syntax](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#lineto_path_commands)).
640    ///
641    /// # Example
642    ///
643    /// ```
644    /// use l_system_fractals::paths::{PathCommand, Point};
645    ///
646    /// let pt = Point::new(3.0, 2.0);
647    /// assert_eq!(PathCommand::LineTo(pt).svg_output(), "L 3.00000,2.00000".to_string());
648    /// ```
649    LineTo(Point),
650    /// An instruction to move to the specified point without drawing (corresponds to [`M` in SVG
651    /// `path`
652    /// syntax](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#moveto_path_commands)).
653    ///
654    /// # Example
655    ///
656    /// ```
657    /// use l_system_fractals::paths::{PathCommand, Point};
658    ///
659    /// let pt = Point::new(3.0, 2.0);
660    /// assert_eq!(PathCommand::MoveTo(pt).svg_output(), "M 3.00000,2.00000".to_string());
661    /// ```
662    MoveTo(Point),
663}
664
665impl AlmostEq for PathCommand {
666    fn almost_eq(&self, other: &Self, epsilon: f64) -> bool {
667        match (self, other) {
668            (Self::LineTo(_), Self::MoveTo(_)) => false,
669            (Self::MoveTo(_), Self::LineTo(_)) => false,
670            (a, b) => a.point().almost_eq(&b.point(), epsilon),
671        }
672    }
673}
674
675impl PathCommand {
676    /// Returns `true` if the underlying [`Point`] is [valid](`Point::is_valid`) (has no
677    /// coordinates that are [NaN or &pm;&infin;](`num_validity::is_valid`)).
678    ///
679    /// # Examples
680    ///
681    /// ```
682    /// use l_system_fractals::paths::{PathCommand, Point};
683    ///
684    /// assert!(PathCommand::LineTo(Point::new(2.5, -3.0)).is_valid());
685    /// assert!(!PathCommand::MoveTo(Point::new(f64::NAN, -3.0)).is_valid());
686    /// assert!(!PathCommand::LineTo(Point::new(f64::INFINITY, -3.0)).is_valid());
687    ///
688    /// ```
689    pub fn is_valid(&self) -> bool {
690        self.point().is_valid()
691    }
692
693    /// Raises an error if the underlying [`Point`] is [invalid](`Point::is_valid`) (either
694    /// coordinate is [NaN or &pm;&infin;](`num_validity::is_valid`)).
695    ///
696    /// The error raised is [`LSystemError::InvalidFloat`].
697    ///
698    /// # Examples
699    ///
700    /// ```
701    /// use l_system_fractals::paths::{PathCommand, Point};
702    ///
703    /// assert!(PathCommand::LineTo(Point::new(2.5, -3.0)).err_if_invalid().is_ok());
704    /// assert!(PathCommand::MoveTo(Point::new(f64::NAN, -3.0)).err_if_invalid().is_err());
705    /// assert!(PathCommand::LineTo(Point::new(f64::INFINITY, -3.0)).err_if_invalid().is_err());
706    /// ```
707    pub fn err_if_invalid(self) -> Result<Self, LSystemError> {
708        if self.is_valid() {
709            Ok(self)
710        } else {
711            Err(LSystemError::InvalidFloat)
712        }
713    }
714
715    /// Returns the underlying [`Point`] of the command.
716    ///
717    /// # Examples
718    ///
719    /// ```
720    /// use l_system_fractals::paths::{PathCommand, Point};
721    ///
722    /// let pt = Point::new(3.0, 2.0);
723    /// let pc1 = PathCommand::MoveTo(pt);
724    /// let pc2 = PathCommand::LineTo(pt);
725    ///
726    /// assert_eq!(pc1.point(), pt);
727    /// assert_eq!(pc2.point(), pt);
728    /// ```
729    ///
730    pub fn point(&self) -> Point {
731        match self {
732            Self::LineTo(q) => *q,
733            Self::MoveTo(q) => *q,
734        }
735    }
736
737    /// Returns the [SVG `path`
738    /// syntax](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d) for the path command.
739    ///
740    /// # Examples
741    ///
742    /// ```
743    /// use l_system_fractals::paths::{PathCommand, Point};
744    ///
745    /// let pt = Point::new(3.0, 2.0);
746    /// let pc1 = PathCommand::MoveTo(pt);
747    /// let pc2 = PathCommand::LineTo(pt);
748    ///
749    /// assert_eq!(pc1.svg_output(), "M 3.00000,2.00000".to_string());
750    /// assert_eq!(pc2.svg_output(), "L 3.00000,2.00000".to_string());
751    /// ```
752    pub fn svg_output(&self) -> String {
753        format!(
754            "{} {}",
755            match self {
756                Self::LineTo(_) => "L",
757                Self::MoveTo(_) => "M",
758            },
759            self.point().svg_output()
760        )
761    }
762
763    /// Returns a PathCommand of the same type ([`LineTo`](`PathCommand::LineTo`) or
764    /// [`MoveTo`](`PathCommand::MoveTo`)) but with the specified [`Point`].
765    ///
766    /// # Examples
767    ///
768    /// ```
769    /// use l_system_fractals::paths::{PathCommand, Point};
770    ///
771    /// let pt1 = Point::new(3.0, 2.0);
772    /// let pt2 = Point::new(4.0, 1.0);
773    /// let pc1 = PathCommand::LineTo(pt1);
774    /// let pc2 = PathCommand::LineTo(pt2);
775    ///
776    /// assert_eq!(pc1.same_type(pt2), pc2);
777    /// ```
778    pub fn same_type(&self, pt: Point) -> Self {
779        match self {
780            Self::LineTo(_) => Self::LineTo(pt),
781            Self::MoveTo(_) => Self::MoveTo(pt),
782        }
783    }
784
785    /// Multiplies the horizontal coordinate of the point in the PathCommand by the specified
786    /// factor.
787    ///
788    /// # Examples
789    ///
790    /// ```
791    /// use l_system_fractals::paths::{PathCommand, Point};
792    /// use l_system_fractals::num_validity::AlmostEq;
793    ///
794    /// let pt1 = Point::new(3.0, 2.0);
795    /// let pt2 = pt1.rescale_horiz(8.125);
796    ///
797    /// assert!(pt2.almost_eq(&Point::new(24.375, 2.0), 0.001));
798    ///
799    /// let pc1 = PathCommand::LineTo(pt1);
800    /// let pc2 = pc1.rescale_horiz(8.125);
801    ///
802    /// assert_eq!(PathCommand::LineTo(pt2), pc2);
803    /// ```
804    pub fn rescale_horiz(&self, factor: f64) -> Self {
805        self.same_type(self.point().rescale_horiz(factor))
806    }
807
808    /// Multiplies the vertical coordinate of the point in the PathCommand by the specified
809    /// factor.
810    ///
811    /// # Examples
812    ///
813    /// ```
814    /// use l_system_fractals::paths::{PathCommand, Point};
815    /// use l_system_fractals::num_validity::AlmostEq;
816    ///
817    /// let pt1 = Point::new(3.0, 2.0);
818    /// let pt2 = pt1.rescale_vert(8.125);
819    ///
820    /// assert!(pt2.almost_eq(&Point::new(3.0, 16.25), 0.001));
821    ///
822    /// let pc1 = PathCommand::MoveTo(pt1);
823    /// let pc2 = pc1.rescale_vert(8.125);
824    ///
825    /// assert_eq!(PathCommand::MoveTo(pt2), pc2);
826    /// ```
827    pub fn rescale_vert(&self, factor: f64) -> Self {
828        self.same_type(self.point().rescale_vert(factor))
829    }
830
831    /// Returns a `PathCommand` of the same type (`LineTo` or `MoveTo`) but with the underlying
832    /// point having been rotated about the origin by the specified angle.
833    ///
834    /// # Examples
835    ///
836    /// ```
837    /// use std::f64::consts::PI;
838    ///
839    /// use l_system_fractals::paths::{PathCommand, Point};
840    /// use l_system_fractals::num_validity::AlmostEq;
841    ///
842    /// let pt1 = Point::new(1.0, 1.0);
843    /// let pt2 = pt1.rotate_about_origin(0.25 * PI);
844    ///
845    /// assert!(pt2.almost_eq(&Point::new(0.0, 2.0_f64.sqrt()), 0.001));
846    ///
847    /// let pc1 = PathCommand::MoveTo(pt1);
848    /// let pc2 = pc1.rotate_about_origin(0.25 * PI);
849    ///
850    /// assert_eq!(PathCommand::MoveTo(pt2), pc2);
851    /// ```
852    pub fn rotate_about_origin(&self, angle: f64) -> Self {
853        self.same_type(self.point().rotate_about_origin(angle))
854    }
855
856    /// Returns a `PathCommand` of the same type (`LineTo` or `MoveTo`) but with the underlying
857    /// point having been rotated about the specified point by the specified angle.
858    ///
859    /// # Examples
860    ///
861    /// ```
862    /// use std::f64::consts::PI;
863    ///
864    /// use l_system_fractals::paths::{PathCommand, Point};
865    /// use l_system_fractals::num_validity::AlmostEq;
866    ///
867    /// let pt1 = Point::new(3.0, 5.0);
868    /// let ax = Point::new(2.0, 4.0);
869    /// let pt2 = pt1.rotate(&ax, 0.25 * PI);
870    ///
871    /// assert!(pt2.almost_eq(&Point::new(2.0, 4.0 + 2.0_f64.sqrt()), 0.001));
872    ///
873    /// let pc1 = PathCommand::MoveTo(pt1);
874    /// let pc2 = pc1.rotate(&ax, 0.25 * PI);
875    ///
876    /// assert_eq!(PathCommand::MoveTo(pt2), pc2);
877    /// ```
878    pub fn rotate(&self, axis: &Point, angle: f64) -> Self {
879        self.same_type(self.point().rotate(axis, angle))
880    }
881}
882
883impl Add<Point> for PathCommand {
884    type Output = PathCommand;
885
886    fn add(self, p: Point) -> Self {
887        self.same_type(self.point() + p)
888    }
889}
890
891impl Sub<Point> for PathCommand {
892    type Output = PathCommand;
893
894    fn sub(self, p: Point) -> Self {
895        self.same_type(self.point() - p)
896    }
897}
898
899impl Add<PathCommand> for Point {
900    type Output = PathCommand;
901
902    fn add(self, pc: PathCommand) -> PathCommand {
903        pc.same_type(pc.point() + self)
904    }
905}
906
907impl Mul<f64> for PathCommand {
908    type Output = PathCommand;
909
910    fn mul(self, x: f64) -> Self {
911        self.same_type(x * self.point())
912    }
913}
914
915impl Mul<PathCommand> for f64 {
916    type Output = PathCommand;
917
918    fn mul(self, pc: PathCommand) -> PathCommand {
919        pc.same_type(self * pc.point())
920    }
921}
922
923/// Representation of an [SVG
924/// `path`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path), where every [path
925/// command](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d) is `L` (line to) or `M`
926/// (move to).
927///
928/// A `Path` is essentially a series of [`PathCommand`]s, and is implemented as a wrapper for
929/// `Vec<PathCommand>`. That said, the first `PathCommand` should be of the
930/// [`MoveTo`](`PathCommand::MoveTo`) variant in order to produce valid SVG.
931///
932/// # Examples
933///
934/// ```
935/// use l_system_fractals::paths::{Path, PathCommand, Point};
936/// use l_system_fractals::num_validity::AlmostEq;
937///
938/// let pth = Path(vec![
939///     PathCommand::MoveTo(Point::new(1.0, 2.0)),
940///     PathCommand::LineTo(Point::new(5.0, 3.0)),
941///     PathCommand::LineTo(Point::new(2.0, 4.0))
942/// ]);
943///
944/// assert!(pth.0.get(0).unwrap().almost_eq(&PathCommand::MoveTo(Point::new(1.0, 2.0)), 0.001));
945/// assert!(pth.0.get(1).unwrap().almost_eq(&PathCommand::LineTo(Point::new(5.0, 3.0)), 0.001));
946/// assert!(pth.0.get(2).unwrap().almost_eq(&PathCommand::LineTo(Point::new(2.0, 4.0)), 0.001));
947/// ```
948///
949/// `From<Vec<Point>>` is implemented for Path; it converts the vector into a Path where the first
950/// path command is a MoveTo and the rest are LineTo.
951///
952/// ```
953/// use l_system_fractals::paths::{Path, PathCommand, Point};
954/// use l_system_fractals::num_validity::AlmostEq;
955///
956/// let pth = Path(vec![
957///     PathCommand::MoveTo(Point::new(1.0, 2.0)),
958///     PathCommand::LineTo(Point::new(5.0, 3.0)),
959///     PathCommand::LineTo(Point::new(2.0, 4.0))
960/// ]);
961///
962/// let pth2: Path = vec![
963///     Point::new(1.0, 2.0),
964///     Point::new(5.0, 3.0),
965///     Point::new(2.0, 4.0)
966/// ].into();
967///
968/// assert!(pth.almost_eq(&pth2, 0.001));
969/// ```
970///
971/// [Add](`Add`)ing a [`Point`] to a `Path` will add the underlying points. Likewise
972/// [sub](`Sub`)tracting a [`Point`] from a `Path` subtracts the underlying points.
973///
974/// ```
975/// use l_system_fractals::paths::{Path, PathCommand, Point};
976/// use l_system_fractals::num_validity::AlmostEq;
977///
978/// let pth = Path(vec![
979///     PathCommand::MoveTo(Point::new(1.0, 2.0)),
980///     PathCommand::LineTo(Point::new(5.0, 3.0)),
981///     PathCommand::MoveTo(Point::new(2.0, 4.0)),
982///     PathCommand::LineTo(Point::new(1.5, 3.5))
983/// ]);
984///
985/// let pth2 = pth.clone() + Point::new(1.0, -1.0);
986/// let pth2_a = Path(vec![
987///     PathCommand::MoveTo(Point::new(2.0, 1.0)),
988///     PathCommand::LineTo(Point::new(6.0, 2.0)),
989///     PathCommand::MoveTo(Point::new(3.0, 3.0)),
990///     PathCommand::LineTo(Point::new(2.5, 2.5))
991/// ]);
992/// assert!(pth2.almost_eq(&pth2_a, 0.001));
993///
994/// let pth3 = pth.clone() - Point::new(0.0, 0.5);
995/// let pth3_a = Path(vec![
996///     PathCommand::MoveTo(Point::new(1.0, 1.5)),
997///     PathCommand::LineTo(Point::new(5.0, 2.5)),
998///     PathCommand::MoveTo(Point::new(2.0, 3.5)),
999///     PathCommand::LineTo(Point::new(1.5, 3.0))
1000/// ]);
1001/// assert!(pth3.almost_eq(&pth3_a, 0.001));
1002///
1003/// let pth4 = Point::new(5.0, 4.0) + pth;
1004/// let pth4_a = Path(vec![
1005///     PathCommand::MoveTo(Point::new(6.0, 6.0)),
1006///     PathCommand::LineTo(Point::new(10.0, 7.0)),
1007///     PathCommand::MoveTo(Point::new(7.0, 8.0)),
1008///     PathCommand::LineTo(Point::new(6.5, 7.5))
1009/// ]);
1010/// assert!(pth4.almost_eq(&pth4_a, 0.001));
1011/// ```
1012///
1013/// Likewise, [mul](`Mul`)tiplying a `Path` by a [`f64`] factor will multiply the
1014/// coordinates of the underlying [`Point`]s.
1015///
1016/// ```
1017/// use l_system_fractals::paths::{Path, PathCommand, Point};
1018/// use l_system_fractals::num_validity::AlmostEq;
1019///
1020/// let pth = Path(vec![
1021///     PathCommand::MoveTo(Point::new(1.0, 2.0)),
1022///     PathCommand::LineTo(Point::new(5.0, 3.0)),
1023///     PathCommand::MoveTo(Point::new(2.0, 4.0)),
1024///     PathCommand::LineTo(Point::new(1.5, 3.5))
1025/// ]);
1026///
1027/// let pth2 = 2.0 * pth.clone();
1028/// let pth2_a = Path(vec![
1029///     PathCommand::MoveTo(Point::new(2.0, 4.0)),
1030///     PathCommand::LineTo(Point::new(10.0, 6.0)),
1031///     PathCommand::MoveTo(Point::new(4.0, 8.0)),
1032///     PathCommand::LineTo(Point::new(3.0, 7.0))
1033/// ]);
1034/// assert!(pth2.almost_eq(&pth2_a, 0.001));
1035/// ```
1036#[derive(Debug, Clone, PartialEq)]
1037pub struct Path(pub Vec<PathCommand>);
1038
1039impl AlmostEq for Path {
1040    fn almost_eq(&self, other: &Self, epsilon: f64) -> bool {
1041        self.0.almost_eq(&other.0, epsilon)
1042    }
1043}
1044
1045impl From<&[Point]> for Path {
1046    fn from(p: &[Point]) -> Self {
1047        let mut out_vec: Vec<PathCommand> = vec![];
1048        for (idx, pt) in p.iter().enumerate() {
1049            out_vec.push(if idx == 0 {
1050                PathCommand::MoveTo(*pt)
1051            } else {
1052                PathCommand::LineTo(*pt)
1053            });
1054        }
1055        Self(out_vec)
1056    }
1057}
1058
1059impl From<Vec<Point>> for Path {
1060    fn from(v: Vec<Point>) -> Self {
1061        let mut out_vec: Vec<PathCommand> = vec![];
1062        for (idx, pt) in v.iter().enumerate() {
1063            out_vec.push(if idx == 0 {
1064                PathCommand::MoveTo(*pt)
1065            } else {
1066                PathCommand::LineTo(*pt)
1067            });
1068        }
1069        Self(out_vec)
1070    }
1071}
1072
1073impl From<Vec<PathCommand>> for Path {
1074    fn from(v: Vec<PathCommand>) -> Self {
1075        Self(v)
1076    }
1077}
1078
1079impl Add<Point> for Path {
1080    type Output = Self;
1081
1082    fn add(self, other: Point) -> Self {
1083        let new_path: Vec<PathCommand> = self.0.iter().map(|p| *p + other).collect();
1084        Self(new_path)
1085    }
1086}
1087
1088impl Sub<Point> for Path {
1089    type Output = Self;
1090
1091    fn sub(self, other: Point) -> Self {
1092        let new_path: Vec<PathCommand> = self.0.iter().map(|p| *p - other).collect();
1093        Self(new_path)
1094    }
1095}
1096
1097impl Add<Path> for Point {
1098    type Output = Path;
1099
1100    fn add(self, other: Path) -> Self::Output {
1101        let new_path: Vec<PathCommand> = other.0.iter().map(|p| *p + self).collect();
1102        Path(new_path)
1103    }
1104}
1105
1106impl Mul<f64> for Path {
1107    type Output = Self;
1108
1109    fn mul(self, other: f64) -> Self {
1110        let new_path: Vec<PathCommand> = self.0.iter().map(|p| other * *p).collect();
1111        Self(new_path)
1112    }
1113}
1114
1115impl Mul<Path> for f64 {
1116    type Output = Path;
1117
1118    fn mul(self, other: Path) -> Self::Output {
1119        let new_path: Vec<PathCommand> = other.0.iter().map(|p| self * *p).collect();
1120        Path(new_path)
1121    }
1122}
1123
1124impl Path {
1125    /// Returns `true` if all of the underlying [`Point`]s are [valid](`Point::is_valid`) (have no
1126    /// coordinates that are [NaN or &pm;&infin;](`num_validity::is_valid`)).
1127    ///
1128    /// # Examples
1129    ///
1130    /// ```
1131    /// use l_system_fractals::paths::{Path, Point};
1132    ///
1133    /// let pth1 = Path::from(vec![
1134    ///     Point::new(3.0, 2.0),
1135    ///     Point::new(1.0, 1.0),
1136    ///     Point::new(4.0, 0.0)
1137    /// ]);
1138    /// let pth2 = Path::from(vec![
1139    ///     Point::new(f64::NAN, 2.0),
1140    ///     Point::new(1.0, 1.0),
1141    ///     Point::new(4.0, 0.0)
1142    /// ]);
1143    /// let pth3 = Path::from(vec![
1144    ///     Point::new(f64::INFINITY, 2.0),
1145    ///     Point::new(1.0, 1.0),
1146    ///     Point::new(4.0, 0.0)
1147    /// ]);
1148    ///
1149    /// assert!(pth1.is_valid());
1150    /// assert!(!pth2.is_valid());
1151    /// assert!(!pth3.is_valid());
1152    /// ```
1153    pub fn is_valid(&self) -> bool {
1154        self.0.iter().all(|p| p.is_valid())
1155    }
1156
1157    /// Raises an error if any of the underlying [`Point`]s are [invalid](`Point::is_valid`) (either
1158    /// coordinate is [NaN or &pm;&infin;](`num_validity::is_valid`)).
1159    ///
1160    /// The error raised is [`LSystemError::InvalidFloat`].
1161    ///
1162    /// # Examples
1163    ///
1164    /// ```
1165    /// use l_system_fractals::paths::{Path, Point};
1166    ///
1167    /// let pth1 = Path::from(vec![
1168    ///     Point::new(3.0, 2.0),
1169    ///     Point::new(1.0, 1.0),
1170    ///     Point::new(4.0, 0.0)
1171    /// ]);
1172    /// let pth2 = Path::from(vec![
1173    ///     Point::new(f64::NAN, 2.0),
1174    ///     Point::new(1.0, 1.0),
1175    ///     Point::new(4.0, 0.0)
1176    /// ]);
1177    /// let pth3 = Path::from(vec![
1178    ///     Point::new(f64::INFINITY, 2.0),
1179    ///     Point::new(1.0, 1.0),
1180    ///     Point::new(4.0, 0.0)
1181    /// ]);
1182    ///
1183    /// assert!(pth1.err_if_invalid().is_ok());
1184    /// assert!(pth2.err_if_invalid().is_err());
1185    /// assert!(pth3.err_if_invalid().is_err());
1186    /// ```
1187    pub fn err_if_invalid(self) -> Result<Self, LSystemError> {
1188        if self.is_valid() {
1189            Ok(self)
1190        } else {
1191            Err(LSystemError::InvalidFloat)
1192        }
1193    }
1194
1195    /// Returns `true` if the path contains no points.
1196    ///
1197    /// # Examples
1198    ///
1199    /// ```
1200    /// use l_system_fractals::paths::{Path, Point};
1201    ///
1202    /// let pts1: Vec<Point> = Vec::new();
1203    /// let pth1: Path = pts1.into();
1204    /// assert!(pth1.is_empty());
1205    ///
1206    /// let pts2: Vec<Point> = vec![Point::new(1.0, 1.0)];
1207    /// let pth2: Path = pts2.into();
1208    /// assert!(!pth2.is_empty());
1209    /// ```
1210    pub fn is_empty(&self) -> bool {
1211        self.0.is_empty()
1212    }
1213
1214    /// Returns the smallest [`BoundingBox`] that contains all the valid [`Point`]s in the `Path`.
1215    ///
1216    /// # Examples
1217    ///
1218    /// ```
1219    /// use std::f64::consts::PI;
1220    ///
1221    /// use l_system_fractals::paths::{BoundingBox, Path, Point};
1222    /// use l_system_fractals::num_validity::AlmostEq;
1223    ///
1224    /// let pts: Vec<Point> = (0..15_839)
1225    ///     .map(|x| (x as f64) * PI / 7919.0 + PI / 4.0)
1226    ///     .map(
1227    ///         |x| Point::new(5.0 * x.cos() + 6.0, 5.0 * x.sin() + 6.0)
1228    ///     ).collect();
1229    ///
1230    /// let pth: Path = pts.into();
1231    ///
1232    /// let bb1: BoundingBox = pth.bounding_box().unwrap();
1233    ///
1234    /// let bb2 = BoundingBox {
1235    ///     upper_left: Point::new(1.0, 1.0),
1236    ///     lower_right: Point::new(11.0, 11.0)
1237    /// };
1238    ///
1239    /// assert!(bb1.almost_eq(&bb2, 0.00001));
1240    /// ```
1241    ///
1242    /// An [`LSystemError`] is returned if the Path is empty (or all points are invalid).
1243    ///
1244    /// ```
1245    /// use l_system_fractals::paths::{Path, Point};
1246    ///
1247    /// let pts1: Vec<Point> = Vec::new();
1248    /// let pth1: Path = pts1.into();
1249    /// assert!(pth1.bounding_box().is_err());
1250    /// ```
1251    pub fn bounding_box(&self) -> Result<BoundingBox, LSystemError> {
1252        let x_vals: Vec<f64> = self.0.iter().map(|p| p.point().x).collect();
1253        let y_vals: Vec<f64> = self.0.iter().map(|p| p.point().y).collect();
1254        let x_max = num_validity::max(x_vals.as_slice());
1255        let x_min = num_validity::min(x_vals.as_slice());
1256        let y_max = num_validity::max(y_vals.as_slice());
1257        let y_min = num_validity::min(y_vals.as_slice());
1258        match (x_max, x_min, y_max, y_min) {
1259            (Some(x_max_val), Some(x_min_val), Some(y_max_val), Some(y_min_val)) => {
1260                Ok(BoundingBox {
1261                    upper_left: Point {
1262                        x: x_min_val,
1263                        y: y_min_val,
1264                    },
1265                    lower_right: Point {
1266                        x: x_max_val,
1267                        y: y_max_val,
1268                    },
1269                })
1270            }
1271            _ => Err(LSystemError::NoBoundingBox),
1272        }
1273    }
1274
1275    /// Returns a new `Path` where all the underlying points have been rescaled horizontally by the
1276    /// given factor.
1277    ///
1278    /// # Example
1279    ///
1280    /// ```
1281    /// use l_system_fractals::paths::{Path, PathCommand, Point};
1282    /// use l_system_fractals::num_validity::AlmostEq;
1283    ///
1284    /// let pth = Path(vec![
1285    ///     PathCommand::MoveTo(Point::new(1.0, 2.0)),
1286    ///     PathCommand::LineTo(Point::new(5.0, 3.0)),
1287    ///     PathCommand::MoveTo(Point::new(2.0, 4.0)),
1288    ///     PathCommand::LineTo(Point::new(1.5, 3.5))
1289    /// ]);
1290    ///
1291    /// let pth2 = pth.rescale_horiz(2.0);
1292    /// let pth2_a = Path(vec![
1293    ///     PathCommand::MoveTo(Point::new(2.0, 2.0)),
1294    ///     PathCommand::LineTo(Point::new(10.0, 3.0)),
1295    ///     PathCommand::MoveTo(Point::new(4.0, 4.0)),
1296    ///     PathCommand::LineTo(Point::new(3.0, 3.5))
1297    /// ]);
1298    /// assert!(pth2.almost_eq(&pth2_a, 0.001));
1299    pub fn rescale_horiz(&self, factor: f64) -> Self {
1300        let new_path: Vec<PathCommand> = self.0.iter().map(|p| p.rescale_horiz(factor)).collect();
1301        Self(new_path)
1302    }
1303
1304    /// Returns a new `Path` where all the underlying points have been rescaled vertically by the
1305    /// given factor.
1306    ///
1307    /// # Example
1308    ///
1309    /// ```
1310    /// use l_system_fractals::paths::{Path, PathCommand, Point};
1311    /// use l_system_fractals::num_validity::AlmostEq;
1312    ///
1313    /// let pth = Path(vec![
1314    ///     PathCommand::MoveTo(Point::new(1.0, 2.0)),
1315    ///     PathCommand::LineTo(Point::new(5.0, 3.0)),
1316    ///     PathCommand::MoveTo(Point::new(2.0, 4.0)),
1317    ///     PathCommand::LineTo(Point::new(1.5, 3.5))
1318    /// ]);
1319    ///
1320    /// let pth2 = pth.rescale_vert(2.0);
1321    /// let pth2_a = Path(vec![
1322    ///     PathCommand::MoveTo(Point::new(1.0, 4.0)),
1323    ///     PathCommand::LineTo(Point::new(5.0, 6.0)),
1324    ///     PathCommand::MoveTo(Point::new(2.0, 8.0)),
1325    ///     PathCommand::LineTo(Point::new(1.5, 7.0))
1326    /// ]);
1327    /// assert!(pth2.almost_eq(&pth2_a, 0.001));
1328    pub fn rescale_vert(&self, factor: f64) -> Self {
1329        let new_path: Vec<PathCommand> = self.0.iter().map(|p| p.rescale_vert(factor)).collect();
1330        Self(new_path)
1331    }
1332
1333    /// Rescales the `Path` so that, including a border of the specified size around the path, its
1334    /// width and height are below the specified maximums.
1335    ///
1336    /// Outputs a rescaled `Path` and a [`BoundingBox`] for the path and border.
1337    pub fn rescale(
1338        &self,
1339        max_width: f64,
1340        max_height: f64,
1341        border: f64,
1342    ) -> Result<(Self, BoundingBox), LSystemError> {
1343        if max_width == 0.0 || max_height == 0.0 {
1344            return Err(LSystemError::DivideByZero);
1345        }
1346        if self.is_empty() {
1347            return Ok((
1348                self.clone(),
1349                BoundingBox {
1350                    upper_left: Point::new(0.0, 0.0),
1351                    lower_right: Point::new(0.0, 0.0),
1352                },
1353            ));
1354        }
1355        let mut new_path: Self;
1356        let bb = self.bounding_box()?;
1357        if bb.width() == 0.0 {
1358            if bb.height() == 0.0 {
1359                new_path = 0.0 * self.clone();
1360            } else {
1361                let scale_factor_height = num_validity::err_if_invalid(max_height / bb.height())?;
1362                new_path = self.rescale_vert(scale_factor_height);
1363            }
1364        } else if bb.height() == 0.0 {
1365            // but bb.width != 0.0
1366            let scale_factor_width = num_validity::err_if_invalid(max_width / bb.width())?;
1367            new_path = self.rescale_horiz(scale_factor_width);
1368        } else {
1369            let scale_factor_width = num_validity::err_if_invalid(max_width / bb.width())?;
1370            let scale_factor_height = num_validity::err_if_invalid(max_height / bb.height())?;
1371            let scale_factor = scale_factor_width.min(scale_factor_height);
1372            new_path = scale_factor * self.clone();
1373        }
1374        let bb = new_path.bounding_box()?;
1375        new_path = (new_path - bb.upper_left
1376            + Point {
1377                x: border,
1378                y: border,
1379            })
1380        .err_if_invalid()?;
1381        let bb2 = new_path.bounding_box()?;
1382        let new_bb = BoundingBox {
1383            upper_left: Point::new(0.0, 0.0),
1384            lower_right: bb2.lower_right + Point::new(border, border),
1385        };
1386        Ok((new_path, new_bb))
1387    }
1388
1389    /// Returns the [SVG `path
1390    /// commands`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d) for the `Path`.
1391    ///
1392    /// The `Path` is first moved by the given offset.
1393    ///
1394    /// If the `Path` is [invalid](`Path::is_valid`), an [`LSystemError::InvalidFloat`] is returned.
1395    ///
1396    /// # Example
1397    ///
1398    /// ```
1399    /// use l_system_fractals::paths::{Path, PathCommand, Point};
1400    ///
1401    /// let pth = Path(vec![
1402    ///     PathCommand::MoveTo(Point::new(1.0, 2.0)),
1403    ///     PathCommand::LineTo(Point::new(5.0, 3.0)),
1404    ///     PathCommand::MoveTo(Point::new(2.0, 4.0)),
1405    ///     PathCommand::LineTo(Point::new(1.5, 3.5))
1406    /// ]);
1407    ///
1408    /// assert_eq!(
1409    ///     pth.svg_path_command_output(Point::new(0.0, 0.0)).unwrap(),
1410    ///     "M 1.00000,2.00000 L 5.00000,3.00000 M 2.00000,4.00000 L 1.50000,3.50000 ".to_string()
1411    /// );
1412    ///
1413    /// assert_eq!(
1414    ///     pth.svg_path_command_output(Point::new(1.0, 1.0)).unwrap(),
1415    ///     "M 2.00000,3.00000 L 6.00000,4.00000 M 3.00000,5.00000 L 2.50000,4.50000 ".to_string()
1416    /// );
1417    /// ```
1418    pub fn svg_path_command_output(&self, offset: Point) -> Result<String, LSystemError> {
1419        let new_path: Path = (self.clone() + offset).err_if_invalid()?;
1420        let mut out_str = String::new();
1421        for pc in new_path.0.iter() {
1422            out_str += &(pc.svg_output() + " ");
1423        }
1424        Ok(out_str)
1425    }
1426
1427    /// Returns the [full SVG `path` element
1428    /// syntax](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path) for the `Path` and
1429    /// the specified offset and `stroke`, `fill`, and `stroke-width` attributes.
1430    ///
1431    /// If the `Path` is [invalid](`Path::is_valid`), an [`LSystemError::InvalidFloat`] is returned.
1432    ///
1433    /// # Example
1434    ///
1435    /// ```
1436    /// use l_system_fractals::paths::{Path, PathCommand, Point};
1437    ///
1438    /// let pth = Path(vec![
1439    ///     PathCommand::MoveTo(Point::new(1.0, 2.0)),
1440    ///     PathCommand::LineTo(Point::new(5.0, 3.0)),
1441    ///     PathCommand::MoveTo(Point::new(2.0, 4.0)),
1442    ///     PathCommand::LineTo(Point::new(1.5, 3.5))
1443    /// ]);
1444    ///
1445    /// let svg_text1 = pth.svg_path_output(
1446    ///     Point::new(0.0, 0.0),
1447    ///     "none",
1448    ///     "black",
1449    ///     0.25
1450    /// ).unwrap();
1451    ///
1452    /// let svg_text2 = (
1453    ///    "<path fill=\"none\" stroke=\"black\" stroke-width=\"0.25000\" d=\"".to_string() +
1454    ///        "M 1.00000,2.00000 L 5.00000,3.00000 M 2.00000,4.00000 L 1.50000,3.50000 " +
1455    ///        "\" />"
1456    /// );
1457    /// assert_eq!(svg_text1, svg_text2);
1458    /// ```
1459    pub fn svg_path_output(
1460        &self,
1461        offset: Point,
1462        fill: &str,
1463        stroke: &str,
1464        stroke_width: f64,
1465    ) -> Result<String, LSystemError> {
1466        Ok(format!(
1467            "<path fill=\"{}\" stroke=\"{}\" stroke-width=\"{:0.5}\" d=\"{}\" />",
1468            fill,
1469            stroke,
1470            stroke_width,
1471            self.svg_path_command_output(offset)?
1472        ))
1473    }
1474
1475    /// Produces the complete text of an SVG file containing the `Path`, rescaled to fit within
1476    /// the specified maximum width and height.
1477    pub fn svg_output(&self, params: &PlotParameters) -> Result<String, LSystemError> {
1478        let (resized, bb) = self.rescale(params.width, params.height, params.border)?;
1479        let mut out_str = format!(
1480            "<svg ViewBox=\"0 0 {:0.5} {:0.5}\" xmlns=\"http://www.w3.org/2000/svg\">",
1481            bb.lower_right.x, bb.lower_right.y
1482        );
1483        if let Some(color) = params.background.clone() {
1484            out_str += &bb.background_box(&color);
1485        }
1486        out_str += &(resized.svg_path_output(
1487            bb.upper_left,
1488            &params.fill,
1489            &params.stroke,
1490            params.stroke_width,
1491        )?);
1492        out_str += "</svg>";
1493        Ok(out_str)
1494    }
1495
1496    /// Joins two paths.
1497    ///
1498    /// If `connect` is `true`, then there will be a [`LineTo`](`PathCommand::LineTo`) between the
1499    /// last point of `self` and the first point of `other` in the returned `Path`. Otherwise,
1500    /// there will be a [`MoveTo`](`PathCommand::MoveTo`) between these points.
1501    ///
1502    /// # Examples
1503    ///
1504    /// ```
1505    /// use l_system_fractals::paths::{Path, PathCommand, Point};
1506    /// use l_system_fractals::num_validity::AlmostEq;
1507    ///
1508    /// let pth1: Path = Path(
1509    ///     vec![
1510    ///         PathCommand::MoveTo(Point::new(4.0, 1.0)),
1511    ///         PathCommand::LineTo(Point::new(1.0, 1.0)),
1512    ///         PathCommand::LineTo(Point::new(1.0, 4.0)),
1513    ///         PathCommand::LineTo(Point::new(4.0, 1.0)),
1514    ///     ]
1515    /// ).into();
1516    ///
1517    ///
1518    /// let pth2: Path = Path(
1519    ///     vec![
1520    ///         PathCommand::MoveTo(Point::new(5.0, 2.0)),
1521    ///         PathCommand::LineTo(Point::new(8.0, 2.0)),
1522    ///         PathCommand::LineTo(Point::new(8.0, 5.0)),
1523    ///         PathCommand::LineTo(Point::new(5.0, 2.0)),
1524    ///     ]
1525    /// ).into();
1526    ///
1527    /// let pth3: Path = Path(
1528    ///     vec![
1529    ///         PathCommand::MoveTo(Point::new(4.0, 1.0)),
1530    ///         PathCommand::LineTo(Point::new(1.0, 1.0)),
1531    ///         PathCommand::LineTo(Point::new(1.0, 4.0)),
1532    ///         PathCommand::LineTo(Point::new(4.0, 1.0)),
1533    ///         // not connected: two components
1534    ///         PathCommand::MoveTo(Point::new(5.0, 2.0)),
1535    ///         PathCommand::LineTo(Point::new(8.0, 2.0)),
1536    ///         PathCommand::LineTo(Point::new(8.0, 5.0)),
1537    ///         PathCommand::LineTo(Point::new(5.0, 2.0)),
1538    ///     ]
1539    /// ).into();
1540    ///
1541    /// let pth4: Path = Path(
1542    ///     vec![
1543    ///         PathCommand::MoveTo(Point::new(4.0, 1.0)),
1544    ///         PathCommand::LineTo(Point::new(1.0, 1.0)),
1545    ///         PathCommand::LineTo(Point::new(1.0, 4.0)),
1546    ///         PathCommand::LineTo(Point::new(4.0, 1.0)),
1547    ///         // connected; one component
1548    ///         PathCommand::LineTo(Point::new(5.0, 2.0)),
1549    ///         PathCommand::LineTo(Point::new(8.0, 2.0)),
1550    ///         PathCommand::LineTo(Point::new(8.0, 5.0)),
1551    ///         PathCommand::LineTo(Point::new(5.0, 2.0)),
1552    ///     ]
1553    /// ).into();
1554    ///
1555    /// assert!(pth1.concatenate(&pth2, false).almost_eq(&pth3, 0.001));
1556    /// assert!(pth1.concatenate(&pth2, true).almost_eq(&pth4, 0.001));
1557    /// ```
1558    pub fn concatenate(&self, other: &Self, connect: bool) -> Self {
1559        let mut out_vec = self.0.clone();
1560        for (idx, val) in other.0.iter().enumerate() {
1561            if idx == 0 && connect {
1562                out_vec.push(PathCommand::LineTo(val.point()));
1563            } else {
1564                out_vec.push(*val);
1565            }
1566        }
1567        Self(out_vec)
1568    }
1569
1570    /// Joins two paths, with the second path translated so that its first point is the last point
1571    /// of the first path.
1572    ///
1573    ///
1574    /// # Examples
1575    ///
1576    /// ```
1577    /// use l_system_fractals::paths::{Path, PathCommand, Point};
1578    /// use l_system_fractals::num_validity::AlmostEq;
1579    ///
1580    /// let pth1: Path = Path(
1581    ///     vec![
1582    ///         PathCommand::MoveTo(Point::new(4.0, 1.0)),
1583    ///         PathCommand::LineTo(Point::new(1.0, 1.0)),
1584    ///         PathCommand::LineTo(Point::new(1.0, 4.0)),
1585    ///         PathCommand::LineTo(Point::new(4.0, 1.0)),
1586    ///     ]
1587    /// ).into();
1588    ///
1589    ///
1590    /// let pth2: Path = Path(
1591    ///     vec![
1592    ///         PathCommand::MoveTo(Point::new(5.0, 2.0)),
1593    ///         PathCommand::LineTo(Point::new(8.0, 2.0)),
1594    ///         PathCommand::LineTo(Point::new(8.0, 5.0)),
1595    ///         PathCommand::LineTo(Point::new(5.0, 2.0)),
1596    ///     ]
1597    /// ).into();
1598    ///
1599    /// let pth3: Path = Path(
1600    ///     vec![
1601    ///         PathCommand::MoveTo(Point::new(4.0, 1.0)),
1602    ///         PathCommand::LineTo(Point::new(1.0, 1.0)),
1603    ///         PathCommand::LineTo(Point::new(1.0, 4.0)),
1604    ///         PathCommand::LineTo(Point::new(4.0, 1.0)),
1605    ///         PathCommand::LineTo(Point::new(7.0, 1.0)),
1606    ///         PathCommand::LineTo(Point::new(7.0, 4.0)),
1607    ///         PathCommand::LineTo(Point::new(4.0, 1.0)),
1608    ///     ]
1609    /// ).into();
1610    ///
1611    /// assert!(
1612    ///     pth1
1613    ///     .concatenate_matched_endpoints(&pth2)
1614    ///     .almost_eq(&pth3, 0.001)
1615    /// );
1616    /// ```
1617    pub fn concatenate_matched_endpoints(&self, other: &Self) -> Self {
1618        if self.0.is_empty() {
1619            other.clone()
1620        } else if other.0.is_empty() {
1621            self.clone()
1622        } else {
1623            let mut out_vec = self.0.clone();
1624            let other_new_initial = out_vec.last().unwrap().point();
1625            let other_old_initial = other.0.first().unwrap().point();
1626            let diff = other_new_initial - other_old_initial;
1627            for (idx, val) in other.0.iter().enumerate() {
1628                if idx != 0 {
1629                    out_vec.push(*val + diff);
1630                }
1631            }
1632            Self(out_vec)
1633        }
1634    }
1635
1636    /// Rotate the path about the origin by the specified angle.
1637    ///
1638    /// # Example
1639    ///
1640    /// ```
1641    /// use std::f64::consts::PI;
1642    ///
1643    /// use l_system_fractals::paths::{Path, PathCommand, Point};
1644    /// use l_system_fractals::num_validity::AlmostEq;
1645    ///
1646    /// let pth1: Path = Path(
1647    ///     vec![
1648    ///         PathCommand::MoveTo(Point::new(4.0, 1.0)),
1649    ///         PathCommand::LineTo(Point::new(1.0, 1.0)),
1650    ///         PathCommand::LineTo(Point::new(1.0, 4.0)),
1651    ///         PathCommand::LineTo(Point::new(4.0, 1.0)),
1652    ///     ]
1653    /// ).into();
1654    ///
1655    /// let pth2: Path = Path(
1656    ///     vec![
1657    ///         PathCommand::MoveTo(Point::from_polar(
1658    ///             17.0_f64.sqrt(),
1659    ///             1.0_f64.atan2(4.0) + PI / 4.0
1660    ///         )),
1661    ///         PathCommand::LineTo(Point::new(0.0, 2.0_f64.sqrt())),
1662    ///         PathCommand::LineTo(Point::from_polar(
1663    ///             17.0_f64.sqrt(),
1664    ///             4.0_f64.atan2(1.0) + PI / 4.0
1665    ///         )),
1666    ///         PathCommand::LineTo(Point::from_polar(
1667    ///             17.0_f64.sqrt(),
1668    ///             1.0_f64.atan2(4.0) + PI / 4.0
1669    ///         ))
1670    ///     ]
1671    /// ).into();
1672    ///
1673    /// assert!(pth1.rotate_about_origin(PI / 4.0).almost_eq(&pth2, 0.001));
1674    /// ```
1675    pub fn rotate_about_origin(&self, angle: f64) -> Self {
1676        Self(
1677            self.0
1678                .iter()
1679                .map(|pc| pc.same_type(pc.point().rotate_about_origin(angle)))
1680                .collect(),
1681        )
1682    }
1683
1684    /// Rotate the path about its first point by the specified angle.
1685    ///
1686    /// # Example
1687    ///
1688    /// ```
1689    /// use std::f64::consts::PI;
1690    ///
1691    /// use l_system_fractals::paths::{Path, PathCommand, Point};
1692    /// use l_system_fractals::num_validity::AlmostEq;
1693    ///
1694    /// let pth1: Path = Path(
1695    ///     vec![
1696    ///         PathCommand::MoveTo(Point::new(5.0, 4.0)),
1697    ///         PathCommand::LineTo(Point::new(2.0, 4.0)),
1698    ///         PathCommand::LineTo(Point::new(2.0, 7.0)),
1699    ///         PathCommand::LineTo(Point::new(5.0, 4.0)),
1700    ///     ]
1701    /// ).into();
1702    ///
1703    /// let pth2: Path = Path(
1704    ///     vec![
1705    ///         PathCommand::MoveTo(Point::new(5.0, 4.0)),
1706    ///         PathCommand::LineTo(Point::new(
1707    ///             5.0 - 3.0 / 2.0_f64.sqrt(),
1708    ///             4.0 - 3.0 / 2.0_f64.sqrt()
1709    ///         )),
1710    ///         PathCommand::LineTo(Point::new(
1711    ///             5.0 - 3.0 * 2.0_f64.sqrt(),
1712    ///             4.0
1713    ///         )),
1714    ///         PathCommand::LineTo(Point::new(5.0, 4.0))
1715    ///     ]
1716    /// ).into();
1717    ///
1718    /// assert!(pth1.rotate_about_first_point(PI / 4.0).almost_eq(&pth2, 0.001));
1719    /// ```
1720    pub fn rotate_about_first_point(&self, angle: f64) -> Self {
1721        match self.0.first() {
1722            Some(pc) => self.rotate(&pc.point(), angle),
1723            None => self.clone(),
1724        }
1725    }
1726
1727    /// Rotate the path about the specified point by the specified angle.
1728    ///
1729    /// # Example
1730    ///
1731    /// ```
1732    /// use std::f64::consts::PI;
1733    ///
1734    /// use l_system_fractals::paths::{Path, PathCommand, Point};
1735    /// use l_system_fractals::num_validity::AlmostEq;
1736    ///
1737    /// let pth1: Path = Path(
1738    ///     vec![
1739    ///         PathCommand::MoveTo(Point::new(5.0, 8.0)),
1740    ///         PathCommand::LineTo(Point::new(2.0, 2.0)),
1741    ///         PathCommand::LineTo(Point::new(8.0, 2.0)),
1742    ///         PathCommand::LineTo(Point::new(5.0, 8.0)),
1743    ///     ]
1744    /// ).into();
1745    ///
1746    /// let axis = Point::new(5.0, 5.0);
1747    ///
1748    /// let pth2: Path = Path(
1749    ///     vec![
1750    ///         PathCommand::MoveTo(Point::new(
1751    ///             5.0 - 1.5 * 3.0_f64.sqrt(),
1752    ///             6.5
1753    ///         )),
1754    ///         PathCommand::LineTo(Point::new(
1755    ///             5.0 - 3.0 * 2.0_f64.sqrt() * (7.0 * PI / 12.0).cos(),
1756    ///             5.0 - 3.0 * 2.0_f64.sqrt() * (7.0 * PI / 12.0).sin()
1757    ///         )),
1758    ///         PathCommand::LineTo(Point::new(
1759    ///             5.0 + 3.0 * 2.0_f64.sqrt() * (PI / 12.0).cos(),
1760    ///             5.0 + 3.0 * 2.0_f64.sqrt() * (PI / 12.0).sin()
1761    ///         )),
1762    ///         PathCommand::LineTo(Point::new(
1763    ///             5.0 - 1.5 * 3.0_f64.sqrt(),
1764    ///             6.5
1765    ///         ))
1766    ///     ]
1767    /// ).into();
1768    ///
1769    /// assert!(pth1.rotate(&axis, PI / 3.0).almost_eq(&pth2, 0.001));
1770    /// ```
1771    pub fn rotate(&self, axis: &Point, angle: f64) -> Self {
1772        Self(
1773            self.0
1774                .iter()
1775                .map(|pc| pc.same_type(pc.point().rotate(axis, angle)))
1776                .collect(),
1777        )
1778    }
1779}