1use axiolid_contracts::{BackendId, GeomError, GeomResult, Operation};
62use axiolid_core::{Point2, Point3, Scalar, Vec3};
63use axiolid_curve::{
64 BSplineCurve, Banked3, CantLaw, ChainPiece2, Curve2, Curve3, Elevated3, ElevationLaw, SeamSide,
65};
66
67use super::{
68 banked_frame, invalid, planar_frame, reference_up_frame, station_length2, station_length3,
69 SectionFrame,
70};
71use crate::arc_parameter::{arc_length2, arc_length3, ARC_LENGTH_TOLERANCE};
72use crate::curve::{derivative2, derivative3, domain2, domain3, evaluate2, evaluate3};
73
74pub const SEAM_TANGENT_TOLERANCE: Scalar = 1e-9;
77
78pub const MITRE_TOLERANCE: Scalar = 1e-6;
82
83#[non_exhaustive]
86#[derive(Debug, Clone, Copy, PartialEq)]
87pub struct StationSeam {
88 pub distance: Scalar,
91 pub parameter: Scalar,
95 pub smooth: bool,
99 pub exact: bool,
102}
103
104impl StationSeam {
105 #[must_use]
109 pub const fn new(distance: Scalar, parameter: Scalar, smooth: bool, exact: bool) -> Self {
110 Self {
111 distance,
112 parameter,
113 smooth,
114 exact,
115 }
116 }
117
118 const fn measured(distance: Scalar, smooth: bool) -> Self {
120 Self::new(distance, distance, smooth, true)
121 }
122}
123
124fn slack(distance: Scalar) -> Scalar {
126 ARC_LENGTH_TOLERANCE * distance.abs().max(1.0)
127}
128
129pub(super) fn tidy(mut seams: Vec<StationSeam>, length: Option<Scalar>) -> Vec<StationSeam> {
132 seams.retain(|seam| {
133 seam.distance.is_finite()
134 && seam.distance > slack(0.0)
135 && length.is_none_or(|l| seam.distance < l - slack(l))
136 });
137 seams.sort_by(|a, b| a.distance.total_cmp(&b.distance));
138 let mut out: Vec<StationSeam> = Vec::with_capacity(seams.len());
139 for seam in seams {
140 match out.last_mut() {
141 Some(last) if seam.distance - last.distance <= slack(seam.distance) => {
142 last.smooth &= seam.smooth;
143 last.exact &= seam.exact;
144 }
145 _ => out.push(seam),
146 }
147 }
148 out
149}
150
151fn polyline_segments<P>(
154 points: &[P],
155 closed: bool,
156 length: impl Fn(P, P) -> Scalar,
157) -> Vec<(usize, usize, Scalar)>
158where
159 P: Copy,
160{
161 let count = points.len();
162 let spans: Vec<(usize, usize)> = if closed {
163 (0..count).map(|i| (i, (i + 1) % count)).collect()
164 } else {
165 (0..count.saturating_sub(1)).map(|i| (i, i + 1)).collect()
166 };
167 spans
168 .into_iter()
169 .map(|(i, j)| (i, j, length(points[i], points[j])))
170 .collect()
171}
172
173fn polyline_seams(segments: &[(usize, usize, Scalar)]) -> Vec<StationSeam> {
177 let mut out = Vec::new();
178 let mut run = 0.0;
179 let mut started = false;
180 for &(start, _, length) in segments {
181 if length > 0.0 {
182 if started {
183 out.push(StationSeam::new(run, start as Scalar, false, true));
184 }
185 started = true;
186 }
187 run += length;
188 }
189 out
190}
191
192fn polyline_side(
196 segments: &[(usize, usize, Scalar)],
197 vertex: usize,
198 side: SeamSide,
199) -> GeomResult<(usize, usize)> {
200 let at = segments
201 .iter()
202 .position(|&(start, _, _)| start == vertex)
203 .ok_or_else(|| invalid("station: the seam is not a vertex of the polyline".into()))?;
204 let found = match side {
205 SeamSide::Incoming => segments[..at].iter().rev().find(|s| s.2 > 0.0),
206 _ => segments[at..].iter().find(|s| s.2 > 0.0),
207 };
208 found.map(|&(i, j, _)| (i, j)).ok_or_else(|| {
209 invalid("station: the polyline has no segment on that side of the seam".into())
210 })
211}
212
213fn corner_knots(breaks: Vec<Scalar>, lo: Scalar, hi: Scalar) -> Vec<Scalar> {
216 breaks.into_iter().filter(|&t| t > lo && t < hi).collect()
217}
218
219fn reversed_spline<P: Clone>(spline: &BSplineCurve<P>) -> (BSplineCurve<P>, Scalar) {
223 let first = spline.knots.first().copied().unwrap_or(0.0);
224 let last = spline.knots.last().copied().unwrap_or(0.0);
225 let pivot = first + last;
226 let mut reversed = spline.clone();
227 reversed.knots = spline.knots.iter().rev().map(|t| pivot - t).collect();
228 reversed.multiplicities.reverse();
229 reversed.control_points.reverse();
230 if let Some(weights) = reversed.weights.as_mut() {
231 weights.reverse();
232 }
233 (reversed, pivot)
234}
235
236pub fn station_seams2(curve: &Curve2) -> GeomResult<Vec<StationSeam>> {
245 let length = station_length2(curve)?;
246 let seams = match curve {
247 Curve2::Polyline(polyline) => polyline_seams(&polyline_segments(
248 &polyline.points,
249 polyline.closed,
250 |a: Point2, b: Point2| (b - a).length(),
251 )),
252 Curve2::BSpline(_) => {
253 let domain = domain2(curve);
254 let (lo, hi) = (domain.start.min(domain.end), domain.start.max(domain.end));
255 corner_knots(crate::bound::continuity_breaks2(curve, 1), lo, hi)
256 .into_iter()
257 .map(|t| {
258 Ok(StationSeam::new(
259 arc_length2(curve, lo, t)?,
260 t,
261 false,
262 false,
263 ))
264 })
265 .collect::<GeomResult<_>>()?
266 }
267 Curve2::Intrinsic(intrinsic) => intrinsic
268 .curvature
269 .seams_within(intrinsic.length)
270 .into_iter()
271 .map(|s| StationSeam::measured(s, true))
272 .collect(),
273 Curve2::Chain(chain) => chain
274 .joins()
275 .unwrap_or_default()
276 .into_iter()
277 .chain(crate::chain::chain_curvature_seams(chain))
278 .map(|s| StationSeam::measured(s, true))
279 .collect(),
280 _ => Vec::new(),
281 };
282 Ok(tidy(seams, length))
283}
284
285pub fn station_seams3(curve: &Curve3) -> GeomResult<Vec<StationSeam>> {
291 let length = station_length3(curve)?;
292 let seams = match curve {
293 Curve3::Polyline(polyline) => polyline_seams(&polyline_segments(
294 &polyline.points,
295 polyline.closed,
296 |a: Point3, b: Point3| (b - a).length(),
297 )),
298 Curve3::BSpline(_) => {
299 let domain = domain3(curve);
300 let (lo, hi) = (domain.start.min(domain.end), domain.start.max(domain.end));
301 corner_knots(crate::bound::continuity_breaks3(curve, 1), lo, hi)
302 .into_iter()
303 .map(|t| {
304 Ok(StationSeam::new(
305 arc_length3(curve, lo, t)?,
306 t,
307 false,
308 false,
309 ))
310 })
311 .collect::<GeomResult<_>>()?
312 }
313 Curve3::Intrinsic(intrinsic) => intrinsic
314 .curvature
315 .seams_within(intrinsic.length)
316 .into_iter()
317 .chain(intrinsic.torsion.seams_within(intrinsic.length))
318 .map(|s| StationSeam::measured(s, true))
319 .collect(),
320 Curve3::Elevated(elevated) => elevated_seams(elevated)?,
321 Curve3::Banked(banked) => {
322 let mut seams = elevated_seams(&banked.base)?;
323 seams.extend(
324 banked
325 .cant
326 .seams()
327 .into_iter()
328 .chain(banked.pivot.seams())
329 .map(|s| StationSeam::measured(s, false)),
330 );
331 seams
332 }
333 _ => Vec::new(),
334 };
335 Ok(tidy(seams, length))
336}
337
338fn elevated_seams(curve: &Elevated3) -> GeomResult<Vec<StationSeam>> {
340 let mut seams = match curve.plan.as_ref() {
341 plan @ (Curve2::Intrinsic(_) | Curve2::Chain(_)) => station_seams2(plan)?,
344 _ => Vec::new(),
345 };
346 seams.extend(
347 crate::elevation::elevation_seams(&curve.elevation)
348 .into_iter()
349 .map(|s| StationSeam::measured(s, false)),
350 );
351 Ok(seams)
352}
353
354fn not_exact(seams: Vec<StationSeam>) -> GeomResult<Vec<StationSeam>> {
355 if seams.iter().any(|seam| !seam.exact) {
356 return Err(GeomError::UnsupportedInput {
357 backend: BackendId::new("axiolid-evaluate"),
358 operation: Operation::CurveEvaluation,
359 input: "exact seam distances of a B-spline with a corner knot: its arc length to \
360 the knot is a quadrature",
361 });
362 }
363 Ok(seams)
364}
365
366fn chain_hides_corners(curve: &Curve2) -> bool {
369 match curve {
370 Curve2::Chain(chain) => chain.pieces.iter().any(|piece| match piece {
371 ChainPiece2::Parametric { curve, .. } => {
372 !crate::bound::continuity_breaks2(curve, 1).is_empty()
373 }
374 _ => false,
375 }),
376 _ => false,
377 }
378}
379
380fn hidden_corner() -> GeomError {
381 GeomError::UnsupportedInput {
382 backend: BackendId::new("axiolid-evaluate"),
383 operation: Operation::CurveEvaluation,
384 input: "exact seam distances of a chain whose parametric piece turns a corner inside \
385 itself: no stored length locates it",
386 }
387}
388
389pub fn exact_station_seams2(curve: &Curve2) -> GeomResult<Vec<StationSeam>> {
397 if chain_hides_corners(curve) {
398 return Err(hidden_corner());
399 }
400 not_exact(station_seams2(curve)?)
401}
402
403pub fn exact_station_seams3(curve: &Curve3) -> GeomResult<Vec<StationSeam>> {
411 let plan = match curve {
412 Curve3::Elevated(elevated) => Some(elevated.plan.as_ref()),
413 Curve3::Banked(banked) => Some(banked.base.plan.as_ref()),
414 _ => None,
415 };
416 if plan.is_some_and(chain_hides_corners) {
417 return Err(hidden_corner());
418 }
419 not_exact(station_seams3(curve)?)
420}
421
422fn seam_near(seams: Vec<StationSeam>, distance: Scalar) -> Option<StationSeam> {
427 seams
428 .into_iter()
429 .filter(|seam| !seam.smooth && (seam.distance - distance).abs() <= slack(distance))
430 .min_by(|a, b| {
431 (a.distance - distance)
432 .abs()
433 .total_cmp(&(b.distance - distance).abs())
434 })
435}
436
437fn sided2(curve: &Curve2) -> bool {
440 matches!(curve, Curve2::Polyline(_) | Curve2::BSpline(_))
441}
442
443pub(super) fn sided3(curve: &Curve3) -> bool {
444 matches!(
445 curve,
446 Curve3::Polyline(_) | Curve3::BSpline(_) | Curve3::Elevated(_) | Curve3::Banked(_)
447 )
448}
449
450pub(super) fn seam_section2(
453 curve: &Curve2,
454 distance: Scalar,
455 side: SeamSide,
456) -> GeomResult<Option<SectionFrame>> {
457 if !sided2(curve) {
458 return Ok(None);
459 }
460 let Some(seam) = seam_near(station_seams2(curve)?, distance) else {
461 return Ok(None);
462 };
463 let frame = match curve {
464 Curve2::Polyline(polyline) => {
465 let segments = polyline_segments(&polyline.points, polyline.closed, |a: Point2, b| {
466 (b - a).length()
467 });
468 let vertex = seam.parameter as usize;
469 let (i, j) = polyline_side(&segments, vertex, side)?;
470 planar_frame(
471 polyline.points[vertex],
472 polyline.points[j] - polyline.points[i],
473 )?
474 }
475 Curve2::BSpline(spline) => {
476 let point = evaluate2(curve, seam.parameter)?;
477 let tangent = match side {
478 SeamSide::Incoming => {
479 let (reversed, pivot) = reversed_spline(spline);
480 -derivative2(&Curve2::BSpline(reversed), pivot - seam.parameter)?
481 }
482 _ => derivative2(curve, seam.parameter)?,
483 };
484 planar_frame(point, tangent)?
485 }
486 _ => return Ok(None),
487 };
488 Ok(Some(frame))
489}
490
491pub(crate) fn seam_at3(curve: &Curve3, distance: Scalar) -> GeomResult<Option<StationSeam>> {
495 if !sided3(curve) {
496 return Ok(None);
497 }
498 Ok(seam_near(station_seams3(curve)?, distance))
499}
500
501pub(super) fn seam_section3(
504 curve: &Curve3,
505 distance: Scalar,
506 side: SeamSide,
507) -> GeomResult<Option<SectionFrame>> {
508 let Some(seam) = seam_at3(curve, distance)? else {
509 return Ok(None);
510 };
511 let incoming = side == SeamSide::Incoming;
512 let at = seam.distance;
513 let frame = match curve {
514 Curve3::Polyline(polyline) => {
515 let segments = polyline_segments(&polyline.points, polyline.closed, |a: Point3, b| {
516 (b - a).length()
517 });
518 let vertex = seam.parameter as usize;
519 let (i, j) = polyline_side(&segments, vertex, side)?;
520 reference_up_frame(
521 polyline.points[vertex],
522 polyline.points[j] - polyline.points[i],
523 )?
524 }
525 Curve3::BSpline(spline) => {
526 let point = evaluate3(curve, seam.parameter)?;
527 let tangent = if incoming {
528 let (reversed, pivot) = reversed_spline(spline);
529 -derivative3(&Curve3::BSpline(reversed), pivot - seam.parameter)?
530 } else {
531 derivative3(curve, seam.parameter)?
532 };
533 reference_up_frame(point, tangent)?
534 }
535 Curve3::Elevated(elevated) => {
536 let truncated;
537 let read = if incoming {
538 truncated = Elevated3 {
539 plan: elevated.plan.clone(),
540 elevation: elevation_ending_at(&elevated.elevation, at),
541 };
542 &truncated
543 } else {
544 elevated
545 };
546 reference_up_frame(
547 crate::arc_length::elevated_point(read, at)?,
548 crate::arc_length::elevated_tangent(read, at)?,
549 )?
550 }
551 Curve3::Banked(banked) => {
552 let truncated;
553 let read = if incoming {
554 truncated = Banked3 {
555 base: Elevated3 {
556 plan: banked.base.plan.clone(),
557 elevation: elevation_ending_at(&banked.base.elevation, at),
558 },
559 cant: law_ending_at(&banked.cant, at),
560 pivot: law_ending_at(&banked.pivot, at),
561 ..banked.clone()
562 };
563 &truncated
564 } else {
565 banked
566 };
567 banked_frame(read, at)?
568 }
569 _ => return Ok(None),
570 };
571 Ok(Some(frame))
572}
573
574fn elevation_ending_at(law: &ElevationLaw, distance: Scalar) -> ElevationLaw {
578 match law {
579 ElevationLaw::Piecewise { breaks, laws } if laws.len() == breaks.len() + 1 => {
580 let keep = breaks.partition_point(|b| *b < distance - slack(distance));
581 let start = if keep == 0 { 0.0 } else { breaks[keep - 1] };
582 let mut kept = laws[..keep].to_vec();
583 kept.push(elevation_ending_at(&laws[keep], distance - start));
584 ElevationLaw::Piecewise {
585 breaks: breaks[..keep].to_vec(),
586 laws: kept,
587 }
588 }
589 other => other.clone(),
590 }
591}
592
593fn law_ending_at(law: &CantLaw, distance: Scalar) -> CantLaw {
596 let mut start = 0.0;
597 let mut pieces = Vec::with_capacity(law.pieces.len());
598 for piece in &law.pieces {
599 if !pieces.is_empty() && start >= distance - slack(distance) {
600 break;
601 }
602 pieces.push(piece.clone());
603 start += piece.length;
604 }
605 CantLaw::new(pieces)
606}
607
608#[non_exhaustive]
613#[derive(Debug, Clone, Copy, PartialEq)]
614pub struct Mitre {
615 pub incoming: SectionFrame,
617 pub outgoing: SectionFrame,
619 pub normal: Vec3,
621}
622
623impl Mitre {
624 pub fn between(incoming: &SectionFrame, outgoing: &SectionFrame) -> GeomResult<Option<Self>> {
633 let (t_in, t_out) = (incoming.tangent, outgoing.tangent);
634 let turn = t_in.cross(t_out).length().atan2(t_in.dot(t_out));
635 if !turn.is_finite() {
636 return Err(invalid("station: a seam's tangents are not finite".into()));
637 }
638 if turn <= SEAM_TANGENT_TOLERANCE {
639 return Ok(None);
640 }
641 let sum = t_in + t_out;
642 let half = 0.5 * sum.length();
643 if half <= MITRE_TOLERANCE {
644 return Err(GeomError::Degenerate(format!(
645 "station: the curve turns back on itself at a seam (by {turn} rad), so a run \
646 across it has no mitre plane"
647 )));
648 }
649 let gap = (outgoing.point - incoming.point).length();
650 let scale = incoming.point.abs().max_element().max(1.0);
651 if gap > ARC_LENGTH_TOLERANCE * scale {
652 return Err(invalid(format!(
653 "station: the curve's pieces are {gap} apart at a seam, so a run across it has \
654 no mitre"
655 )));
656 }
657 Ok(Some(Self {
658 incoming: *incoming,
659 outgoing: *outgoing,
660 normal: sum / (2.0 * half),
661 }))
662 }
663
664 #[must_use]
670 pub fn place(&self, incoming: Point3, outgoing: Point3, longitudinal: Scalar) -> Point3 {
671 let project = |frame: &SectionFrame, point: Point3| {
672 let height = (point - frame.point).dot(self.normal);
673 point - frame.tangent * (height / frame.tangent.dot(self.normal))
674 };
675 let a = project(&self.incoming, incoming);
676 let b = project(&self.outgoing, outgoing);
677 (a + b) * 0.5 + self.normal * longitudinal
678 }
679}