1use axiolid_contracts::{GeomError, GeomResult};
35use axiolid_core::{Frame2, Frame3, Interval, Point2, Point3, Scalar, Tolerance, Vec2, Vec3};
36use axiolid_curve::{
37 BSplineCurve, BSplineCurve2, BSplineCurve3, Circle2, Circle3, Curve2, Curve3, CurveEvaluator,
38 Ellipse2, Ellipse3, Line2, Line3, Polyline2, Polyline3,
39};
40
41use crate::nurbs::SplineAxis;
42
43#[derive(Debug, Clone, Copy, Default)]
48pub struct ScalarCurve;
49
50impl ScalarCurve {
51 #[must_use]
53 pub const fn new() -> Self {
54 Self
55 }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq)]
64pub struct CurveJet<P, D> {
65 pub point: P,
67 pub first: D,
69 pub second: D,
71}
72
73#[must_use]
77pub fn domain2(curve: &Curve2) -> Interval {
78 match curve {
79 Curve2::Line(_) => Interval::UNIT,
82 Curve2::Circle(_) | Curve2::Ellipse(_) => full_turn(),
83 Curve2::Polyline(p) => polyline_domain(p.points.len(), p.closed),
84 Curve2::BSpline(b) => spline_domain(b),
85 Curve2::Intrinsic(i) if i.length.is_finite() && i.length > 0.0 => Interval {
89 start: 0.0,
90 end: i.length,
91 },
92 Curve2::Sinusoid(_) => full_turn(),
94 Curve2::Chain(c) => c.length().map_or(
96 Interval {
97 start: 0.0,
98 end: 0.0,
99 },
100 |end| Interval { start: 0.0, end },
101 ),
102 _ => Interval {
104 start: 0.0,
105 end: 0.0,
106 },
107 }
108}
109
110#[must_use]
112pub fn domain3(curve: &Curve3) -> Interval {
113 match curve {
114 Curve3::Line(_) => Interval::UNIT,
115 Curve3::Circle(_) | Curve3::Ellipse(_) => full_turn(),
116 Curve3::Polyline(p) => polyline_domain(p.points.len(), p.closed),
117 Curve3::BSpline(b) => spline_domain(b),
118 Curve3::Intrinsic(i) if i.length.is_finite() && i.length > 0.0 => Interval {
122 start: 0.0,
123 end: i.length,
124 },
125 Curve3::Banked(b) if b.cant.is_well_formed() => Interval {
128 start: 0.0,
129 end: b.span(),
130 },
131 Curve3::Elevated(e) => crate::elevated::elevated_domain(e),
134 _ => Interval {
135 start: 0.0,
136 end: 0.0,
137 },
138 }
139}
140
141fn full_turn() -> Interval {
142 Interval {
143 start: 0.0,
144 end: core::f64::consts::TAU,
145 }
146}
147
148fn polyline_domain(count: usize, closed: bool) -> Interval {
150 let segments = if closed {
151 count
152 } else {
153 count.saturating_sub(1)
154 };
155 Interval {
156 start: 0.0,
157 end: segments as Scalar,
158 }
159}
160
161fn spline_domain<P>(b: &BSplineCurve<P>) -> Interval {
164 SplineAxis::new(
165 &b.knots,
166 &b.multiplicities,
167 b.degree,
168 b.control_points.len(),
169 "B-spline curve",
170 )
171 .map_or(
172 Interval {
173 start: 0.0,
174 end: 0.0,
175 },
176 |axis| {
177 let (start, end) = axis.domain();
178 Interval { start, end }
179 },
180 )
181}
182
183pub fn evaluate2(curve: &Curve2, t: Scalar) -> GeomResult<Point2> {
187 finite(t)?;
188 let value = match curve {
189 Curve2::Line(l) => Ok(line_point(l.origin, l.direction, t)),
190 Curve2::Circle(c) => Ok(conic_point2(&c.frame, c.radius, c.radius, t)),
191 Curve2::Ellipse(e) => Ok(conic_point2(&e.frame, e.semi_axis_x, e.semi_axis_y, t)),
192 Curve2::Polyline(p) => polyline_point(&p.points, p.closed, t),
193 Curve2::BSpline(b) => de_boor(b, t, |p| [p.x, p.y], |c| Point2::new(c[0], c[1])),
194 Curve2::Intrinsic(i) => crate::arc_length::intrinsic_point(i, t),
198 Curve2::Chain(c) => crate::chain::chain_point(c, t),
200 Curve2::Sinusoid(w) => Ok(Point2::new(t, w.height(t))),
202 Curve2::QuadraticGraph(g) => g
204 .height(t)
205 .map(|v| Point2::new(t, v))
206 .ok_or_else(|| outside_graph(t)),
207 Curve2::AngleGraph(g) => g
209 .angle(t)
210 .map(|u| Point2::new(u, t))
211 .ok_or_else(|| outside_graph(t)),
212 Curve2::Implicit(c) => c.point(t).ok_or_else(|| outside_graph(t)),
214 Curve2::Lifted(l) => {
216 if let Some((section, first)) = pair_side(l) {
218 let (a, b, _) = section.solve(t).ok_or_else(|| outside_graph(t))?;
219 return finite2(if first { a } else { b }, "curve point");
220 }
221 let p = evaluate3(&l.curve, t)?;
222 l.unwrap_at(t, p).ok_or_else(|| outside_graph(t))
223 }
224 _ => Err(GeomError::Unsupported {
227 backend: axiolid_contracts::BackendId::new("axiolid-reference"),
228 operation: axiolid_contracts::Operation::CurveEvaluation,
229 }),
230 }?;
231 finite2(value, "curve point")
232}
233
234pub fn derivative2(curve: &Curve2, t: Scalar) -> GeomResult<Vec2> {
236 finite(t)?;
237 let value = match curve {
238 Curve2::Line(l) => Ok(l.direction),
239 Curve2::Circle(c) => Ok(conic_tangent2(&c.frame, c.radius, c.radius, t)),
240 Curve2::Ellipse(e) => Ok(conic_tangent2(&e.frame, e.semi_axis_x, e.semi_axis_y, t)),
241 Curve2::Polyline(p) => polyline_tangent(&p.points, p.closed, t),
242 Curve2::BSpline(b) => de_boor_derivative(b, t, |p| [p.x, p.y], |c| Vec2::new(c[0], c[1])),
243 Curve2::Intrinsic(i) => crate::arc_length::intrinsic_tangent(i, t),
247 Curve2::Chain(c) => crate::chain::chain_tangent(c, t),
248 Curve2::Sinusoid(w) => {
249 let (sin, cos) = t.sin_cos();
250 Ok(Vec2::new(1.0, -w.cosine * sin + w.sine * cos))
251 }
252 Curve2::QuadraticGraph(g) => g
253 .slope(t)
254 .map(|slope| Vec2::new(1.0, slope))
255 .ok_or_else(|| outside_graph(t)),
256 Curve2::AngleGraph(g) => g
257 .slope(t)
258 .map(|slope| Vec2::new(slope, 1.0))
259 .ok_or_else(|| outside_graph(t)),
260 Curve2::Implicit(c) => c.derivative(t).ok_or_else(|| outside_graph(t)),
261 Curve2::Lifted(l) => lifted_rates(l, t).map(|(d, _)| d),
262 _ => Err(GeomError::Unsupported {
265 backend: axiolid_contracts::BackendId::new("axiolid-reference"),
266 operation: axiolid_contracts::Operation::CurveEvaluation,
267 }),
268 }?;
269 finite2(value, "curve derivative")
270}
271
272pub fn second_derivative2(curve: &Curve2, t: Scalar) -> GeomResult<Vec2> {
274 finite(t)?;
275 let value = match curve {
276 Curve2::Line(_) | Curve2::Polyline(_) => Ok(Vec2::ZERO),
277 Curve2::Circle(c) => Ok(conic_second2(&c.frame, c.radius, c.radius, t)),
278 Curve2::Ellipse(e) => Ok(conic_second2(&e.frame, e.semi_axis_x, e.semi_axis_y, t)),
279 Curve2::BSpline(b) => {
280 de_boor_second_derivative(b, t, |p| [p.x, p.y], |c| Vec2::new(c[0], c[1]))
281 }
282 Curve2::Sinusoid(w) => {
283 let (sin, cos) = t.sin_cos();
284 Ok(Vec2::new(0.0, -w.cosine * cos - w.sine * sin))
285 }
286 Curve2::QuadraticGraph(g) => g
287 .bend(t)
288 .map(|bend| Vec2::new(0.0, bend))
289 .ok_or_else(|| outside_graph(t)),
290 Curve2::AngleGraph(g) => g
291 .bend(t)
292 .map(|bend| Vec2::new(bend, 0.0))
293 .ok_or_else(|| outside_graph(t)),
294 Curve2::Implicit(c) => c.second_derivative(t).ok_or_else(|| outside_graph(t)),
295 Curve2::Lifted(l) => lifted_rates(l, t).map(|(_, dd)| dd),
296 _ => Err(GeomError::Unsupported {
297 backend: axiolid_contracts::BackendId::new("axiolid-reference"),
298 operation: axiolid_contracts::Operation::CurveEvaluation,
299 }),
300 }?;
301 finite2(value, "curve second derivative")
302}
303
304pub fn bspline_jet2(curve: &BSplineCurve2, t: Scalar) -> GeomResult<CurveJet<Point2, Vec2>> {
306 Ok(CurveJet {
307 point: de_boor(curve, t, |p| [p.x, p.y], |c| Point2::new(c[0], c[1]))?,
308 first: de_boor_derivative(curve, t, |p| [p.x, p.y], |c| Vec2::new(c[0], c[1]))?,
309 second: de_boor_second_derivative(curve, t, |p| [p.x, p.y], |c| Vec2::new(c[0], c[1]))?,
310 })
311}
312
313pub fn jet2(curve: &Curve2, t: Scalar) -> GeomResult<CurveJet<Point2, Vec2>> {
315 Ok(CurveJet {
316 point: evaluate2(curve, t)?,
317 first: derivative2(curve, t)?,
318 second: second_derivative2(curve, t)?,
319 })
320}
321
322fn lifted_rates(l: &axiolid_curve::LiftedCurve2, t: Scalar) -> GeomResult<(Vec2, Vec2)> {
326 if let Some((section, first)) = pair_side(l) {
327 let (a, b, _) = section.rates(t).ok_or_else(|| outside_graph(t))?;
328 let (aa, bb, _) = section.second_rates(t).ok_or_else(|| outside_graph(t))?;
329 let pick = |x: Point2, y: Point2| if first { x } else { y };
330 let (d, dd) = (pick(a, b), pick(aa, bb));
331 return Ok((Vec2::new(d.x, d.y), Vec2::new(dd.x, dd.y)));
332 }
333 let p = evaluate3(&l.curve, t)?;
334 let uv = l.unwrap_at(t, p).ok_or_else(|| outside_graph(t))?;
335 let jet = l.carrier.jet(uv.x, uv.y);
336 let (a, b, c) = (jet.u.dot(jet.u), jet.u.dot(jet.v), jet.v.dot(jet.v));
337 let det = a * c - b * b;
338 if det == 0.0 || !det.is_finite() {
339 return Err(outside_graph(t));
340 }
341 let solve = |w: Vec3| {
342 let (g0, g1) = (jet.u.dot(w), jet.v.dot(w));
343 Vec2::new((c * g0 - b * g1) / det, (a * g1 - b * g0) / det)
344 };
345 let d = solve(derivative3(&l.curve, t)?);
346 let rest = second_derivative3(&l.curve, t)?
347 - (jet.uu * (d.x * d.x) + jet.uv * (2.0 * d.x * d.y) + jet.vv * (d.y * d.y));
348 Ok((d, solve(rest)))
349}
350
351pub(crate) fn pair_side(
354 l: &axiolid_curve::LiftedCurve2,
355) -> Option<(&axiolid_curve::PairSection3, bool)> {
356 match l.curve.as_ref() {
357 Curve3::PairSection(section) => section.side(&l.carrier).map(|first| (section, first)),
358 _ => None,
359 }
360}
361
362fn outside_graph(t: Scalar) -> GeomError {
365 GeomError::Degenerate(format!(
366 "quadratic section graph has no regular point at t = {t}"
367 ))
368}
369
370pub fn evaluate3(curve: &Curve3, t: Scalar) -> GeomResult<Point3> {
374 finite(t)?;
375 let value = match curve {
376 Curve3::Line(l) => Ok(line_point(l.origin, l.direction, t)),
377 Curve3::Circle(c) => Ok(conic_point3(&c.frame, c.radius, c.radius, t)),
378 Curve3::Ellipse(e) => Ok(conic_point3(&e.frame, e.semi_axis_x, e.semi_axis_y, t)),
379 Curve3::Polyline(p) => polyline_point(&p.points, p.closed, t),
380 Curve3::BSpline(b) => de_boor(b, t, |p| [p.x, p.y, p.z], |c| Point3::new(c[0], c[1], c[2])),
381 Curve3::Intrinsic(i) => crate::frenet::frenet_point(i, t),
386 Curve3::RuledSection(r) => r.point(t).ok_or_else(|| outside_graph(t)),
388 Curve3::TorusSection(r) => r.point(t).ok_or_else(|| outside_graph(t)),
389 Curve3::ImplicitSection(r) => r.point(t).ok_or_else(|| outside_graph(t)),
390 Curve3::PairSection(r) => r.point(t).ok_or_else(|| outside_graph(t)),
391 Curve3::Banked(b) => crate::banked::banked_point(b, t),
393 Curve3::Elevated(e) => crate::arc_length::elevated_point(e, t),
396 _ => Err(GeomError::Unsupported {
399 backend: axiolid_contracts::BackendId::new("axiolid-reference"),
400 operation: axiolid_contracts::Operation::CurveEvaluation,
401 }),
402 }?;
403 finite3(value, "curve point")
404}
405
406pub fn derivative3(curve: &Curve3, t: Scalar) -> GeomResult<Vec3> {
408 finite(t)?;
409 let value = match curve {
410 Curve3::Line(l) => Ok(l.direction),
411 Curve3::Circle(c) => Ok(conic_tangent3(&c.frame, c.radius, c.radius, t)),
412 Curve3::Ellipse(e) => Ok(conic_tangent3(&e.frame, e.semi_axis_x, e.semi_axis_y, t)),
413 Curve3::Polyline(p) => polyline_tangent(&p.points, p.closed, t),
414 Curve3::BSpline(b) => {
415 de_boor_derivative(b, t, |p| [p.x, p.y, p.z], |c| Vec3::new(c[0], c[1], c[2]))
416 }
417 Curve3::Intrinsic(i) => crate::frenet::frenet_tangent(i, t),
419 Curve3::RuledSection(r) => r.tangent(t).ok_or_else(|| outside_graph(t)),
420 Curve3::TorusSection(r) => r.tangent(t).ok_or_else(|| outside_graph(t)),
421 Curve3::ImplicitSection(r) => r.tangent(t).ok_or_else(|| outside_graph(t)),
422 Curve3::PairSection(r) => r.tangent(t).ok_or_else(|| outside_graph(t)),
423 Curve3::Banked(b) => crate::banked::banked_derivative(b, t),
425 Curve3::Elevated(e) => crate::elevated::elevated_derivative(e, t),
427 _ => Err(GeomError::Unsupported {
430 backend: axiolid_contracts::BackendId::new("axiolid-reference"),
431 operation: axiolid_contracts::Operation::CurveEvaluation,
432 }),
433 }?;
434 finite3(value, "curve derivative")
435}
436
437pub fn second_derivative3(curve: &Curve3, t: Scalar) -> GeomResult<Vec3> {
439 finite(t)?;
440 let value = match curve {
441 Curve3::Line(_) | Curve3::Polyline(_) => Ok(Vec3::ZERO),
442 Curve3::Circle(c) => Ok(conic_second3(&c.frame, c.radius, c.radius, t)),
443 Curve3::Ellipse(e) => Ok(conic_second3(&e.frame, e.semi_axis_x, e.semi_axis_y, t)),
444 Curve3::BSpline(b) => {
445 de_boor_second_derivative(b, t, |p| [p.x, p.y, p.z], |c| Vec3::new(c[0], c[1], c[2]))
446 }
447 Curve3::RuledSection(r) => r.bend(t).ok_or_else(|| outside_graph(t)),
448 Curve3::TorusSection(r) => r.bend(t).ok_or_else(|| outside_graph(t)),
449 Curve3::ImplicitSection(r) => r.bend(t).ok_or_else(|| outside_graph(t)),
450 Curve3::PairSection(r) => r.bend(t).ok_or_else(|| outside_graph(t)),
451 Curve3::Elevated(e) => crate::elevated::elevated_second_derivative(e, t),
453 Curve3::Banked(b) => crate::banked::banked_second_derivative(b, t),
454 _ => Err(GeomError::Unsupported {
455 backend: axiolid_contracts::BackendId::new("axiolid-reference"),
456 operation: axiolid_contracts::Operation::CurveEvaluation,
457 }),
458 }?;
459 finite3(value, "curve second derivative")
460}
461
462pub fn bspline_jet3(curve: &BSplineCurve3, t: Scalar) -> GeomResult<CurveJet<Point3, Vec3>> {
464 Ok(CurveJet {
465 point: de_boor(
466 curve,
467 t,
468 |p| [p.x, p.y, p.z],
469 |c| Point3::new(c[0], c[1], c[2]),
470 )?,
471 first: de_boor_derivative(
472 curve,
473 t,
474 |p| [p.x, p.y, p.z],
475 |c| Vec3::new(c[0], c[1], c[2]),
476 )?,
477 second: de_boor_second_derivative(
478 curve,
479 t,
480 |p| [p.x, p.y, p.z],
481 |c| Vec3::new(c[0], c[1], c[2]),
482 )?,
483 })
484}
485
486pub fn jet3(curve: &Curve3, t: Scalar) -> GeomResult<CurveJet<Point3, Vec3>> {
488 Ok(CurveJet {
489 point: evaluate3(curve, t)?,
490 first: derivative3(curve, t)?,
491 second: second_derivative3(curve, t)?,
492 })
493}
494
495fn finite(t: Scalar) -> GeomResult<()> {
498 if t.is_finite() {
499 Ok(())
500 } else {
501 Err(GeomError::InvalidInput(format!(
502 "curve parameter must be finite, got {t}"
503 )))
504 }
505}
506
507fn finite2(value: Vec2, what: &str) -> GeomResult<Vec2> {
508 if value.is_finite() {
509 Ok(value)
510 } else {
511 Err(GeomError::Degenerate(format!("{what} is non-finite")))
512 }
513}
514
515fn finite3(value: Vec3, what: &str) -> GeomResult<Vec3> {
516 if value.is_finite() {
517 Ok(value)
518 } else {
519 Err(GeomError::Degenerate(format!("{what} is non-finite")))
520 }
521}
522
523fn line_point<P>(origin: P, direction: P, t: Scalar) -> P
524where
525 P: core::ops::Add<Output = P> + core::ops::Mul<Scalar, Output = P>,
526{
527 origin + direction * t
528}
529
530fn conic_point2(frame: &Frame2, rx: Scalar, ry: Scalar, t: Scalar) -> Point2 {
531 frame.origin + frame.x * (rx * t.cos()) + frame.y * (ry * t.sin())
532}
533
534fn conic_tangent2(frame: &Frame2, rx: Scalar, ry: Scalar, t: Scalar) -> Vec2 {
535 frame.x * (-rx * t.sin()) + frame.y * (ry * t.cos())
536}
537
538fn conic_second2(frame: &Frame2, rx: Scalar, ry: Scalar, t: Scalar) -> Vec2 {
539 frame.x * (-rx * t.cos()) + frame.y * (-ry * t.sin())
540}
541
542fn conic_point3(frame: &Frame3, rx: Scalar, ry: Scalar, t: Scalar) -> Point3 {
543 frame.origin + frame.x * (rx * t.cos()) + frame.y * (ry * t.sin())
544}
545
546fn conic_tangent3(frame: &Frame3, rx: Scalar, ry: Scalar, t: Scalar) -> Vec3 {
547 frame.x * (-rx * t.sin()) + frame.y * (ry * t.cos())
548}
549
550fn conic_second3(frame: &Frame3, rx: Scalar, ry: Scalar, t: Scalar) -> Vec3 {
551 frame.x * (-rx * t.cos()) + frame.y * (-ry * t.sin())
552}
553
554fn polyline_span(count: usize, closed: bool, t: Scalar) -> Option<(usize, usize, Scalar)> {
558 let segments = if closed {
559 count
560 } else {
561 count.saturating_sub(1)
562 };
563 if count < 2 || segments == 0 {
564 return None;
565 }
566 let clamped = t.clamp(0.0, segments as Scalar);
569 let mut index = clamped.floor() as usize;
570 if index >= segments {
571 index = segments - 1;
572 }
573 let local = clamped - index as Scalar;
574 let next = (index + 1) % count;
575 Some((index, next, local))
576}
577
578fn polyline_point<P>(points: &[P], closed: bool, t: Scalar) -> GeomResult<P>
579where
580 P: Copy
581 + core::ops::Add<Output = P>
582 + core::ops::Sub<Output = P>
583 + core::ops::Mul<Scalar, Output = P>,
584{
585 let (i, j, local) = polyline_span(points.len(), closed, t).ok_or_else(|| {
586 GeomError::Degenerate(format!(
587 "polyline with {} points has no evaluable segment",
588 points.len()
589 ))
590 })?;
591 Ok(points[i] + (points[j] - points[i]) * local)
592}
593
594fn polyline_tangent<P>(points: &[P], closed: bool, t: Scalar) -> GeomResult<P>
595where
596 P: Copy + core::ops::Sub<Output = P>,
597{
598 let (i, j, _) = polyline_span(points.len(), closed, t).ok_or_else(|| {
599 GeomError::Degenerate(format!(
600 "polyline with {} points has no evaluable segment",
601 points.len()
602 ))
603 })?;
604 Ok(points[j] - points[i])
606}
607
608pub(crate) fn span_in(knots: &[Scalar], n: usize, d: usize, u: Scalar) -> usize {
616 let mut span = d;
617 for (k, knot) in knots.iter().enumerate().take(n).skip(d) {
618 if *knot <= u {
619 span = k;
620 } else {
621 break;
622 }
623 }
624 span
625}
626
627pub(crate) fn de_boor_recurrence<const N: usize>(
634 knots: &[Scalar],
635 span: usize,
636 d: usize,
637 u: Scalar,
638 points: &mut [[Scalar; N]],
639 weights: &mut [Scalar],
640) {
641 for r in 1..=d {
642 for j in (r..=d).rev() {
643 let i = span - d + j;
644 let denom = knots[i + d + 1 - r] - knots[i];
645 let alpha = if denom.abs() > 0.0 {
646 (u - knots[i]) / denom
647 } else {
648 0.0
649 };
650 let (prev, cur) = (points[j - 1], points[j]);
651 points[j] = core::array::from_fn(|k| prev[k] * (1.0 - alpha) + cur[k] * alpha);
652 weights[j] = weights[j - 1] * (1.0 - alpha) + weights[j] * alpha;
653 }
654 }
655}
656
657fn spline_span<P>(b: &BSplineCurve<P>, t: Scalar) -> GeomResult<(Vec<Scalar>, usize, usize)> {
661 let axis = SplineAxis::new(
662 &b.knots,
663 &b.multiplicities,
664 b.degree,
665 b.control_points.len(),
666 "B-spline curve",
667 )?;
668 if let Some(weights) = &b.weights {
669 if weights.len() != b.control_points.len() {
670 return Err(GeomError::InvalidInput(format!(
671 "B-spline has {} weights for {} control points",
672 weights.len(),
673 b.control_points.len()
674 )));
675 }
676 if weights
677 .iter()
678 .any(|weight| !weight.is_finite() || *weight <= 0.0)
679 {
680 return Err(GeomError::InvalidInput(
681 "B-spline weights must be finite and strictly positive".to_owned(),
682 ));
683 }
684 }
685 let t = axis.clamp(t);
686 let span = span_in(&axis.knots, axis.count, axis.degree, t);
687 Ok((axis.knots, span, axis.degree))
688}
689
690fn finite_control_points<P, const N: usize, F>(
694 control_points: &[P],
695 to: &F,
696) -> GeomResult<Vec<[Scalar; N]>>
697where
698 F: Fn(&P) -> [Scalar; N],
699{
700 let points: Vec<[Scalar; N]> = control_points.iter().map(to).collect();
701 if points
702 .iter()
703 .flatten()
704 .any(|coordinate| !coordinate.is_finite())
705 {
706 return Err(GeomError::InvalidInput(
707 "B-spline control points must be finite".to_owned(),
708 ));
709 }
710 Ok(points)
711}
712
713fn de_boor<P, const N: usize, F, G, Q>(
719 b: &BSplineCurve<P>,
720 t: Scalar,
721 to: F,
722 from: G,
723) -> GeomResult<Q>
724where
725 F: Fn(&P) -> [Scalar; N],
726 G: Fn([Scalar; N]) -> Q,
727{
728 let (knots, span, d) = spline_span(b, t)?;
729 let control_points = finite_control_points(&b.control_points, &to)?;
730 let u = t.clamp(knots[d], knots[b.control_points.len()]);
731
732 let mut work: Vec<[Scalar; N]> = Vec::with_capacity(d + 1);
735 let mut weights: Vec<Scalar> = Vec::with_capacity(d + 1);
736 for j in 0..=d {
737 let idx = span - d + j;
738 let w = b.weights.as_ref().map_or(1.0, |ws| ws[idx]);
739 let c = control_points[idx];
740 let homogeneous = core::array::from_fn(|k| c[k] * w);
743 if homogeneous.iter().any(|value| !value.is_finite()) {
744 return Err(GeomError::Degenerate(
745 "B-spline homogeneous control point overflowed".to_owned(),
746 ));
747 }
748 work.push(homogeneous);
749 weights.push(w);
750 }
751
752 de_boor_recurrence(&knots, span, d, u, &mut work, &mut weights);
755
756 let w = weights[d];
757 if !w.is_finite() || w == 0.0 {
758 return Err(GeomError::Degenerate(
759 "B-spline weight collapsed to zero".to_owned(),
760 ));
761 }
762 Ok(from(core::array::from_fn(|k| work[d][k] / w)))
763}
764
765fn de_boor_derivative<P, const N: usize, F, G, Q>(
774 b: &BSplineCurve<P>,
775 t: Scalar,
776 to: F,
777 from: G,
778) -> GeomResult<Q>
779where
780 F: Fn(&P) -> [Scalar; N],
781 G: Fn([Scalar; N]) -> Q,
782{
783 let (knots, _, d) = spline_span(b, t)?;
784 let control_points = finite_control_points(&b.control_points, &to)?;
785 let n = b.control_points.len();
786 let u = t.clamp(knots[d], knots[n]);
787
788 let hom: Vec<[Scalar; N]> = (0..n)
790 .map(|i| {
791 let w = b.weights.as_ref().map_or(1.0, |ws| ws[i]);
792 let c = control_points[i];
793 core::array::from_fn(|k| c[k] * w)
794 })
795 .collect();
796 if hom.iter().flatten().any(|value| !value.is_finite()) {
797 return Err(GeomError::Degenerate(
798 "B-spline homogeneous control point overflowed".to_owned(),
799 ));
800 }
801 let hw: Vec<Scalar> = (0..n)
802 .map(|i| b.weights.as_ref().map_or(1.0, |ws| ws[i]))
803 .collect();
804
805 let mut dhom: Vec<[Scalar; N]> = Vec::with_capacity(n - 1);
807 let mut dhw: Vec<Scalar> = Vec::with_capacity(n - 1);
808 for i in 0..n - 1 {
809 let denom = knots[i + d + 1] - knots[i + 1];
810 let f = if denom.abs() > 0.0 {
811 d as Scalar / denom
812 } else {
813 0.0
814 };
815 dhom.push(core::array::from_fn(|k| (hom[i + 1][k] - hom[i][k]) * f));
816 dhw.push((hw[i + 1] - hw[i]) * f);
817 }
818
819 let dknots = &knots[1..knots.len() - 1];
821 let (da, dw) = eval_homogeneous(dknots, d - 1, &dhom, &dhw, u);
822 let (a, w) = eval_homogeneous(&knots, d, &hom, &hw, u);
824
825 if !w.is_finite() || w == 0.0 {
826 return Err(GeomError::Degenerate(
827 "B-spline weight collapsed to zero".to_owned(),
828 ));
829 }
830 Ok(from(core::array::from_fn(|k| {
832 (da[k] - (a[k] / w) * dw) / w
833 })))
834}
835
836fn de_boor_second_derivative<P, const N: usize, F, G, Q>(
841 b: &BSplineCurve<P>,
842 t: Scalar,
843 to: F,
844 from: G,
845) -> GeomResult<Q>
846where
847 F: Fn(&P) -> [Scalar; N],
848 G: Fn([Scalar; N]) -> Q,
849{
850 let (knots, _, degree) = spline_span(b, t)?;
851 let control_points = finite_control_points(&b.control_points, &to)?;
852 let count = b.control_points.len();
853 let u = t.clamp(knots[degree], knots[count]);
854
855 let points: Vec<[Scalar; N]> = (0..count)
856 .map(|i| {
857 let weight = b.weights.as_ref().map_or(1.0, |weights| weights[i]);
858 core::array::from_fn(|axis| control_points[i][axis] * weight)
859 })
860 .collect();
861 if points.iter().flatten().any(|value| !value.is_finite()) {
862 return Err(GeomError::Degenerate(
863 "B-spline homogeneous control point overflowed".to_owned(),
864 ));
865 }
866 let weights: Vec<Scalar> = (0..count)
867 .map(|i| b.weights.as_ref().map_or(1.0, |values| values[i]))
868 .collect();
869
870 let (point, weight) = eval_homogeneous(&knots, degree, &points, &weights, u);
871 if !weight.is_finite() || weight == 0.0 {
872 return Err(GeomError::Degenerate(
873 "B-spline weight collapsed to zero".to_owned(),
874 ));
875 }
876
877 let (first_points, first_weights) = derivative_controls(&points, &weights, &knots, degree);
878 let first_knots = &knots[1..knots.len() - 1];
879 let (first, first_weight) =
880 eval_homogeneous(first_knots, degree - 1, &first_points, &first_weights, u);
881 let position: [Scalar; N] = core::array::from_fn(|axis| point[axis] / weight);
882 let first_projected: [Scalar; N] =
883 core::array::from_fn(|axis| (first[axis] - position[axis] * first_weight) / weight);
884
885 let (second, second_weight) = if degree == 1 {
886 ([0.0; N], 0.0)
887 } else {
888 let (second_points, second_weights) =
889 derivative_controls(&first_points, &first_weights, first_knots, degree - 1);
890 let second_knots = &first_knots[1..first_knots.len() - 1];
891 eval_homogeneous(second_knots, degree - 2, &second_points, &second_weights, u)
892 };
893 Ok(from(core::array::from_fn(|axis| {
894 (second[axis] - 2.0 * first_weight * first_projected[axis] - second_weight * position[axis])
895 / weight
896 })))
897}
898
899fn derivative_controls<const N: usize>(
901 points: &[[Scalar; N]],
902 weights: &[Scalar],
903 knots: &[Scalar],
904 degree: usize,
905) -> (Vec<[Scalar; N]>, Vec<Scalar>) {
906 let mut derivative_points = Vec::with_capacity(points.len() - 1);
907 let mut derivative_weights = Vec::with_capacity(weights.len() - 1);
908 for i in 0..points.len() - 1 {
909 let denominator = knots[i + degree + 1] - knots[i + 1];
910 let factor = if denominator.abs() > 0.0 {
911 degree as Scalar / denominator
912 } else {
913 0.0
914 };
915 derivative_points.push(core::array::from_fn(|axis| {
916 (points[i + 1][axis] - points[i][axis]) * factor
917 }));
918 derivative_weights.push((weights[i + 1] - weights[i]) * factor);
919 }
920 (derivative_points, derivative_weights)
921}
922
923pub(crate) fn eval_homogeneous<const N: usize>(
925 knots: &[Scalar],
926 d: usize,
927 hom: &[[Scalar; N]],
928 hw: &[Scalar],
929 u: Scalar,
930) -> ([Scalar; N], Scalar) {
931 let n = hom.len();
932 if d == 0 {
933 let mut idx = 0;
935 for (k, knot) in knots.iter().enumerate().take(n) {
936 if *knot <= u {
937 idx = k;
938 }
939 }
940 return (hom[idx.min(n - 1)], hw[idx.min(n - 1)]);
941 }
942 let mut span = d;
943 for (k, knot) in knots.iter().enumerate().take(n).skip(d) {
944 if *knot <= u {
945 span = k;
946 } else {
947 break;
948 }
949 }
950 let mut work: Vec<[Scalar; N]> = (0..=d).map(|j| hom[span - d + j]).collect();
951 let mut weights: Vec<Scalar> = (0..=d).map(|j| hw[span - d + j]).collect();
952 for r in 1..=d {
953 for j in (r..=d).rev() {
954 let i = span - d + j;
955 let denom = knots[i + d + 1 - r] - knots[i];
956 let alpha = if denom.abs() > 0.0 {
957 (u - knots[i]) / denom
958 } else {
959 0.0
960 };
961 let (prev, cur) = (work[j - 1], work[j]);
962 work[j] = core::array::from_fn(|k| prev[k] * (1.0 - alpha) + cur[k] * alpha);
963 weights[j] = weights[j - 1] * (1.0 - alpha) + weights[j] * alpha;
964 }
965 }
966 (work[d], weights[d])
967}
968
969pub fn flatten2(
991 curve: &Curve2,
992 domain: Interval,
993 chord_tolerance: Scalar,
994 max_depth: u32,
995) -> GeomResult<Vec<Point2>> {
996 const MAX_POINTS: usize = 1 << 16;
1000 if !(chord_tolerance.is_finite()
1001 && chord_tolerance.is_sign_positive()
1002 && chord_tolerance != 0.0)
1003 {
1004 return Err(GeomError::InvalidInput(format!(
1005 "chord tolerance must be positive and finite, got {chord_tolerance}"
1006 )));
1007 }
1008 if let Curve2::Line(_) = curve {
1011 return Ok(vec![
1012 evaluate2(curve, domain.start)?,
1013 evaluate2(curve, domain.end)?,
1014 ]);
1015 }
1016 if let Curve2::Polyline(p) = curve {
1017 let natural = polyline_domain(p.points.len(), p.closed);
1023 let requested = (domain.end - domain.start).abs();
1024 if natural.end > 1.0 && requested <= 1.0 {
1025 return Err(GeomError::InvalidInput(format!(
1026 "polyline domain {:?} spans {requested} of {} segments; a \
1027 polyline parameter is one unit per segment, so this would \
1028 discard {} vertices",
1029 domain,
1030 natural.end,
1031 p.points.len().saturating_sub(2)
1032 )));
1033 }
1034 return polyline_flatten(&p.points, p.closed, domain, |t| evaluate2(curve, t));
1035 }
1036
1037 let mut out = vec![evaluate2(curve, domain.start)?];
1038 let eval = |t| evaluate2(curve, t);
1039 let bound = |a, b| crate::bound::chord_bound2(curve, a, b);
1040 for (a, b) in smooth_pieces(domain, &crate::bound::continuity_breaks2(curve, 1)) {
1041 subdivide(
1042 &eval,
1043 &bound,
1044 a,
1045 b,
1046 chord_tolerance,
1047 max_depth.min(MAX_DEPTH_CEILING),
1048 MAX_POINTS,
1049 crate::bound::certifies_flattening2(curve),
1050 &mut out,
1051 )?;
1052 out.push(evaluate2(curve, b)?);
1053 }
1054 Ok(out)
1055}
1056
1057fn smooth_pieces(domain: Interval, breaks: &[Scalar]) -> Vec<(Scalar, Scalar)> {
1063 let (lo, hi) = if domain.start <= domain.end {
1064 (domain.start, domain.end)
1065 } else {
1066 (domain.end, domain.start)
1067 };
1068 let mut cuts: Vec<Scalar> = breaks
1069 .iter()
1070 .copied()
1071 .filter(|&t| t > lo && t < hi)
1072 .collect();
1073 cuts.sort_by(Scalar::total_cmp);
1074 cuts.dedup();
1075 if domain.start > domain.end {
1076 cuts.reverse();
1077 }
1078 let mut out = Vec::with_capacity(cuts.len() + 1);
1079 let mut from = domain.start;
1080 for cut in cuts {
1081 out.push((from, cut));
1082 from = cut;
1083 }
1084 out.push((from, domain.end));
1085 out
1086}
1087
1088const MAX_DEPTH_CEILING: u32 = 20;
1093
1094trait ChordPoint: Copy {
1107 fn sub(self, other: Self) -> Self;
1108 fn add_scaled(self, direction: Self, scale: Scalar) -> Self;
1109 fn dot(self, other: Self) -> Scalar;
1110 fn length(self) -> Scalar;
1111 fn length_squared(self) -> Scalar;
1112}
1113
1114impl ChordPoint for Point2 {
1115 fn sub(self, other: Self) -> Self {
1116 self - other
1117 }
1118 fn add_scaled(self, direction: Self, scale: Scalar) -> Self {
1119 self + direction * scale
1120 }
1121 fn dot(self, other: Self) -> Scalar {
1122 Point2::dot(self, other)
1123 }
1124 fn length(self) -> Scalar {
1125 Point2::length(self)
1126 }
1127 fn length_squared(self) -> Scalar {
1128 Point2::length_squared(self)
1129 }
1130}
1131
1132impl ChordPoint for Point3 {
1133 fn sub(self, other: Self) -> Self {
1134 self - other
1135 }
1136 fn add_scaled(self, direction: Self, scale: Scalar) -> Self {
1137 self + direction * scale
1138 }
1139 fn dot(self, other: Self) -> Scalar {
1140 Point3::dot(self, other)
1141 }
1142 fn length(self) -> Scalar {
1143 Point3::length(self)
1144 }
1145 fn length_squared(self) -> Scalar {
1146 Point3::length_squared(self)
1147 }
1148}
1149
1150fn sagitta<P: ChordPoint>(a: P, b: P, m: P) -> Scalar {
1152 let ab = b.sub(a);
1153 let len2 = ab.length_squared();
1154 if len2 <= 0.0 {
1155 return m.sub(a).length();
1158 }
1159 let t = (m.sub(a).dot(ab) / len2).clamp(0.0, 1.0);
1160 m.sub(a.add_scaled(ab, t)).length()
1161}
1162
1163#[allow(clippy::too_many_arguments)]
1178fn subdivide<P, F, B>(
1179 eval: &F,
1180 bound: &B,
1181 a: Scalar,
1182 b: Scalar,
1183 tol: Scalar,
1184 depth: u32,
1185 budget: usize,
1186 claimed: bool,
1187 out: &mut Vec<P>,
1188) -> GeomResult<()>
1189where
1190 P: ChordPoint,
1191 F: Fn(Scalar) -> GeomResult<P>,
1192 B: Fn(Scalar, Scalar) -> Option<Scalar>,
1193{
1194 if out.len() >= budget {
1195 return Err(GeomError::Degenerate(format!(
1196 "curve flattening exceeded {budget} points before meeting the \
1197 chord tolerance {tol}; the curve may be degenerate"
1198 )));
1199 }
1200 let mid = 0.5 * (a + b);
1201 let pa = eval(a)?;
1202 let pb = eval(b)?;
1203 if !(mid > a && mid < b) {
1211 let certified = !claimed || bound(a, b).is_some_and(|d| d <= tol);
1212 if sagitta(pa, pb, pa) <= tol && (pb.sub(pa)).length() <= tol && certified {
1213 return Ok(());
1214 }
1215 return Err(GeomError::Degenerate(format!(
1216 "curve parameter interval ({a}, {b}) is too small to bisect but \
1217 its chord still exceeds the tolerance {tol}"
1218 )));
1219 }
1220 let pm = eval(mid)?;
1221 let certified = match bound(a, b) {
1226 Some(certified) => certified <= tol,
1227 None => !claimed,
1228 };
1229 if sagitta(pa, pb, pm) <= tol && certified {
1230 return Ok(());
1232 }
1233 if depth == 0 {
1234 return Err(GeomError::BudgetExceeded {
1235 resource: "curve flattening depth",
1236 });
1237 }
1238 subdivide(eval, bound, a, mid, tol, depth - 1, budget, claimed, out)?;
1239 out.push(pm);
1240 subdivide(eval, bound, mid, b, tol, depth - 1, budget, claimed, out)?;
1241 Ok(())
1242}
1243
1244fn polyline_flatten<P, F>(
1246 points: &[P],
1247 closed: bool,
1248 domain: Interval,
1249 eval: F,
1250) -> GeomResult<Vec<P>>
1251where
1252 P: Copy,
1253 F: Fn(Scalar) -> GeomResult<P>,
1254{
1255 let segments = if closed {
1256 points.len()
1257 } else {
1258 points.len().saturating_sub(1)
1259 };
1260 if segments == 0 {
1261 return Err(GeomError::Degenerate(
1262 "polyline has no evaluable segment".to_owned(),
1263 ));
1264 }
1265 let lo = domain.start.min(domain.end);
1266 let hi = domain.start.max(domain.end);
1267 let mut out = vec![eval(lo)?];
1268 let first = lo.floor() as i64 + 1;
1270 let last = hi.ceil() as i64 - 1;
1271 for k in first..=last {
1272 let t = k as Scalar;
1273 if t > lo && t < hi {
1274 out.push(eval(t)?);
1275 }
1276 }
1277 out.push(eval(hi)?);
1278 Ok(out)
1279}
1280
1281impl CurveEvaluator<Curve2> for ScalarCurve {
1284 type Point = Point2;
1285 type Derivative = Vec2;
1286 type Error = GeomError;
1287
1288 fn domain(&self, curve: &Curve2) -> Interval {
1289 domain2(curve)
1290 }
1291
1292 fn evaluate(
1293 &self,
1294 curve: &Curve2,
1295 t: Scalar,
1296 _tolerance: Tolerance,
1297 ) -> Result<Self::Point, Self::Error> {
1298 evaluate2(curve, t)
1299 }
1300
1301 fn derivative(
1302 &self,
1303 curve: &Curve2,
1304 t: Scalar,
1305 _tolerance: Tolerance,
1306 ) -> Result<Self::Derivative, Self::Error> {
1307 derivative2(curve, t)
1308 }
1309}
1310
1311impl CurveEvaluator<Curve3> for ScalarCurve {
1312 type Point = Point3;
1313 type Derivative = Vec3;
1314 type Error = GeomError;
1315
1316 fn domain(&self, curve: &Curve3) -> Interval {
1317 domain3(curve)
1318 }
1319
1320 fn evaluate(
1321 &self,
1322 curve: &Curve3,
1323 t: Scalar,
1324 _tolerance: Tolerance,
1325 ) -> Result<Self::Point, Self::Error> {
1326 evaluate3(curve, t)
1327 }
1328
1329 fn derivative(
1330 &self,
1331 curve: &Curve3,
1332 t: Scalar,
1333 _tolerance: Tolerance,
1334 ) -> Result<Self::Derivative, Self::Error> {
1335 derivative3(curve, t)
1336 }
1337}
1338
1339#[allow(unused)]
1341fn _type_anchors(_: Circle2, _: Circle3, _: Ellipse2, _: Ellipse3, _: Line2, _: Line3) {}
1342#[allow(unused)]
1343fn _poly_anchors(_: Polyline2, _: Polyline3) {}
1344
1345pub fn flatten3(
1353 curve: &Curve3,
1354 domain: Interval,
1355 chord_tolerance: Scalar,
1356 max_depth: u32,
1357) -> GeomResult<Vec<Point3>> {
1358 const MAX_POINTS: usize = 1 << 16;
1362 if !(chord_tolerance.is_finite()
1363 && chord_tolerance.is_sign_positive()
1364 && chord_tolerance != 0.0)
1365 {
1366 return Err(GeomError::InvalidInput(format!(
1367 "chord tolerance must be positive and finite, got {chord_tolerance}"
1368 )));
1369 }
1370 if let Curve3::Line(_) = curve {
1373 return Ok(vec![
1374 evaluate3(curve, domain.start)?,
1375 evaluate3(curve, domain.end)?,
1376 ]);
1377 }
1378 if let Curve3::Polyline(p) = curve {
1379 let natural = polyline_domain(p.points.len(), p.closed);
1383 let requested = (domain.end - domain.start).abs();
1384 if natural.end > 1.0 && requested <= 1.0 {
1385 return Err(GeomError::InvalidInput(format!(
1386 "polyline domain {:?} spans {requested} of {} segments; a \
1387 polyline parameter is one unit per segment, so this would \
1388 discard {} vertices",
1389 domain,
1390 natural.end,
1391 p.points.len().saturating_sub(2)
1392 )));
1393 }
1394 return polyline_flatten(&p.points, p.closed, domain, |t| evaluate3(curve, t));
1395 }
1396
1397 let eval = |t| evaluate3(curve, t);
1398 let bound = |a, b| crate::bound::chord_bound3(curve, a, b);
1399 let mut out = vec![eval(domain.start)?];
1400 for (a, b) in smooth_pieces(domain, &crate::bound::continuity_breaks3(curve, 1)) {
1401 subdivide(
1402 &eval,
1403 &bound,
1404 a,
1405 b,
1406 chord_tolerance,
1407 max_depth.min(MAX_DEPTH_CEILING),
1408 MAX_POINTS,
1409 crate::bound::certifies_flattening3(curve),
1410 &mut out,
1411 )?;
1412 out.push(eval(b)?);
1413 }
1414 Ok(out)
1415}
1416
1417fn no_closed_form_inversion() -> GeomError {
1422 GeomError::InvalidInput(
1423 "curve family has no closed form inversion; a point trim on this basis \
1424 would require iteration, which trim resolution does not perform"
1425 .to_owned(),
1426 )
1427}
1428
1429fn point_not_on_curve(distance: Scalar, tolerance: Scalar) -> GeomError {
1431 GeomError::InvalidInput(format!(
1432 "point is {distance} from the curve, outside the {tolerance} tolerance; \
1433 refusing rather than projecting it onto the nearest parameter"
1434 ))
1435}
1436
1437fn invert_line(
1443 origin: impl Into<[Scalar; 3]>,
1444 direction: [Scalar; 3],
1445 point: [Scalar; 3],
1446 tolerance: Scalar,
1447) -> GeomResult<Scalar> {
1448 let origin = origin.into();
1449 let dd = direction.iter().map(|c| c * c).sum::<Scalar>();
1450 if !dd.is_finite() || dd <= Scalar::EPSILON {
1451 return Err(GeomError::InvalidInput(
1452 "line direction is degenerate, so no parameter names a point".to_owned(),
1453 ));
1454 }
1455 let offset = [
1456 point[0] - origin[0],
1457 point[1] - origin[1],
1458 point[2] - origin[2],
1459 ];
1460 let t = offset
1461 .iter()
1462 .zip(direction.iter())
1463 .map(|(o, d)| o * d)
1464 .sum::<Scalar>()
1465 / dd;
1466 let residual = [
1469 offset[0] - direction[0] * t,
1470 offset[1] - direction[1] * t,
1471 offset[2] - direction[2] * t,
1472 ];
1473 let distance = residual.iter().map(|c| c * c).sum::<Scalar>().sqrt();
1474 if distance > tolerance {
1475 return Err(point_not_on_curve(distance, tolerance));
1476 }
1477 Ok(t)
1478}
1479
1480fn invert_conic(
1486 local_x: Scalar,
1487 local_y: Scalar,
1488 semi_x: Scalar,
1489 semi_y: Scalar,
1490) -> GeomResult<Scalar> {
1491 if !(semi_x.is_finite() && semi_y.is_finite()) || semi_x <= 0.0 || semi_y <= 0.0 {
1492 return Err(GeomError::InvalidInput(
1493 "conic semi-axes must be finite and positive to invert a point".to_owned(),
1494 ));
1495 }
1496 let angle = (local_y / semi_y).atan2(local_x / semi_x);
1497 if !angle.is_finite() {
1498 return Err(GeomError::InvalidInput(
1499 "conic inversion produced a non-finite angle".to_owned(),
1500 ));
1501 }
1502 Ok(angle.rem_euclid(std::f64::consts::TAU))
1505}
1506
1507pub fn invert2(curve: &Curve2, point: Point2, tolerance: Tolerance) -> GeomResult<Scalar> {
1519 finite2(point, "inversion point")?;
1520 let linear = tolerance.linear();
1521 match curve {
1522 Curve2::Line(l) => invert_line(
1523 [l.origin.x, l.origin.y, 0.0],
1524 [l.direction.x, l.direction.y, 0.0],
1525 [point.x, point.y, 0.0],
1526 linear,
1527 ),
1528 Curve2::Circle(c) => {
1529 let t = invert_conic_in_frame2(&c.frame, point, c.radius, c.radius)?;
1530 verify2(curve, t, point, linear)
1531 }
1532 Curve2::Ellipse(e) => {
1533 let t = invert_conic_in_frame2(&e.frame, point, e.semi_axis_x, e.semi_axis_y)?;
1534 verify2(curve, t, point, linear)
1535 }
1536 Curve2::Sinusoid(_) | Curve2::QuadraticGraph(_) => verify2(curve, point.x, point, linear),
1539 Curve2::AngleGraph(g) => {
1542 let u = g.angle(point.y).ok_or_else(|| outside_graph(point.y))?;
1543 let turns = ((point.x - u) / std::f64::consts::TAU).round();
1544 let shifted = Point2::new(point.x - turns * std::f64::consts::TAU, point.y);
1545 verify2(curve, point.y, shifted, linear)
1546 }
1547 Curve2::Lifted(l) => {
1549 let lifted = l.carrier.jet(point.x, point.y).point;
1550 let t = invert3(&l.curve, lifted, tolerance)?;
1551 verify2(curve, t, point, linear)
1552 }
1553 Curve2::Implicit(c) => {
1555 let t = c
1556 .parameter_of(point)
1557 .ok_or_else(|| point_not_on_curve(Scalar::INFINITY, linear))?;
1558 verify2(curve, t, point, linear)
1559 }
1560 _ => Err(no_closed_form_inversion()),
1561 }
1562}
1563
1564fn invert_conic_in_frame2(
1566 frame: &axiolid_core::Frame2,
1567 point: Point2,
1568 semi_x: Scalar,
1569 semi_y: Scalar,
1570) -> GeomResult<Scalar> {
1571 let offset = point - frame.origin;
1572 invert_conic(offset.dot(frame.x), offset.dot(frame.y), semi_x, semi_y)
1573}
1574
1575fn verify2(curve: &Curve2, t: Scalar, point: Point2, tolerance: Scalar) -> GeomResult<Scalar> {
1582 let found = evaluate2(curve, t)?;
1583 let distance = (found - point).length();
1584 if distance > tolerance {
1585 return Err(point_not_on_curve(distance, tolerance));
1586 }
1587 Ok(t)
1588}
1589
1590pub fn invert3(curve: &Curve3, point: Point3, tolerance: Tolerance) -> GeomResult<Scalar> {
1598 finite3(point, "inversion point")?;
1599 let linear = tolerance.linear();
1600 match curve {
1601 Curve3::Line(l) => invert_line(
1602 [l.origin.x, l.origin.y, l.origin.z],
1603 [l.direction.x, l.direction.y, l.direction.z],
1604 [point.x, point.y, point.z],
1605 linear,
1606 ),
1607 Curve3::Circle(c) => {
1608 let t = invert_conic_in_frame3(&c.frame, point, c.radius, c.radius)?;
1609 verify3(curve, t, point, linear)
1610 }
1611 Curve3::Ellipse(e) => {
1612 let t = invert_conic_in_frame3(&e.frame, point, e.semi_axis_x, e.semi_axis_y)?;
1613 verify3(curve, t, point, linear)
1614 }
1615 Curve3::RuledSection(r) => {
1618 let carrier = axiolid_curve::Carrier::Ruled(r.carrier);
1619 let (u, _) = carrier.parameters(point);
1620 first_on_curve(curve, &turns_of(u), point, linear)
1621 }
1622 Curve3::TorusSection(r) => {
1623 let carrier = axiolid_curve::Carrier::Torus(r.torus);
1624 let (_, v) = carrier.parameters(point);
1625 first_on_curve(curve, &turns_of(v), point, linear)
1626 }
1627 Curve3::PairSection(r) => {
1629 let t = r
1630 .parameter_of(point)
1631 .ok_or_else(|| point_not_on_curve(Scalar::INFINITY, linear))?;
1632 verify3(curve, t, point, linear)
1633 }
1634 Curve3::ImplicitSection(r) => {
1637 if let axiolid_curve::Carrier::Spline(b) = &r.carrier {
1640 let surface = axiolid_surface::Surface::BSpline((**b).clone());
1641 let (u, v) = crate::surface::locate(&surface, point, tolerance)?;
1642 let t = r
1643 .curve
1644 .parameter_of(Point2::new(u, v))
1645 .ok_or_else(|| point_not_on_curve(Scalar::INFINITY, linear))?;
1646 return verify3(curve, t, point, linear);
1647 }
1648 let (u, v) = r.carrier.parameters(point);
1649 let (pu, pv) = r.carrier.periodic();
1650 let turns = |periodic: bool| -> &'static [Scalar] {
1651 if periodic {
1652 &[0.0, 1.0, -1.0, 2.0, -2.0]
1653 } else {
1654 &[0.0]
1655 }
1656 };
1657 let mut candidates = Vec::new();
1658 for &ku in turns(pu) {
1659 for &kv in turns(pv) {
1660 let tau = std::f64::consts::TAU;
1661 if let Some(t) = r
1662 .curve
1663 .parameter_of(Point2::new(u + ku * tau, v + kv * tau))
1664 {
1665 candidates.push(t);
1666 }
1667 }
1668 }
1669 first_on_curve(curve, &candidates, point, linear)
1670 }
1671 _ => Err(no_closed_form_inversion()),
1672 }
1673}
1674
1675fn turns_of(angle: Scalar) -> Vec<Scalar> {
1677 let tau = std::f64::consts::TAU;
1678 vec![
1679 angle,
1680 angle + tau,
1681 angle - tau,
1682 angle + 2.0 * tau,
1683 angle - 2.0 * tau,
1684 ]
1685}
1686
1687fn first_on_curve(
1689 curve: &Curve3,
1690 candidates: &[Scalar],
1691 point: Point3,
1692 tolerance: Scalar,
1693) -> GeomResult<Scalar> {
1694 let mut nearest = Scalar::INFINITY;
1695 for &t in candidates {
1696 if let Ok(found) = evaluate3(curve, t) {
1697 let distance = (found - point).length();
1698 if distance <= tolerance {
1699 return Ok(t);
1700 }
1701 nearest = nearest.min(distance);
1702 }
1703 }
1704 Err(point_not_on_curve(nearest, tolerance))
1705}
1706
1707pub fn locate3(curve: &Curve3, point: Point3, tolerance: Tolerance) -> GeomResult<Scalar> {
1716 match invert3(curve, point, tolerance) {
1717 Ok(t) => Ok(t),
1718 Err(error) => match curve {
1719 Curve3::BSpline(b) => {
1720 let domain = spline_domain(b);
1721 let t = nearest_parameter(
1722 domain,
1723 b.control_points.len().max(2) * 64,
1724 |t| evaluate3(curve, t).map(|p| (p - point).length()),
1725 |t| {
1726 let (p, d, dd) = (
1727 evaluate3(curve, t)?,
1728 derivative3(curve, t)?,
1729 second_derivative3(curve, t)?,
1730 );
1731 let r = p - point;
1732 Ok((r.dot(d), d.dot(d) + r.dot(dd)))
1733 },
1734 )?;
1735 verify3(curve, t, point, tolerance.linear())
1736 }
1737 _ => Err(error),
1738 },
1739 }
1740}
1741
1742pub fn locate2(curve: &Curve2, point: Point2, tolerance: Tolerance) -> GeomResult<Scalar> {
1749 match invert2(curve, point, tolerance) {
1750 Ok(t) => Ok(t),
1751 Err(error) => match curve {
1752 Curve2::BSpline(b) => {
1753 let domain = spline_domain(b);
1754 let t = nearest_parameter(
1755 domain,
1756 b.control_points.len().max(2) * 64,
1757 |t| evaluate2(curve, t).map(|p| (p - point).length()),
1758 |t| {
1759 let (p, d, dd) = (
1760 evaluate2(curve, t)?,
1761 derivative2(curve, t)?,
1762 second_derivative2(curve, t)?,
1763 );
1764 let r = p - point;
1765 Ok((r.dot(d), d.dot(d) + r.dot(dd)))
1766 },
1767 )?;
1768 verify2(curve, t, point, tolerance.linear())
1769 }
1770 _ => Err(error),
1771 },
1772 }
1773}
1774
1775fn nearest_parameter(
1779 domain: Interval,
1780 samples: usize,
1781 distance: impl Fn(Scalar) -> GeomResult<Scalar>,
1782 gradient: impl Fn(Scalar) -> GeomResult<(Scalar, Scalar)>,
1783) -> GeomResult<Scalar> {
1784 let (lo, hi) = (domain.start.min(domain.end), domain.start.max(domain.end));
1785 let mut best = (Scalar::INFINITY, lo);
1786 for i in 0..=samples {
1787 let t = lo + (hi - lo) * i as Scalar / samples as Scalar;
1788 let d = distance(t)?;
1789 if d < best.0 {
1790 best = (d, t);
1791 }
1792 }
1793 let mut t = best.1;
1794 for _ in 0..60 {
1795 let (g, h) = gradient(t)?;
1796 if h <= 0.0 || !h.is_finite() {
1797 break;
1798 }
1799 let next = (t - g / h).clamp(lo, hi);
1800 let moved = (next - t).abs();
1801 t = next;
1802 if moved <= 4.0 * Scalar::EPSILON * (1.0 + t.abs()) {
1803 break;
1804 }
1805 }
1806 Ok(t)
1807}
1808
1809fn invert_conic_in_frame3(
1811 frame: &axiolid_core::Frame3,
1812 point: Point3,
1813 semi_x: Scalar,
1814 semi_y: Scalar,
1815) -> GeomResult<Scalar> {
1816 let offset = point - frame.origin;
1817 invert_conic(offset.dot(frame.x), offset.dot(frame.y), semi_x, semi_y)
1818}
1819
1820fn verify3(curve: &Curve3, t: Scalar, point: Point3, tolerance: Scalar) -> GeomResult<Scalar> {
1822 let found = evaluate3(curve, t)?;
1823 let distance = (found - point).length();
1824 if distance > tolerance {
1825 return Err(point_not_on_curve(distance, tolerance));
1826 }
1827 Ok(t)
1828}
1829
1830#[cfg(test)]
1831mod tests {
1832 use super::*;
1833
1834 #[test]
1838 fn a_claimed_family_never_passes_a_span_without_a_bound() {
1839 let eval = |t: Scalar| -> GeomResult<Point2> { Ok(Point2::new(t, 0.0)) };
1840 let none = |_: Scalar, _: Scalar| None;
1841 let mut out = Vec::new();
1842 subdivide(&eval, &none, 0.0, 1.0, 1e-3, 6, 1 << 10, false, &mut out).unwrap();
1843 assert!(out.is_empty());
1844 let refused = subdivide(&eval, &none, 0.0, 1.0, 1e-3, 6, 1 << 10, true, &mut out);
1845 assert!(matches!(refused, Err(GeomError::BudgetExceeded { .. })));
1846 let some = |_: Scalar, _: Scalar| Some(1e-4);
1848 subdivide(&eval, &some, 0.0, 1.0, 1e-3, 6, 1 << 10, true, &mut out).unwrap();
1849 let loose = |_: Scalar, _: Scalar| Some(1.0);
1850 let refused = subdivide(&eval, &loose, 0.0, 1.0, 1e-3, 6, 1 << 10, true, &mut out);
1851 assert!(matches!(refused, Err(GeomError::BudgetExceeded { .. })));
1852 let mut point = Vec::new();
1855 subdivide(&eval, &none, 1.0, 1.0, 1e-3, 6, 1 << 10, false, &mut point).unwrap();
1856 let refused = subdivide(&eval, &none, 1.0, 1.0, 1e-3, 6, 1 << 10, true, &mut point);
1857 assert!(matches!(refused, Err(GeomError::Degenerate(_))));
1858 let refused = subdivide(&eval, &loose, 1.0, 1.0, 1e-3, 6, 1 << 10, true, &mut point);
1859 assert!(matches!(refused, Err(GeomError::Degenerate(_))));
1860 subdivide(&eval, &some, 1.0, 1.0, 1e-3, 6, 1 << 10, true, &mut point).unwrap();
1861 }
1862}