Skip to content

Commit f37816a

Browse files
tomcurjneem
andauthored
Change Line::nearest implementation to be branchless (#505)
This is a branchless calculation of a point-onto-line-segment projection. Also marks the method `#[inline]` and adds some testing. I started looking at this because of the math we introduced in linebender/vello#1214. This might allow us to introduce some tighter bounds there without introducing additional branching. This should be useful here regardless of whether the above happens. --------- Co-authored-by: jneem <[email protected]>
1 parent 7f02be8 commit f37816a

File tree

2 files changed

+57
-11
lines changed

2 files changed

+57
-11
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ This release has an [MSRV][] of 1.82.
2222
## Changed
2323

2424
- Speed up methods like `Ellipse::radii` by reworking the singular value decomposition expression. ([#499][] by [@tomcur][])
25+
- The `Line::nearest` method to calculate the projection of a point onto a line segment has been made more performant. ([#505][] by [@tomcur][])
2526

2627
## [0.12.0] (2025-09-04)
2728

kurbo/src/line.rs

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -158,19 +158,29 @@ impl ParamCurveArea for Line {
158158
}
159159

160160
impl ParamCurveNearest for Line {
161+
#[inline]
161162
fn nearest(&self, p: Point, _accuracy: f64) -> Nearest {
162163
let d = self.p1 - self.p0;
163-
let dotp = d.dot(p - self.p0);
164-
let d_squared = d.dot(d);
165-
let (t, distance_sq) = if dotp <= 0.0 {
166-
(0.0, (p - self.p0).hypot2())
167-
} else if dotp >= d_squared {
168-
(1.0, (p - self.p1).hypot2())
169-
} else {
170-
let t = dotp / d_squared;
171-
let dist = (p - self.eval(t)).hypot2();
172-
(t, dist)
173-
};
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+
174184
Nearest { distance_sq, t }
175185
}
176186
}
@@ -394,4 +404,39 @@ mod tests {
394404
})
395405
.is_finite());
396406
}
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+
}
397442
}

0 commit comments

Comments
 (0)