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 _ => Interval {
96 start: 0.0,
97 end: 0.0,
98 },
99 }
100}
101
102#[must_use]
104pub fn domain3(curve: &Curve3) -> Interval {
105 match curve {
106 Curve3::Line(_) => Interval::UNIT,
107 Curve3::Circle(_) | Curve3::Ellipse(_) => full_turn(),
108 Curve3::Polyline(p) => polyline_domain(p.points.len(), p.closed),
109 Curve3::BSpline(b) => spline_domain(b),
110 Curve3::Intrinsic(i) if i.length.is_finite() && i.length > 0.0 => Interval {
114 start: 0.0,
115 end: i.length,
116 },
117 _ => Interval {
118 start: 0.0,
119 end: 0.0,
120 },
121 }
122}
123
124fn full_turn() -> Interval {
125 Interval {
126 start: 0.0,
127 end: core::f64::consts::TAU,
128 }
129}
130
131fn polyline_domain(count: usize, closed: bool) -> Interval {
133 let segments = if closed {
134 count
135 } else {
136 count.saturating_sub(1)
137 };
138 Interval {
139 start: 0.0,
140 end: segments as Scalar,
141 }
142}
143
144fn spline_domain<P>(b: &BSplineCurve<P>) -> Interval {
147 SplineAxis::new(
148 &b.knots,
149 &b.multiplicities,
150 b.degree,
151 b.control_points.len(),
152 "B-spline curve",
153 )
154 .map_or(
155 Interval {
156 start: 0.0,
157 end: 0.0,
158 },
159 |axis| {
160 let (start, end) = axis.domain();
161 Interval { start, end }
162 },
163 )
164}
165
166pub fn evaluate2(curve: &Curve2, t: Scalar) -> GeomResult<Point2> {
170 finite(t)?;
171 let value = match curve {
172 Curve2::Line(l) => Ok(line_point(l.origin, l.direction, t)),
173 Curve2::Circle(c) => Ok(conic_point2(&c.frame, c.radius, c.radius, t)),
174 Curve2::Ellipse(e) => Ok(conic_point2(&e.frame, e.semi_axis_x, e.semi_axis_y, t)),
175 Curve2::Polyline(p) => polyline_point(&p.points, p.closed, t),
176 Curve2::BSpline(b) => de_boor(b, t, |p| [p.x, p.y], |c| Point2::new(c[0], c[1])),
177 Curve2::Intrinsic(i) => crate::arc_length::intrinsic_point(i, t),
181 Curve2::Sinusoid(w) => Ok(Point2::new(t, w.height(t))),
183 Curve2::QuadraticGraph(g) => g
185 .height(t)
186 .map(|v| Point2::new(t, v))
187 .ok_or_else(|| outside_graph(t)),
188 Curve2::AngleGraph(g) => g
190 .angle(t)
191 .map(|u| Point2::new(u, t))
192 .ok_or_else(|| outside_graph(t)),
193 Curve2::Implicit(c) => c.point(t).ok_or_else(|| outside_graph(t)),
195 Curve2::Lifted(l) => {
197 if let Some((section, first)) = pair_side(l) {
199 let (a, b, _) = section.solve(t).ok_or_else(|| outside_graph(t))?;
200 return finite2(if first { a } else { b }, "curve point");
201 }
202 let p = evaluate3(&l.curve, t)?;
203 l.unwrap_at(t, p).ok_or_else(|| outside_graph(t))
204 }
205 _ => Err(GeomError::Unsupported {
208 backend: axiolid_contracts::BackendId::new("axiolid-reference"),
209 operation: axiolid_contracts::Operation::CurveEvaluation,
210 }),
211 }?;
212 finite2(value, "curve point")
213}
214
215pub fn derivative2(curve: &Curve2, t: Scalar) -> GeomResult<Vec2> {
217 finite(t)?;
218 let value = match curve {
219 Curve2::Line(l) => Ok(l.direction),
220 Curve2::Circle(c) => Ok(conic_tangent2(&c.frame, c.radius, c.radius, t)),
221 Curve2::Ellipse(e) => Ok(conic_tangent2(&e.frame, e.semi_axis_x, e.semi_axis_y, t)),
222 Curve2::Polyline(p) => polyline_tangent(&p.points, p.closed, t),
223 Curve2::BSpline(b) => de_boor_derivative(b, t, |p| [p.x, p.y], |c| Vec2::new(c[0], c[1])),
224 Curve2::Intrinsic(i) => crate::arc_length::intrinsic_tangent(i, t),
228 Curve2::Sinusoid(w) => {
229 let (sin, cos) = t.sin_cos();
230 Ok(Vec2::new(1.0, -w.cosine * sin + w.sine * cos))
231 }
232 Curve2::QuadraticGraph(g) => g
233 .slope(t)
234 .map(|slope| Vec2::new(1.0, slope))
235 .ok_or_else(|| outside_graph(t)),
236 Curve2::AngleGraph(g) => g
237 .slope(t)
238 .map(|slope| Vec2::new(slope, 1.0))
239 .ok_or_else(|| outside_graph(t)),
240 Curve2::Implicit(c) => c.derivative(t).ok_or_else(|| outside_graph(t)),
241 Curve2::Lifted(l) => lifted_rates(l, t).map(|(d, _)| d),
242 _ => Err(GeomError::Unsupported {
245 backend: axiolid_contracts::BackendId::new("axiolid-reference"),
246 operation: axiolid_contracts::Operation::CurveEvaluation,
247 }),
248 }?;
249 finite2(value, "curve derivative")
250}
251
252pub fn second_derivative2(curve: &Curve2, t: Scalar) -> GeomResult<Vec2> {
254 finite(t)?;
255 let value = match curve {
256 Curve2::Line(_) | Curve2::Polyline(_) => Ok(Vec2::ZERO),
257 Curve2::Circle(c) => Ok(conic_second2(&c.frame, c.radius, c.radius, t)),
258 Curve2::Ellipse(e) => Ok(conic_second2(&e.frame, e.semi_axis_x, e.semi_axis_y, t)),
259 Curve2::BSpline(b) => {
260 de_boor_second_derivative(b, t, |p| [p.x, p.y], |c| Vec2::new(c[0], c[1]))
261 }
262 Curve2::Sinusoid(w) => {
263 let (sin, cos) = t.sin_cos();
264 Ok(Vec2::new(0.0, -w.cosine * cos - w.sine * sin))
265 }
266 Curve2::QuadraticGraph(g) => g
267 .bend(t)
268 .map(|bend| Vec2::new(0.0, bend))
269 .ok_or_else(|| outside_graph(t)),
270 Curve2::AngleGraph(g) => g
271 .bend(t)
272 .map(|bend| Vec2::new(bend, 0.0))
273 .ok_or_else(|| outside_graph(t)),
274 Curve2::Implicit(c) => c.second_derivative(t).ok_or_else(|| outside_graph(t)),
275 Curve2::Lifted(l) => lifted_rates(l, t).map(|(_, dd)| dd),
276 _ => Err(GeomError::Unsupported {
277 backend: axiolid_contracts::BackendId::new("axiolid-reference"),
278 operation: axiolid_contracts::Operation::CurveEvaluation,
279 }),
280 }?;
281 finite2(value, "curve second derivative")
282}
283
284pub fn bspline_jet2(curve: &BSplineCurve2, t: Scalar) -> GeomResult<CurveJet<Point2, Vec2>> {
286 Ok(CurveJet {
287 point: de_boor(curve, t, |p| [p.x, p.y], |c| Point2::new(c[0], c[1]))?,
288 first: de_boor_derivative(curve, t, |p| [p.x, p.y], |c| Vec2::new(c[0], c[1]))?,
289 second: de_boor_second_derivative(curve, t, |p| [p.x, p.y], |c| Vec2::new(c[0], c[1]))?,
290 })
291}
292
293pub fn jet2(curve: &Curve2, t: Scalar) -> GeomResult<CurveJet<Point2, Vec2>> {
295 Ok(CurveJet {
296 point: evaluate2(curve, t)?,
297 first: derivative2(curve, t)?,
298 second: second_derivative2(curve, t)?,
299 })
300}
301
302fn lifted_rates(l: &axiolid_curve::LiftedCurve2, t: Scalar) -> GeomResult<(Vec2, Vec2)> {
306 if let Some((section, first)) = pair_side(l) {
307 let (a, b, _) = section.rates(t).ok_or_else(|| outside_graph(t))?;
308 let (aa, bb, _) = section.second_rates(t).ok_or_else(|| outside_graph(t))?;
309 let pick = |x: Point2, y: Point2| if first { x } else { y };
310 let (d, dd) = (pick(a, b), pick(aa, bb));
311 return Ok((Vec2::new(d.x, d.y), Vec2::new(dd.x, dd.y)));
312 }
313 let p = evaluate3(&l.curve, t)?;
314 let uv = l.unwrap_at(t, p).ok_or_else(|| outside_graph(t))?;
315 let jet = l.carrier.jet(uv.x, uv.y);
316 let (a, b, c) = (jet.u.dot(jet.u), jet.u.dot(jet.v), jet.v.dot(jet.v));
317 let det = a * c - b * b;
318 if det == 0.0 || !det.is_finite() {
319 return Err(outside_graph(t));
320 }
321 let solve = |w: Vec3| {
322 let (g0, g1) = (jet.u.dot(w), jet.v.dot(w));
323 Vec2::new((c * g0 - b * g1) / det, (a * g1 - b * g0) / det)
324 };
325 let d = solve(derivative3(&l.curve, t)?);
326 let rest = second_derivative3(&l.curve, t)?
327 - (jet.uu * (d.x * d.x) + jet.uv * (2.0 * d.x * d.y) + jet.vv * (d.y * d.y));
328 Ok((d, solve(rest)))
329}
330
331fn pair_side(l: &axiolid_curve::LiftedCurve2) -> Option<(&axiolid_curve::PairSection3, bool)> {
334 match l.curve.as_ref() {
335 Curve3::PairSection(section) => section.side(&l.carrier).map(|first| (section, first)),
336 _ => None,
337 }
338}
339
340fn outside_graph(t: Scalar) -> GeomError {
343 GeomError::Degenerate(format!(
344 "quadratic section graph has no regular point at t = {t}"
345 ))
346}
347
348pub fn evaluate3(curve: &Curve3, t: Scalar) -> GeomResult<Point3> {
352 finite(t)?;
353 let value = match curve {
354 Curve3::Line(l) => Ok(line_point(l.origin, l.direction, t)),
355 Curve3::Circle(c) => Ok(conic_point3(&c.frame, c.radius, c.radius, t)),
356 Curve3::Ellipse(e) => Ok(conic_point3(&e.frame, e.semi_axis_x, e.semi_axis_y, t)),
357 Curve3::Polyline(p) => polyline_point(&p.points, p.closed, t),
358 Curve3::BSpline(b) => de_boor(b, t, |p| [p.x, p.y, p.z], |c| Point3::new(c[0], c[1], c[2])),
359 Curve3::Intrinsic(i) => crate::frenet::frenet_point(i, t),
364 Curve3::RuledSection(r) => r.point(t).ok_or_else(|| outside_graph(t)),
366 Curve3::TorusSection(r) => r.point(t).ok_or_else(|| outside_graph(t)),
367 Curve3::ImplicitSection(r) => r.point(t).ok_or_else(|| outside_graph(t)),
368 Curve3::PairSection(r) => r.point(t).ok_or_else(|| outside_graph(t)),
369 _ => Err(GeomError::Unsupported {
372 backend: axiolid_contracts::BackendId::new("axiolid-reference"),
373 operation: axiolid_contracts::Operation::CurveEvaluation,
374 }),
375 }?;
376 finite3(value, "curve point")
377}
378
379pub fn derivative3(curve: &Curve3, t: Scalar) -> GeomResult<Vec3> {
381 finite(t)?;
382 let value = match curve {
383 Curve3::Line(l) => Ok(l.direction),
384 Curve3::Circle(c) => Ok(conic_tangent3(&c.frame, c.radius, c.radius, t)),
385 Curve3::Ellipse(e) => Ok(conic_tangent3(&e.frame, e.semi_axis_x, e.semi_axis_y, t)),
386 Curve3::Polyline(p) => polyline_tangent(&p.points, p.closed, t),
387 Curve3::BSpline(b) => {
388 de_boor_derivative(b, t, |p| [p.x, p.y, p.z], |c| Vec3::new(c[0], c[1], c[2]))
389 }
390 Curve3::Intrinsic(i) => crate::frenet::frenet_tangent(i, t),
392 Curve3::RuledSection(r) => r.tangent(t).ok_or_else(|| outside_graph(t)),
393 Curve3::TorusSection(r) => r.tangent(t).ok_or_else(|| outside_graph(t)),
394 Curve3::ImplicitSection(r) => r.tangent(t).ok_or_else(|| outside_graph(t)),
395 Curve3::PairSection(r) => r.tangent(t).ok_or_else(|| outside_graph(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 derivative")
404}
405
406pub fn second_derivative3(curve: &Curve3, t: Scalar) -> GeomResult<Vec3> {
408 finite(t)?;
409 let value = match curve {
410 Curve3::Line(_) | Curve3::Polyline(_) => Ok(Vec3::ZERO),
411 Curve3::Circle(c) => Ok(conic_second3(&c.frame, c.radius, c.radius, t)),
412 Curve3::Ellipse(e) => Ok(conic_second3(&e.frame, e.semi_axis_x, e.semi_axis_y, t)),
413 Curve3::BSpline(b) => {
414 de_boor_second_derivative(b, t, |p| [p.x, p.y, p.z], |c| Vec3::new(c[0], c[1], c[2]))
415 }
416 Curve3::RuledSection(r) => r.bend(t).ok_or_else(|| outside_graph(t)),
417 Curve3::TorusSection(r) => r.bend(t).ok_or_else(|| outside_graph(t)),
418 Curve3::ImplicitSection(r) => r.bend(t).ok_or_else(|| outside_graph(t)),
419 Curve3::PairSection(r) => r.bend(t).ok_or_else(|| outside_graph(t)),
420 _ => Err(GeomError::Unsupported {
421 backend: axiolid_contracts::BackendId::new("axiolid-reference"),
422 operation: axiolid_contracts::Operation::CurveEvaluation,
423 }),
424 }?;
425 finite3(value, "curve second derivative")
426}
427
428pub fn bspline_jet3(curve: &BSplineCurve3, t: Scalar) -> GeomResult<CurveJet<Point3, Vec3>> {
430 Ok(CurveJet {
431 point: de_boor(
432 curve,
433 t,
434 |p| [p.x, p.y, p.z],
435 |c| Point3::new(c[0], c[1], c[2]),
436 )?,
437 first: de_boor_derivative(
438 curve,
439 t,
440 |p| [p.x, p.y, p.z],
441 |c| Vec3::new(c[0], c[1], c[2]),
442 )?,
443 second: de_boor_second_derivative(
444 curve,
445 t,
446 |p| [p.x, p.y, p.z],
447 |c| Vec3::new(c[0], c[1], c[2]),
448 )?,
449 })
450}
451
452pub fn jet3(curve: &Curve3, t: Scalar) -> GeomResult<CurveJet<Point3, Vec3>> {
454 Ok(CurveJet {
455 point: evaluate3(curve, t)?,
456 first: derivative3(curve, t)?,
457 second: second_derivative3(curve, t)?,
458 })
459}
460
461fn finite(t: Scalar) -> GeomResult<()> {
464 if t.is_finite() {
465 Ok(())
466 } else {
467 Err(GeomError::InvalidInput(format!(
468 "curve parameter must be finite, got {t}"
469 )))
470 }
471}
472
473fn finite2(value: Vec2, what: &str) -> GeomResult<Vec2> {
474 if value.is_finite() {
475 Ok(value)
476 } else {
477 Err(GeomError::Degenerate(format!("{what} is non-finite")))
478 }
479}
480
481fn finite3(value: Vec3, what: &str) -> GeomResult<Vec3> {
482 if value.is_finite() {
483 Ok(value)
484 } else {
485 Err(GeomError::Degenerate(format!("{what} is non-finite")))
486 }
487}
488
489fn line_point<P>(origin: P, direction: P, t: Scalar) -> P
490where
491 P: core::ops::Add<Output = P> + core::ops::Mul<Scalar, Output = P>,
492{
493 origin + direction * t
494}
495
496fn conic_point2(frame: &Frame2, rx: Scalar, ry: Scalar, t: Scalar) -> Point2 {
497 frame.origin + frame.x * (rx * t.cos()) + frame.y * (ry * t.sin())
498}
499
500fn conic_tangent2(frame: &Frame2, rx: Scalar, ry: Scalar, t: Scalar) -> Vec2 {
501 frame.x * (-rx * t.sin()) + frame.y * (ry * t.cos())
502}
503
504fn conic_second2(frame: &Frame2, rx: Scalar, ry: Scalar, t: Scalar) -> Vec2 {
505 frame.x * (-rx * t.cos()) + frame.y * (-ry * t.sin())
506}
507
508fn conic_point3(frame: &Frame3, rx: Scalar, ry: Scalar, t: Scalar) -> Point3 {
509 frame.origin + frame.x * (rx * t.cos()) + frame.y * (ry * t.sin())
510}
511
512fn conic_tangent3(frame: &Frame3, rx: Scalar, ry: Scalar, t: Scalar) -> Vec3 {
513 frame.x * (-rx * t.sin()) + frame.y * (ry * t.cos())
514}
515
516fn conic_second3(frame: &Frame3, rx: Scalar, ry: Scalar, t: Scalar) -> Vec3 {
517 frame.x * (-rx * t.cos()) + frame.y * (-ry * t.sin())
518}
519
520fn polyline_span(count: usize, closed: bool, t: Scalar) -> Option<(usize, usize, Scalar)> {
524 let segments = if closed {
525 count
526 } else {
527 count.saturating_sub(1)
528 };
529 if count < 2 || segments == 0 {
530 return None;
531 }
532 let clamped = t.clamp(0.0, segments as Scalar);
535 let mut index = clamped.floor() as usize;
536 if index >= segments {
537 index = segments - 1;
538 }
539 let local = clamped - index as Scalar;
540 let next = (index + 1) % count;
541 Some((index, next, local))
542}
543
544fn polyline_point<P>(points: &[P], closed: bool, t: Scalar) -> GeomResult<P>
545where
546 P: Copy
547 + core::ops::Add<Output = P>
548 + core::ops::Sub<Output = P>
549 + core::ops::Mul<Scalar, Output = P>,
550{
551 let (i, j, local) = polyline_span(points.len(), closed, t).ok_or_else(|| {
552 GeomError::Degenerate(format!(
553 "polyline with {} points has no evaluable segment",
554 points.len()
555 ))
556 })?;
557 Ok(points[i] + (points[j] - points[i]) * local)
558}
559
560fn polyline_tangent<P>(points: &[P], closed: bool, t: Scalar) -> GeomResult<P>
561where
562 P: Copy + core::ops::Sub<Output = P>,
563{
564 let (i, j, _) = polyline_span(points.len(), closed, t).ok_or_else(|| {
565 GeomError::Degenerate(format!(
566 "polyline with {} points has no evaluable segment",
567 points.len()
568 ))
569 })?;
570 Ok(points[j] - points[i])
572}
573
574pub(crate) fn span_in(knots: &[Scalar], n: usize, d: usize, u: Scalar) -> usize {
582 let mut span = d;
583 for (k, knot) in knots.iter().enumerate().take(n).skip(d) {
584 if *knot <= u {
585 span = k;
586 } else {
587 break;
588 }
589 }
590 span
591}
592
593pub(crate) fn de_boor_recurrence<const N: usize>(
600 knots: &[Scalar],
601 span: usize,
602 d: usize,
603 u: Scalar,
604 points: &mut [[Scalar; N]],
605 weights: &mut [Scalar],
606) {
607 for r in 1..=d {
608 for j in (r..=d).rev() {
609 let i = span - d + j;
610 let denom = knots[i + d + 1 - r] - knots[i];
611 let alpha = if denom.abs() > 0.0 {
612 (u - knots[i]) / denom
613 } else {
614 0.0
615 };
616 for k in 0..N {
617 points[j][k] = points[j - 1][k] * (1.0 - alpha) + points[j][k] * alpha;
618 }
619 weights[j] = weights[j - 1] * (1.0 - alpha) + weights[j] * alpha;
620 }
621 }
622}
623
624fn spline_span<P>(b: &BSplineCurve<P>, t: Scalar) -> GeomResult<(Vec<Scalar>, usize, usize)> {
628 let axis = SplineAxis::new(
629 &b.knots,
630 &b.multiplicities,
631 b.degree,
632 b.control_points.len(),
633 "B-spline curve",
634 )?;
635 if let Some(weights) = &b.weights {
636 if weights.len() != b.control_points.len() {
637 return Err(GeomError::InvalidInput(format!(
638 "B-spline has {} weights for {} control points",
639 weights.len(),
640 b.control_points.len()
641 )));
642 }
643 if weights
644 .iter()
645 .any(|weight| !weight.is_finite() || *weight <= 0.0)
646 {
647 return Err(GeomError::InvalidInput(
648 "B-spline weights must be finite and strictly positive".to_owned(),
649 ));
650 }
651 }
652 let t = axis.clamp(t);
653 let span = span_in(&axis.knots, axis.count, axis.degree, t);
654 Ok((axis.knots, span, axis.degree))
655}
656
657fn finite_control_points<P, const N: usize, F>(
661 control_points: &[P],
662 to: &F,
663) -> GeomResult<Vec<[Scalar; N]>>
664where
665 F: Fn(&P) -> [Scalar; N],
666{
667 let points: Vec<[Scalar; N]> = control_points.iter().map(to).collect();
668 if points
669 .iter()
670 .flatten()
671 .any(|coordinate| !coordinate.is_finite())
672 {
673 return Err(GeomError::InvalidInput(
674 "B-spline control points must be finite".to_owned(),
675 ));
676 }
677 Ok(points)
678}
679
680fn de_boor<P, const N: usize, F, G, Q>(
686 b: &BSplineCurve<P>,
687 t: Scalar,
688 to: F,
689 from: G,
690) -> GeomResult<Q>
691where
692 F: Fn(&P) -> [Scalar; N],
693 G: Fn([Scalar; N]) -> Q,
694{
695 let (knots, span, d) = spline_span(b, t)?;
696 let control_points = finite_control_points(&b.control_points, &to)?;
697 let u = t.clamp(knots[d], knots[b.control_points.len()]);
698
699 let mut work: Vec<[Scalar; N]> = Vec::with_capacity(d + 1);
702 let mut weights: Vec<Scalar> = Vec::with_capacity(d + 1);
703 for j in 0..=d {
704 let idx = span - d + j;
705 let w = b.weights.as_ref().map_or(1.0, |ws| ws[idx]);
706 let c = control_points[idx];
707 let homogeneous = core::array::from_fn(|k| c[k] * w);
710 if homogeneous.iter().any(|value| !value.is_finite()) {
711 return Err(GeomError::Degenerate(
712 "B-spline homogeneous control point overflowed".to_owned(),
713 ));
714 }
715 work.push(homogeneous);
716 weights.push(w);
717 }
718
719 de_boor_recurrence(&knots, span, d, u, &mut work, &mut weights);
722
723 let w = weights[d];
724 if !w.is_finite() || w == 0.0 {
725 return Err(GeomError::Degenerate(
726 "B-spline weight collapsed to zero".to_owned(),
727 ));
728 }
729 Ok(from(core::array::from_fn(|k| work[d][k] / w)))
730}
731
732fn de_boor_derivative<P, const N: usize, F, G, Q>(
741 b: &BSplineCurve<P>,
742 t: Scalar,
743 to: F,
744 from: G,
745) -> GeomResult<Q>
746where
747 F: Fn(&P) -> [Scalar; N],
748 G: Fn([Scalar; N]) -> Q,
749{
750 let (knots, _, d) = spline_span(b, t)?;
751 let control_points = finite_control_points(&b.control_points, &to)?;
752 let n = b.control_points.len();
753 let u = t.clamp(knots[d], knots[n]);
754
755 let hom: Vec<[Scalar; N]> = (0..n)
757 .map(|i| {
758 let w = b.weights.as_ref().map_or(1.0, |ws| ws[i]);
759 let c = control_points[i];
760 core::array::from_fn(|k| c[k] * w)
761 })
762 .collect();
763 if hom.iter().flatten().any(|value| !value.is_finite()) {
764 return Err(GeomError::Degenerate(
765 "B-spline homogeneous control point overflowed".to_owned(),
766 ));
767 }
768 let hw: Vec<Scalar> = (0..n)
769 .map(|i| b.weights.as_ref().map_or(1.0, |ws| ws[i]))
770 .collect();
771
772 let mut dhom: Vec<[Scalar; N]> = Vec::with_capacity(n - 1);
774 let mut dhw: Vec<Scalar> = Vec::with_capacity(n - 1);
775 for i in 0..n - 1 {
776 let denom = knots[i + d + 1] - knots[i + 1];
777 let f = if denom.abs() > 0.0 {
778 d as Scalar / denom
779 } else {
780 0.0
781 };
782 dhom.push(core::array::from_fn(|k| (hom[i + 1][k] - hom[i][k]) * f));
783 dhw.push((hw[i + 1] - hw[i]) * f);
784 }
785
786 let dknots = &knots[1..knots.len() - 1];
788 let (da, dw) = eval_homogeneous(dknots, d - 1, &dhom, &dhw, u);
789 let (a, w) = eval_homogeneous(&knots, d, &hom, &hw, u);
791
792 if !w.is_finite() || w == 0.0 {
793 return Err(GeomError::Degenerate(
794 "B-spline weight collapsed to zero".to_owned(),
795 ));
796 }
797 Ok(from(core::array::from_fn(|k| {
799 (da[k] - (a[k] / w) * dw) / w
800 })))
801}
802
803fn de_boor_second_derivative<P, const N: usize, F, G, Q>(
808 b: &BSplineCurve<P>,
809 t: Scalar,
810 to: F,
811 from: G,
812) -> GeomResult<Q>
813where
814 F: Fn(&P) -> [Scalar; N],
815 G: Fn([Scalar; N]) -> Q,
816{
817 let (knots, _, degree) = spline_span(b, t)?;
818 let control_points = finite_control_points(&b.control_points, &to)?;
819 let count = b.control_points.len();
820 let u = t.clamp(knots[degree], knots[count]);
821
822 let points: Vec<[Scalar; N]> = (0..count)
823 .map(|i| {
824 let weight = b.weights.as_ref().map_or(1.0, |weights| weights[i]);
825 core::array::from_fn(|axis| control_points[i][axis] * weight)
826 })
827 .collect();
828 if points.iter().flatten().any(|value| !value.is_finite()) {
829 return Err(GeomError::Degenerate(
830 "B-spline homogeneous control point overflowed".to_owned(),
831 ));
832 }
833 let weights: Vec<Scalar> = (0..count)
834 .map(|i| b.weights.as_ref().map_or(1.0, |values| values[i]))
835 .collect();
836
837 let (point, weight) = eval_homogeneous(&knots, degree, &points, &weights, u);
838 if !weight.is_finite() || weight == 0.0 {
839 return Err(GeomError::Degenerate(
840 "B-spline weight collapsed to zero".to_owned(),
841 ));
842 }
843
844 let (first_points, first_weights) = derivative_controls(&points, &weights, &knots, degree);
845 let first_knots = &knots[1..knots.len() - 1];
846 let (first, first_weight) =
847 eval_homogeneous(first_knots, degree - 1, &first_points, &first_weights, u);
848 let position: [Scalar; N] = core::array::from_fn(|axis| point[axis] / weight);
849 let first_projected: [Scalar; N] =
850 core::array::from_fn(|axis| (first[axis] - position[axis] * first_weight) / weight);
851
852 let (second, second_weight) = if degree == 1 {
853 ([0.0; N], 0.0)
854 } else {
855 let (second_points, second_weights) =
856 derivative_controls(&first_points, &first_weights, first_knots, degree - 1);
857 let second_knots = &first_knots[1..first_knots.len() - 1];
858 eval_homogeneous(second_knots, degree - 2, &second_points, &second_weights, u)
859 };
860 Ok(from(core::array::from_fn(|axis| {
861 (second[axis] - 2.0 * first_weight * first_projected[axis] - second_weight * position[axis])
862 / weight
863 })))
864}
865
866fn derivative_controls<const N: usize>(
868 points: &[[Scalar; N]],
869 weights: &[Scalar],
870 knots: &[Scalar],
871 degree: usize,
872) -> (Vec<[Scalar; N]>, Vec<Scalar>) {
873 let mut derivative_points = Vec::with_capacity(points.len() - 1);
874 let mut derivative_weights = Vec::with_capacity(weights.len() - 1);
875 for i in 0..points.len() - 1 {
876 let denominator = knots[i + degree + 1] - knots[i + 1];
877 let factor = if denominator.abs() > 0.0 {
878 degree as Scalar / denominator
879 } else {
880 0.0
881 };
882 derivative_points.push(core::array::from_fn(|axis| {
883 (points[i + 1][axis] - points[i][axis]) * factor
884 }));
885 derivative_weights.push((weights[i + 1] - weights[i]) * factor);
886 }
887 (derivative_points, derivative_weights)
888}
889
890pub(crate) fn eval_homogeneous<const N: usize>(
892 knots: &[Scalar],
893 d: usize,
894 hom: &[[Scalar; N]],
895 hw: &[Scalar],
896 u: Scalar,
897) -> ([Scalar; N], Scalar) {
898 let n = hom.len();
899 if d == 0 {
900 let mut idx = 0;
902 for (k, knot) in knots.iter().enumerate().take(n) {
903 if *knot <= u {
904 idx = k;
905 }
906 }
907 return (hom[idx.min(n - 1)], hw[idx.min(n - 1)]);
908 }
909 let mut span = d;
910 for (k, knot) in knots.iter().enumerate().take(n).skip(d) {
911 if *knot <= u {
912 span = k;
913 } else {
914 break;
915 }
916 }
917 let mut work: Vec<[Scalar; N]> = (0..=d).map(|j| hom[span - d + j]).collect();
918 let mut weights: Vec<Scalar> = (0..=d).map(|j| hw[span - d + j]).collect();
919 for r in 1..=d {
920 for j in (r..=d).rev() {
921 let i = span - d + j;
922 let denom = knots[i + d + 1 - r] - knots[i];
923 let alpha = if denom.abs() > 0.0 {
924 (u - knots[i]) / denom
925 } else {
926 0.0
927 };
928 for k in 0..N {
929 work[j][k] = work[j - 1][k] * (1.0 - alpha) + work[j][k] * alpha;
930 }
931 weights[j] = weights[j - 1] * (1.0 - alpha) + weights[j] * alpha;
932 }
933 }
934 (work[d], weights[d])
935}
936
937pub fn flatten2(
959 curve: &Curve2,
960 domain: Interval,
961 chord_tolerance: Scalar,
962 max_depth: u32,
963) -> GeomResult<Vec<Point2>> {
964 const MAX_POINTS: usize = 1 << 16;
968 if !(chord_tolerance.is_finite()
969 && chord_tolerance.is_sign_positive()
970 && chord_tolerance != 0.0)
971 {
972 return Err(GeomError::InvalidInput(format!(
973 "chord tolerance must be positive and finite, got {chord_tolerance}"
974 )));
975 }
976 if let Curve2::Line(_) = curve {
979 return Ok(vec![
980 evaluate2(curve, domain.start)?,
981 evaluate2(curve, domain.end)?,
982 ]);
983 }
984 if let Curve2::Polyline(p) = curve {
985 let natural = polyline_domain(p.points.len(), p.closed);
991 let requested = (domain.end - domain.start).abs();
992 if natural.end > 1.0 && requested <= 1.0 {
993 return Err(GeomError::InvalidInput(format!(
994 "polyline domain {:?} spans {requested} of {} segments; a \
995 polyline parameter is one unit per segment, so this would \
996 discard {} vertices",
997 domain,
998 natural.end,
999 p.points.len().saturating_sub(2)
1000 )));
1001 }
1002 return polyline_flatten(&p.points, p.closed, domain, |t| evaluate2(curve, t));
1003 }
1004
1005 let mut out = vec![evaluate2(curve, domain.start)?];
1006 let eval = |t| evaluate2(curve, t);
1007 let bound = |a, b| crate::bound::chord_bound2(curve, a, b);
1008 for (a, b) in smooth_pieces(domain, &crate::bound::continuity_breaks2(curve, 1)) {
1009 subdivide(
1010 &eval,
1011 &bound,
1012 a,
1013 b,
1014 chord_tolerance,
1015 max_depth.min(MAX_DEPTH_CEILING),
1016 MAX_POINTS,
1017 &mut out,
1018 )?;
1019 out.push(evaluate2(curve, b)?);
1020 }
1021 Ok(out)
1022}
1023
1024fn smooth_pieces(domain: Interval, breaks: &[Scalar]) -> Vec<(Scalar, Scalar)> {
1030 let (lo, hi) = if domain.start <= domain.end {
1031 (domain.start, domain.end)
1032 } else {
1033 (domain.end, domain.start)
1034 };
1035 let mut cuts: Vec<Scalar> = breaks
1036 .iter()
1037 .copied()
1038 .filter(|&t| t > lo && t < hi)
1039 .collect();
1040 cuts.sort_by(Scalar::total_cmp);
1041 cuts.dedup();
1042 if domain.start > domain.end {
1043 cuts.reverse();
1044 }
1045 let mut out = Vec::with_capacity(cuts.len() + 1);
1046 let mut from = domain.start;
1047 for cut in cuts {
1048 out.push((from, cut));
1049 from = cut;
1050 }
1051 out.push((from, domain.end));
1052 out
1053}
1054
1055const MAX_DEPTH_CEILING: u32 = 20;
1060
1061trait ChordPoint: Copy {
1074 fn sub(self, other: Self) -> Self;
1075 fn add_scaled(self, direction: Self, scale: Scalar) -> Self;
1076 fn dot(self, other: Self) -> Scalar;
1077 fn length(self) -> Scalar;
1078 fn length_squared(self) -> Scalar;
1079}
1080
1081impl ChordPoint for Point2 {
1082 fn sub(self, other: Self) -> Self {
1083 self - other
1084 }
1085 fn add_scaled(self, direction: Self, scale: Scalar) -> Self {
1086 self + direction * scale
1087 }
1088 fn dot(self, other: Self) -> Scalar {
1089 Point2::dot(self, other)
1090 }
1091 fn length(self) -> Scalar {
1092 Point2::length(self)
1093 }
1094 fn length_squared(self) -> Scalar {
1095 Point2::length_squared(self)
1096 }
1097}
1098
1099impl ChordPoint for Point3 {
1100 fn sub(self, other: Self) -> Self {
1101 self - other
1102 }
1103 fn add_scaled(self, direction: Self, scale: Scalar) -> Self {
1104 self + direction * scale
1105 }
1106 fn dot(self, other: Self) -> Scalar {
1107 Point3::dot(self, other)
1108 }
1109 fn length(self) -> Scalar {
1110 Point3::length(self)
1111 }
1112 fn length_squared(self) -> Scalar {
1113 Point3::length_squared(self)
1114 }
1115}
1116
1117fn sagitta<P: ChordPoint>(a: P, b: P, m: P) -> Scalar {
1119 let ab = b.sub(a);
1120 let len2 = ab.length_squared();
1121 if len2 <= 0.0 {
1122 return m.sub(a).length();
1125 }
1126 let t = (m.sub(a).dot(ab) / len2).clamp(0.0, 1.0);
1127 m.sub(a.add_scaled(ab, t)).length()
1128}
1129
1130#[allow(clippy::too_many_arguments)]
1145fn subdivide<P, F, B>(
1146 eval: &F,
1147 bound: &B,
1148 a: Scalar,
1149 b: Scalar,
1150 tol: Scalar,
1151 depth: u32,
1152 budget: usize,
1153 out: &mut Vec<P>,
1154) -> GeomResult<()>
1155where
1156 P: ChordPoint,
1157 F: Fn(Scalar) -> GeomResult<P>,
1158 B: Fn(Scalar, Scalar) -> Option<Scalar>,
1159{
1160 if out.len() >= budget {
1161 return Err(GeomError::Degenerate(format!(
1162 "curve flattening exceeded {budget} points before meeting the \
1163 chord tolerance {tol}; the curve may be degenerate"
1164 )));
1165 }
1166 let mid = 0.5 * (a + b);
1167 let pa = eval(a)?;
1168 let pb = eval(b)?;
1169 if !(mid > a && mid < b) {
1175 if sagitta(pa, pb, pa) <= tol && (pb.sub(pa)).length() <= tol {
1176 return Ok(());
1177 }
1178 return Err(GeomError::Degenerate(format!(
1179 "curve parameter interval ({a}, {b}) is too small to bisect but \
1180 its chord still exceeds the tolerance {tol}"
1181 )));
1182 }
1183 let pm = eval(mid)?;
1184 if sagitta(pa, pb, pm) <= tol && bound(a, b).is_none_or(|certified| certified <= tol) {
1185 return Ok(());
1187 }
1188 if depth == 0 {
1189 return Err(GeomError::BudgetExceeded {
1190 resource: "curve flattening depth",
1191 });
1192 }
1193 subdivide(eval, bound, a, mid, tol, depth - 1, budget, out)?;
1194 out.push(pm);
1195 subdivide(eval, bound, mid, b, tol, depth - 1, budget, out)?;
1196 Ok(())
1197}
1198
1199fn polyline_flatten<P, F>(
1201 points: &[P],
1202 closed: bool,
1203 domain: Interval,
1204 eval: F,
1205) -> GeomResult<Vec<P>>
1206where
1207 P: Copy,
1208 F: Fn(Scalar) -> GeomResult<P>,
1209{
1210 let segments = if closed {
1211 points.len()
1212 } else {
1213 points.len().saturating_sub(1)
1214 };
1215 if segments == 0 {
1216 return Err(GeomError::Degenerate(
1217 "polyline has no evaluable segment".to_owned(),
1218 ));
1219 }
1220 let lo = domain.start.min(domain.end);
1221 let hi = domain.start.max(domain.end);
1222 let mut out = vec![eval(lo)?];
1223 let first = lo.floor() as i64 + 1;
1225 let last = hi.ceil() as i64 - 1;
1226 for k in first..=last {
1227 let t = k as Scalar;
1228 if t > lo && t < hi {
1229 out.push(eval(t)?);
1230 }
1231 }
1232 out.push(eval(hi)?);
1233 Ok(out)
1234}
1235
1236impl CurveEvaluator<Curve2> for ScalarCurve {
1239 type Point = Point2;
1240 type Derivative = Vec2;
1241 type Error = GeomError;
1242
1243 fn domain(&self, curve: &Curve2) -> Interval {
1244 domain2(curve)
1245 }
1246
1247 fn evaluate(
1248 &self,
1249 curve: &Curve2,
1250 t: Scalar,
1251 _tolerance: Tolerance,
1252 ) -> Result<Self::Point, Self::Error> {
1253 evaluate2(curve, t)
1254 }
1255
1256 fn derivative(
1257 &self,
1258 curve: &Curve2,
1259 t: Scalar,
1260 _tolerance: Tolerance,
1261 ) -> Result<Self::Derivative, Self::Error> {
1262 derivative2(curve, t)
1263 }
1264}
1265
1266impl CurveEvaluator<Curve3> for ScalarCurve {
1267 type Point = Point3;
1268 type Derivative = Vec3;
1269 type Error = GeomError;
1270
1271 fn domain(&self, curve: &Curve3) -> Interval {
1272 domain3(curve)
1273 }
1274
1275 fn evaluate(
1276 &self,
1277 curve: &Curve3,
1278 t: Scalar,
1279 _tolerance: Tolerance,
1280 ) -> Result<Self::Point, Self::Error> {
1281 evaluate3(curve, t)
1282 }
1283
1284 fn derivative(
1285 &self,
1286 curve: &Curve3,
1287 t: Scalar,
1288 _tolerance: Tolerance,
1289 ) -> Result<Self::Derivative, Self::Error> {
1290 derivative3(curve, t)
1291 }
1292}
1293
1294#[allow(unused)]
1296fn _type_anchors(_: Circle2, _: Circle3, _: Ellipse2, _: Ellipse3, _: Line2, _: Line3) {}
1297#[allow(unused)]
1298fn _poly_anchors(_: Polyline2, _: Polyline3) {}
1299
1300pub fn flatten3(
1308 curve: &Curve3,
1309 domain: Interval,
1310 chord_tolerance: Scalar,
1311 max_depth: u32,
1312) -> GeomResult<Vec<Point3>> {
1313 const MAX_POINTS: usize = 1 << 16;
1317 if !(chord_tolerance.is_finite()
1318 && chord_tolerance.is_sign_positive()
1319 && chord_tolerance != 0.0)
1320 {
1321 return Err(GeomError::InvalidInput(format!(
1322 "chord tolerance must be positive and finite, got {chord_tolerance}"
1323 )));
1324 }
1325 if let Curve3::Line(_) = curve {
1328 return Ok(vec![
1329 evaluate3(curve, domain.start)?,
1330 evaluate3(curve, domain.end)?,
1331 ]);
1332 }
1333 if let Curve3::Polyline(p) = curve {
1334 let natural = polyline_domain(p.points.len(), p.closed);
1338 let requested = (domain.end - domain.start).abs();
1339 if natural.end > 1.0 && requested <= 1.0 {
1340 return Err(GeomError::InvalidInput(format!(
1341 "polyline domain {:?} spans {requested} of {} segments; a \
1342 polyline parameter is one unit per segment, so this would \
1343 discard {} vertices",
1344 domain,
1345 natural.end,
1346 p.points.len().saturating_sub(2)
1347 )));
1348 }
1349 return polyline_flatten(&p.points, p.closed, domain, |t| evaluate3(curve, t));
1350 }
1351
1352 let eval = |t| evaluate3(curve, t);
1353 let bound = |a, b| crate::bound::chord_bound3(curve, a, b);
1354 let mut out = vec![eval(domain.start)?];
1355 for (a, b) in smooth_pieces(domain, &crate::bound::continuity_breaks3(curve, 1)) {
1356 subdivide(
1357 &eval,
1358 &bound,
1359 a,
1360 b,
1361 chord_tolerance,
1362 max_depth.min(MAX_DEPTH_CEILING),
1363 MAX_POINTS,
1364 &mut out,
1365 )?;
1366 out.push(eval(b)?);
1367 }
1368 Ok(out)
1369}
1370
1371fn no_closed_form_inversion() -> GeomError {
1376 GeomError::InvalidInput(
1377 "curve family has no closed form inversion; a point trim on this basis \
1378 would require iteration, which trim resolution does not perform"
1379 .to_owned(),
1380 )
1381}
1382
1383fn point_not_on_curve(distance: Scalar, tolerance: Scalar) -> GeomError {
1385 GeomError::InvalidInput(format!(
1386 "point is {distance} from the curve, outside the {tolerance} tolerance; \
1387 refusing rather than projecting it onto the nearest parameter"
1388 ))
1389}
1390
1391fn invert_line(
1397 origin: impl Into<[Scalar; 3]>,
1398 direction: [Scalar; 3],
1399 point: [Scalar; 3],
1400 tolerance: Scalar,
1401) -> GeomResult<Scalar> {
1402 let origin = origin.into();
1403 let dd = direction.iter().map(|c| c * c).sum::<Scalar>();
1404 if !dd.is_finite() || dd <= Scalar::EPSILON {
1405 return Err(GeomError::InvalidInput(
1406 "line direction is degenerate, so no parameter names a point".to_owned(),
1407 ));
1408 }
1409 let offset = [
1410 point[0] - origin[0],
1411 point[1] - origin[1],
1412 point[2] - origin[2],
1413 ];
1414 let t = offset
1415 .iter()
1416 .zip(direction.iter())
1417 .map(|(o, d)| o * d)
1418 .sum::<Scalar>()
1419 / dd;
1420 let residual = [
1423 offset[0] - direction[0] * t,
1424 offset[1] - direction[1] * t,
1425 offset[2] - direction[2] * t,
1426 ];
1427 let distance = residual.iter().map(|c| c * c).sum::<Scalar>().sqrt();
1428 if distance > tolerance {
1429 return Err(point_not_on_curve(distance, tolerance));
1430 }
1431 Ok(t)
1432}
1433
1434fn invert_conic(
1440 local_x: Scalar,
1441 local_y: Scalar,
1442 semi_x: Scalar,
1443 semi_y: Scalar,
1444) -> GeomResult<Scalar> {
1445 if !(semi_x.is_finite() && semi_y.is_finite()) || semi_x <= 0.0 || semi_y <= 0.0 {
1446 return Err(GeomError::InvalidInput(
1447 "conic semi-axes must be finite and positive to invert a point".to_owned(),
1448 ));
1449 }
1450 let angle = (local_y / semi_y).atan2(local_x / semi_x);
1451 if !angle.is_finite() {
1452 return Err(GeomError::InvalidInput(
1453 "conic inversion produced a non-finite angle".to_owned(),
1454 ));
1455 }
1456 Ok(angle.rem_euclid(std::f64::consts::TAU))
1459}
1460
1461pub fn invert2(curve: &Curve2, point: Point2, tolerance: Tolerance) -> GeomResult<Scalar> {
1473 finite2(point, "inversion point")?;
1474 let linear = tolerance.linear();
1475 match curve {
1476 Curve2::Line(l) => invert_line(
1477 [l.origin.x, l.origin.y, 0.0],
1478 [l.direction.x, l.direction.y, 0.0],
1479 [point.x, point.y, 0.0],
1480 linear,
1481 ),
1482 Curve2::Circle(c) => {
1483 let t = invert_conic_in_frame2(&c.frame, point, c.radius, c.radius)?;
1484 verify2(curve, t, point, linear)
1485 }
1486 Curve2::Ellipse(e) => {
1487 let t = invert_conic_in_frame2(&e.frame, point, e.semi_axis_x, e.semi_axis_y)?;
1488 verify2(curve, t, point, linear)
1489 }
1490 Curve2::Sinusoid(_) | Curve2::QuadraticGraph(_) => verify2(curve, point.x, point, linear),
1493 Curve2::AngleGraph(g) => {
1496 let u = g.angle(point.y).ok_or_else(|| outside_graph(point.y))?;
1497 let turns = ((point.x - u) / std::f64::consts::TAU).round();
1498 let shifted = Point2::new(point.x - turns * std::f64::consts::TAU, point.y);
1499 verify2(curve, point.y, shifted, linear)
1500 }
1501 Curve2::Lifted(l) => {
1503 let lifted = l.carrier.jet(point.x, point.y).point;
1504 let t = invert3(&l.curve, lifted, tolerance)?;
1505 verify2(curve, t, point, linear)
1506 }
1507 Curve2::Implicit(c) => {
1509 let t = c
1510 .parameter_of(point)
1511 .ok_or_else(|| point_not_on_curve(Scalar::INFINITY, linear))?;
1512 verify2(curve, t, point, linear)
1513 }
1514 _ => Err(no_closed_form_inversion()),
1515 }
1516}
1517
1518fn invert_conic_in_frame2(
1520 frame: &axiolid_core::Frame2,
1521 point: Point2,
1522 semi_x: Scalar,
1523 semi_y: Scalar,
1524) -> GeomResult<Scalar> {
1525 let offset = point - frame.origin;
1526 invert_conic(offset.dot(frame.x), offset.dot(frame.y), semi_x, semi_y)
1527}
1528
1529fn verify2(curve: &Curve2, t: Scalar, point: Point2, tolerance: Scalar) -> GeomResult<Scalar> {
1536 let found = evaluate2(curve, t)?;
1537 let distance = (found - point).length();
1538 if distance > tolerance {
1539 return Err(point_not_on_curve(distance, tolerance));
1540 }
1541 Ok(t)
1542}
1543
1544pub fn invert3(curve: &Curve3, point: Point3, tolerance: Tolerance) -> GeomResult<Scalar> {
1552 finite3(point, "inversion point")?;
1553 let linear = tolerance.linear();
1554 match curve {
1555 Curve3::Line(l) => invert_line(
1556 [l.origin.x, l.origin.y, l.origin.z],
1557 [l.direction.x, l.direction.y, l.direction.z],
1558 [point.x, point.y, point.z],
1559 linear,
1560 ),
1561 Curve3::Circle(c) => {
1562 let t = invert_conic_in_frame3(&c.frame, point, c.radius, c.radius)?;
1563 verify3(curve, t, point, linear)
1564 }
1565 Curve3::Ellipse(e) => {
1566 let t = invert_conic_in_frame3(&e.frame, point, e.semi_axis_x, e.semi_axis_y)?;
1567 verify3(curve, t, point, linear)
1568 }
1569 Curve3::RuledSection(r) => {
1572 let carrier = axiolid_curve::Carrier::Ruled(r.carrier);
1573 let (u, _) = carrier.parameters(point);
1574 first_on_curve(curve, &turns_of(u), point, linear)
1575 }
1576 Curve3::TorusSection(r) => {
1577 let carrier = axiolid_curve::Carrier::Torus(r.torus);
1578 let (_, v) = carrier.parameters(point);
1579 first_on_curve(curve, &turns_of(v), point, linear)
1580 }
1581 Curve3::PairSection(r) => {
1583 let t = r
1584 .parameter_of(point)
1585 .ok_or_else(|| point_not_on_curve(Scalar::INFINITY, linear))?;
1586 verify3(curve, t, point, linear)
1587 }
1588 Curve3::ImplicitSection(r) => {
1591 if let axiolid_curve::Carrier::Spline(b) = &r.carrier {
1594 let surface = axiolid_surface::Surface::BSpline((**b).clone());
1595 let (u, v) = crate::surface::locate(&surface, point, tolerance)?;
1596 let t = r
1597 .curve
1598 .parameter_of(Point2::new(u, v))
1599 .ok_or_else(|| point_not_on_curve(Scalar::INFINITY, linear))?;
1600 return verify3(curve, t, point, linear);
1601 }
1602 let (u, v) = r.carrier.parameters(point);
1603 let (pu, pv) = r.carrier.periodic();
1604 let turns = |periodic: bool| -> &'static [Scalar] {
1605 if periodic {
1606 &[0.0, 1.0, -1.0, 2.0, -2.0]
1607 } else {
1608 &[0.0]
1609 }
1610 };
1611 let mut candidates = Vec::new();
1612 for &ku in turns(pu) {
1613 for &kv in turns(pv) {
1614 let tau = std::f64::consts::TAU;
1615 if let Some(t) = r
1616 .curve
1617 .parameter_of(Point2::new(u + ku * tau, v + kv * tau))
1618 {
1619 candidates.push(t);
1620 }
1621 }
1622 }
1623 first_on_curve(curve, &candidates, point, linear)
1624 }
1625 _ => Err(no_closed_form_inversion()),
1626 }
1627}
1628
1629fn turns_of(angle: Scalar) -> Vec<Scalar> {
1631 let tau = std::f64::consts::TAU;
1632 vec![
1633 angle,
1634 angle + tau,
1635 angle - tau,
1636 angle + 2.0 * tau,
1637 angle - 2.0 * tau,
1638 ]
1639}
1640
1641fn first_on_curve(
1643 curve: &Curve3,
1644 candidates: &[Scalar],
1645 point: Point3,
1646 tolerance: Scalar,
1647) -> GeomResult<Scalar> {
1648 let mut nearest = Scalar::INFINITY;
1649 for &t in candidates {
1650 if let Ok(found) = evaluate3(curve, t) {
1651 let distance = (found - point).length();
1652 if distance <= tolerance {
1653 return Ok(t);
1654 }
1655 nearest = nearest.min(distance);
1656 }
1657 }
1658 Err(point_not_on_curve(nearest, tolerance))
1659}
1660
1661pub fn locate3(curve: &Curve3, point: Point3, tolerance: Tolerance) -> GeomResult<Scalar> {
1670 match invert3(curve, point, tolerance) {
1671 Ok(t) => Ok(t),
1672 Err(error) => match curve {
1673 Curve3::BSpline(b) => {
1674 let domain = spline_domain(b);
1675 let t = nearest_parameter(
1676 domain,
1677 b.control_points.len().max(2) * 64,
1678 |t| evaluate3(curve, t).map(|p| (p - point).length()),
1679 |t| {
1680 let (p, d, dd) = (
1681 evaluate3(curve, t)?,
1682 derivative3(curve, t)?,
1683 second_derivative3(curve, t)?,
1684 );
1685 let r = p - point;
1686 Ok((r.dot(d), d.dot(d) + r.dot(dd)))
1687 },
1688 )?;
1689 verify3(curve, t, point, tolerance.linear())
1690 }
1691 _ => Err(error),
1692 },
1693 }
1694}
1695
1696pub fn locate2(curve: &Curve2, point: Point2, tolerance: Tolerance) -> GeomResult<Scalar> {
1703 match invert2(curve, point, tolerance) {
1704 Ok(t) => Ok(t),
1705 Err(error) => match curve {
1706 Curve2::BSpline(b) => {
1707 let domain = spline_domain(b);
1708 let t = nearest_parameter(
1709 domain,
1710 b.control_points.len().max(2) * 64,
1711 |t| evaluate2(curve, t).map(|p| (p - point).length()),
1712 |t| {
1713 let (p, d, dd) = (
1714 evaluate2(curve, t)?,
1715 derivative2(curve, t)?,
1716 second_derivative2(curve, t)?,
1717 );
1718 let r = p - point;
1719 Ok((r.dot(d), d.dot(d) + r.dot(dd)))
1720 },
1721 )?;
1722 verify2(curve, t, point, tolerance.linear())
1723 }
1724 _ => Err(error),
1725 },
1726 }
1727}
1728
1729fn nearest_parameter(
1733 domain: Interval,
1734 samples: usize,
1735 distance: impl Fn(Scalar) -> GeomResult<Scalar>,
1736 gradient: impl Fn(Scalar) -> GeomResult<(Scalar, Scalar)>,
1737) -> GeomResult<Scalar> {
1738 let (lo, hi) = (domain.start.min(domain.end), domain.start.max(domain.end));
1739 let mut best = (Scalar::INFINITY, lo);
1740 for i in 0..=samples {
1741 let t = lo + (hi - lo) * i as Scalar / samples as Scalar;
1742 let d = distance(t)?;
1743 if d < best.0 {
1744 best = (d, t);
1745 }
1746 }
1747 let mut t = best.1;
1748 for _ in 0..60 {
1749 let (g, h) = gradient(t)?;
1750 if h <= 0.0 || !h.is_finite() {
1751 break;
1752 }
1753 let next = (t - g / h).clamp(lo, hi);
1754 let moved = (next - t).abs();
1755 t = next;
1756 if moved <= 4.0 * Scalar::EPSILON * (1.0 + t.abs()) {
1757 break;
1758 }
1759 }
1760 Ok(t)
1761}
1762
1763fn invert_conic_in_frame3(
1765 frame: &axiolid_core::Frame3,
1766 point: Point3,
1767 semi_x: Scalar,
1768 semi_y: Scalar,
1769) -> GeomResult<Scalar> {
1770 let offset = point - frame.origin;
1771 invert_conic(offset.dot(frame.x), offset.dot(frame.y), semi_x, semi_y)
1772}
1773
1774fn verify3(curve: &Curve3, t: Scalar, point: Point3, tolerance: Scalar) -> GeomResult<Scalar> {
1776 let found = evaluate3(curve, t)?;
1777 let distance = (found - point).length();
1778 if distance > tolerance {
1779 return Err(point_not_on_curve(distance, tolerance));
1780 }
1781 Ok(t)
1782}