geop_core_geometry/nurb_curve/
offset.rs1use geop_core_math::{
14 geop_error::{GeopError, GeopResult},
15 scalars::Scalar,
16 vector::Vector2,
17};
18
19use super::NurbCurve2D;
20
21const MISS_SPLITS: usize = 12;
24
25fn left(v: Vector2) -> Vector2 {
27 Vector2::from_array([v[1].neg(), v[0]])
28}
29
30impl NurbCurve2D {
31 pub fn offset_miss(
40 &self,
41 distance: Scalar,
42 (t0, t1): (Scalar, Scalar),
43 fit: &NurbCurve2D,
44 tolerance: Scalar,
45 ) -> GeopResult<Scalar> {
46 let ctx = |e: GeopError| {
47 e.with_context(format!(
48 "NurbCurve2D::offset_miss(distance={distance:?}, t=({t0:?}, {t1:?}))"
49 ))
50 };
51 let (s0, s1) = fit.domain();
52 let rate = t1.sub(t0).div(s1.sub(s0)).map_err(ctx)?;
53 let along = |s: Scalar| t0.add(s.sub(s0).mul(rate));
54 let offset_at = |t: Scalar| -> GeopResult<Vector2> {
55 let normal = left(self.tangent(t)?).normalize()?;
56 Ok(self.evaluate(t)?.add(&normal.prod_scalar(distance)))
57 };
58 let miss_over = |a: Scalar, b: Scalar| -> GeopResult<(Scalar, bool)> {
62 let middle = a.add(b).div(Scalar::TWO)?.sharpen();
63 let here = fit.evaluate(middle)?.sub(&offset_at(along(middle))?);
64 let s = a.union(b);
65 let t = along(s);
66 let (first, second) = (self.tangent(t)?, self.second_derivative(t)?);
67 let speed_sq = first.prod_dot(&first);
68 let speed = speed_sq.sqrt()?;
69 let turning = left(second)
70 .prod_scalar(speed_sq)
71 .sub(&left(first).prod_scalar(first.prod_dot(&second)))
72 .prod_scalar(Scalar::ONE.div(speed_sq.mul(speed))?);
73 let slope = fit
74 .tangent(s)?
75 .sub(&first.add(&turning.prod_scalar(distance)).prod_scalar(rate));
76 let half = b.sub(a).div(Scalar::TWO)?;
77 let reach = half.neg().union(half);
78 let e = here.add(&slope.prod_scalar(reach));
79 let (x, y) = (e[0].abs(), e[1].abs());
80 let off = here.prod_dot(&here).sqrt()?.definitely_greater(tolerance);
81 Ok((x.mul(x).add(y.mul(y)).sqrt()?.upper(), off))
82 };
83 let breaks = fit.breakpoints();
84 let mut stack: Vec<(Scalar, Scalar, usize)> =
85 breaks.windows(2).map(|w| (w[0], w[1], 0)).collect();
86 let mut worst = Scalar::ZERO;
87 while let Some((a, b, depth)) = stack.pop() {
88 let miss = match miss_over(a, b) {
91 Err(_) if depth < MISS_SPLITS => None,
92 miss => Some(miss.map_err(ctx)?),
93 };
94 if miss.is_none_or(|(m, off)| m.definitely_greater(tolerance) && !off)
95 && depth < MISS_SPLITS
96 {
97 let middle = a.add(b).div(Scalar::TWO).map_err(ctx)?.sharpen();
98 stack.push((a, middle, depth + 1));
99 stack.push((middle, b, depth + 1));
100 continue;
101 }
102 worst = worst.max(miss.expect("decided at the last split").0);
103 }
104 Ok(worst)
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use crate::test_fixtures::knots;
112
113 fn h(x: f64, y: f64, w: f64) -> geop_core_math::vector::Vector3 {
114 geop_core_math::vector::Vector3::from_array([x * w, y * w, w].map(Scalar::from_f64))
115 }
116
117 #[test]
121 fn an_arc_offset_is_missed_by_no_more_than_it_is_off() {
122 let r2 = std::f64::consts::FRAC_1_SQRT_2;
123 let arc = |r: f64| {
124 NurbCurve2D::try_new(
125 2,
126 vec![h(r, 0., 1.), h(r, r, r2), h(0., r, 1.)],
127 knots(&[0., 0., 0., 1., 1., 1.]),
128 )
129 .unwrap()
130 };
131 let domain = (Scalar::ZERO, Scalar::ONE);
132 let tolerance = Scalar::from_f64(1e-6);
133 let exact = arc(2.).offset_miss(Scalar::from_f64(0.5), domain, &arc(1.5), tolerance);
134 let exact = exact.unwrap().to_f64();
135 assert!(exact < 1e-6, "{exact}");
136 let off = arc(2.)
137 .offset_miss(Scalar::from_f64(0.6), domain, &arc(1.5), tolerance)
138 .unwrap()
139 .to_f64();
140 assert!(off >= 0.1, "{off}");
141 }
142}