geomcore/curves/curve2d.rs
1//! [`ParametricCurve2D`] trait and the [`Curve2D`] enum adaptor unifying the
2//! concrete 2D curve types.
3
4use crate::curves::{Circle2D, Line2D};
5use crate::projection::CurveProjection2D;
6use crate::{Point2D, Tolerance, Vector2D};
7use std::f64::consts::TAU;
8
9/// Common interface for 2D parametric curves. Implement this to add a new
10/// curve type that can be used generically (see [`Curve2D`] for a
11/// heterogeneous-collection adaptor built on top of it).
12///
13/// Mirrors [`crate::curves::ParametricCurve`] with [`Point2D`]/[`Vector2D`]
14/// in place of `Point`/`Vector`. Concrete curve types (e.g. [`Line2D`],
15/// [`Circle2D`]) already provide inherent methods with the same names; their
16/// trait impls simply delegate to those inherent methods, so calling code
17/// can use either form.
18///
19/// # Examples
20///
21/// The default [`eval_points`](ParametricCurve2D::eval_points) maps
22/// [`eval_point`](ParametricCurve2D::eval_point) over each parameter:
23///
24/// ```
25/// use geomcore::curves::ParametricCurve2D;
26/// use geomcore::{Line2D, Point2D, Vector2D};
27///
28/// let line = Line2D::new(Point2D::ORIGIN, Vector2D::X).unwrap();
29/// let us = [0.0, 1.0, 2.0];
30/// let expected: Vec<Point2D> = us.iter().map(|&u| line.eval_point(u)).collect();
31/// assert_eq!(ParametricCurve2D::eval_points(&line, &us), expected);
32/// ```
33///
34/// The default [`is_periodic`](ParametricCurve2D::is_periodic) reports
35/// whether [`period`](ParametricCurve2D::period) is `Some`:
36///
37/// ```
38/// use geomcore::curves::ParametricCurve2D;
39/// use geomcore::{Circle2D, Line2D, Point2D};
40///
41/// let line = Line2D::new(Point2D::ORIGIN, geomcore::Vector2D::X).unwrap();
42/// assert!(!ParametricCurve2D::is_periodic(&line));
43///
44/// let circle = Circle2D::new(Point2D::ORIGIN, 1.0).unwrap();
45/// assert!(ParametricCurve2D::is_periodic(&circle));
46/// ```
47pub trait ParametricCurve2D {
48 /// Evaluates the point on the curve at parameter `u`.
49 fn eval_point(&self, u: f64) -> Point2D;
50
51 /// Evaluates the derivative of the given `order` at parameter `u`.
52 fn eval_derivative(&self, u: f64, order: u32) -> Vector2D;
53
54 /// Evaluates the point and the first derivative at parameter `u`.
55 ///
56 /// Default implementation: [`eval_point`](Self::eval_point) and
57 /// [`eval_derivative`](Self::eval_derivative) of order 1.
58 ///
59 /// # Examples
60 ///
61 /// ```
62 /// use geomcore::curves::ParametricCurve2D;
63 /// use geomcore::{Circle2D, Point2D, Vector2D};
64 ///
65 /// let circle = Circle2D::new(Point2D::ORIGIN, 2.0).unwrap();
66 /// let (point, tangent) = ParametricCurve2D::eval_d1(&circle, 0.0);
67 /// assert_eq!(point, Point2D::new(2.0, 0.0));
68 /// assert!((tangent - Vector2D::new(0.0, 2.0)).magnitude() < 1e-15);
69 /// ```
70 fn eval_d1(&self, u: f64) -> (Point2D, Vector2D) {
71 (self.eval_point(u), self.eval_derivative(u, 1))
72 }
73
74 /// Evaluates the point and the first two derivatives at parameter `u`.
75 ///
76 /// Default implementation: [`eval_point`](Self::eval_point) and
77 /// [`eval_derivative`](Self::eval_derivative) of orders 1 and 2.
78 ///
79 /// # Examples
80 ///
81 /// ```
82 /// use geomcore::curves::ParametricCurve2D;
83 /// use geomcore::{Circle2D, Point2D, Vector2D};
84 ///
85 /// let circle = Circle2D::new(Point2D::ORIGIN, 2.0).unwrap();
86 /// let (point, _, acceleration) = ParametricCurve2D::eval_d2(&circle, 0.0);
87 /// assert_eq!(point, Point2D::new(2.0, 0.0));
88 /// assert!((acceleration - Vector2D::new(-2.0, 0.0)).magnitude() < 1e-15);
89 /// ```
90 fn eval_d2(&self, u: f64) -> (Point2D, Vector2D, Vector2D) {
91 (
92 self.eval_point(u),
93 self.eval_derivative(u, 1),
94 self.eval_derivative(u, 2),
95 )
96 }
97
98 /// Returns the curve's parameter bounds as `(first, last)`. Unbounded
99 /// curves use `(f64::NEG_INFINITY, f64::INFINITY)`.
100 fn bounds(&self) -> (f64, f64);
101
102 /// Returns the curve's period, or `None` if the curve is not periodic.
103 fn period(&self) -> Option<f64>;
104
105 /// Evaluates the points on the curve at each parameter in `us`.
106 ///
107 /// Default implementation: maps [`eval_point`](Self::eval_point) over
108 /// `us`.
109 fn eval_points(&self, us: &[f64]) -> Vec<Point2D> {
110 us.iter().map(|&u| self.eval_point(u)).collect()
111 }
112
113 /// Returns whether the curve is periodic, i.e. whether
114 /// [`period`](Self::period) is `Some`.
115 fn is_periodic(&self) -> bool {
116 self.period().is_some()
117 }
118}
119
120/// Distance extrema from a point to a 2D curve: what a
121/// [`TrimmedCurve2D`](crate::curves::TrimmedCurve2D) needs from its basis
122/// to project onto a piece of it. Mirrors
123/// [`crate::curves::PointExtrema`].
124///
125/// # Examples
126///
127/// ```
128/// use geomcore::curves::PointExtrema2D;
129/// use geomcore::{Circle2D, Point2D, Tolerance};
130///
131/// let circle = Circle2D::new(Point2D::ORIGIN, 1.0).unwrap();
132/// let extrema = circle.extrema(Point2D::new(3.0, 0.0), Tolerance::DEFAULT);
133/// assert_eq!(extrema[0].parameter, 0.0);
134/// assert_eq!(extrema[0].distance, 2.0);
135/// ```
136pub trait PointExtrema2D: ParametricCurve2D {
137 /// Parameters on the curve's own domain that include every local
138 /// minimum of the distance from `point` (other stationary points may
139 /// be listed too; a valley of equal minima is represented by one of
140 /// its points), ordered by ascending distance, the global closest
141 /// point first. Distances within `tol.confusion` snap to `0.0`.
142 ///
143 /// A query point with a non-finite coordinate has no extrema.
144 fn extrema(&self, point: Point2D, tol: Tolerance) -> Vec<CurveProjection2D>;
145}
146
147/// A reference to a curve is the curve, so a
148/// [`TrimmedCurve2D`](crate::curves::TrimmedCurve2D) can borrow its
149/// basis instead of owning it.
150impl<C: ParametricCurve2D + ?Sized> ParametricCurve2D for &C {
151 fn eval_point(&self, u: f64) -> Point2D {
152 (**self).eval_point(u)
153 }
154
155 fn eval_derivative(&self, u: f64, order: u32) -> Vector2D {
156 (**self).eval_derivative(u, order)
157 }
158
159 fn eval_d1(&self, u: f64) -> (Point2D, Vector2D) {
160 (**self).eval_d1(u)
161 }
162
163 fn eval_d2(&self, u: f64) -> (Point2D, Vector2D, Vector2D) {
164 (**self).eval_d2(u)
165 }
166
167 fn bounds(&self) -> (f64, f64) {
168 (**self).bounds()
169 }
170
171 fn period(&self) -> Option<f64> {
172 (**self).period()
173 }
174
175 fn eval_points(&self, us: &[f64]) -> Vec<Point2D> {
176 (**self).eval_points(us)
177 }
178}
179
180impl<C: PointExtrema2D + ?Sized> PointExtrema2D for &C {
181 fn extrema(&self, point: Point2D, tol: Tolerance) -> Vec<CurveProjection2D> {
182 (**self).extrema(point, tol)
183 }
184}
185
186/// The single local minimum of a line, or of a circle (whose distance
187/// from a point falls monotonically to it from the farthest point).
188fn only_minimum(point: Point2D, project: impl Fn() -> CurveProjection2D) -> Vec<CurveProjection2D> {
189 if point.is_finite() {
190 vec![project()]
191 } else {
192 Vec::new()
193 }
194}
195
196impl PointExtrema2D for Line2D {
197 fn extrema(&self, point: Point2D, tol: Tolerance) -> Vec<CurveProjection2D> {
198 only_minimum(point, || self.project_point(point, tol))
199 }
200}
201
202impl PointExtrema2D for Circle2D {
203 fn extrema(&self, point: Point2D, tol: Tolerance) -> Vec<CurveProjection2D> {
204 only_minimum(point, || self.project_point(point, tol))
205 }
206}
207
208impl ParametricCurve2D for Line2D {
209 fn eval_point(&self, u: f64) -> Point2D {
210 Line2D::eval_point(self, u)
211 }
212
213 fn eval_derivative(&self, u: f64, order: u32) -> Vector2D {
214 Line2D::eval_derivative(self, u, order)
215 }
216
217 fn bounds(&self) -> (f64, f64) {
218 (f64::NEG_INFINITY, f64::INFINITY)
219 }
220
221 fn period(&self) -> Option<f64> {
222 None
223 }
224
225 fn eval_points(&self, us: &[f64]) -> Vec<Point2D> {
226 Line2D::eval_points(self, us)
227 }
228}
229
230impl ParametricCurve2D for Circle2D {
231 fn eval_point(&self, u: f64) -> Point2D {
232 Circle2D::eval_point(self, u)
233 }
234
235 fn eval_derivative(&self, u: f64, order: u32) -> Vector2D {
236 Circle2D::eval_derivative(self, u, order)
237 }
238
239 fn bounds(&self) -> (f64, f64) {
240 (0.0, TAU)
241 }
242
243 fn period(&self) -> Option<f64> {
244 Some(TAU)
245 }
246
247 fn eval_points(&self, us: &[f64]) -> Vec<Point2D> {
248 Circle2D::eval_points(self, us)
249 }
250}
251
252/// Enum adaptor unifying the concrete 2D curve types behind a single value
253/// type, so callers can build a heterogeneous collection of curves and
254/// evaluate them uniformly through [`ParametricCurve2D`].
255///
256/// Marked `#[non_exhaustive]`: future tasks add further variants without
257/// that being a breaking change.
258///
259/// # Examples
260///
261/// ```
262/// use geomcore::curves::{Curve2D, ParametricCurve2D};
263/// use geomcore::{Circle2D, Line2D, Point2D, Vector2D};
264///
265/// let curves: Vec<Curve2D> = vec![
266/// Line2D::new(Point2D::ORIGIN, Vector2D::X).unwrap().into(),
267/// Circle2D::new(Point2D::ORIGIN, 1.0).unwrap().into(),
268/// ];
269/// let points: Vec<Point2D> = curves.iter().map(|c| c.eval_point(0.0)).collect();
270/// assert_eq!(points[0], Point2D::ORIGIN);
271/// assert_eq!(points[1], Point2D::new(1.0, 0.0));
272/// ```
273#[derive(Debug, Clone, Copy, PartialEq)]
274#[non_exhaustive]
275pub enum Curve2D {
276 /// A [`Line2D`] curve.
277 Line(Line2D),
278 /// A [`Circle2D`] curve.
279 Circle(Circle2D),
280}
281
282impl ParametricCurve2D for Curve2D {
283 fn eval_point(&self, u: f64) -> Point2D {
284 match self {
285 Curve2D::Line(c) => c.eval_point(u),
286 Curve2D::Circle(c) => c.eval_point(u),
287 }
288 }
289
290 fn eval_derivative(&self, u: f64, order: u32) -> Vector2D {
291 match self {
292 Curve2D::Line(c) => c.eval_derivative(u, order),
293 Curve2D::Circle(c) => c.eval_derivative(u, order),
294 }
295 }
296
297 fn bounds(&self) -> (f64, f64) {
298 match self {
299 Curve2D::Line(c) => c.bounds(),
300 Curve2D::Circle(c) => c.bounds(),
301 }
302 }
303
304 fn period(&self) -> Option<f64> {
305 match self {
306 Curve2D::Line(c) => c.period(),
307 Curve2D::Circle(c) => c.period(),
308 }
309 }
310
311 fn eval_points(&self, us: &[f64]) -> Vec<Point2D> {
312 match self {
313 Curve2D::Line(c) => c.eval_points(us),
314 Curve2D::Circle(c) => c.eval_points(us),
315 }
316 }
317}
318
319impl PointExtrema2D for Curve2D {
320 fn extrema(&self, point: Point2D, tol: Tolerance) -> Vec<CurveProjection2D> {
321 match self {
322 Curve2D::Line(c) => PointExtrema2D::extrema(c, point, tol),
323 Curve2D::Circle(c) => PointExtrema2D::extrema(c, point, tol),
324 }
325 }
326}
327
328impl Curve2D {
329 /// Projects `point` onto the curve: the closest parameter, the closest
330 /// point and the distance. Distances within `tol.confusion` snap to
331 /// `0.0`.
332 ///
333 /// # Examples
334 ///
335 /// ```
336 /// use geomcore::curves::Curve2D;
337 /// use geomcore::{Line2D, Point2D, Tolerance, Vector2D};
338 ///
339 /// let curve = Curve2D::from(Line2D::new(Point2D::ORIGIN, Vector2D::X).unwrap());
340 /// let projection = curve.project_point(Point2D::new(2.0, 1.0), Tolerance::DEFAULT);
341 /// assert_eq!(projection.point, Point2D::new(2.0, 0.0));
342 /// assert_eq!(projection.distance, 1.0);
343 /// ```
344 pub fn project_point(&self, point: Point2D, tol: Tolerance) -> CurveProjection2D {
345 match self {
346 Curve2D::Line(c) => c.project_point(point, tol),
347 Curve2D::Circle(c) => c.project_point(point, tol),
348 }
349 }
350
351 /// Projects each point in `points` onto the curve.
352 pub fn project_points(&self, points: &[Point2D], tol: Tolerance) -> Vec<CurveProjection2D> {
353 points.iter().map(|&p| self.project_point(p, tol)).collect()
354 }
355
356 /// The parameter of the point of the curve closest to `point`, by the
357 /// curve's closed form.
358 ///
359 /// # Examples
360 ///
361 /// ```
362 /// use geomcore::curves::Curve2D;
363 /// use geomcore::{Circle2D, Point2D};
364 ///
365 /// let curve = Curve2D::from(Circle2D::new(Point2D::ORIGIN, 2.0).unwrap());
366 /// let parameter = curve.parameter_of(Point2D::new(0.0, 2.0));
367 /// assert!((parameter - std::f64::consts::FRAC_PI_2).abs() < 1e-12);
368 /// ```
369 pub fn parameter_of(&self, point: Point2D) -> f64 {
370 match self {
371 Curve2D::Line(c) => c.parameter_of(point),
372 Curve2D::Circle(c) => c.parameter_of(point),
373 }
374 }
375}
376
377impl From<Line2D> for Curve2D {
378 /// Wraps a [`Line2D`] as a [`Curve2D::Line`].
379 fn from(curve: Line2D) -> Curve2D {
380 Curve2D::Line(curve)
381 }
382}
383
384impl From<Circle2D> for Curve2D {
385 /// Wraps a [`Circle2D`] as a [`Curve2D::Circle`].
386 fn from(curve: Circle2D) -> Curve2D {
387 Curve2D::Circle(curve)
388 }
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394
395 fn sample<C: ParametricCurve2D>(c: &C) -> Point2D {
396 c.eval_point(0.5)
397 }
398
399 fn line() -> Line2D {
400 Line2D::new(Point2D::ORIGIN, Vector2D::X).unwrap()
401 }
402
403 fn circle() -> Circle2D {
404 Circle2D::new(Point2D::ORIGIN, 2.0).unwrap()
405 }
406
407 // ---- generic fn agrees with inherent calls ----
408
409 #[test]
410 fn test_sample_line_agrees_with_inherent() {
411 let l = line();
412 assert_eq!(sample(&l), l.eval_point(0.5));
413 }
414
415 #[test]
416 fn test_sample_circle_agrees_with_inherent() {
417 let c = circle();
418 assert_eq!(sample(&c), c.eval_point(0.5));
419 }
420
421 #[test]
422 fn test_sample_curve2d_enum_agrees_with_inherent() {
423 let l = line();
424 let wrapped: Curve2D = l.into();
425 assert_eq!(sample(&wrapped), l.eval_point(0.5));
426 }
427
428 // ---- heterogeneous Vec<Curve2D> eval ----
429
430 #[test]
431 fn test_heterogeneous_vec_eval() {
432 let l = line();
433 let c = circle();
434 let curves: Vec<Curve2D> = vec![l.into(), c.into()];
435 let points: Vec<Point2D> = curves.iter().map(|curve| curve.eval_point(0.25)).collect();
436 assert_eq!(points[0], l.eval_point(0.25));
437 assert_eq!(points[1], c.eval_point(0.25));
438 }
439
440 // ---- From round-trips ----
441
442 #[test]
443 fn test_from_line_round_trips() {
444 let l = line();
445 match Curve2D::from(l) {
446 Curve2D::Line(inner) => assert_eq!(inner, l),
447 _ => panic!("expected Curve2D::Line"),
448 }
449 }
450
451 #[test]
452 fn test_from_circle_round_trips() {
453 let c = circle();
454 match Curve2D::from(c) {
455 Curve2D::Circle(inner) => assert_eq!(inner, c),
456 _ => panic!("expected Curve2D::Circle"),
457 }
458 }
459
460 // ---- bounds/period table, per type (inherent trait impl + enum dispatch) ----
461
462 #[test]
463 fn test_line_bounds_and_period() {
464 let l = line();
465 assert_eq!(
466 ParametricCurve2D::bounds(&l),
467 (f64::NEG_INFINITY, f64::INFINITY)
468 );
469 assert_eq!(ParametricCurve2D::period(&l), None);
470 assert!(!ParametricCurve2D::is_periodic(&l));
471
472 let wrapped: Curve2D = l.into();
473 assert_eq!(wrapped.bounds(), (f64::NEG_INFINITY, f64::INFINITY));
474 assert_eq!(wrapped.period(), None);
475 assert!(!wrapped.is_periodic());
476 }
477
478 #[test]
479 fn test_circle_bounds_and_period() {
480 let c = circle();
481 assert_eq!(ParametricCurve2D::bounds(&c), (0.0, TAU));
482 assert_eq!(ParametricCurve2D::period(&c), Some(TAU));
483 assert!(ParametricCurve2D::is_periodic(&c));
484
485 let wrapped: Curve2D = c.into();
486 assert_eq!(wrapped.bounds(), (0.0, TAU));
487 assert_eq!(wrapped.period(), Some(TAU));
488 assert!(wrapped.is_periodic());
489 }
490
491 // ---- default eval_points == mapped eval_point ----
492
493 #[test]
494 fn test_default_eval_points_matches_mapped_eval_point_for_each_type() {
495 let us = [0.0, 0.5, 1.0, -2.0];
496
497 let l = line();
498 let expected: Vec<Point2D> = us.iter().map(|&u| l.eval_point(u)).collect();
499 assert_eq!(ParametricCurve2D::eval_points(&l, &us), expected);
500
501 let c = circle();
502 let expected: Vec<Point2D> = us.iter().map(|&u| c.eval_point(u)).collect();
503 assert_eq!(ParametricCurve2D::eval_points(&c, &us), expected);
504 }
505
506 #[test]
507 fn test_curve2d_enum_eval_points_matches_mapped_eval_point() {
508 let us = [0.0, 0.5, 1.0, -2.0];
509 let curves: Vec<Curve2D> = vec![line().into(), circle().into()];
510 for curve in &curves {
511 let expected: Vec<Point2D> = us.iter().map(|&u| curve.eval_point(u)).collect();
512 assert_eq!(curve.eval_points(&us), expected);
513 }
514 }
515
516 #[test]
517 fn test_eval_derivative_delegates_to_inherent() {
518 let l = line();
519 assert_eq!(
520 ParametricCurve2D::eval_derivative(&l, 0.5, 1),
521 l.eval_derivative(0.5, 1)
522 );
523
524 let c = circle();
525 assert_eq!(
526 ParametricCurve2D::eval_derivative(&c, 0.5, 1),
527 c.eval_derivative(0.5, 1)
528 );
529 }
530}