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(¶m2, 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 /// ±∞](`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 ±∞](`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 ±∞](`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 ±∞](`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 ±∞](`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 ±∞](`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 ¶ms.fill,
1489 ¶ms.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}