stroke/
bezier_segment.rs

1//! Sum type for specialized Bezier segments.
2
3use super::{CubicBezier, LineSegment, Point, PointDot, PointIndex, PointNorm, QuadraticBezier};
4use num_traits::{Float, NumCast};
5
6const DEFAULT_LENGTH_STEPS: usize = 64;
7
8/// Sum type for line/quadratic/cubic Bezier segments.
9///
10/// Methods that need component access or norms add `PointIndex`/`PointNorm`
11/// bounds as required.
12#[derive(Copy, Clone, Debug, PartialEq)]
13pub enum BezierSegment<P: Point> {
14    Linear(LineSegment<P>),
15    Quadratic(QuadraticBezier<P>),
16    Cubic(CubicBezier<P>),
17}
18
19impl<P> BezierSegment<P>
20where
21    P: Point,
22{
23    /// Evaluate the segment at `t` in `[0, 1]`.
24    pub fn eval(&self, t: P::Scalar) -> P {
25        match self {
26            BezierSegment::Linear(segment) => segment.eval(t),
27            BezierSegment::Quadratic(segment) => segment.eval(t),
28            BezierSegment::Cubic(segment) => segment.eval(t),
29        }
30    }
31
32    /// Return the segment start point.
33    pub fn start(&self) -> P {
34        match self {
35            BezierSegment::Linear(segment) => segment.start,
36            BezierSegment::Quadratic(segment) => segment.start,
37            BezierSegment::Cubic(segment) => segment.start,
38        }
39    }
40
41    #[inline]
42    /// Return the segment end point.
43    pub fn end(&self) -> P {
44        match self {
45            BezierSegment::Linear(segment) => segment.end,
46            BezierSegment::Quadratic(segment) => segment.end,
47            BezierSegment::Cubic(segment) => segment.end,
48        }
49    }
50
51    /// Return a segment with reversed direction.
52    pub fn reverse(&self) -> Self {
53        match self {
54            BezierSegment::Linear(segment) => BezierSegment::Linear(segment.reverse()),
55            BezierSegment::Quadratic(segment) => BezierSegment::Quadratic(segment.reverse()),
56            BezierSegment::Cubic(segment) => BezierSegment::Cubic(segment.reverse()),
57        }
58    }
59
60    #[inline]
61    /// Return true if the segment is linear within `tolerance`.
62    pub fn is_linear<F>(&self, tolerance: F) -> bool
63    where
64        P: PointNorm,
65        F: Float + NumCast,
66    {
67        let tolerance = <P::Scalar as NumCast>::from(tolerance).unwrap();
68        match self {
69            BezierSegment::Linear(..) => true,
70            BezierSegment::Quadratic(segment) => segment.is_linear(tolerance),
71            BezierSegment::Cubic(segment) => segment.is_linear(tolerance),
72        }
73    }
74
75    #[inline]
76    /// Return the baseline line segment between start and end.
77    pub fn baseline(&self) -> LineSegment<P> {
78        match self {
79            BezierSegment::Linear(segment) => *segment,
80            BezierSegment::Quadratic(segment) => segment.baseline(),
81            BezierSegment::Cubic(segment) => segment.baseline(),
82        }
83    }
84
85    #[inline]
86    /// Return the bounding box across all axes.
87    pub fn bounding_box(&self) -> [(P::Scalar, P::Scalar); P::DIM]
88    where
89        P: PointIndex,
90        [P::Scalar; 1]: tinyvec::Array<Item = P::Scalar>,
91        [P::Scalar; 2]: tinyvec::Array<Item = P::Scalar>,
92        [P::Scalar; 3]: tinyvec::Array<Item = P::Scalar>,
93        [P::Scalar; 4]: tinyvec::Array<Item = P::Scalar>,
94    {
95        match self {
96            BezierSegment::Linear(segment) => segment.bounding_box(),
97            BezierSegment::Quadratic(segment) => segment.bounding_box(),
98            BezierSegment::Cubic(segment) => segment.bounding_box(),
99        }
100    }
101
102    /// Split this segment into two sub-segments at `t`.
103    pub fn split<F>(&self, t: F) -> (BezierSegment<P>, BezierSegment<P>)
104    where
105        F: Float + NumCast,
106    {
107        let t = <P::Scalar as NumCast>::from(t).unwrap();
108        match self {
109            BezierSegment::Linear(segment) => {
110                let (a, b) = segment.split(t);
111                (BezierSegment::Linear(a), BezierSegment::Linear(b))
112            }
113            BezierSegment::Quadratic(segment) => {
114                let (a, b) = segment.split(t);
115                (BezierSegment::Quadratic(a), BezierSegment::Quadratic(b))
116            }
117            BezierSegment::Cubic(segment) => {
118                let (a, b) = segment.split(t);
119                (BezierSegment::Cubic(a), BezierSegment::Cubic(b))
120            }
121        }
122    }
123
124    /// Return the unit tangent direction at `t`.
125    pub fn tangent(&self, t: P::Scalar) -> P
126    where
127        P: PointNorm,
128    {
129        match self {
130            BezierSegment::Linear(segment) => segment.tangent(t),
131            BezierSegment::Quadratic(segment) => segment.tangent(t),
132            BezierSegment::Cubic(segment) => segment.tangent(t),
133        }
134    }
135
136    /// Return the curvature magnitude at `t`.
137    pub fn curvature(&self, t: P::Scalar) -> P::Scalar
138    where
139        P: PointNorm + PointDot,
140    {
141        match self {
142            BezierSegment::Linear(segment) => segment.curvature(t),
143            BezierSegment::Quadratic(segment) => segment.curvature(t),
144            BezierSegment::Cubic(segment) => segment.curvature(t),
145        }
146    }
147
148    /// Return the principal normal direction at `t`.
149    ///
150    /// Returns `None` if the velocity is zero or curvature is undefined.
151    pub fn normal(&self, t: P::Scalar) -> Option<P>
152    where
153        P: PointNorm + PointDot,
154    {
155        match self {
156            BezierSegment::Linear(segment) => segment.normal(t),
157            BezierSegment::Quadratic(segment) => segment.normal(t),
158            BezierSegment::Cubic(segment) => segment.normal(t),
159        }
160    }
161
162    /// Approximate parameter `t` at arc length `s`.
163    pub fn t_at_length_approx(&self, s: P::Scalar, nsteps: usize) -> P::Scalar
164    where
165        P: PointNorm,
166    {
167        match self {
168            BezierSegment::Linear(segment) => segment.t_at_length_approx(s, nsteps),
169            BezierSegment::Quadratic(segment) => segment.t_at_length_approx(s, nsteps),
170            BezierSegment::Cubic(segment) => segment.t_at_length_approx(s, nsteps),
171        }
172    }
173
174    /// Approximate parameter `t` at arc length `s` using a default resolution.
175    pub fn t_at_length(&self, s: P::Scalar) -> P::Scalar
176    where
177        P: PointNorm,
178    {
179        self.t_at_length_approx(s, DEFAULT_LENGTH_STEPS)
180    }
181
182    /// Evaluate the point at arc length `s`.
183    pub fn point_at_length_approx(&self, s: P::Scalar, nsteps: usize) -> P
184    where
185        P: PointNorm,
186    {
187        self.eval(self.t_at_length_approx(s, nsteps))
188    }
189
190    /// Evaluate the point at arc length `s` using a default resolution.
191    pub fn point_at_length(&self, s: P::Scalar) -> P
192    where
193        P: PointNorm,
194    {
195        self.point_at_length_approx(s, DEFAULT_LENGTH_STEPS)
196    }
197}
198
199impl<P> From<LineSegment<P>> for BezierSegment<P>
200where
201    P: Point,
202{
203    fn from(s: LineSegment<P>) -> Self {
204        BezierSegment::Linear(s)
205    }
206}
207
208impl<P> From<QuadraticBezier<P>> for BezierSegment<P>
209where
210    P: Point,
211{
212    fn from(s: QuadraticBezier<P>) -> Self {
213        BezierSegment::Quadratic(s)
214    }
215}
216
217impl<P> From<CubicBezier<P>> for BezierSegment<P>
218where
219    P: Point,
220{
221    fn from(s: CubicBezier<P>) -> Self {
222        BezierSegment::Cubic(s)
223    }
224}
225
226impl<P> Default for BezierSegment<P>
227where
228    P: Point + Default,
229{
230    fn default() -> Self {
231        BezierSegment::Linear(LineSegment::new(P::default(), P::default()))
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238    use crate::{EPSILON, PointN, PointNorm};
239
240    #[test]
241    fn bezier_segment_api_parity() {
242        let line = LineSegment::new(PointN::new([0f64, 0f64]), PointN::new([2f64, 0f64]));
243        let segment = BezierSegment::from(line);
244
245        assert_eq!(segment.start(), segment.eval(0.0));
246        assert_eq!(segment.end(), segment.eval(1.0));
247
248        let reversed = segment.reverse();
249        let p0 = segment.eval(0.25);
250        let p1 = reversed.eval(0.75);
251        assert!((p0 - p1).squared_norm() < EPSILON);
252
253        let tangent = segment.tangent(0.3);
254        assert!((tangent[0] - 1.0).abs() < EPSILON);
255        assert!(tangent[1].abs() < EPSILON);
256
257        let curvature = segment.curvature(0.3);
258        assert!(curvature.abs() < EPSILON);
259        assert!(segment.normal(0.3).is_none());
260
261        let t = segment.t_at_length(1.0);
262        assert!((t - 0.5).abs() < EPSILON);
263
264        let p = segment.point_at_length(1.0);
265        assert!((p - PointN::new([1.0, 0.0])).squared_norm() < EPSILON);
266    }
267}