1use axiolid_contracts::{GeomError, GeomResult, Operation};
37use axiolid_core::{Scalar, Tolerance, Transform3};
38use axiolid_curve::{Curve2, Curve3, CurvePath, OffsetFrame, OffsetLaw, PathOffsets, PathPiece};
39use axiolid_model::{
40 CurveRelation, GeometryGraph, GeometryNode, MasterRepresentation, NodeId, RunStation, SeamSide,
41 StationFrame, TrimSelector, TrimmingPreference,
42};
43use axiolid_reference::station::{
44 exact_station_seams2, exact_station_seams3, offset_pieces, snapped_distance2,
45 snapped_distance3, station_frame_is_exact2, station_frame_is_exact3, station_seams2,
46 station_seams3, station_section2_on, station_section3_on, CompositeBasis, DistanceConvention,
47 FrameLaw, SectionFrame, StationCurve, StationPiece, StationSeam, JOINT_TOLERANCE,
48};
49
50const MAX_BASIS_DEPTH: usize = 64;
52
53pub(crate) const UNSUPPORTED_BASIS: &str =
55 "a station along an instanced curve, or along a curve relation other than a composite, a \
56 trim, a surface curve whose 3D curve governs, a curve placed at a station, or an offset";
57
58fn unsupported(input: &'static str) -> GeomError {
59 GeomError::UnsupportedInput {
60 backend: crate::BACKEND_ID,
61 operation: Operation::CurveEvaluation,
62 input,
63 }
64}
65
66#[allow(clippy::large_enum_variant)]
71enum Flat<'g> {
72 Atomic {
73 curve: StationCurve<'g>,
74 placed: Option<Transform3>,
75 exact: bool,
77 },
78 Pieces(Vec<StationPiece<'g>>),
79}
80
81impl<'g> Flat<'g> {
82 fn into_pieces(self) -> GeomResult<Vec<StationPiece<'g>>> {
84 match self {
85 Self::Atomic {
86 curve,
87 placed,
88 exact,
89 } => {
90 let piece = StationPiece::whole(curve)?;
91 Ok(vec![match placed {
92 Some(rigid) => piece.placed(rigid, exact),
93 None => piece,
94 }])
95 }
96 Self::Pieces(pieces) => Ok(pieces),
97 }
98 }
99}
100
101pub fn curve_path(graph: &GeometryGraph, id: NodeId) -> GeomResult<CurvePath> {
127 Ok(flatten(graph, id, 0)?
128 .into_pieces()?
129 .into_iter()
130 .map(PathPiece::from)
131 .collect())
132}
133
134pub(crate) fn relation_pieces(
141 graph: &GeometryGraph,
142 id: NodeId,
143) -> GeomResult<Vec<StationPiece<'_>>> {
144 flatten(graph, id, 0)?.into_pieces()
145}
146
147fn reversed(pieces: Vec<StationPiece<'_>>) -> Vec<StationPiece<'_>> {
149 pieces
150 .into_iter()
151 .rev()
152 .map(StationPiece::reversed)
153 .collect()
154}
155
156#[allow(clippy::large_enum_variant)]
161#[derive(Debug, Clone)]
162pub(crate) enum Basis<'g> {
163 Atomic {
164 curve: StationCurve<'g>,
165 placed: Option<Transform3>,
167 exact: bool,
170 placed_exactly: bool,
174 },
175 Composite(CompositeBasis<'g>),
176}
177
178impl<'g> Basis<'g> {
179 pub(crate) fn of(graph: &'g GeometryGraph, id: NodeId) -> GeomResult<Self> {
180 Self::of_depth(graph, id, 0)
181 }
182
183 pub(super) fn of_depth(graph: &'g GeometryGraph, id: NodeId, depth: usize) -> GeomResult<Self> {
184 Ok(match flatten(graph, id, depth)? {
185 Flat::Atomic {
186 curve,
187 placed,
188 exact,
189 } => Basis::Atomic {
190 curve,
191 placed,
192 placed_exactly: exact,
193 exact: exact
194 && match curve {
195 StationCurve::Two(curve) => station_frame_is_exact2(curve),
196 StationCurve::Three(curve) => station_frame_is_exact3(curve),
197 _ => false,
198 },
199 },
200 Flat::Pieces(pieces) => Basis::Composite(CompositeBasis::new(pieces)?),
201 })
202 }
203
204 pub(crate) fn section_on(
207 &self,
208 distance: Scalar,
209 frame: StationFrame,
210 side: SeamSide,
211 ) -> GeomResult<SectionFrame> {
212 let section = match self {
213 Self::Atomic { curve, placed, .. } => {
214 let section = match curve {
215 StationCurve::Two(curve) => station_section2_on(curve, distance, side)?,
216 StationCurve::Three(curve) => station_section3_on(curve, distance, side)?,
217 _ => {
218 return Err(unsupported(
219 "a station basis curve this compiler does not know",
220 ))
221 }
222 };
223 match placed {
224 Some(rigid) => section.carried(*rigid, curve.convention())?,
225 None => section,
226 }
227 }
228 Self::Composite(composite) => composite.section_on(distance, side)?,
229 };
230 match frame {
231 StationFrame::Section => Ok(section),
232 StationFrame::Plan => section.plan(),
233 _ => Err(unsupported("a station frame this compiler does not know")),
234 }
235 }
236
237 pub(crate) fn snapped_distance(&self, distance: Scalar, window: Scalar) -> GeomResult<Scalar> {
242 match self {
243 Self::Atomic { curve, .. } => match curve {
244 StationCurve::Two(curve) => snapped_distance2(curve, distance, window),
245 StationCurve::Three(curve) => snapped_distance3(curve, distance, window),
246 _ => Err(unsupported(
247 "a station basis curve this compiler does not know",
248 )),
249 },
250 Self::Composite(composite) => composite.snapped_distance(distance, window),
251 }
252 }
253
254 pub(crate) fn length(&self) -> GeomResult<Scalar> {
260 match self {
261 Self::Atomic { curve, .. } => Ok(StationPiece::whole(*curve)?.length()),
262 Self::Composite(composite) => Ok(composite.length()),
263 }
264 }
265
266 pub(super) fn frame_is_exact(&self, distance: Scalar, side: SeamSide) -> bool {
269 match self {
270 Self::Atomic { exact, .. } => *exact,
271 Self::Composite(composite) => composite.frame_is_exact_at(distance, side),
272 }
273 }
274
275 pub(super) fn frame_law(&self, tolerance: Scalar) -> GeomResult<FrameLaw<'_, 'g>> {
280 match self {
281 Self::Atomic {
282 curve,
283 placed,
284 placed_exactly,
285 ..
286 } => FrameLaw::atomic_placed(*curve, *placed, *placed_exactly, tolerance),
287 Self::Composite(composite) => composite.frame_law(tolerance),
288 }
289 }
290
291 pub(super) fn seams(&self) -> GeomResult<Vec<StationSeam>> {
294 match self {
295 Self::Atomic { curve, .. } => match curve {
296 StationCurve::Two(curve) => station_seams2(curve),
297 StationCurve::Three(curve) => station_seams3(curve),
298 _ => Err(unsupported(
299 "a station basis curve this compiler does not know",
300 )),
301 },
302 Self::Composite(composite) => composite.seams(),
303 }
304 }
305
306 pub(super) fn exact_seams(&self) -> GeomResult<Vec<StationSeam>> {
308 match self {
309 Self::Atomic { curve, .. } => match curve {
310 StationCurve::Two(curve) => exact_station_seams2(curve),
311 StationCurve::Three(curve) => exact_station_seams3(curve),
312 _ => Err(unsupported(
313 "a station basis curve this compiler does not know",
314 )),
315 },
316 Self::Composite(composite) => composite.exact_seams(),
317 }
318 }
319}
320
321fn flatten<'g>(graph: &'g GeometryGraph, id: NodeId, depth: usize) -> GeomResult<Flat<'g>> {
322 flatten_in(graph, id, None, depth)
323}
324
325fn is_trim_through_placements(graph: &GeometryGraph, mut id: NodeId) -> bool {
329 loop {
330 match graph.get(id) {
331 Some(GeometryNode::InstanceAtStation(placed)) => id = placed.source,
332 Some(GeometryNode::CurveRelation(CurveRelation::Trimmed { .. })) => return true,
333 _ => return false,
334 }
335 }
336}
337
338fn flatten_in<'g>(
342 graph: &'g GeometryGraph,
343 id: NodeId,
344 window: Option<(Scalar, Scalar)>,
345 depth: usize,
346) -> GeomResult<Flat<'g>> {
347 if depth > MAX_BASIS_DEPTH {
348 return Err(GeomError::BudgetExceeded {
349 resource: "relations and placements nested in a station basis",
350 });
351 }
352 match graph.get(id) {
353 Some(GeometryNode::Curve2(curve)) => Ok(Flat::Atomic {
354 curve: StationCurve::Two(curve),
355 placed: None,
356 exact: true,
357 }),
358 Some(GeometryNode::Curve3(curve)) => Ok(Flat::Atomic {
359 curve: StationCurve::Three(curve),
360 placed: None,
361 exact: true,
362 }),
363 Some(GeometryNode::InstanceAtStation(placed)) => {
366 let source = flatten_in(graph, placed.source, window, depth + 1)?;
367 let placement = super::placement_at(graph, &placed.station, depth + 1)?;
368 Ok(match source {
369 Flat::Atomic {
370 curve,
371 placed: inner,
372 exact,
373 } => Flat::Atomic {
374 curve,
375 placed: Some(match inner {
376 Some(inner) => placement.transform * inner,
377 None => placement.transform,
378 }),
379 exact: exact && placement.exact,
380 },
381 Flat::Pieces(pieces) => Flat::Pieces(
382 pieces
383 .into_iter()
384 .map(|piece| piece.placed(placement.transform, placement.exact))
385 .collect(),
386 ),
387 })
388 }
389 Some(GeometryNode::CurveRelation(CurveRelation::Composite { segments })) => {
392 let mut out = Vec::new();
393 for segment in segments {
394 let pieces = flatten(graph, segment.curve, depth + 1)?.into_pieces()?;
395 out.extend(match segment.same_sense {
396 true => pieces,
397 false => reversed(pieces),
398 });
399 }
400 Ok(Flat::Pieces(out))
401 }
402 Some(GeometryNode::CurveRelation(CurveRelation::Trimmed {
403 basis,
404 start,
405 end,
406 sense_agreement,
407 preference,
408 })) => {
409 if matches!(
410 graph.get(*basis),
411 Some(GeometryNode::CurveRelation(
412 CurveRelation::Offset { .. }
413 | CurveRelation::OffsetByStations { .. }
414 | CurveRelation::OffsetsAtStations { .. }
415 ))
416 ) {
417 return Err(unsupported(
418 "a trim of an offset curve as a station basis: an offset takes its basis's \
419 parameter, not its own length, and that parameter is not read along it",
420 ));
421 }
422 let nested = is_trim_through_placements(graph, *basis);
427 let span = if nested || window.is_some() {
428 Some(super::trimmed_span(
429 graph,
430 *basis,
431 (start, end),
432 (*sense_agreement, *preference),
433 window,
434 None,
435 depth + 1,
436 )?)
437 } else {
438 None
439 };
440 let pieces = match flatten_in(graph, *basis, span.filter(|_| nested), depth + 1)? {
441 Flat::Atomic {
442 curve,
443 placed,
444 exact,
445 } => {
446 let (lo, hi) = match span {
447 Some(span) => span,
448 None => trim_parameters(curve, start, end, *sense_agreement, *preference)?,
449 };
450 let piece = StationPiece::between_parameters(curve, lo, hi)?;
451 vec![match placed {
452 Some(rigid) => piece.placed(rigid, exact),
453 None => piece,
454 }]
455 }
456 Flat::Pieces(pieces) if nested => pieces,
457 Flat::Pieces(pieces) => {
458 let composite = CompositeBasis::new(pieces)?;
459 if composite.convention() != DistanceConvention::ArcLength3d {
460 return Err(unsupported(
461 "a trim of a plan-measured curve relation as a station basis: its \
462 parameter is its arc length, not the plan distance it is measured \
463 in",
464 ));
465 }
466 let (a, b) = match span {
467 Some(span) => span,
468 None => {
469 let a = crate::directrix::parameter_only(start, *preference, "start")?;
470 let b = crate::directrix::parameter_only(end, *preference, "end")?;
471 (a.min(b), a.max(b))
472 }
473 };
474 composite.pieces_between(a, b)?
475 }
476 };
477 Ok(Flat::Pieces(if *sense_agreement {
478 pieces
479 } else {
480 reversed(pieces)
481 }))
482 }
483 Some(GeometryNode::CurveRelation(CurveRelation::SurfaceCurve {
484 curve_3d,
485 master: MasterRepresentation::Curve3d,
486 ..
487 })) => flatten(graph, *curve_3d, depth + 1),
488 Some(GeometryNode::CurveRelation(CurveRelation::Offset {
491 basis,
492 distance,
493 reference_direction,
494 })) => {
495 let law = match reference_direction {
496 None => OffsetLaw::Planar {
497 distance: *distance,
498 },
499 Some(direction) => OffsetLaw::Directed {
500 distance: *distance,
501 reference_direction: *direction,
502 },
503 };
504 let base = flatten(graph, *basis, depth + 1)?.into_pieces()?;
505 Ok(Flat::Pieces(offset_pieces(&base, law)?))
506 }
507 Some(GeometryNode::CurveRelation(
511 relation @ (CurveRelation::OffsetByStations { .. }
512 | CurveRelation::OffsetsAtStations { .. }),
513 )) => {
514 let (basis, stations, frame) = super::offset_run(relation)?;
515 offset_by_stations(graph, basis, &stations, frame, depth)
516 }
517 Some(GeometryNode::Instance(_) | GeometryNode::CurveRelation(_)) => {
518 Err(unsupported(UNSUPPORTED_BASIS))
519 }
520 Some(_) => Err(GeomError::InvalidInput(format!(
521 "station basis {id:?} is not a curve"
522 ))),
523 None => Err(GeomError::InvalidInput(format!(
524 "station basis {id:?} is outside the graph"
525 ))),
526 }
527}
528
529#[allow(clippy::large_enum_variant)]
534enum Along<'g> {
535 Atomic(StationCurve<'g>, Option<Transform3>, bool),
536 Composite(CompositeBasis<'g>),
537}
538
539impl Along<'_> {
540 fn snapped_distance(&self, distance: Scalar, window: Scalar) -> GeomResult<Scalar> {
543 match self {
544 Self::Atomic(StationCurve::Two(curve), ..) => {
545 snapped_distance2(curve, distance, window)
546 }
547 Self::Atomic(StationCurve::Three(curve), ..) => {
548 snapped_distance3(curve, distance, window)
549 }
550 Self::Atomic(..) => Err(unsupported(
551 "a station basis curve this compiler does not know",
552 )),
553 Self::Composite(composite) => composite.snapped_distance(distance, window),
554 }
555 }
556
557 fn length(&self) -> GeomResult<Scalar> {
560 match self {
561 Self::Atomic(curve, ..) => Ok(StationPiece::whole(*curve)?.length()),
562 Self::Composite(composite) => Ok(composite.length()),
563 }
564 }
565}
566
567fn offset_by_stations<'g>(
575 graph: &'g GeometryGraph,
576 basis: NodeId,
577 stations: &[RunStation],
578 frame: StationFrame,
579 depth: usize,
580) -> GeomResult<Flat<'g>> {
581 if stations.len() < 2 {
582 return Err(GeomError::InvalidInput(
583 "an offset curve by stations needs at least two stations".into(),
584 ));
585 }
586 let frame = match frame {
587 StationFrame::Section => OffsetFrame::Section,
588 StationFrame::Plan => OffsetFrame::Plan,
589 _ => return Err(unsupported("a station frame this compiler does not know")),
590 };
591 let offsets = |station: &RunStation| {
592 let offsets = &station.station.offsets;
593 PathOffsets::new(offsets.lateral, offsets.vertical, offsets.longitudinal)
594 };
595 let base = match flatten(graph, basis, depth + 1)? {
596 Flat::Pieces(pieces) => Along::Composite(CompositeBasis::new(pieces)?),
597 Flat::Atomic {
598 curve,
599 placed,
600 exact,
601 } => Along::Atomic(curve, placed, exact),
602 };
603 let distances = super::snapped_run(
604 |distance, window| base.snapped_distance(distance, window),
605 super::resolved_run(|| base.length(), stations)?,
606 )?;
607 let mut out = Vec::new();
608 for (pair, at) in stations.windows(2).zip(distances.windows(2)) {
609 let (a, b) = (at[0], at[1]);
610 let span = match &base {
613 Along::Atomic(curve, placed, exact) => {
614 let piece = StationPiece::between(*curve, a, b)?;
615 vec![match placed {
616 Some(rigid) => piece.placed(*rigid, *exact),
617 None => piece,
618 }]
619 }
620 Along::Composite(composite) => composite.pieces_between(a, b)?,
621 };
622 let law = OffsetLaw::Linear {
623 start: offsets(&pair[0]),
624 end: offsets(&pair[1]),
625 frame,
626 };
627 out.extend(offset_pieces(&span, law)?);
628 }
629 Ok(Flat::Pieces(out))
630}
631
632fn point_tolerance(selectors: &[&[TrimSelector]]) -> Tolerance {
635 let scale = selectors
636 .iter()
637 .flat_map(|list| list.iter())
638 .map(|selector| match selector {
639 TrimSelector::Point2(p) => p.abs().max_element(),
640 TrimSelector::Point3(p) => p.abs().max_element(),
641 _ => 0.0,
642 })
643 .fold(1.0, Scalar::max);
644 Tolerance::new(JOINT_TOLERANCE * scale, Tolerance::METRE.angular()).unwrap_or(Tolerance::METRE)
645}
646
647pub(crate) fn trim_parameters(
652 curve: StationCurve<'_>,
653 start: &[TrimSelector],
654 end: &[TrimSelector],
655 sense: bool,
656 preference: TrimmingPreference,
657) -> GeomResult<(Scalar, Scalar)> {
658 let tolerance = point_tolerance(&[start, end]);
659 let (a, b) = match curve {
660 StationCurve::Three(curve) => (
661 crate::directrix::parameter(start, preference, "start", curve, sense, tolerance)?,
662 crate::directrix::parameter(end, preference, "end", curve, sense, tolerance)?,
663 ),
664 StationCurve::Two(curve) => (
665 parameter2(start, preference, "start", curve, sense, tolerance)?,
666 parameter2(end, preference, "end", curve, sense, tolerance)?,
667 ),
668 _ => {
669 return Err(unsupported(
670 "a station basis curve this compiler does not know",
671 ))
672 }
673 };
674 if a == b {
675 return Err(GeomError::Degenerate(
676 "a trimmed station basis has an empty interval".into(),
677 ));
678 }
679 let closed = matches!(
680 curve,
681 StationCurve::Two(Curve2::Circle(_) | Curve2::Ellipse(_))
682 | StationCurve::Three(Curve3::Circle(_) | Curve3::Ellipse(_))
683 );
684 if closed {
685 return crate::directrix::periodic_trim_interval(
686 core::f64::consts::TAU,
687 a,
688 b,
689 sense,
690 None,
691 crate::directrix::parameter_slack(&[a, b]),
692 );
693 }
694 Ok((a.min(b), a.max(b)))
695}
696
697fn parameter2(
703 selectors: &[TrimSelector],
704 preference: TrimmingPreference,
705 label: &str,
706 curve: &Curve2,
707 sense: bool,
708 tolerance: Tolerance,
709) -> GeomResult<Scalar> {
710 let measured = |selector: &TrimSelector| -> GeomResult<Option<Scalar>> {
711 match selector {
712 TrimSelector::Parameter(value) => Ok(Some(*value)),
713 TrimSelector::ArcLength(length) => {
714 if !length.is_finite() {
715 return Err(GeomError::InvalidInput(format!(
716 "trimmed station basis {label} arc length must be finite"
717 )));
718 }
719 let signed = if sense { *length } else { -*length };
720 axiolid_reference::arc_parameter::parameter_at_arc_length2(curve, 0.0, signed)
721 .map(Some)
722 .map_err(|error| {
723 GeomError::InvalidInput(format!(
724 "trimmed station basis {label} arc length {length} does not resolve \
725 on its basis: {error}"
726 ))
727 })
728 }
729 _ => Ok(None),
730 }
731 };
732 let is_measure = |selector: &&TrimSelector| {
733 matches!(
734 selector,
735 TrimSelector::Parameter(_) | TrimSelector::ArcLength(_)
736 )
737 };
738 let first_measure = || -> GeomResult<Option<Scalar>> {
739 selectors.iter().find(is_measure).map_or(Ok(None), measured)
740 };
741 let mut refusal = None;
742 let mut invert = || {
743 crate::directrix::first_point(
744 selectors,
745 |selector| match selector {
746 TrimSelector::Point2(p) => Some(axiolid_core::Point3::new(p.x, p.y, 0.0)),
747 _ => None,
748 },
749 StationCurve::Two(curve),
750 tolerance,
751 &mut refusal,
752 )
753 };
754 let selected = match preference {
755 TrimmingPreference::Parameter => first_measure()?,
756 TrimmingPreference::Unspecified => match selectors.first() {
757 Some(selector) if is_measure(&selector) => measured(selector)?,
758 _ => invert(),
759 },
760 TrimmingPreference::Cartesian => match invert() {
761 Some(value) => Some(value),
762 None => first_measure()?,
763 },
764 };
765 selected
766 .filter(|value| value.is_finite())
767 .ok_or_else(|| match refusal {
768 Some(refusal) => refusal,
769 None => GeomError::InvalidInput(format!(
770 "trimmed station basis {label} needs a parameter selector, or a point selector \
771 that lies on the basis curve"
772 )),
773 })
774}