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 _ => Interval {
94 start: 0.0,
95 end: 0.0,
96 },
97 }
98}
99
100#[must_use]
102pub fn domain3(curve: &Curve3) -> Interval {
103 match curve {
104 Curve3::Line(_) => Interval::UNIT,
105 Curve3::Circle(_) | Curve3::Ellipse(_) => full_turn(),
106 Curve3::Polyline(p) => polyline_domain(p.points.len(), p.closed),
107 Curve3::BSpline(b) => spline_domain(b),
108 Curve3::Intrinsic(i) if i.length.is_finite() && i.length > 0.0 => Interval {
112 start: 0.0,
113 end: i.length,
114 },
115 _ => Interval {
116 start: 0.0,
117 end: 0.0,
118 },
119 }
120}
121
122fn full_turn() -> Interval {
123 Interval {
124 start: 0.0,
125 end: core::f64::consts::TAU,
126 }
127}
128
129fn polyline_domain(count: usize, closed: bool) -> Interval {
131 let segments = if closed {
132 count
133 } else {
134 count.saturating_sub(1)
135 };
136 Interval {
137 start: 0.0,
138 end: segments as Scalar,
139 }
140}
141
142fn spline_domain<P>(b: &BSplineCurve<P>) -> Interval {
145 SplineAxis::new(
146 &b.knots,
147 &b.multiplicities,
148 b.degree,
149 b.control_points.len(),
150 "B-spline curve",
151 )
152 .map_or(
153 Interval {
154 start: 0.0,
155 end: 0.0,
156 },
157 |axis| {
158 let (start, end) = axis.domain();
159 Interval { start, end }
160 },
161 )
162}
163
164pub fn evaluate2(curve: &Curve2, t: Scalar) -> GeomResult<Point2> {
168 finite(t)?;
169 let value = match curve {
170 Curve2::Line(l) => Ok(line_point(l.origin, l.direction, t)),
171 Curve2::Circle(c) => Ok(conic_point2(&c.frame, c.radius, c.radius, t)),
172 Curve2::Ellipse(e) => Ok(conic_point2(&e.frame, e.semi_axis_x, e.semi_axis_y, t)),
173 Curve2::Polyline(p) => polyline_point(&p.points, p.closed, t),
174 Curve2::BSpline(b) => de_boor(b, t, |p| [p.x, p.y], |c| Point2::new(c[0], c[1])),
175 Curve2::Intrinsic(i) => crate::arc_length::intrinsic_point(i, t),
179 _ => Err(GeomError::Unsupported {
182 backend: axiolid_contracts::BackendId::new("axiolid-reference"),
183 operation: axiolid_contracts::Operation::CurveEvaluation,
184 }),
185 }?;
186 finite2(value, "curve point")
187}
188
189pub fn derivative2(curve: &Curve2, t: Scalar) -> GeomResult<Vec2> {
191 finite(t)?;
192 let value = match curve {
193 Curve2::Line(l) => Ok(l.direction),
194 Curve2::Circle(c) => Ok(conic_tangent2(&c.frame, c.radius, c.radius, t)),
195 Curve2::Ellipse(e) => Ok(conic_tangent2(&e.frame, e.semi_axis_x, e.semi_axis_y, t)),
196 Curve2::Polyline(p) => polyline_tangent(&p.points, p.closed, t),
197 Curve2::BSpline(b) => de_boor_derivative(b, t, |p| [p.x, p.y], |c| Vec2::new(c[0], c[1])),
198 Curve2::Intrinsic(i) => crate::arc_length::intrinsic_tangent(i, t),
202 _ => Err(GeomError::Unsupported {
205 backend: axiolid_contracts::BackendId::new("axiolid-reference"),
206 operation: axiolid_contracts::Operation::CurveEvaluation,
207 }),
208 }?;
209 finite2(value, "curve derivative")
210}
211
212pub fn second_derivative2(curve: &Curve2, t: Scalar) -> GeomResult<Vec2> {
214 finite(t)?;
215 let value = match curve {
216 Curve2::Line(_) | Curve2::Polyline(_) => Ok(Vec2::ZERO),
217 Curve2::Circle(c) => Ok(conic_second2(&c.frame, c.radius, c.radius, t)),
218 Curve2::Ellipse(e) => Ok(conic_second2(&e.frame, e.semi_axis_x, e.semi_axis_y, t)),
219 Curve2::BSpline(b) => {
220 de_boor_second_derivative(b, t, |p| [p.x, p.y], |c| Vec2::new(c[0], c[1]))
221 }
222 _ => Err(GeomError::Unsupported {
223 backend: axiolid_contracts::BackendId::new("axiolid-reference"),
224 operation: axiolid_contracts::Operation::CurveEvaluation,
225 }),
226 }?;
227 finite2(value, "curve second derivative")
228}
229
230pub fn bspline_jet2(curve: &BSplineCurve2, t: Scalar) -> GeomResult<CurveJet<Point2, Vec2>> {
232 Ok(CurveJet {
233 point: de_boor(curve, t, |p| [p.x, p.y], |c| Point2::new(c[0], c[1]))?,
234 first: de_boor_derivative(curve, t, |p| [p.x, p.y], |c| Vec2::new(c[0], c[1]))?,
235 second: de_boor_second_derivative(curve, t, |p| [p.x, p.y], |c| Vec2::new(c[0], c[1]))?,
236 })
237}
238
239pub fn jet2(curve: &Curve2, t: Scalar) -> GeomResult<CurveJet<Point2, Vec2>> {
241 Ok(CurveJet {
242 point: evaluate2(curve, t)?,
243 first: derivative2(curve, t)?,
244 second: second_derivative2(curve, t)?,
245 })
246}
247
248pub fn evaluate3(curve: &Curve3, t: Scalar) -> GeomResult<Point3> {
252 finite(t)?;
253 let value = match curve {
254 Curve3::Line(l) => Ok(line_point(l.origin, l.direction, t)),
255 Curve3::Circle(c) => Ok(conic_point3(&c.frame, c.radius, c.radius, t)),
256 Curve3::Ellipse(e) => Ok(conic_point3(&e.frame, e.semi_axis_x, e.semi_axis_y, t)),
257 Curve3::Polyline(p) => polyline_point(&p.points, p.closed, t),
258 Curve3::BSpline(b) => de_boor(b, t, |p| [p.x, p.y, p.z], |c| Point3::new(c[0], c[1], c[2])),
259 Curve3::Intrinsic(i) => crate::frenet::frenet_point(i, t),
264 _ => Err(GeomError::Unsupported {
267 backend: axiolid_contracts::BackendId::new("axiolid-reference"),
268 operation: axiolid_contracts::Operation::CurveEvaluation,
269 }),
270 }?;
271 finite3(value, "curve point")
272}
273
274pub fn derivative3(curve: &Curve3, t: Scalar) -> GeomResult<Vec3> {
276 finite(t)?;
277 let value = match curve {
278 Curve3::Line(l) => Ok(l.direction),
279 Curve3::Circle(c) => Ok(conic_tangent3(&c.frame, c.radius, c.radius, t)),
280 Curve3::Ellipse(e) => Ok(conic_tangent3(&e.frame, e.semi_axis_x, e.semi_axis_y, t)),
281 Curve3::Polyline(p) => polyline_tangent(&p.points, p.closed, t),
282 Curve3::BSpline(b) => {
283 de_boor_derivative(b, t, |p| [p.x, p.y, p.z], |c| Vec3::new(c[0], c[1], c[2]))
284 }
285 Curve3::Intrinsic(i) => crate::frenet::frenet_tangent(i, t),
287 _ => Err(GeomError::Unsupported {
290 backend: axiolid_contracts::BackendId::new("axiolid-reference"),
291 operation: axiolid_contracts::Operation::CurveEvaluation,
292 }),
293 }?;
294 finite3(value, "curve derivative")
295}
296
297pub fn second_derivative3(curve: &Curve3, t: Scalar) -> GeomResult<Vec3> {
299 finite(t)?;
300 let value = match curve {
301 Curve3::Line(_) | Curve3::Polyline(_) => Ok(Vec3::ZERO),
302 Curve3::Circle(c) => Ok(conic_second3(&c.frame, c.radius, c.radius, t)),
303 Curve3::Ellipse(e) => Ok(conic_second3(&e.frame, e.semi_axis_x, e.semi_axis_y, t)),
304 Curve3::BSpline(b) => {
305 de_boor_second_derivative(b, t, |p| [p.x, p.y, p.z], |c| Vec3::new(c[0], c[1], c[2]))
306 }
307 _ => Err(GeomError::Unsupported {
308 backend: axiolid_contracts::BackendId::new("axiolid-reference"),
309 operation: axiolid_contracts::Operation::CurveEvaluation,
310 }),
311 }?;
312 finite3(value, "curve second derivative")
313}
314
315pub fn bspline_jet3(curve: &BSplineCurve3, t: Scalar) -> GeomResult<CurveJet<Point3, Vec3>> {
317 Ok(CurveJet {
318 point: de_boor(
319 curve,
320 t,
321 |p| [p.x, p.y, p.z],
322 |c| Point3::new(c[0], c[1], c[2]),
323 )?,
324 first: de_boor_derivative(
325 curve,
326 t,
327 |p| [p.x, p.y, p.z],
328 |c| Vec3::new(c[0], c[1], c[2]),
329 )?,
330 second: de_boor_second_derivative(
331 curve,
332 t,
333 |p| [p.x, p.y, p.z],
334 |c| Vec3::new(c[0], c[1], c[2]),
335 )?,
336 })
337}
338
339pub fn jet3(curve: &Curve3, t: Scalar) -> GeomResult<CurveJet<Point3, Vec3>> {
341 Ok(CurveJet {
342 point: evaluate3(curve, t)?,
343 first: derivative3(curve, t)?,
344 second: second_derivative3(curve, t)?,
345 })
346}
347
348fn finite(t: Scalar) -> GeomResult<()> {
351 if t.is_finite() {
352 Ok(())
353 } else {
354 Err(GeomError::InvalidInput(format!(
355 "curve parameter must be finite, got {t}"
356 )))
357 }
358}
359
360fn finite2(value: Vec2, what: &str) -> GeomResult<Vec2> {
361 if value.is_finite() {
362 Ok(value)
363 } else {
364 Err(GeomError::Degenerate(format!("{what} is non-finite")))
365 }
366}
367
368fn finite3(value: Vec3, what: &str) -> GeomResult<Vec3> {
369 if value.is_finite() {
370 Ok(value)
371 } else {
372 Err(GeomError::Degenerate(format!("{what} is non-finite")))
373 }
374}
375
376fn line_point<P>(origin: P, direction: P, t: Scalar) -> P
377where
378 P: core::ops::Add<Output = P> + core::ops::Mul<Scalar, Output = P>,
379{
380 origin + direction * t
381}
382
383fn conic_point2(frame: &Frame2, rx: Scalar, ry: Scalar, t: Scalar) -> Point2 {
384 frame.origin + frame.x * (rx * t.cos()) + frame.y * (ry * t.sin())
385}
386
387fn conic_tangent2(frame: &Frame2, rx: Scalar, ry: Scalar, t: Scalar) -> Vec2 {
388 frame.x * (-rx * t.sin()) + frame.y * (ry * t.cos())
389}
390
391fn conic_second2(frame: &Frame2, rx: Scalar, ry: Scalar, t: Scalar) -> Vec2 {
392 frame.x * (-rx * t.cos()) + frame.y * (-ry * t.sin())
393}
394
395fn conic_point3(frame: &Frame3, rx: Scalar, ry: Scalar, t: Scalar) -> Point3 {
396 frame.origin + frame.x * (rx * t.cos()) + frame.y * (ry * t.sin())
397}
398
399fn conic_tangent3(frame: &Frame3, rx: Scalar, ry: Scalar, t: Scalar) -> Vec3 {
400 frame.x * (-rx * t.sin()) + frame.y * (ry * t.cos())
401}
402
403fn conic_second3(frame: &Frame3, rx: Scalar, ry: Scalar, t: Scalar) -> Vec3 {
404 frame.x * (-rx * t.cos()) + frame.y * (-ry * t.sin())
405}
406
407fn polyline_span(count: usize, closed: bool, t: Scalar) -> Option<(usize, usize, Scalar)> {
411 let segments = if closed {
412 count
413 } else {
414 count.saturating_sub(1)
415 };
416 if count < 2 || segments == 0 {
417 return None;
418 }
419 let clamped = t.clamp(0.0, segments as Scalar);
422 let mut index = clamped.floor() as usize;
423 if index >= segments {
424 index = segments - 1;
425 }
426 let local = clamped - index as Scalar;
427 let next = (index + 1) % count;
428 Some((index, next, local))
429}
430
431fn polyline_point<P>(points: &[P], closed: bool, t: Scalar) -> GeomResult<P>
432where
433 P: Copy
434 + core::ops::Add<Output = P>
435 + core::ops::Sub<Output = P>
436 + core::ops::Mul<Scalar, Output = P>,
437{
438 let (i, j, local) = polyline_span(points.len(), closed, t).ok_or_else(|| {
439 GeomError::Degenerate(format!(
440 "polyline with {} points has no evaluable segment",
441 points.len()
442 ))
443 })?;
444 Ok(points[i] + (points[j] - points[i]) * local)
445}
446
447fn polyline_tangent<P>(points: &[P], closed: bool, t: Scalar) -> GeomResult<P>
448where
449 P: Copy + core::ops::Sub<Output = P>,
450{
451 let (i, j, _) = polyline_span(points.len(), closed, t).ok_or_else(|| {
452 GeomError::Degenerate(format!(
453 "polyline with {} points has no evaluable segment",
454 points.len()
455 ))
456 })?;
457 Ok(points[j] - points[i])
459}
460
461pub(crate) fn span_in(knots: &[Scalar], n: usize, d: usize, u: Scalar) -> usize {
469 let mut span = d;
470 for (k, knot) in knots.iter().enumerate().take(n).skip(d) {
471 if *knot <= u {
472 span = k;
473 } else {
474 break;
475 }
476 }
477 span
478}
479
480pub(crate) fn de_boor_recurrence<const N: usize>(
487 knots: &[Scalar],
488 span: usize,
489 d: usize,
490 u: Scalar,
491 points: &mut [[Scalar; N]],
492 weights: &mut [Scalar],
493) {
494 for r in 1..=d {
495 for j in (r..=d).rev() {
496 let i = span - d + j;
497 let denom = knots[i + d + 1 - r] - knots[i];
498 let alpha = if denom.abs() > 0.0 {
499 (u - knots[i]) / denom
500 } else {
501 0.0
502 };
503 for k in 0..N {
504 points[j][k] = points[j - 1][k] * (1.0 - alpha) + points[j][k] * alpha;
505 }
506 weights[j] = weights[j - 1] * (1.0 - alpha) + weights[j] * alpha;
507 }
508 }
509}
510
511fn spline_span<P>(b: &BSplineCurve<P>, t: Scalar) -> GeomResult<(Vec<Scalar>, usize, usize)> {
515 let axis = SplineAxis::new(
516 &b.knots,
517 &b.multiplicities,
518 b.degree,
519 b.control_points.len(),
520 "B-spline curve",
521 )?;
522 if let Some(weights) = &b.weights {
523 if weights.len() != b.control_points.len() {
524 return Err(GeomError::InvalidInput(format!(
525 "B-spline has {} weights for {} control points",
526 weights.len(),
527 b.control_points.len()
528 )));
529 }
530 if weights
531 .iter()
532 .any(|weight| !weight.is_finite() || *weight <= 0.0)
533 {
534 return Err(GeomError::InvalidInput(
535 "B-spline weights must be finite and strictly positive".to_owned(),
536 ));
537 }
538 }
539 let t = axis.clamp(t);
540 let span = span_in(&axis.knots, axis.count, axis.degree, t);
541 Ok((axis.knots, span, axis.degree))
542}
543
544fn finite_control_points<P, const N: usize, F>(
548 control_points: &[P],
549 to: &F,
550) -> GeomResult<Vec<[Scalar; N]>>
551where
552 F: Fn(&P) -> [Scalar; N],
553{
554 let points: Vec<[Scalar; N]> = control_points.iter().map(to).collect();
555 if points
556 .iter()
557 .flatten()
558 .any(|coordinate| !coordinate.is_finite())
559 {
560 return Err(GeomError::InvalidInput(
561 "B-spline control points must be finite".to_owned(),
562 ));
563 }
564 Ok(points)
565}
566
567fn de_boor<P, const N: usize, F, G, Q>(
573 b: &BSplineCurve<P>,
574 t: Scalar,
575 to: F,
576 from: G,
577) -> GeomResult<Q>
578where
579 F: Fn(&P) -> [Scalar; N],
580 G: Fn([Scalar; N]) -> Q,
581{
582 let (knots, span, d) = spline_span(b, t)?;
583 let control_points = finite_control_points(&b.control_points, &to)?;
584 let u = t.clamp(knots[d], knots[b.control_points.len()]);
585
586 let mut work: Vec<[Scalar; N]> = Vec::with_capacity(d + 1);
589 let mut weights: Vec<Scalar> = Vec::with_capacity(d + 1);
590 for j in 0..=d {
591 let idx = span - d + j;
592 let w = b.weights.as_ref().map_or(1.0, |ws| ws[idx]);
593 let c = control_points[idx];
594 let homogeneous = core::array::from_fn(|k| c[k] * w);
597 if homogeneous.iter().any(|value| !value.is_finite()) {
598 return Err(GeomError::Degenerate(
599 "B-spline homogeneous control point overflowed".to_owned(),
600 ));
601 }
602 work.push(homogeneous);
603 weights.push(w);
604 }
605
606 de_boor_recurrence(&knots, span, d, u, &mut work, &mut weights);
609
610 let w = weights[d];
611 if !w.is_finite() || w == 0.0 {
612 return Err(GeomError::Degenerate(
613 "B-spline weight collapsed to zero".to_owned(),
614 ));
615 }
616 Ok(from(core::array::from_fn(|k| work[d][k] / w)))
617}
618
619fn de_boor_derivative<P, const N: usize, F, G, Q>(
628 b: &BSplineCurve<P>,
629 t: Scalar,
630 to: F,
631 from: G,
632) -> GeomResult<Q>
633where
634 F: Fn(&P) -> [Scalar; N],
635 G: Fn([Scalar; N]) -> Q,
636{
637 let (knots, _, d) = spline_span(b, t)?;
638 let control_points = finite_control_points(&b.control_points, &to)?;
639 let n = b.control_points.len();
640 let u = t.clamp(knots[d], knots[n]);
641
642 let hom: Vec<[Scalar; N]> = (0..n)
644 .map(|i| {
645 let w = b.weights.as_ref().map_or(1.0, |ws| ws[i]);
646 let c = control_points[i];
647 core::array::from_fn(|k| c[k] * w)
648 })
649 .collect();
650 if hom.iter().flatten().any(|value| !value.is_finite()) {
651 return Err(GeomError::Degenerate(
652 "B-spline homogeneous control point overflowed".to_owned(),
653 ));
654 }
655 let hw: Vec<Scalar> = (0..n)
656 .map(|i| b.weights.as_ref().map_or(1.0, |ws| ws[i]))
657 .collect();
658
659 let mut dhom: Vec<[Scalar; N]> = Vec::with_capacity(n - 1);
661 let mut dhw: Vec<Scalar> = Vec::with_capacity(n - 1);
662 for i in 0..n - 1 {
663 let denom = knots[i + d + 1] - knots[i + 1];
664 let f = if denom.abs() > 0.0 {
665 d as Scalar / denom
666 } else {
667 0.0
668 };
669 dhom.push(core::array::from_fn(|k| (hom[i + 1][k] - hom[i][k]) * f));
670 dhw.push((hw[i + 1] - hw[i]) * f);
671 }
672
673 let dknots = &knots[1..knots.len() - 1];
675 let (da, dw) = eval_homogeneous(dknots, d - 1, &dhom, &dhw, u);
676 let (a, w) = eval_homogeneous(&knots, d, &hom, &hw, u);
678
679 if !w.is_finite() || w == 0.0 {
680 return Err(GeomError::Degenerate(
681 "B-spline weight collapsed to zero".to_owned(),
682 ));
683 }
684 Ok(from(core::array::from_fn(|k| {
686 (da[k] - (a[k] / w) * dw) / w
687 })))
688}
689
690fn de_boor_second_derivative<P, const N: usize, F, G, Q>(
695 b: &BSplineCurve<P>,
696 t: Scalar,
697 to: F,
698 from: G,
699) -> GeomResult<Q>
700where
701 F: Fn(&P) -> [Scalar; N],
702 G: Fn([Scalar; N]) -> Q,
703{
704 let (knots, _, degree) = spline_span(b, t)?;
705 let control_points = finite_control_points(&b.control_points, &to)?;
706 let count = b.control_points.len();
707 let u = t.clamp(knots[degree], knots[count]);
708
709 let points: Vec<[Scalar; N]> = (0..count)
710 .map(|i| {
711 let weight = b.weights.as_ref().map_or(1.0, |weights| weights[i]);
712 core::array::from_fn(|axis| control_points[i][axis] * weight)
713 })
714 .collect();
715 if points.iter().flatten().any(|value| !value.is_finite()) {
716 return Err(GeomError::Degenerate(
717 "B-spline homogeneous control point overflowed".to_owned(),
718 ));
719 }
720 let weights: Vec<Scalar> = (0..count)
721 .map(|i| b.weights.as_ref().map_or(1.0, |values| values[i]))
722 .collect();
723
724 let (point, weight) = eval_homogeneous(&knots, degree, &points, &weights, u);
725 if !weight.is_finite() || weight == 0.0 {
726 return Err(GeomError::Degenerate(
727 "B-spline weight collapsed to zero".to_owned(),
728 ));
729 }
730
731 let (first_points, first_weights) = derivative_controls(&points, &weights, &knots, degree);
732 let first_knots = &knots[1..knots.len() - 1];
733 let (first, first_weight) =
734 eval_homogeneous(first_knots, degree - 1, &first_points, &first_weights, u);
735 let position: [Scalar; N] = core::array::from_fn(|axis| point[axis] / weight);
736 let first_projected: [Scalar; N] =
737 core::array::from_fn(|axis| (first[axis] - position[axis] * first_weight) / weight);
738
739 let (second, second_weight) = if degree == 1 {
740 ([0.0; N], 0.0)
741 } else {
742 let (second_points, second_weights) =
743 derivative_controls(&first_points, &first_weights, first_knots, degree - 1);
744 let second_knots = &first_knots[1..first_knots.len() - 1];
745 eval_homogeneous(second_knots, degree - 2, &second_points, &second_weights, u)
746 };
747 Ok(from(core::array::from_fn(|axis| {
748 (second[axis] - 2.0 * first_weight * first_projected[axis] - second_weight * position[axis])
749 / weight
750 })))
751}
752
753fn derivative_controls<const N: usize>(
755 points: &[[Scalar; N]],
756 weights: &[Scalar],
757 knots: &[Scalar],
758 degree: usize,
759) -> (Vec<[Scalar; N]>, Vec<Scalar>) {
760 let mut derivative_points = Vec::with_capacity(points.len() - 1);
761 let mut derivative_weights = Vec::with_capacity(weights.len() - 1);
762 for i in 0..points.len() - 1 {
763 let denominator = knots[i + degree + 1] - knots[i + 1];
764 let factor = if denominator.abs() > 0.0 {
765 degree as Scalar / denominator
766 } else {
767 0.0
768 };
769 derivative_points.push(core::array::from_fn(|axis| {
770 (points[i + 1][axis] - points[i][axis]) * factor
771 }));
772 derivative_weights.push((weights[i + 1] - weights[i]) * factor);
773 }
774 (derivative_points, derivative_weights)
775}
776
777pub(crate) fn eval_homogeneous<const N: usize>(
779 knots: &[Scalar],
780 d: usize,
781 hom: &[[Scalar; N]],
782 hw: &[Scalar],
783 u: Scalar,
784) -> ([Scalar; N], Scalar) {
785 let n = hom.len();
786 if d == 0 {
787 let mut idx = 0;
789 for (k, knot) in knots.iter().enumerate().take(n) {
790 if *knot <= u {
791 idx = k;
792 }
793 }
794 return (hom[idx.min(n - 1)], hw[idx.min(n - 1)]);
795 }
796 let mut span = d;
797 for (k, knot) in knots.iter().enumerate().take(n).skip(d) {
798 if *knot <= u {
799 span = k;
800 } else {
801 break;
802 }
803 }
804 let mut work: Vec<[Scalar; N]> = (0..=d).map(|j| hom[span - d + j]).collect();
805 let mut weights: Vec<Scalar> = (0..=d).map(|j| hw[span - d + j]).collect();
806 for r in 1..=d {
807 for j in (r..=d).rev() {
808 let i = span - d + j;
809 let denom = knots[i + d + 1 - r] - knots[i];
810 let alpha = if denom.abs() > 0.0 {
811 (u - knots[i]) / denom
812 } else {
813 0.0
814 };
815 for k in 0..N {
816 work[j][k] = work[j - 1][k] * (1.0 - alpha) + work[j][k] * alpha;
817 }
818 weights[j] = weights[j - 1] * (1.0 - alpha) + weights[j] * alpha;
819 }
820 }
821 (work[d], weights[d])
822}
823
824pub fn flatten2(
841 curve: &Curve2,
842 domain: Interval,
843 chord_tolerance: Scalar,
844 max_depth: u32,
845) -> GeomResult<Vec<Point2>> {
846 const MAX_POINTS: usize = 1 << 16;
850 if !(chord_tolerance.is_finite()
851 && chord_tolerance.is_sign_positive()
852 && chord_tolerance != 0.0)
853 {
854 return Err(GeomError::InvalidInput(format!(
855 "chord tolerance must be positive and finite, got {chord_tolerance}"
856 )));
857 }
858 if let Curve2::Line(_) = curve {
861 return Ok(vec![
862 evaluate2(curve, domain.start)?,
863 evaluate2(curve, domain.end)?,
864 ]);
865 }
866 if let Curve2::Polyline(p) = curve {
867 let natural = polyline_domain(p.points.len(), p.closed);
873 let requested = (domain.end - domain.start).abs();
874 if natural.end > 1.0 && requested <= 1.0 {
875 return Err(GeomError::InvalidInput(format!(
876 "polyline domain {:?} spans {requested} of {} segments; a \
877 polyline parameter is one unit per segment, so this would \
878 discard {} vertices",
879 domain,
880 natural.end,
881 p.points.len().saturating_sub(2)
882 )));
883 }
884 return polyline_flatten(&p.points, p.closed, domain, |t| evaluate2(curve, t));
885 }
886
887 let mut out = vec![evaluate2(curve, domain.start)?];
888 let eval = |t| evaluate2(curve, t);
889 subdivide(
890 &eval,
891 domain.start,
892 domain.end,
893 chord_tolerance,
894 max_depth.min(MAX_DEPTH_CEILING),
895 MAX_POINTS,
896 &mut out,
897 )?;
898 out.push(evaluate2(curve, domain.end)?);
899 Ok(out)
900}
901
902const MAX_DEPTH_CEILING: u32 = 20;
907
908trait ChordPoint: Copy {
921 fn sub(self, other: Self) -> Self;
922 fn add_scaled(self, direction: Self, scale: Scalar) -> Self;
923 fn dot(self, other: Self) -> Scalar;
924 fn length(self) -> Scalar;
925 fn length_squared(self) -> Scalar;
926}
927
928impl ChordPoint for Point2 {
929 fn sub(self, other: Self) -> Self {
930 self - other
931 }
932 fn add_scaled(self, direction: Self, scale: Scalar) -> Self {
933 self + direction * scale
934 }
935 fn dot(self, other: Self) -> Scalar {
936 Point2::dot(self, other)
937 }
938 fn length(self) -> Scalar {
939 Point2::length(self)
940 }
941 fn length_squared(self) -> Scalar {
942 Point2::length_squared(self)
943 }
944}
945
946impl ChordPoint for Point3 {
947 fn sub(self, other: Self) -> Self {
948 self - other
949 }
950 fn add_scaled(self, direction: Self, scale: Scalar) -> Self {
951 self + direction * scale
952 }
953 fn dot(self, other: Self) -> Scalar {
954 Point3::dot(self, other)
955 }
956 fn length(self) -> Scalar {
957 Point3::length(self)
958 }
959 fn length_squared(self) -> Scalar {
960 Point3::length_squared(self)
961 }
962}
963
964fn sagitta<P: ChordPoint>(a: P, b: P, m: P) -> Scalar {
966 let ab = b.sub(a);
967 let len2 = ab.length_squared();
968 if len2 <= 0.0 {
969 return m.sub(a).length();
972 }
973 let t = (m.sub(a).dot(ab) / len2).clamp(0.0, 1.0);
974 m.sub(a.add_scaled(ab, t)).length()
975}
976
977fn subdivide<P, F>(
984 eval: &F,
985 a: Scalar,
986 b: Scalar,
987 tol: Scalar,
988 depth: u32,
989 budget: usize,
990 out: &mut Vec<P>,
991) -> GeomResult<()>
992where
993 P: ChordPoint,
994 F: Fn(Scalar) -> GeomResult<P>,
995{
996 if out.len() >= budget {
997 return Err(GeomError::Degenerate(format!(
998 "curve flattening exceeded {budget} points before meeting the \
999 chord tolerance {tol}; the curve may be degenerate"
1000 )));
1001 }
1002 let mid = 0.5 * (a + b);
1003 let pa = eval(a)?;
1004 let pb = eval(b)?;
1005 if !(mid > a && mid < b) {
1011 if sagitta(pa, pb, pa) <= tol && (pb.sub(pa)).length() <= tol {
1012 return Ok(());
1013 }
1014 return Err(GeomError::Degenerate(format!(
1015 "curve parameter interval ({a}, {b}) is too small to bisect but \
1016 its chord still exceeds the tolerance {tol}"
1017 )));
1018 }
1019 let pm = eval(mid)?;
1020 if sagitta(pa, pb, pm) <= tol {
1021 return Ok(());
1023 }
1024 if depth == 0 {
1025 return Err(GeomError::BudgetExceeded {
1026 resource: "curve flattening depth",
1027 });
1028 }
1029 subdivide(eval, a, mid, tol, depth - 1, budget, out)?;
1030 out.push(pm);
1031 subdivide(eval, mid, b, tol, depth - 1, budget, out)?;
1032 Ok(())
1033}
1034
1035fn polyline_flatten<P, F>(
1037 points: &[P],
1038 closed: bool,
1039 domain: Interval,
1040 eval: F,
1041) -> GeomResult<Vec<P>>
1042where
1043 P: Copy,
1044 F: Fn(Scalar) -> GeomResult<P>,
1045{
1046 let segments = if closed {
1047 points.len()
1048 } else {
1049 points.len().saturating_sub(1)
1050 };
1051 if segments == 0 {
1052 return Err(GeomError::Degenerate(
1053 "polyline has no evaluable segment".to_owned(),
1054 ));
1055 }
1056 let lo = domain.start.min(domain.end);
1057 let hi = domain.start.max(domain.end);
1058 let mut out = vec![eval(lo)?];
1059 let first = lo.floor() as i64 + 1;
1061 let last = hi.ceil() as i64 - 1;
1062 for k in first..=last {
1063 let t = k as Scalar;
1064 if t > lo && t < hi {
1065 out.push(eval(t)?);
1066 }
1067 }
1068 out.push(eval(hi)?);
1069 Ok(out)
1070}
1071
1072impl CurveEvaluator<Curve2> for ScalarCurve {
1075 type Point = Point2;
1076 type Derivative = Vec2;
1077 type Error = GeomError;
1078
1079 fn domain(&self, curve: &Curve2) -> Interval {
1080 domain2(curve)
1081 }
1082
1083 fn evaluate(
1084 &self,
1085 curve: &Curve2,
1086 t: Scalar,
1087 _tolerance: Tolerance,
1088 ) -> Result<Self::Point, Self::Error> {
1089 evaluate2(curve, t)
1090 }
1091
1092 fn derivative(
1093 &self,
1094 curve: &Curve2,
1095 t: Scalar,
1096 _tolerance: Tolerance,
1097 ) -> Result<Self::Derivative, Self::Error> {
1098 derivative2(curve, t)
1099 }
1100}
1101
1102impl CurveEvaluator<Curve3> for ScalarCurve {
1103 type Point = Point3;
1104 type Derivative = Vec3;
1105 type Error = GeomError;
1106
1107 fn domain(&self, curve: &Curve3) -> Interval {
1108 domain3(curve)
1109 }
1110
1111 fn evaluate(
1112 &self,
1113 curve: &Curve3,
1114 t: Scalar,
1115 _tolerance: Tolerance,
1116 ) -> Result<Self::Point, Self::Error> {
1117 evaluate3(curve, t)
1118 }
1119
1120 fn derivative(
1121 &self,
1122 curve: &Curve3,
1123 t: Scalar,
1124 _tolerance: Tolerance,
1125 ) -> Result<Self::Derivative, Self::Error> {
1126 derivative3(curve, t)
1127 }
1128}
1129
1130#[allow(unused)]
1132fn _type_anchors(_: Circle2, _: Circle3, _: Ellipse2, _: Ellipse3, _: Line2, _: Line3) {}
1133#[allow(unused)]
1134fn _poly_anchors(_: Polyline2, _: Polyline3) {}
1135
1136pub fn flatten3(
1144 curve: &Curve3,
1145 domain: Interval,
1146 chord_tolerance: Scalar,
1147 max_depth: u32,
1148) -> GeomResult<Vec<Point3>> {
1149 const MAX_POINTS: usize = 1 << 16;
1153 if !(chord_tolerance.is_finite()
1154 && chord_tolerance.is_sign_positive()
1155 && chord_tolerance != 0.0)
1156 {
1157 return Err(GeomError::InvalidInput(format!(
1158 "chord tolerance must be positive and finite, got {chord_tolerance}"
1159 )));
1160 }
1161 if let Curve3::Line(_) = curve {
1164 return Ok(vec![
1165 evaluate3(curve, domain.start)?,
1166 evaluate3(curve, domain.end)?,
1167 ]);
1168 }
1169 if let Curve3::Polyline(p) = curve {
1170 let natural = polyline_domain(p.points.len(), p.closed);
1174 let requested = (domain.end - domain.start).abs();
1175 if natural.end > 1.0 && requested <= 1.0 {
1176 return Err(GeomError::InvalidInput(format!(
1177 "polyline domain {:?} spans {requested} of {} segments; a \
1178 polyline parameter is one unit per segment, so this would \
1179 discard {} vertices",
1180 domain,
1181 natural.end,
1182 p.points.len().saturating_sub(2)
1183 )));
1184 }
1185 return polyline_flatten(&p.points, p.closed, domain, |t| evaluate3(curve, t));
1186 }
1187
1188 let eval = |t| evaluate3(curve, t);
1189 let mut out = vec![eval(domain.start)?];
1190 subdivide(
1191 &eval,
1192 domain.start,
1193 domain.end,
1194 chord_tolerance,
1195 max_depth.min(MAX_DEPTH_CEILING),
1196 MAX_POINTS,
1197 &mut out,
1198 )?;
1199 out.push(eval(domain.end)?);
1200 Ok(out)
1201}
1202
1203fn no_closed_form_inversion() -> GeomError {
1208 GeomError::InvalidInput(
1209 "curve family has no closed form inversion; a point trim on this basis \
1210 would require iteration, which trim resolution does not perform"
1211 .to_owned(),
1212 )
1213}
1214
1215fn point_not_on_curve(distance: Scalar, tolerance: Scalar) -> GeomError {
1217 GeomError::InvalidInput(format!(
1218 "point is {distance} from the curve, outside the {tolerance} tolerance; \
1219 refusing rather than projecting it onto the nearest parameter"
1220 ))
1221}
1222
1223fn invert_line(
1229 origin: impl Into<[Scalar; 3]>,
1230 direction: [Scalar; 3],
1231 point: [Scalar; 3],
1232 tolerance: Scalar,
1233) -> GeomResult<Scalar> {
1234 let origin = origin.into();
1235 let dd = direction.iter().map(|c| c * c).sum::<Scalar>();
1236 if !dd.is_finite() || dd <= Scalar::EPSILON {
1237 return Err(GeomError::InvalidInput(
1238 "line direction is degenerate, so no parameter names a point".to_owned(),
1239 ));
1240 }
1241 let offset = [
1242 point[0] - origin[0],
1243 point[1] - origin[1],
1244 point[2] - origin[2],
1245 ];
1246 let t = offset
1247 .iter()
1248 .zip(direction.iter())
1249 .map(|(o, d)| o * d)
1250 .sum::<Scalar>()
1251 / dd;
1252 let residual = [
1255 offset[0] - direction[0] * t,
1256 offset[1] - direction[1] * t,
1257 offset[2] - direction[2] * t,
1258 ];
1259 let distance = residual.iter().map(|c| c * c).sum::<Scalar>().sqrt();
1260 if distance > tolerance {
1261 return Err(point_not_on_curve(distance, tolerance));
1262 }
1263 Ok(t)
1264}
1265
1266fn invert_conic(
1272 local_x: Scalar,
1273 local_y: Scalar,
1274 semi_x: Scalar,
1275 semi_y: Scalar,
1276) -> GeomResult<Scalar> {
1277 if !(semi_x.is_finite() && semi_y.is_finite()) || semi_x <= 0.0 || semi_y <= 0.0 {
1278 return Err(GeomError::InvalidInput(
1279 "conic semi-axes must be finite and positive to invert a point".to_owned(),
1280 ));
1281 }
1282 let angle = (local_y / semi_y).atan2(local_x / semi_x);
1283 if !angle.is_finite() {
1284 return Err(GeomError::InvalidInput(
1285 "conic inversion produced a non-finite angle".to_owned(),
1286 ));
1287 }
1288 Ok(angle.rem_euclid(std::f64::consts::TAU))
1291}
1292
1293pub fn invert2(curve: &Curve2, point: Point2, tolerance: Tolerance) -> GeomResult<Scalar> {
1305 finite2(point, "inversion point")?;
1306 let linear = tolerance.linear();
1307 match curve {
1308 Curve2::Line(l) => invert_line(
1309 [l.origin.x, l.origin.y, 0.0],
1310 [l.direction.x, l.direction.y, 0.0],
1311 [point.x, point.y, 0.0],
1312 linear,
1313 ),
1314 Curve2::Circle(c) => {
1315 let t = invert_conic_in_frame2(&c.frame, point, c.radius, c.radius)?;
1316 verify2(curve, t, point, linear)
1317 }
1318 Curve2::Ellipse(e) => {
1319 let t = invert_conic_in_frame2(&e.frame, point, e.semi_axis_x, e.semi_axis_y)?;
1320 verify2(curve, t, point, linear)
1321 }
1322 _ => Err(no_closed_form_inversion()),
1323 }
1324}
1325
1326fn invert_conic_in_frame2(
1328 frame: &axiolid_core::Frame2,
1329 point: Point2,
1330 semi_x: Scalar,
1331 semi_y: Scalar,
1332) -> GeomResult<Scalar> {
1333 let offset = point - frame.origin;
1334 invert_conic(offset.dot(frame.x), offset.dot(frame.y), semi_x, semi_y)
1335}
1336
1337fn verify2(curve: &Curve2, t: Scalar, point: Point2, tolerance: Scalar) -> GeomResult<Scalar> {
1344 let found = evaluate2(curve, t)?;
1345 let distance = (found - point).length();
1346 if distance > tolerance {
1347 return Err(point_not_on_curve(distance, tolerance));
1348 }
1349 Ok(t)
1350}
1351
1352pub fn invert3(curve: &Curve3, point: Point3, tolerance: Tolerance) -> GeomResult<Scalar> {
1360 finite3(point, "inversion point")?;
1361 let linear = tolerance.linear();
1362 match curve {
1363 Curve3::Line(l) => invert_line(
1364 [l.origin.x, l.origin.y, l.origin.z],
1365 [l.direction.x, l.direction.y, l.direction.z],
1366 [point.x, point.y, point.z],
1367 linear,
1368 ),
1369 Curve3::Circle(c) => {
1370 let t = invert_conic_in_frame3(&c.frame, point, c.radius, c.radius)?;
1371 verify3(curve, t, point, linear)
1372 }
1373 Curve3::Ellipse(e) => {
1374 let t = invert_conic_in_frame3(&e.frame, point, e.semi_axis_x, e.semi_axis_y)?;
1375 verify3(curve, t, point, linear)
1376 }
1377 _ => Err(no_closed_form_inversion()),
1378 }
1379}
1380
1381fn invert_conic_in_frame3(
1383 frame: &axiolid_core::Frame3,
1384 point: Point3,
1385 semi_x: Scalar,
1386 semi_y: Scalar,
1387) -> GeomResult<Scalar> {
1388 let offset = point - frame.origin;
1389 invert_conic(offset.dot(frame.x), offset.dot(frame.y), semi_x, semi_y)
1390}
1391
1392fn verify3(curve: &Curve3, t: Scalar, point: Point3, tolerance: Scalar) -> GeomResult<Scalar> {
1394 let found = evaluate3(curve, t)?;
1395 let distance = (found - point).length();
1396 if distance > tolerance {
1397 return Err(point_not_on_curve(distance, tolerance));
1398 }
1399 Ok(t)
1400}