Skip to main content

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}