1use 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#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
31#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
32pub enum PathEndType<T> {
33 Flat,
35 Extended(T, T),
37 Round,
39}
40
41#[derive(Clone, PartialEq, Eq, Hash, Debug)]
44#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
45pub struct Path<T> {
46 pub points: PointString<T>,
48 pub width: T,
50 pub path_type: PathEndType<T>,
52}
53
54impl<T> Path<T> {
55 pub fn len(&self) -> usize {
57 self.points.len()
58 }
59
60 pub fn is_empty(&self) -> bool {
62 self.points.is_empty()
63 }
64}
65
66impl<T: Copy> Path<T> {
67 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 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 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 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 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 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 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 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 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 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) .map(|e| e.cast_to_float())
194 .collect();
195
196 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 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 let width = NumCast::from(self.width).unwrap();
229 let half_width = width * 0.5;
230
231 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 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 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 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 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 None
315 }
316 }
317 }
318}
319
320impl<T: Copy + PartialOrd + Num> TryBoundingBox<T> for Path<T> {
321 fn try_bounding_box(&self) -> Option<Rect<T>> {
338 #![allow(clippy::just_underscores_and_digits)]
339 let bbox = self.points.try_bounding_box();
341 let _1 = T::one();
342 let _2 = _1 + _1;
343 bbox.map(|bbox| {
344 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