kurbo/
line.rs

1// Copyright 2018 the Kurbo Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Lines.
5
6use core::ops::{Add, Mul, Range, Sub};
7
8use arrayvec::ArrayVec;
9
10use crate::{
11    Affine, Nearest, ParamCurve, ParamCurveArclen, ParamCurveArea, ParamCurveCurvature,
12    ParamCurveDeriv, ParamCurveExtrema, ParamCurveNearest, PathEl, Point, Rect, Shape, Vec2,
13    DEFAULT_ACCURACY, MAX_EXTREMA,
14};
15
16/// A single line.
17#[derive(Clone, Copy, Debug, PartialEq)]
18#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
19#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
20pub struct Line {
21    /// The line's start point.
22    pub p0: Point,
23    /// The line's end point.
24    pub p1: Point,
25}
26
27impl Line {
28    /// Create a new line.
29    #[inline(always)]
30    pub fn new(p0: impl Into<Point>, p1: impl Into<Point>) -> Line {
31        Line {
32            p0: p0.into(),
33            p1: p1.into(),
34        }
35    }
36
37    /// Returns a copy of this `Line` with the end points swapped so that it
38    /// points in the opposite direction.
39    #[must_use]
40    #[inline(always)]
41    pub fn reversed(&self) -> Line {
42        Self {
43            p0: self.p1,
44            p1: self.p0,
45        }
46    }
47
48    /// The length of the line.
49    #[inline]
50    pub fn length(self) -> f64 {
51        self.arclen(DEFAULT_ACCURACY)
52    }
53
54    /// The midpoint of the line.
55    ///
56    /// This is the same as calling [`Point::midpoint`] with
57    /// the endpoints of this line.
58    #[must_use]
59    #[inline]
60    pub fn midpoint(&self) -> Point {
61        self.p0.midpoint(self.p1)
62    }
63
64    /// Computes the point where two lines, if extended to infinity, would cross.
65    pub fn crossing_point(self, other: Line) -> Option<Point> {
66        let ab = self.p1 - self.p0;
67        let cd = other.p1 - other.p0;
68        let pcd = ab.cross(cd);
69        if pcd == 0.0 {
70            return None;
71        }
72        let h = ab.cross(self.p0 - other.p0) / pcd;
73        Some(other.p0 + cd * h)
74    }
75
76    /// Is this line `finite`?
77    ///
78    /// [finite]: f64::is_finite
79    #[inline]
80    pub const fn is_finite(self) -> bool {
81        self.p0.is_finite() && self.p1.is_finite()
82    }
83
84    /// Is this line `NaN`?
85    ///
86    /// [NaN]: f64::is_nan
87    #[inline]
88    pub const fn is_nan(self) -> bool {
89        self.p0.is_nan() || self.p1.is_nan()
90    }
91}
92
93impl From<(Point, Point)> for Line {
94    #[inline(always)]
95    fn from((from, to): (Point, Point)) -> Self {
96        Line::new(from, to)
97    }
98}
99
100impl From<(Point, Vec2)> for Line {
101    #[inline(always)]
102    fn from((origin, displacement): (Point, Vec2)) -> Self {
103        Line::new(origin, origin + displacement)
104    }
105}
106
107impl ParamCurve for Line {
108    #[inline]
109    fn eval(&self, t: f64) -> Point {
110        self.p0.lerp(self.p1, t)
111    }
112
113    #[inline]
114    fn subsegment(&self, range: Range<f64>) -> Line {
115        Line {
116            p0: self.eval(range.start),
117            p1: self.eval(range.end),
118        }
119    }
120
121    #[inline(always)]
122    fn start(&self) -> Point {
123        self.p0
124    }
125
126    #[inline(always)]
127    fn end(&self) -> Point {
128        self.p1
129    }
130}
131
132impl ParamCurveDeriv for Line {
133    type DerivResult = ConstPoint;
134
135    #[inline]
136    fn deriv(&self) -> ConstPoint {
137        ConstPoint((self.p1 - self.p0).to_point())
138    }
139}
140
141impl ParamCurveArclen for Line {
142    #[inline]
143    fn arclen(&self, _accuracy: f64) -> f64 {
144        (self.p1 - self.p0).hypot()
145    }
146
147    #[inline]
148    fn inv_arclen(&self, arclen: f64, _accuracy: f64) -> f64 {
149        arclen / (self.p1 - self.p0).hypot()
150    }
151}
152
153impl ParamCurveArea for Line {
154    #[inline]
155    fn signed_area(&self) -> f64 {
156        self.p0.to_vec2().cross(self.p1.to_vec2()) * 0.5
157    }
158}
159
160impl ParamCurveNearest for Line {
161    #[inline]
162    fn nearest(&self, p: Point, _accuracy: f64) -> Nearest {
163        let d = self.p1 - self.p0;
164        let v = p - self.p0;
165
166        // Calculate projection parameter `t` of the point onto the line segment s(t), with
167        // s(t) = (1-t) * p0 + t * p1.
168        //
169        // Note when the segment has 0 length, this will be positive or negative infinity or NaN;
170        // see the clamping below.
171        let t = d.dot(v) / d.hypot2();
172
173        // Clamp the parameter to be on the line segment. This clamps negative infinity and NaN to
174        // `0.`, and positive infinity to `1.`.
175        #[expect(
176            clippy::manual_clamp,
177            reason = "`clamp` uses slightly more instructions than chained `max` and `min` on x86 and aarch64"
178        )]
179        let t = { t.max(0.).min(1.) };
180
181        // Calculate ||p - s(t)||^2.
182        let distance_sq = (v - t * d).hypot2();
183
184        Nearest { distance_sq, t }
185    }
186}
187
188impl ParamCurveCurvature for Line {
189    #[inline(always)]
190    fn curvature(&self, _t: f64) -> f64 {
191        0.0
192    }
193}
194
195impl ParamCurveExtrema for Line {
196    #[inline]
197    fn extrema(&self) -> ArrayVec<f64, MAX_EXTREMA> {
198        ArrayVec::new()
199    }
200}
201
202/// A trivial "curve" that is just a constant.
203#[derive(Clone, Copy, Debug)]
204#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
205#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
206pub struct ConstPoint(Point);
207
208impl ConstPoint {
209    /// Is this point [finite]?
210    ///
211    /// [finite]: f64::is_finite
212    #[inline]
213    pub const fn is_finite(self) -> bool {
214        self.0.is_finite()
215    }
216
217    /// Is this point [NaN]?
218    ///
219    /// [NaN]: f64::is_nan
220    #[inline]
221    pub const fn is_nan(self) -> bool {
222        self.0.is_nan()
223    }
224}
225
226impl ParamCurve for ConstPoint {
227    #[inline(always)]
228    fn eval(&self, _t: f64) -> Point {
229        self.0
230    }
231
232    #[inline(always)]
233    fn subsegment(&self, _range: Range<f64>) -> ConstPoint {
234        *self
235    }
236}
237
238impl ParamCurveDeriv for ConstPoint {
239    type DerivResult = ConstPoint;
240
241    #[inline(always)]
242    fn deriv(&self) -> ConstPoint {
243        ConstPoint(Point::new(0.0, 0.0))
244    }
245}
246
247impl ParamCurveArclen for ConstPoint {
248    #[inline(always)]
249    fn arclen(&self, _accuracy: f64) -> f64 {
250        0.0
251    }
252
253    #[inline(always)]
254    fn inv_arclen(&self, _arclen: f64, _accuracy: f64) -> f64 {
255        0.0
256    }
257}
258
259impl Mul<Line> for Affine {
260    type Output = Line;
261
262    #[inline]
263    fn mul(self, other: Line) -> Line {
264        Line {
265            p0: self * other.p0,
266            p1: self * other.p1,
267        }
268    }
269}
270
271impl Add<Vec2> for Line {
272    type Output = Line;
273
274    #[inline]
275    fn add(self, v: Vec2) -> Line {
276        Line::new(self.p0 + v, self.p1 + v)
277    }
278}
279
280impl Sub<Vec2> for Line {
281    type Output = Line;
282
283    #[inline]
284    fn sub(self, v: Vec2) -> Line {
285        Line::new(self.p0 - v, self.p1 - v)
286    }
287}
288
289/// An iterator yielding the path for a single line.
290#[doc(hidden)]
291pub struct LinePathIter {
292    line: Line,
293    ix: usize,
294}
295
296impl Shape for Line {
297    type PathElementsIter<'iter> = LinePathIter;
298
299    #[inline]
300    fn path_elements(&self, _tolerance: f64) -> LinePathIter {
301        LinePathIter { line: *self, ix: 0 }
302    }
303
304    /// Returning zero here is consistent with the contract (area is
305    /// only meaningful for closed shapes), but an argument can be made
306    /// that the contract should be tightened to include the Green's
307    /// theorem contribution.
308    #[inline(always)]
309    fn area(&self) -> f64 {
310        0.0
311    }
312
313    #[inline]
314    fn perimeter(&self, _accuracy: f64) -> f64 {
315        (self.p1 - self.p0).hypot()
316    }
317
318    /// Same consideration as `area`.
319    #[inline(always)]
320    fn winding(&self, _pt: Point) -> i32 {
321        0
322    }
323
324    #[inline(always)]
325    fn bounding_box(&self) -> Rect {
326        Rect::from_points(self.p0, self.p1)
327    }
328
329    #[inline(always)]
330    fn as_line(&self) -> Option<Line> {
331        Some(*self)
332    }
333}
334
335impl Iterator for LinePathIter {
336    type Item = PathEl;
337
338    fn next(&mut self) -> Option<PathEl> {
339        self.ix += 1;
340        match self.ix {
341            1 => Some(PathEl::MoveTo(self.line.p0)),
342            2 => Some(PathEl::LineTo(self.line.p1)),
343            _ => None,
344        }
345    }
346}
347
348#[cfg(test)]
349mod tests {
350    use crate::{Line, ParamCurveArclen, Point};
351
352    #[test]
353    fn line_reversed() {
354        let l = Line::new((0.0, 0.0), (1.0, 1.0));
355        let f = l.reversed();
356
357        assert_eq!(l.p0, f.p1);
358        assert_eq!(l.p1, f.p0);
359
360        // Reversing it again should result in the original line
361        assert_eq!(l, f.reversed());
362    }
363
364    #[test]
365    fn line_arclen() {
366        let l = Line::new((0.0, 0.0), (1.0, 1.0));
367        let true_len = 2.0f64.sqrt();
368        let epsilon = 1e-9;
369        assert!(l.arclen(epsilon) - true_len < epsilon);
370
371        let t = l.inv_arclen(true_len / 3.0, epsilon);
372        assert!((t - 1.0 / 3.0).abs() < epsilon);
373    }
374
375    #[test]
376    fn line_midpoint() {
377        let l = Line::new((0.0, 0.0), (2.0, 4.0));
378        assert_eq!(l.midpoint(), Point::new(1.0, 2.0));
379    }
380
381    #[test]
382    fn line_is_finite() {
383        assert!((Line {
384            p0: Point { x: 0., y: 0. },
385            p1: Point { x: 1., y: 1. }
386        })
387        .is_finite());
388
389        assert!(!(Line {
390            p0: Point { x: 0., y: 0. },
391            p1: Point {
392                x: f64::INFINITY,
393                y: 1.
394            }
395        })
396        .is_finite());
397
398        assert!(!(Line {
399            p0: Point { x: 0., y: 0. },
400            p1: Point {
401                x: 0.,
402                y: f64::INFINITY
403            }
404        })
405        .is_finite());
406    }
407
408    #[test]
409    fn line_nearest() {
410        use crate::{ParamCurve, ParamCurveNearest};
411
412        const EPSILON: f64 = 1e-9;
413
414        let line = Line::new((-4., 0.), (2., 1.));
415
416        // Projects onto the line segment end point.
417        let point = Point::new(4., 0.);
418        let nearest = line.nearest(point, 0.);
419        assert_eq!(nearest.t, 1.);
420        assert!((nearest.distance_sq - line.p1.distance_squared(point)).abs() < EPSILON);
421
422        // Projects onto the line segment start point.
423        let point = Point::new(0., -50.);
424        let nearest = line.nearest(point, 0.);
425        assert_eq!(nearest.t, 0.);
426        assert!((nearest.distance_sq - line.p0.distance_squared(point)).abs() < EPSILON);
427
428        // Projects onto the line segment proper (not just onto one of its extrema).
429        let point = Point::new(-1., 0.5);
430        let nearest = line.nearest(point, 0.);
431        assert!(nearest.t > 0. && nearest.t < 1.);
432        // Ensure evaluating and calculating distance manually has the same result.
433        assert!(
434            (line.eval(nearest.t).distance_squared(point) - nearest.distance_sq).abs() < EPSILON
435        );
436
437        // Test minimality while avoiding reimplementing projection in this test by checking that
438        // moving to a slightly different point on the segment increases the distance.
439        assert!(line.eval(nearest.t * 0.95).distance_squared(point) > nearest.distance_sq);
440        assert!(line.eval(nearest.t * 1.05).distance_squared(point) > nearest.distance_sq);
441    }
442}