iron_shapes/
path.rs

1// Copyright (c) 2018-2020 Thomas Kramer.
2// SPDX-FileCopyrightText: 2018-2022 Thomas Kramer
3//
4// SPDX-License-Identifier: AGPL-3.0-or-later
5
6//! `Path` is essentially a chain of line segments but with a possibly non-zero width.
7//! It can be thought of the shape resulting by a stroke of a thick pen along the line segments.
8
9use crate::point::Point;
10use crate::point_string::PointString;
11use crate::vector::Vector;
12
13use crate::CoordinateType;
14
15pub use crate::traits::{BoundingBox, RotateOrtho};
16use crate::traits::{MapPointwise, Scale, Translate, TryBoundingBox};
17pub use crate::types::Angle;
18
19pub use crate::types::{ContainsResult, Side};
20
21use crate::edge::*;
22use crate::rect::Rect;
23use crate::simple_polygon::SimplePolygon;
24use crate::transform::SimpleTransform;
25use num_traits::{Float, Num, NumCast};
26use std::iter::FromIterator;
27use std::ops::{Add, Mul};
28
29/// Encoding for the type of the beginning and end of the path.
30#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
31#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
32pub enum PathEndType<T> {
33    /// Beginning and end of path are not extended.
34    Flat,
35    /// Define the extension length at the beginning and at the end of the path.
36    Extended(T, T),
37    /// Path ends are round (approximately semi-circles).
38    Round,
39}
40
41/// `Path` is essentially a chain of line segments but with a possibly a non-zero width.
42/// It can be thought of the shape resulting by a stroke of a thick pen along the line segments.
43#[derive(Clone, PartialEq, Eq, Hash, Debug)]
44#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
45pub struct Path<T> {
46    /// The vertices of the path which define the sequence of line segments.
47    pub points: PointString<T>,
48    /// Width of the path.
49    pub width: T,
50    /// Type of the path endings.
51    pub path_type: PathEndType<T>,
52}
53
54impl<T> Path<T> {
55    /// Get number of vertices defining the path.
56    pub fn len(&self) -> usize {
57        self.points.len()
58    }
59
60    /// Check if path has zero length.
61    pub fn is_empty(&self) -> bool {
62        self.points.is_empty()
63    }
64}
65
66impl<T: Copy> Path<T> {
67    /// Create new path by taking vertices from a type that implements `Into<PointString<T>>`.
68    pub fn new<I>(i: I, width: T) -> Self
69    where
70        I: Into<PointString<T>>,
71    {
72        Path {
73            points: i.into(),
74            width,
75            path_type: PathEndType::Flat,
76        }
77    }
78
79    /// Create a path with extended beginning and end.
80    pub fn new_extended<I>(i: I, width: T, ext_begin: T, ext_end: T) -> Self
81    where
82        I: Into<PointString<T>>,
83    {
84        Path {
85            points: i.into(),
86            width,
87            path_type: PathEndType::Extended(ext_begin, ext_end),
88        }
89    }
90
91    /// Create a path with rounded beginning and end.
92    pub fn new_rounded<I>(i: I, width: T) -> Self
93    where
94        I: Into<PointString<T>>,
95    {
96        Path {
97            points: i.into(),
98            width,
99            path_type: PathEndType::Round,
100        }
101    }
102}
103
104impl<T: Copy + Add<Output = T>> Path<T> {
105    /// Translate the path by an offset vector.
106    pub fn translate(&self, v: Vector<T>) -> Self {
107        Path {
108            points: self.points.translate(v),
109            width: self.width,
110            path_type: self.path_type,
111        }
112    }
113}
114
115impl<T: Copy + Mul<Output = T>> Path<T> {
116    /// Scale the path. Scaling center is the origin `(0, 0)`.
117    pub fn scale(&self, factor: T) -> Self {
118        Path {
119            points: self.points.scale(factor),
120            width: self.width * factor,
121            path_type: self.path_type,
122        }
123    }
124}
125
126impl<T: CoordinateType> Path<T> {
127    /// Rotate the path by a multiple of 90 degrees around the origin `(0, 0)`.
128    pub fn rotate_ortho(&self, angle: Angle) -> Self {
129        Path {
130            points: self.points.rotate_ortho(angle),
131            width: self.width,
132            path_type: self.path_type,
133        }
134    }
135
136    /// Get the transformed version of this path by applying `tf`.
137    pub fn transform(&self, tf: &SimpleTransform<T>) -> Self {
138        Self {
139            points: self.points.transform(|p| tf.transform_point(p)),
140            width: tf.transform_distance(self.width),
141            path_type: self.path_type,
142        }
143    }
144}
145
146impl<T: CoordinateType + NumCast> Path<T> {
147    /// Compute approximate area occupied by the path.
148    /// Simply computes length*width.
149    ///
150    /// # Examples
151    ///
152    /// ```rust
153    /// use iron_shapes::prelude::*;
154    /// let path = Path::new(&[(0, 0), (0, 2)], 1);
155    /// assert_eq!(path.area_approx::<f64>(), 2f64);
156    /// ```
157    pub fn area_approx<F: Float>(&self) -> F {
158        let base_len: F = self.points.path_length();
159        let w = F::from(self.width).unwrap();
160        let l = match self.path_type {
161            PathEndType::Extended(l1, l2) => base_len + F::from(l1 + l2).unwrap(),
162            _ => base_len,
163        };
164
165        let base_area = l * w;
166
167        // Add area of circle if path ends are round.
168        match self.path_type {
169            PathEndType::Round => base_area + F::from(std::f64::consts::PI).unwrap() * w * w,
170            _ => base_area,
171        }
172    }
173
174    /// Convert the path into a polygon.
175    /// The polygon can be self-intersecting.
176    ///
177    /// # Examples
178    ///
179    /// ```rust
180    /// use iron_shapes::prelude::*;
181    /// let path = Path::new(&[(0, 0), (10, 0), (10, 20)], 4);
182    /// let polygon = path.to_polygon_approx();
183    /// assert_eq!(polygon, SimplePolygon::from(&[(0., 2.), (0., -2.), (12., -2.), (12., 20.), (8., 20.), (8., 2.)]));
184    /// ```
185    pub fn to_polygon_approx(&self) -> SimplePolygon<f64> {
186        let mut points_forward: Vec<Point<f64>> = Vec::new();
187        let mut points_backward: Vec<Point<f64>> = Vec::new();
188
189        let edges: Vec<Edge<f64>> = self
190            .points
191            .edges()
192            .filter(|e| e.start != e.end) // Skip zero-length edges.
193            .map(|e| e.cast_to_float())
194            .collect();
195
196        // Construct rectangular start and end caps.
197        let create_flat_cap =
198            |center_edge: Edge<f64>, width: f64, extension: f64| -> Vec<Point<f64>> {
199                let d = center_edge.vector().normalized();
200                let n = d.rotate_ortho(Angle::R90);
201                let p = center_edge.end;
202                let w_half = width / 2.;
203                if extension == 0. {
204                    let p1 = p - n * w_half;
205                    let p2 = p + n * w_half;
206                    vec![p1, p2]
207                } else {
208                    let p1 = p - n * w_half;
209                    let p4 = p + n * w_half;
210                    let p2 = p1 + d * extension;
211                    let p3 = p4 + d * extension;
212                    vec![p1, p2, p3, p4]
213                }
214            };
215
216        // Calculate start/end extensions.
217        let (start_ext, end_ext) = match self.path_type {
218            PathEndType::Extended(start_ext, end_ext) => {
219                let start_ext = NumCast::from(start_ext).unwrap();
220                let end_ext = NumCast::from(end_ext).unwrap();
221                (start_ext, end_ext)
222            }
223            PathEndType::Flat => (0., 0.),
224            PathEndType::Round => unimplemented!("Not implemented for round path ends."),
225        };
226
227        // Path width.
228        let width = NumCast::from(self.width).unwrap();
229        let half_width = width * 0.5;
230
231        // Create caps.
232        let start_cap = edges
233            .first()
234            .map(|e| create_flat_cap(e.reversed(), width, start_ext))
235            .unwrap_or_default();
236        let end_cap = edges
237            .last()
238            .map(|e| create_flat_cap(*e, width, end_ext))
239            .unwrap_or_default();
240
241        // Pre-compute normals (scaled by half the width).
242        let normals: Vec<Vector<f64>> = edges
243            .iter()
244            .map(|e| e.vector().normal() * half_width)
245            .collect();
246
247        let edge_pairs = edges.iter().zip(edges.iter().skip(1));
248        let normal_pairs = normals.iter().zip(normals.iter().skip(1));
249
250        for ((&e1, &e2), (&n1, &n2)) in edge_pairs.zip(normal_pairs) {
251            let border1f = e1.translate(-n1);
252            let border1b = e1.translate(n1);
253            let border2f = e2.translate(-n2);
254            let border2b = e2.translate(n2);
255
256            // Forward.
257            let border_intersection_f = border1f.line_intersection_approx(&border2f, 1e-15);
258
259            match border_intersection_f {
260                LineIntersection::Collinear => {}
261                LineIntersection::None => {}
262                LineIntersection::Point(p, _) => points_forward.push(p),
263            }
264
265            // Backward.
266            let border_intersection_b = border1b.line_intersection_approx(&border2b, 1e-15);
267
268            match border_intersection_b {
269                LineIntersection::Collinear => {}
270                LineIntersection::None => {}
271                LineIntersection::Point(p, _) => points_backward.push(p),
272            }
273        }
274
275        // Concatenate forward and backward points including start and end cap.
276        SimplePolygon::from_iter(
277            start_cap
278                .iter()
279                .chain(points_forward.iter())
280                .chain(end_cap.iter())
281                .chain(points_backward.iter().rev()),
282        )
283        .normalized()
284    }
285}
286
287impl<T: CoordinateType + NumCast, Dst: CoordinateType + NumCast> TryCastCoord<T, Dst> for Path<T> {
288    type Output = Path<Dst>;
289
290    fn try_cast(&self) -> Option<Self::Output> {
291        let new_width = Dst::from(self.width);
292        let new_points = self.points.try_cast();
293        let new_path_type = match self.path_type {
294            PathEndType::Extended(begin_ext, end_ext) => {
295                let new_begin_ext = Dst::from(begin_ext);
296                let new_end_ext = Dst::from(end_ext);
297                match (new_begin_ext, new_end_ext) {
298                    (Some(b), Some(e)) => Some(PathEndType::Extended(b, e)),
299                    _ => None,
300                }
301            }
302            PathEndType::Flat => Some(PathEndType::Flat),
303            PathEndType::Round => Some(PathEndType::Round),
304        };
305
306        match (new_width, new_points, new_path_type) {
307            (Some(width), Some(points), Some(path_type)) => Some(Path {
308                points,
309                width,
310                path_type,
311            }),
312            _ => {
313                // Failed to cast some values.
314                None
315            }
316        }
317    }
318}
319
320impl<T: Copy + PartialOrd + Num> TryBoundingBox<T> for Path<T> {
321    // /// Compute the bounding box of this path.
322    // fn bounding_box(&self) -> Rect<T> {
323    //     // Compute the bounding box by first converting the path into a polygon
324    //     // and then computing the bounding box of the polygon.
325    //     // Since integer Paths do not support conversion to a polygon the path needs
326    //     // to be converted to a float coordinate type.
327    //     // TODO: Make this more efficient and preferably without type conversions.
328    //     let float_path: Path<FloatType> = self.cast();
329    //     let bbox = float_path.to_polygon_approx().bounding_box();
330    //     bbox.cast()
331    // }
332
333    /// Compute the bounding box of this path.
334    /// The returned bounding box is not necessarily the smallest bounding box.
335    ///
336    /// TODO: Find a better approximation.
337    fn try_bounding_box(&self) -> Option<Rect<T>> {
338        #![allow(clippy::just_underscores_and_digits)]
339        // Find the bounding box of a zero-width path.
340        let bbox = self.points.try_bounding_box();
341        let _1 = T::one();
342        let _2 = _1 + _1;
343        bbox.map(|bbox| {
344            // Enlarge it by width/2 in all directions to make sure the path is fully contained
345            // in the bounding box.
346            let (p1, p2) = (bbox.lower_left(), bbox.upper_right());
347            let w_half = (self.width + _1) / _2;
348            let width = Vector::new(w_half, w_half);
349            Rect::new(p1 - width, p2 + width)
350        })
351    }
352}
353
354// impl<T> Translate<T> for Path<T>
355//     where T: CoordinateType {
356//     fn translate(&self, v: Vector<T>) -> Self {
357//         Path {
358//             points: self.points.translate(v),
359//             width: self.width,
360//             path_type: self.path_type,
361//         }
362//     }
363// }
364//
365// impl<T> Scale<T> for Path<T>
366//     where T: CoordinateType {
367//     fn scale(&self, factor: T) -> Self {
368//         Path {
369//             points: self.points.scale(factor),
370//             width: self.width * factor,
371//             path_type: self.path_type,
372//         }
373//     }
374// }