1use axiolid_contracts::{GeomError, GeomResult};
36use axiolid_core::{Frame2, Point2, Scalar, Vec2};
37use axiolid_curve::{Chain2, ChainPiece2, CurvatureLaw, Curve2, Intrinsic2};
38
39use crate::arc_length::{intrinsic_point, intrinsic_tangent};
40use crate::arc_parameter::{parameter_at_arc_length2, ARC_LENGTH_TOLERANCE};
41use crate::curve::{derivative2, evaluate2};
42
43pub const PIECE_FRAME_TOLERANCE: Scalar = 1e-9;
47
48fn invalid(detail: String) -> GeomError {
49 GeomError::InvalidInput(detail)
50}
51
52#[derive(Debug, Clone, Copy)]
55pub(crate) struct Placement {
56 pub(crate) origin: Vec2,
57 pub(crate) x: Vec2,
58}
59
60impl Placement {
61 pub(crate) const IDENTITY: Self = Self {
62 origin: Vec2::ZERO,
63 x: Vec2::X,
64 };
65
66 pub(crate) fn turn(&self, v: Vec2) -> Vec2 {
67 Vec2::new(
68 self.x.x * v.x - self.x.y * v.y,
69 self.x.y * v.x + self.x.x * v.y,
70 )
71 }
72
73 pub(crate) fn place(&self, p: Vec2) -> Vec2 {
74 self.origin + self.turn(p)
75 }
76}
77
78pub(crate) fn local_frame() -> Frame2 {
80 Frame2 {
81 origin: Point2::ZERO,
82 x: Vec2::X,
83 y: Vec2::Y,
84 }
85}
86
87pub(crate) fn unit(v: Vec2, what: &str) -> GeomResult<Vec2> {
88 let length = v.length();
89 if !(length.is_finite() && length > 0.0) {
90 return Err(GeomError::Degenerate(format!(
91 "{what} has no tangent direction"
92 )));
93 }
94 Ok(v / length)
95}
96
97pub(crate) fn check_well_formed(chain: &Chain2) -> GeomResult<()> {
99 if chain.pieces.is_empty() {
100 return Err(invalid("arc-length chain has no pieces".into()));
101 }
102 let frame = &chain.start;
103 if !(frame.origin.is_finite() && frame.x.is_finite() && frame.y.is_finite())
104 || frame.x.perp_dot(frame.y) == 0.0
105 {
106 return Err(invalid(
107 "arc-length chain start frame must be finite with independent axes".into(),
108 ));
109 }
110 for (index, piece) in chain.pieces.iter().enumerate() {
111 let length = piece.length();
112 if !(length.is_finite() && length > 0.0) {
113 return Err(invalid(format!(
114 "arc-length chain piece {index} has length {length}; it must be finite and positive"
115 )));
116 }
117 match piece {
118 ChainPiece2::Intrinsic { curvature, .. } if !curvature.is_well_formed() => {
119 return Err(invalid(format!(
120 "arc-length chain piece {index} has a malformed curvature law"
121 )));
122 }
123 ChainPiece2::Parametric { start, .. } if !start.is_finite() => {
124 return Err(invalid(format!(
125 "arc-length chain piece {index} has a non-finite start parameter"
126 )));
127 }
128 _ => {}
129 }
130 }
131 if !chain.is_well_formed() {
132 return Err(invalid("arc-length chain is malformed".into()));
133 }
134 Ok(())
135}
136
137pub(crate) fn check_local_start(
140 index: usize,
141 curve: &Curve2,
142 start: Scalar,
143 length: Scalar,
144) -> GeomResult<()> {
145 let origin = evaluate2(curve, start)?;
146 let tangent = unit(derivative2(curve, start)?, "chain piece curve")?;
147 let offset = Vec2::new(origin.x, origin.y).length();
148 let angle = tangent.y.atan2(tangent.x).abs();
149 if offset > PIECE_FRAME_TOLERANCE * length.max(1.0) || angle > PIECE_FRAME_TOLERANCE {
150 return Err(invalid(format!(
151 "arc-length chain piece {index} does not start at its local origin with tangent +x \
152 (offset {offset}, tangent angle {angle} rad)"
153 )));
154 }
155 Ok(())
156}
157
158fn piece_local(index: usize, piece: &ChainPiece2, s: Scalar) -> GeomResult<(Vec2, Vec2)> {
161 match piece {
162 ChainPiece2::Intrinsic { curvature, length } => {
163 let curve = Intrinsic2::new(local_frame(), curvature.clone(), *length);
164 let p = intrinsic_point(&curve, s)?;
165 Ok((Vec2::new(p.x, p.y), intrinsic_tangent(&curve, s)?))
166 }
167 ChainPiece2::Parametric {
168 curve,
169 start,
170 length,
171 } => {
172 check_local_start(index, curve, *start, *length)?;
173 let end = parameter_at_arc_length2(curve, *start, *length).map_err(|error| {
176 invalid(format!(
177 "arc-length chain piece {index} asks for arc length {length} from parameter \
178 {start}: {error}"
179 ))
180 })?;
181 let u = if s == *length {
182 end
183 } else {
184 parameter_at_arc_length2(curve, *start, s)?
185 };
186 let p = evaluate2(curve, u)?;
187 Ok((
188 Vec2::new(p.x, p.y),
189 unit(derivative2(curve, u)?, "chain piece curve")?,
190 ))
191 }
192 _ => Err(GeomError::Unsupported {
193 backend: axiolid_contracts::BackendId::new("axiolid-evaluate"),
194 operation: axiolid_contracts::Operation::CurveEvaluation,
195 }),
196 }
197}
198
199fn locate(chain: &Chain2, s: Scalar) -> GeomResult<(usize, Scalar, Placement)> {
201 check_well_formed(chain)?;
202 if !s.is_finite() {
203 return Err(invalid(
204 "distance along an arc-length chain must be finite".into(),
205 ));
206 }
207 let total = chain
208 .length()
209 .ok_or_else(|| invalid("arc-length chain has no finite length".into()))?;
210 let s = if s > total && s <= total + ARC_LENGTH_TOLERANCE * total.max(1.0) {
212 total
213 } else {
214 s
215 };
216 let Some((index, _, local)) = chain.piece_at(s) else {
217 return Err(invalid(format!(
218 "distance {s} is outside the arc-length chain [0, {total}]"
219 )));
220 };
221 let mut placement = Placement::IDENTITY;
222 for (before, piece) in chain.pieces[..index].iter().enumerate() {
223 let (end, tangent) = piece_local(before, piece, piece.length())?;
224 placement = Placement {
225 origin: placement.place(end),
226 x: unit(placement.turn(tangent), "chain join")?,
227 };
228 }
229 Ok((index, local, placement))
230}
231
232fn to_world(frame: &Frame2, v: Vec2) -> Vec2 {
233 Vec2::new(
234 frame.x.x * v.x + frame.y.x * v.y,
235 frame.x.y * v.x + frame.y.y * v.y,
236 )
237}
238
239pub fn chain_point(chain: &Chain2, s: Scalar) -> GeomResult<Point2> {
242 let (index, local, placement) = locate(chain, s)?;
243 let (p, _) = piece_local(index, &chain.pieces[index], local)?;
244 let v = to_world(&chain.start, placement.place(p));
245 Ok(Point2::new(
246 chain.start.origin.x + v.x,
247 chain.start.origin.y + v.y,
248 ))
249}
250
251pub fn chain_tangent(chain: &Chain2, s: Scalar) -> GeomResult<Vec2> {
254 let (index, local, placement) = locate(chain, s)?;
255 let (_, t) = piece_local(index, &chain.pieces[index], local)?;
256 Ok(to_world(&chain.start, placement.turn(t)))
257}
258
259fn piece_parameter(piece: &ChainPiece2, s: Scalar) -> Option<GeomResult<Scalar>> {
262 match piece {
263 ChainPiece2::Parametric {
264 curve,
265 start,
266 length,
267 } if s == *length => Some(parameter_at_arc_length2(curve, *start, *length)),
268 ChainPiece2::Parametric { curve, start, .. } => {
269 Some(parameter_at_arc_length2(curve, *start, s))
270 }
271 _ => None,
272 }
273}
274
275#[must_use]
294pub fn chain_chord_bound(chain: &Chain2, a: Scalar, b: Scalar) -> Option<Scalar> {
295 crate::bound::evaluated_chain_chord_bound(chain, a, b)
296}
297
298#[must_use]
310pub(crate) fn exact_chain_chord_bound(chain: &Chain2, a: Scalar, b: Scalar) -> Option<Scalar> {
311 let (lo, hi) = (a.min(b), a.max(b));
312 let (index, local_lo, _) = locate(chain, lo).ok()?;
313 let piece = &chain.pieces[index];
314 let begin = chain.pieces[..index]
317 .iter()
318 .fold(0.0, |begin, piece| begin + piece.length());
319 let length = piece.length();
320 let local_hi = hi - begin;
321 if local_hi > length + ARC_LENGTH_TOLERANCE * length.max(1.0) {
322 return None;
323 }
324 let local_hi = if length - local_hi <= 4.0 * Scalar::EPSILON * length.max(1.0) {
327 length
328 } else {
329 local_hi
330 };
331 piece_local(index, piece, local_hi).ok()?;
333 let stretch = crate::bound::frame_stretch2(&chain.start);
334 let local = match piece {
335 ChainPiece2::Intrinsic { curvature, .. } => {
336 let k = curvature_bound(curvature, local_lo, local_hi)?;
337 (local_hi - local_lo).powi(2) * 0.125 * k
338 }
339 ChainPiece2::Parametric { curve, .. } => {
340 let u0 = piece_parameter(piece, local_lo)?.ok()?;
341 let u1 = piece_parameter(piece, local_hi)?.ok()?;
342 crate::bound::chord_bound2(curve, u0, u1)?
343 }
344 _ => return None,
345 };
346 let bound = local * stretch * (1.0 + crate::bound::ROUNDING);
347 bound.is_finite().then_some(bound)
348}
349
350pub(crate) fn chain_second_derivative(chain: &Chain2, s: Scalar) -> GeomResult<Vec2> {
358 let (index, local, placement) = locate(chain, s)?;
359 let piece = &chain.pieces[index];
360 let bend = match piece {
361 ChainPiece2::Intrinsic { curvature, length } => {
362 let curve = Intrinsic2::new(local_frame(), curvature.clone(), *length);
363 let heading = curve.heading_at(local).ok_or_else(|| {
364 invalid("chain piece curvature does not integrate to that distance".into())
365 })?;
366 let kappa = crate::frenet::law_value(curvature, local)
367 .ok_or_else(|| invalid("chain piece curvature is not defined there".into()))?;
368 Vec2::new(-heading.sin(), heading.cos()) * kappa
369 }
370 ChainPiece2::Parametric { curve, .. } => {
371 piece_local(index, piece, local)?;
372 let u = piece_parameter(piece, local)
373 .ok_or_else(|| invalid("chain piece has no parameter there".into()))??;
374 parametric_bend(curve, u)?
375 }
376 _ => {
377 return Err(GeomError::Unsupported {
378 backend: axiolid_contracts::BackendId::new("axiolid-evaluate"),
379 operation: axiolid_contracts::Operation::CurveEvaluation,
380 })
381 }
382 };
383 Ok(to_world(&chain.start, placement.turn(bend)))
384}
385
386pub(crate) fn parametric_bend(curve: &Curve2, u: Scalar) -> GeomResult<Vec2> {
389 let c1 = derivative2(curve, u)?;
390 let c2 = crate::curve::second_derivative2(curve, u)?;
391 let speed2 = c1.length_squared();
392 if !(speed2 > 0.0 && speed2.is_finite()) {
393 return Err(invalid("chain piece curve has no tangent there".into()));
394 }
395 let t = c1 / speed2.sqrt();
396 Ok((c2 - t * t.dot(c2)) / speed2)
397}
398
399pub(crate) fn chain_plan_bounds(chain: &Chain2, a: Scalar, b: Scalar) -> Option<(Scalar, Scalar)> {
414 let (lo, hi) = (a.min(b), a.max(b));
415 let (index, local_lo, _) = locate(chain, lo).ok()?;
416 let piece = &chain.pieces[index];
417 let begin = chain.pieces[..index]
418 .iter()
419 .fold(0.0, |begin, piece| begin + piece.length());
420 let length = piece.length();
421 let local_hi = hi - begin;
422 if local_hi > length + ARC_LENGTH_TOLERANCE * length.max(1.0) {
423 return None;
424 }
425 let local_hi = if length - local_hi <= 4.0 * Scalar::EPSILON * length.max(1.0) {
426 length
427 } else {
428 local_hi.min(length)
429 };
430 piece_local(index, piece, local_hi).ok()?;
431 let stretch = crate::bound::frame_stretch2(&chain.start);
432 let (p2, p3) = match piece {
433 ChainPiece2::Intrinsic { curvature, .. } => {
434 let k = curvature_bound(curvature, local_lo, local_hi)?;
435 let seamed = curvature
436 .seams_within(length)
437 .iter()
438 .any(|&seam| seam > local_lo && seam < local_hi);
439 let rate = if seamed {
441 Scalar::INFINITY
442 } else {
443 curvature_bound(&curvature.derivative(), local_lo, local_hi)?
444 };
445 (k, rate + k * k)
446 }
447 ChainPiece2::Parametric { curve, .. } => {
448 let u0 = piece_parameter(piece, local_lo)?.ok()?;
449 let u1 = piece_parameter(piece, local_hi)?.ok()?;
450 parametric_plan_bounds(curve, u0, u1)?
451 }
452 _ => return None,
453 };
454 let scale = stretch * (1.0 + crate::bound::ROUNDING);
455 let out = (p2 * scale, p3 * scale);
456 out.0.is_finite().then_some(out)
457}
458
459pub(crate) fn parametric_plan_bounds(
462 curve: &Curve2,
463 u0: Scalar,
464 u1: Scalar,
465) -> Option<(Scalar, Scalar)> {
466 parametric_plan_jet(curve, u0, u1).map(|(p2, p3, _)| (p2, p3))
467}
468
469pub(crate) fn parametric_plan_jet(
473 curve: &Curve2,
474 u0: Scalar,
475 u1: Scalar,
476) -> Option<(Scalar, Scalar, Scalar)> {
477 let (lo, hi) = (u0.min(u1), u0.max(u1));
478 match curve {
479 Curve2::Line(l) => {
480 let speed = l.direction.length();
481 return (speed > 0.0 && speed.is_finite()).then_some((0.0, 0.0, speed));
482 }
483 Curve2::Circle(c) => {
484 let (x, y) = (c.frame.x, c.frame.y);
485 let unit = |v: Vec2| (v.length() - 1.0).abs() <= 1e-12;
486 if unit(x) && unit(y) && x.dot(y).abs() <= 1e-12 && c.radius > 0.0 {
487 let k = 1.0 / c.radius;
488 return Some((k, k * k, c.radius));
489 }
490 }
491 _ => {}
492 }
493 if crate::bound::continuity_breaks2(curve, 2)
494 .iter()
495 .any(|&t| t > lo && t < hi)
496 {
497 return None;
498 }
499 let bounds = crate::bound::curve_derivative_bounds2(curve, lo, hi)?;
500 let (d2, d3) = (bounds.second, bounds.third);
501 let mid = derivative2(curve, 0.5 * (lo + hi)).ok()?.length();
502 let sigma = mid - 0.5 * (hi - lo) * d2;
503 if !(sigma > 0.0 && sigma.is_finite()) {
504 return None;
505 }
506 let k2 = 2.0 * d3 / sigma + 6.0 * d2 * d2 / (sigma * sigma);
507 let s2 = sigma * sigma;
508 Some((d2 / s2, k2 / s2 + d2 * d2 / (s2 * s2), sigma))
509}
510
511pub(crate) fn chain_curvature_seams(chain: &Chain2) -> Vec<Scalar> {
514 let mut out = Vec::new();
515 let mut begin = 0.0;
516 for piece in &chain.pieces {
517 if let ChainPiece2::Intrinsic { curvature, length } = piece {
518 out.extend(
519 curvature
520 .seams_within(*length)
521 .into_iter()
522 .filter(|&s| s > 0.0 && s < *length)
523 .map(|s| begin + s),
524 );
525 }
526 begin += piece.length();
527 }
528 out
529}
530
531#[must_use]
539pub fn chain_certifies(chain: &Chain2) -> bool {
540 chain.is_well_formed()
541 && chain.pieces.iter().all(|piece| match piece {
542 ChainPiece2::Intrinsic { curvature, length } => {
543 curvature_bound(curvature, 0.0, *length).is_some()
544 }
545 ChainPiece2::Parametric { curve, .. } => {
549 matches!(
550 curve,
551 Curve2::Line(_) | Curve2::Circle(_) | Curve2::Ellipse(_) | Curve2::BSpline(_)
552 ) && crate::bound::certifies_flattening2(curve)
553 && crate::bound::continuity_breaks2(curve, 1).is_empty()
554 }
555 _ => false,
556 })
557}
558
559pub(crate) fn curvature_bound(law: &CurvatureLaw, lo: Scalar, hi: Scalar) -> Option<Scalar> {
562 let reach = lo.abs().max(hi.abs());
563 let polynomial = |coefficients: &[Scalar]| -> Scalar {
564 coefficients
565 .iter()
566 .enumerate()
567 .map(|(power, c)| c.abs() * reach.powi(power as i32))
568 .sum()
569 };
570 let bound = match law {
571 CurvatureLaw::Constant { curvature } => curvature.abs(),
572 CurvatureLaw::Polynomial { coefficients } => polynomial(coefficients),
573 CurvatureLaw::Sinusoid {
574 mean, amplitude, ..
575 } => mean.abs() + amplitude.abs(),
576 CurvatureLaw::Composite {
577 polynomial: coefficients,
578 harmonics,
579 } => polynomial(coefficients) + harmonics.iter().map(|h| h.amplitude.abs()).sum::<Scalar>(),
580 CurvatureLaw::Piecewise { breaks, laws } => {
581 if laws.len() != breaks.len() + 1 {
582 return None;
583 }
584 let mut worst: Scalar = 0.0;
586 let mut begin = 0.0;
587 for (index, piece) in laws.iter().enumerate() {
588 let end = breaks.get(index).copied().unwrap_or(Scalar::INFINITY);
589 if end >= lo && begin <= hi {
590 let from = (lo - begin).max(0.0);
591 let to = (hi.min(end) - begin).max(from);
592 worst = worst.max(curvature_bound(piece, from, to)?);
593 }
594 begin = end;
595 }
596 worst
597 }
598 _ => return None,
599 };
600 let bound = bound * (1.0 + crate::bound::ROUNDING);
601 bound.is_finite().then_some(bound)
602}
603
604pub(crate) fn harmonic_rounding(law: &CurvatureLaw, length: Scalar) -> Option<Scalar> {
616 let term = |h: &axiolid_curve::Harmonic| -> Scalar {
617 if h.angular_frequency == 0.0 {
618 return 0.0;
619 }
620 8.0 * Scalar::EPSILON
621 * (h.amplitude / h.angular_frequency).abs()
622 * (2.0 + h.phase.abs() + h.angular_frequency.abs() * length)
623 };
624 let sum = match law {
625 CurvatureLaw::Constant { .. } | CurvatureLaw::Polynomial { .. } => 0.0,
626 CurvatureLaw::Sinusoid {
627 amplitude,
628 angular_frequency,
629 phase,
630 ..
631 } => term(&axiolid_curve::Harmonic {
632 amplitude: *amplitude,
633 angular_frequency: *angular_frequency,
634 phase: *phase,
635 }),
636 CurvatureLaw::Composite { harmonics, .. } => harmonics.iter().map(term).sum(),
637 CurvatureLaw::Piecewise { laws, .. } => {
638 let mut total = 0.0;
639 for piece in laws {
640 total += harmonic_rounding(piece, length)?;
641 }
642 total
643 }
644 _ => return None,
645 };
646 let out = sum * (1.0 + crate::bound::ROUNDING);
647 out.is_finite().then_some(out)
648}