Skip to main content

kurbo/
mindist.rs

1// Copyright 2021 the Kurbo Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Minimum distance between two Bézier curves
5//!
6//! This implements the algorithm in "Computing the minimum distance between
7//! two Bézier curves", Chen et al., *Journal of Computational and Applied
8//! Mathematics* 229(2009), 294-301
9
10use crate::Vec2;
11use core::cmp::Ordering;
12
13#[cfg(not(feature = "std"))]
14use crate::common::FloatFuncs;
15
16/// Given the control points of n- and m-dimensional Bézier curves in `bez1` and `bez2`
17/// respectively, calculate the minimum distance between the two curves.
18///
19/// This should work for arbitrary Bézier curves up to and including degree 255, but only lines,
20/// quadratics and cubics are tested.
21///
22/// Returns a tuple of the distance, the path time `t1` of the closest point on the first Bézier,
23/// and the path time `t2` of the closest point on the second Bézier.
24///
25/// # Panics
26///
27/// This panics if more than 256 control points are given in `bez1` or `bez2`, i.e., if either
28/// Bézier is of degree greater than 255.
29pub(crate) fn min_dist_param(
30    bez1: &[Vec2],
31    bez2: &[Vec2],
32    u: (f64, f64),
33    v: (f64, f64),
34    epsilon: f64,
35    best_alpha: Option<f64>,
36) -> (f64, f64, f64) {
37    assert!(!bez1.is_empty() && !bez2.is_empty());
38    let n = u8::try_from(bez1.len() - 1).expect("At most 256 control points");
39    let m = u8::try_from(bez2.len() - 1).expect("At most 256 control points");
40    let (umin, umax) = u;
41    let (vmin, vmax) = v;
42    let umid = (umin + umax) / 2.0;
43    let vmid = (vmin + vmax) / 2.0;
44    let svalues: [(f64, f64, f64); 4] = [
45        (S(umin, vmin, bez1, bez2), umin, vmin),
46        (S(umin, vmax, bez1, bez2), umin, vmax),
47        (S(umax, vmin, bez1, bez2), umax, vmin),
48        (S(umax, vmax, bez1, bez2), umax, vmax),
49    ];
50    let alpha: f64 = svalues.iter().map(|(a, _, _)| *a).reduce(f64::min).unwrap();
51    if let Some(best) = best_alpha {
52        if alpha > best {
53            return (alpha, umid, vmid);
54        }
55    }
56
57    if (umax - umin).abs() < epsilon || (vmax - vmin).abs() < epsilon {
58        return (alpha, umid, vmid);
59    }
60
61    // Property one: D(r>k) > alpha
62    let mut is_outside = true;
63    let mut min_drk = None;
64    let mut min_ij = None;
65    for r in 0..(2 * n) {
66        for k in 0..(2 * m) {
67            let d_rk = D_rk(r, k, bez1, bez2);
68            if d_rk < alpha {
69                is_outside = false;
70            }
71            if min_drk.is_none() || Some(d_rk) < min_drk {
72                min_drk = Some(d_rk);
73                min_ij = Some((r, k));
74            }
75        }
76    }
77    if is_outside {
78        return (alpha, umid, vmid);
79    }
80
81    // Property two: boundary check
82    let mut at_boundary0on_bez1 = true;
83    let mut at_boundary1on_bez1 = true;
84    let mut at_boundary0on_bez2 = true;
85    let mut at_boundary1on_bez2 = true;
86    for i in 0..2 * n {
87        for j in 0..2 * m {
88            let dij = D_rk(i, j, bez1, bez2);
89            let dkj = D_rk(0, j, bez1, bez2);
90            if dij < dkj {
91                at_boundary0on_bez1 = false;
92            }
93            let dkj = D_rk(2 * n, j, bez1, bez2);
94            if dij < dkj {
95                at_boundary1on_bez1 = false;
96            }
97            let dkj = D_rk(i, 0, bez1, bez2);
98            if dij < dkj {
99                at_boundary0on_bez2 = false;
100            }
101            let dkj = D_rk(i, 2 * m, bez1, bez2);
102            if dij < dkj {
103                at_boundary1on_bez2 = false;
104            }
105        }
106    }
107    if at_boundary0on_bez1 && at_boundary0on_bez2 {
108        return svalues[0];
109    }
110    if at_boundary0on_bez1 && at_boundary1on_bez2 {
111        return svalues[1];
112    }
113    if at_boundary1on_bez1 && at_boundary0on_bez2 {
114        return svalues[2];
115    }
116    if at_boundary1on_bez1 && at_boundary1on_bez2 {
117        return svalues[3];
118    }
119
120    let (min_i, min_j) = min_ij.unwrap();
121    let new_umid = umin + (umax - umin) * (min_i as f64 / (2 * n) as f64);
122    let new_vmid = vmin + (vmax - vmin) * (min_j as f64 / (2 * m) as f64);
123
124    // Subdivide
125    let results = [
126        min_dist_param(
127            bez1,
128            bez2,
129            (umin, new_umid),
130            (vmin, new_vmid),
131            epsilon,
132            Some(alpha),
133        ),
134        min_dist_param(
135            bez1,
136            bez2,
137            (umin, new_umid),
138            (new_vmid, vmax),
139            epsilon,
140            Some(alpha),
141        ),
142        min_dist_param(
143            bez1,
144            bez2,
145            (new_umid, umax),
146            (vmin, new_vmid),
147            epsilon,
148            Some(alpha),
149        ),
150        min_dist_param(
151            bez1,
152            bez2,
153            (new_umid, umax),
154            (new_vmid, vmax),
155            epsilon,
156            Some(alpha),
157        ),
158    ];
159
160    *results
161        .iter()
162        .min_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Less))
163        .unwrap()
164}
165
166// $ S(u,v)=\sum_{r=0}^{2n} \sum _{k=0}^{2m} D_{r,k} B_{2n}^r(u) B_{2m}^k(v) $
167
168#[allow(non_snake_case)]
169fn S(u: f64, v: f64, bez1: &[Vec2], bez2: &[Vec2]) -> f64 {
170    #[expect(
171        clippy::cast_possible_truncation,
172        reason = "By the checks in `min_dist_param` we know `bez1.len()` and `bez2.len()` are less than or equal to 256"
173    )]
174    let (n, m) = { ((bez1.len() - 1) as u8, (bez2.len() - 1) as u8) };
175    let mut summand = 0.0;
176    for r in 0..=2 * n {
177        for k in 0..=2 * m {
178            summand +=
179                D_rk(r, k, bez1, bez2) * basis_function(2 * n, r, u) * basis_function(2 * m, k, v);
180        }
181    }
182    summand
183}
184
185// $ C_{r,k} = ( \sum_{i=\theta}^\upsilon P_i C_n^i C_n^{r-i} / C_{2n}^r ) \cdot (\sum_{j=\sigma}^\varsigma Q_j C_m^j C_m^{k-j} / C_{2m}^k ) $
186#[allow(non_snake_case)]
187fn C_rk(r: u8, k: u8, bez1: &[Vec2], bez2: &[Vec2]) -> f64 {
188    #[expect(
189        clippy::cast_possible_truncation,
190        reason = "By the checks in `min_dist_param` we know `bez1.len()` and `bez2.len()` are less than or equal to 256"
191    )]
192    let (n, m) = { ((bez1.len() - 1) as u8, (bez2.len() - 1) as u8) };
193    let upsilon = r.min(n);
194    let theta = r - n.min(r);
195    let mut left: Vec2 = Vec2::ZERO;
196    for i in theta..upsilon + 1 {
197        let item = bez1[usize::from(i)];
198        left += item * (choose(n, i) * choose(n, r - i)) as f64 / (choose(2 * n, r) as f64);
199    }
200
201    let varsigma = k.min(m);
202    let sigma = k - m.min(k);
203    let mut right: Vec2 = Vec2::ZERO;
204    for j in sigma..varsigma + 1 {
205        let item = bez2[usize::from(j)];
206        right += item * (choose(m, j) * choose(m, k - j)) as f64 / (choose(2 * m, k) as f64);
207    }
208    left.dot(right)
209}
210
211// $ A_r=\sum _{i=\theta} ^\upsilon (P_i \cdot P_{r-i}) C_n^i C_n^{r-i} / C_{2n}^r $
212// ($ B_k $ is just the same as $ A_r $ but for the other curve.)
213
214#[allow(non_snake_case)]
215fn A_r(r: u8, p: &[Vec2]) -> f64 {
216    #[expect(
217        clippy::cast_possible_truncation,
218        reason = "By the checks in `min_dist_param` we know `p.len()` is less than or equal to 256"
219    )]
220    let n = (p.len() - 1) as u8;
221    let upsilon = r.min(n);
222    let theta = r - n.min(r);
223    (theta..=upsilon)
224        .map(|i| {
225            let dot = p[usize::from(i)].dot(p[usize::from(r - i)]); // These are bounds checked by the sum limits
226            let factor = (choose(n, i) * choose(n, r - i)) as f64 / (choose(2 * n, r) as f64);
227            dot * factor
228        })
229        .sum()
230}
231
232#[allow(non_snake_case)]
233fn D_rk(r: u8, k: u8, bez1: &[Vec2], bez2: &[Vec2]) -> f64 {
234    // In the paper, B_k is used for the second factor, but it's the same thing
235    A_r(r, bez1) + A_r(k, bez2) - 2.0 * C_rk(r, k, bez1, bez2)
236}
237
238// Bézier basis function
239fn basis_function(n: u8, i: u8, u: f64) -> f64 {
240    choose(n, i) as f64 * (1.0 - u).powi((n - i) as i32) * u.powi(i as i32)
241}
242
243// Binomial co-efficient, but returning zeros for values outside of domain
244fn choose(n: u8, k: u8) -> u32 {
245    let mut n = n as u32;
246    let k = k as u32;
247    if k > n {
248        return 0;
249    }
250    let mut p = 1;
251    for i in 1..=(n - k) {
252        p *= n;
253        p /= i;
254        n -= 1;
255    }
256    p
257}
258
259#[cfg(test)]
260mod tests {
261    use crate::mindist::A_r;
262    use crate::mindist::{D_rk, choose};
263    use crate::param_curve::ParamCurve;
264    use crate::{CubicBez, Line, PathSeg, Vec2};
265
266    #[test]
267    fn test_choose() {
268        assert_eq!(choose(6, 0), 1);
269        assert_eq!(choose(6, 1), 6);
270        assert_eq!(choose(6, 2), 15);
271    }
272
273    #[test]
274    fn test_d_rk() {
275        let bez1 = vec![
276            Vec2::new(129.0, 139.0),
277            Vec2::new(190.0, 139.0),
278            Vec2::new(201.0, 364.0),
279            Vec2::new(90.0, 364.0),
280        ];
281        let bez2 = vec![
282            Vec2::new(309.0, 159.0),
283            Vec2::new(178.0, 159.0),
284            Vec2::new(215.0, 408.0),
285            Vec2::new(309.0, 408.0),
286        ];
287        let b = A_r(1, &bez2);
288        assert!((b - 80283.0).abs() < 0.005, "B_1(Q)={b:?}");
289        let d = D_rk(0, 1, &bez1, &bez2);
290        assert!((d - 9220.0).abs() < 0.005, "D={d:?}");
291    }
292
293    #[test]
294    fn test_mindist() {
295        let bez1 = PathSeg::Cubic(CubicBez::new(
296            (129.0, 139.0),
297            (190.0, 139.0),
298            (201.0, 364.0),
299            (90.0, 364.0),
300        ));
301        let bez2 = PathSeg::Cubic(CubicBez::new(
302            (309.0, 159.0),
303            (178.0, 159.0),
304            (215.0, 408.0),
305            (309.0, 408.0),
306        ));
307        let mindist = bez1.min_dist(bez2, 0.001);
308        assert!((mindist.distance - 50.9966).abs() < 0.5);
309    }
310
311    #[test]
312    fn test_overflow() {
313        let bez1 = PathSeg::Cubic(CubicBez::new(
314            (232.0, 126.0),
315            (134.0, 126.0),
316            (139.0, 232.0),
317            (141.0, 301.0),
318        ));
319        let bez2 = PathSeg::Line(Line::new((359.0, 416.0), (367.0, 755.0)));
320        let mindist = bez1.min_dist(bez2, 0.001);
321        assert!((mindist.distance - 246.4731222669117).abs() < 0.5);
322    }
323
324    #[test]
325    fn test_out_of_order() {
326        let bez1 = PathSeg::Cubic(CubicBez::new(
327            (287.0, 182.0),
328            (346.0, 277.0),
329            (356.0, 299.0),
330            (359.0, 416.0),
331        ));
332        let bez2 = PathSeg::Line(Line::new((141.0, 301.0), (152.0, 709.0)));
333        let mindist1 = bez1.min_dist(bez2, 0.5);
334        let mindist2 = bez2.min_dist(bez1, 0.5);
335        assert!((mindist1.distance - mindist2.distance).abs() < 0.5);
336    }
337
338    #[test]
339    fn test_line_curve() {
340        let line = PathSeg::Line(Line::new((929.0, 335.0), (911.0, 340.0)));
341
342        let line_as_bez = PathSeg::Cubic(CubicBez::new(
343            line.eval(0.0),
344            line.eval(1.0 / 3.0),
345            line.eval(2.0 / 3.0),
346            line.eval(1.0),
347        ));
348
349        let bez2 = PathSeg::Cubic(CubicBez::new(
350            (1052.0, 401.0),
351            (1048.0, 305.0),
352            (1046.0, 216.0),
353            (1054.0, 146.0),
354        ));
355        let mindist_as_bez = line_as_bez.min_dist(bez2, 0.5);
356        let mindist_as_line = line.min_dist(bez2, 0.5);
357        assert!((mindist_as_line.distance - mindist_as_bez.distance).abs() < 0.5);
358    }
359}