1use std::cell::OnceCell;
115
116use axiolid_contracts::GeomResult;
117use axiolid_core::{Scalar, Transform3, Vec2, Vec3};
118use axiolid_curve::{Curve2, Curve3};
119
120use super::certify::{checked_tolerance, PieceReader};
121use super::offset::OffsetTable;
122use super::{CompositeBasis, DistanceConvention, StationCurve, StationPiece, KEEPS_UP_TOLERANCE};
123use crate::arc_parameter::ARC_LENGTH_TOLERANCE;
124use crate::bound::ROUNDING;
125
126#[non_exhaustive]
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum FrameReading {
130 Section,
132 Plan,
134}
135
136#[non_exhaustive]
140#[derive(Debug, Clone, Copy, PartialEq)]
141pub struct FrameLawBounds {
142 pub point: [Scalar; 2],
144 pub tangent: [Scalar; 2],
146 pub lateral: [Scalar; 2],
148 pub up: [Scalar; 2],
150 pub measure: Scalar,
153 pub point_error: Scalar,
158 pub axis_error: Scalar,
161}
162
163impl FrameLawBounds {
164 #[must_use]
167 pub fn is_straight(&self) -> bool {
168 self.point[1] == 0.0
169 && self.tangent == [0.0; 2]
170 && self.lateral == [0.0; 2]
171 && self.up == [0.0; 2]
172 }
173}
174
175#[derive(Debug, Clone, Copy)]
177enum Kind {
178 Planar,
180 ReferenceUp,
182 Banked([Scalar; 2]),
184}
185
186#[derive(Debug, Clone, Copy)]
188struct TangentLaw {
189 point: [Scalar; 2],
190 tangent: [Scalar; 2],
191 middle: Vec3,
193 kind: Kind,
194 measure: Scalar,
195}
196
197fn numerical_measure(hi: Scalar) -> Scalar {
200 2.0 * ARC_LENGTH_TOLERANCE * hi.abs().max(1.0)
201}
202
203fn unit(v: Vec3) -> Option<Vec3> {
204 let length = v.length();
205 (length.is_finite() && length > 0.0).then(|| v / length)
206}
207
208fn lift(v: Vec2) -> Vec3 {
209 Vec3::new(v.x, v.y, 0.0)
210}
211
212fn orthonormal(x: Vec3, y: Vec3) -> bool {
215 let near = 1e-12;
216 (x.length() - 1.0).abs() <= near && (y.length() - 1.0).abs() <= near && x.dot(y).abs() <= near
217}
218
219fn straight(direction: Vec3, kind: Kind, measure: Scalar) -> Option<TangentLaw> {
220 Some(TangentLaw {
221 point: [1.0, 0.0],
222 tangent: [0.0, 0.0],
223 middle: unit(direction)?,
224 kind,
225 measure,
226 })
227}
228
229fn circle(radius: Scalar, middle: Vec3, kind: Kind, hi: Scalar) -> Option<TangentLaw> {
230 if !(radius.is_finite() && radius > 0.0) {
231 return None;
232 }
233 let k = 1.0 / radius;
234 Some(TangentLaw {
235 point: [1.0, k],
236 tangent: [k, k * k],
237 middle: unit(middle)?,
238 kind,
239 measure: numerical_measure(hi),
240 })
241}
242
243fn polyline_segment<P: Copy>(
246 points: &[P],
247 closed: bool,
248 lo: Scalar,
249 hi: Scalar,
250 difference: impl Fn(P, P) -> Vec3,
251) -> Option<Vec3> {
252 let count = points.len();
253 let spans: Vec<(usize, usize)> = if closed {
254 (0..count).map(|i| (i, (i + 1) % count)).collect()
255 } else {
256 (0..count.saturating_sub(1)).map(|i| (i, i + 1)).collect()
257 };
258 let total: Scalar = spans
259 .iter()
260 .map(|&(i, j)| difference(points[i], points[j]).length())
261 .sum();
262 let slack = numerical_measure(total);
263 let mut start = 0.0;
264 for (i, j) in spans {
265 let step = difference(points[i], points[j]);
266 let end = start + step.length();
267 if step.length() > 0.0 && lo >= start - slack && hi <= end + slack {
268 return Some(step);
269 }
270 start = end;
271 }
272 None
273}
274
275fn tangent_law2(curve: &Curve2, lo: Scalar, hi: Scalar) -> Option<TangentLaw> {
276 match curve {
277 Curve2::Line(line) => straight(lift(line.direction), Kind::Planar, 0.0),
278 Curve2::Circle(c) => {
279 if !orthonormal(lift(c.frame.x), lift(c.frame.y)) {
280 return None;
281 }
282 let middle = crate::curve::derivative2(curve, 0.5 * (lo + hi) / c.radius).ok()?;
283 circle(c.radius, lift(middle), Kind::Planar, hi)
284 }
285 Curve2::Polyline(polyline) => {
286 let step = polyline_segment(&polyline.points, polyline.closed, lo, hi, |a, b| {
287 lift(b - a)
288 })?;
289 straight(step, Kind::Planar, numerical_measure(hi))
290 }
291 _ => None,
292 }
293}
294
295fn plan_measured(
298 bounds: crate::bound::CurveDerivativeBounds,
299 middle: Vec3,
300 lo: Scalar,
301 hi: Scalar,
302 kind: Kind,
303) -> Option<TangentLaw> {
304 let (d2, d3) = (bounds.second, bounds.third);
305 let sigma = middle.length() - 0.5 * (hi - lo) * d2;
306 if !(sigma.is_finite() && sigma > 0.0) {
307 return None;
308 }
309 Some(TangentLaw {
310 point: [bounds.first, d2],
311 tangent: [
312 d2 / sigma,
313 2.0 * d3 / sigma + 6.0 * d2 * d2 / (sigma * sigma),
314 ],
315 middle: unit(middle)?,
316 kind,
317 measure: 0.0,
318 })
319}
320
321fn closed_form_elevated(curve: &axiolid_curve::Elevated3) -> bool {
324 let plan = match curve.plan.as_ref() {
325 Curve2::Line(_) => true,
326 Curve2::Circle(c) => orthonormal(lift(c.frame.x), lift(c.frame.y)),
327 _ => false,
328 };
329 plan && crate::elevated::certifies_elevated(curve)
330}
331
332fn tangent_law3(curve: &Curve3, lo: Scalar, hi: Scalar) -> Option<TangentLaw> {
333 let mid = 0.5 * (lo + hi);
334 match curve {
335 Curve3::Line(line) => straight(line.direction, Kind::ReferenceUp, 0.0),
336 Curve3::Circle(c) => {
337 if !orthonormal(c.frame.x, c.frame.y) {
338 return None;
339 }
340 let middle = crate::curve::derivative3(curve, mid / c.radius).ok()?;
341 circle(c.radius, middle, Kind::ReferenceUp, hi)
342 }
343 Curve3::Polyline(polyline) => {
344 let step = polyline_segment(&polyline.points, polyline.closed, lo, hi, |a, b| b - a)?;
345 straight(step, Kind::ReferenceUp, numerical_measure(hi))
346 }
347 Curve3::Elevated(elevated) => {
348 if !closed_form_elevated(elevated) {
349 return None;
350 }
351 let bounds = crate::elevated::elevated_derivative_bounds(elevated, lo, hi)?;
352 let middle = crate::elevated::elevated_derivative(elevated, mid).ok()?;
353 plan_measured(bounds, middle, lo, hi, Kind::ReferenceUp)
354 }
355 Curve3::Banked(banked) => {
356 if !closed_form_elevated(&banked.base) {
357 return None;
358 }
359 let bounds = crate::elevated::banked_derivative_bounds(banked, lo, hi)?;
360 let roll = crate::banked::roll_bounds(banked, lo, hi)?;
361 let middle = crate::banked::banked_derivative(banked, mid).ok()?;
362 plan_measured(bounds, middle, lo, hi, Kind::Banked(roll))
363 }
364 _ => None,
365 }
366}
367
368fn normalised(w1: Scalar, w2: Scalar, rho: Scalar) -> [Scalar; 2] {
371 [w1 / rho, 2.0 * w2 / rho + 6.0 * w1 * w1 / (rho * rho)]
372}
373
374fn placement_stretch(rigid: axiolid_core::Transform3) -> Option<Scalar> {
377 let m = rigid.matrix3;
378 let (sx, sy, sz) = (m.x_axis.length(), m.y_axis.length(), m.z_axis.length());
379 let eps = 32.0 * f64::EPSILON;
380 let orthogonal = m.x_axis.dot(m.y_axis).abs() <= eps * sx * sy
381 && m.x_axis.dot(m.z_axis).abs() <= eps * sx * sz
382 && m.y_axis.dot(m.z_axis).abs() <= eps * sy * sz;
383 orthogonal.then(|| sx.max(sy).max(sz) * (1.0 + 3.0 * eps))
384}
385
386#[must_use]
402pub fn piece_frame_bounds(
403 piece: &StationPiece<'_>,
404 from: Scalar,
405 to: Scalar,
406 reading: FrameReading,
407) -> Option<FrameLawBounds> {
408 let (u0, u1) = (from.min(to), from.max(to));
409 let length = piece.length();
410 let slack = numerical_measure(length);
411 if !(u0.is_finite() && u1.is_finite()) || u0 < -slack || u1 > length + slack {
412 return None;
413 }
414 let (u0, u1) = (u0.max(0.0), u1.min(length));
415 let (m0, m1) = if piece.reversed {
416 (piece.end - u1, piece.end - u0)
417 } else {
418 (piece.start + u0, piece.start + u1)
419 };
420 let law = match piece.curve {
421 StationCurve::Two(curve) => tangent_law2(curve, m0, m1)?,
422 StationCurve::Three(curve) => tangent_law3(curve, m0, m1)?,
423 StationCurve::Offset(_) => return None,
424 };
425 finish(piece, law, m1 - m0, reading, [0.0; 2])
426}
427
428pub(crate) fn read_frame_bounds(
432 reader: &PieceReader<'_>,
433 piece: &StationPiece<'_>,
434 from: Scalar,
435 to: Scalar,
436 table: Option<&OffsetTable>,
437 reading: FrameReading,
438) -> Option<FrameLawBounds> {
439 let (u0, u1) = (from.min(to), from.max(to));
440 let length = piece.length();
441 let slack = numerical_measure(length);
442 if !(u0.is_finite() && u1.is_finite()) || u0 < -slack || u1 > length + slack {
443 return None;
444 }
445 let read = reader.read(piece, u0, u1, table).ok()??;
446 let law = TangentLaw {
447 point: read.point,
448 tangent: read.tangent,
449 middle: unit(read.middle)?,
450 kind: if read.planar {
451 Kind::Planar
452 } else {
453 Kind::ReferenceUp
454 },
455 measure: read.measure,
456 };
457 finish(
458 piece,
459 law,
460 u1 - u0 + 2.0 * read.measure,
461 reading,
462 [read.point_error, read.axis_error],
463 )
464}
465
466fn finish(
470 piece: &StationPiece<'_>,
471 mut law: TangentLaw,
472 span: Scalar,
473 reading: FrameReading,
474 errors: [Scalar; 2],
475) -> Option<FrameLawBounds> {
476 if piece.reversed {
477 law.middle = -law.middle;
478 }
479 let mut scale = 1.0;
480 if let Some(rigid) = piece.placement {
481 scale = placement_stretch(rigid)?;
482 law.middle = unit(rigid.transform_vector3(law.middle))?;
483 if (rigid.transform_vector3(Vec3::Z) - Vec3::Z).length() > KEEPS_UP_TOLERANCE {
484 if piece.convention() == DistanceConvention::PlanDistance {
485 return None;
486 }
487 law.kind = Kind::ReferenceUp;
488 }
489 }
490 let [k1, k2] = law.tangent;
491 let horizontal = || -> Option<Scalar> {
492 let rho = law.middle.x.hypot(law.middle.y) - 0.5 * span * k1;
493 (rho.is_finite() && rho > 0.0).then_some(rho)
494 };
495 let tangent = [k1, k2];
496 let (lateral, up) = match law.kind {
497 Kind::Planar => (tangent, [0.0, 0.0]),
498 Kind::ReferenceUp => {
499 let [r1, r2] = normalised(k1, k2, horizontal()?);
500 ([r1, r2], [r1 + k1, r2 + 2.0 * r1 * k1 + k2])
501 }
502 Kind::Banked([p1, p2]) => {
503 let [n1, n2] = normalised(k1, k2, horizontal()?);
504 let (s1, s2) = (k1 + n1, k2 + 2.0 * k1 * n1 + n2);
505 let (a1, a2) = (n1.hypot(s1), n2.hypot(s2));
506 let axis = [p1 + a1, p2 + p1 * p1 + 2.0 * p1 * a1 + a2];
507 (axis, axis)
508 }
509 };
510 let [point_error, mut axis_error] = errors;
511 let (tangent, lateral, up) = match reading {
512 FrameReading::Section => (tangent, lateral, up),
513 FrameReading::Plan if matches!(law.kind, Kind::Planar) => (tangent, lateral, up),
515 FrameReading::Plan => {
516 let rho = horizontal()?;
517 if axis_error > 0.0 {
519 axis_error = 2.0 * axis_error / rho;
520 }
521 let flat = normalised(k1, k2, rho);
522 (flat, flat, [0.0, 0.0])
523 }
524 };
525 let grow = |v: [Scalar; 2]| v.map(|x| x * scale * (1.0 + ROUNDING));
526 let out = FrameLawBounds {
527 point: grow(law.point),
528 tangent: grow(tangent),
529 lateral: grow(lateral),
530 up: grow(up),
531 measure: law.measure,
532 point_error: point_error * scale * (1.0 + ROUNDING),
533 axis_error: axis_error * scale * (1.0 + ROUNDING),
534 };
535 let finite = [out.point, out.tangent, out.lateral, out.up]
536 .iter()
537 .flatten()
538 .chain([&out.measure, &out.point_error, &out.axis_error])
539 .all(|v| v.is_finite());
540 finite.then_some(out)
541}
542
543enum Slot<'c> {
545 Closed,
547 Read(Box<PieceReader<'c>>),
549 Unbounded,
551}
552
553#[allow(clippy::large_enum_variant)]
556enum LawBasis<'b, 'c> {
557 Atomic {
560 curve: StationCurve<'c>,
561 placement: Option<Transform3>,
562 whole: Option<StationPiece<'c>>,
563 },
564 Composite(&'b CompositeBasis<'c>),
565}
566
567pub struct FrameLaw<'b, 'c> {
591 basis: LawBasis<'b, 'c>,
592 tolerance: Scalar,
593 joints: OnceCell<Option<Vec<Scalar>>>,
596 slots: Vec<OnceCell<Slot<'c>>>,
597}
598
599impl core::fmt::Debug for FrameLaw<'_, '_> {
600 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
601 f.debug_struct("FrameLaw")
602 .field("tolerance", &self.tolerance)
603 .finish_non_exhaustive()
604 }
605}
606
607fn by_quadrature(curve: StationCurve<'_>) -> bool {
609 matches!(curve, StationCurve::Offset(_)) || !curve.measure_is_exact()
610}
611
612impl<'b, 'c> FrameLaw<'b, 'c> {
613 pub fn atomic(
622 curve: StationCurve<'c>,
623 placement: Option<Transform3>,
624 tolerance: Scalar,
625 ) -> GeomResult<Self> {
626 let tolerance = checked_tolerance(tolerance)?;
627 let whole = if by_quadrature(curve) {
628 let piece = StationPiece::whole(curve)?;
629 Some(match placement {
630 Some(rigid) => piece.placed(rigid, true),
631 None => piece,
632 })
633 } else {
634 None
635 };
636 Ok(Self {
637 basis: LawBasis::Atomic {
638 curve,
639 placement,
640 whole,
641 },
642 tolerance,
643 joints: OnceCell::new(),
644 slots: vec![OnceCell::new()],
645 })
646 }
647
648 pub fn composite(basis: &'b CompositeBasis<'c>, tolerance: Scalar) -> GeomResult<Self> {
655 let tolerance = checked_tolerance(tolerance)?;
656 Ok(Self {
657 basis: LawBasis::Composite(basis),
658 tolerance,
659 joints: OnceCell::new(),
660 slots: basis.pieces().iter().map(|_| OnceCell::new()).collect(),
661 })
662 }
663
664 #[must_use]
668 pub fn bounds(&self, a: Scalar, b: Scalar, reading: FrameReading) -> Option<FrameLawBounds> {
669 match &self.basis {
670 LawBasis::Atomic {
671 curve,
672 placement,
673 whole,
674 } => match whole {
675 Some(whole) => self.read(0, whole, a, b, None, reading),
676 None => {
677 let mut piece = StationPiece::between(*curve, a, b).ok()?;
678 if let Some(rigid) = placement {
679 piece = piece.placed(*rigid, true);
680 }
681 piece_frame_bounds(&piece, 0.0, b - a, reading)
682 }
683 },
684 LawBasis::Composite(composite) => {
685 let slack = numerical_measure(b);
686 let starts = composite.starts();
687 for (k, piece) in composite.pieces().iter().enumerate() {
688 let (start, end) = (starts[k], starts[k] + piece.length());
689 if a >= start - slack && b <= end + slack {
690 let table = composite.tables()[k].as_ref();
691 let mut bounds =
692 self.read(k, piece, a - start, b - start, table, reading)?;
693 bounds.measure += self.joint_error(composite, k)?;
694 return Some(bounds);
695 }
696 }
697 None
698 }
699 }
700 }
701
702 fn read(
704 &self,
705 k: usize,
706 piece: &StationPiece<'c>,
707 from: Scalar,
708 to: Scalar,
709 table: Option<&OffsetTable>,
710 reading: FrameReading,
711 ) -> Option<FrameLawBounds> {
712 let slot = self.slots[k].get_or_init(|| {
713 if !by_quadrature(piece.curve) {
714 return Slot::Closed;
715 }
716 match PieceReader::new(piece, table, self.tolerance) {
717 Ok(Some(reader)) => Slot::Read(Box::new(reader)),
718 _ => Slot::Unbounded,
719 }
720 });
721 match slot {
722 Slot::Closed => piece_frame_bounds(piece, from, to, reading),
723 Slot::Read(reader) => read_frame_bounds(reader, piece, from, to, table, reading),
724 Slot::Unbounded => None,
725 }
726 }
727
728 fn joint_error(&self, composite: &CompositeBasis<'c>, k: usize) -> Option<Scalar> {
732 if composite.pieces()[..k]
733 .iter()
734 .all(|piece| piece.curve.measure_is_exact())
735 {
736 return Some(0.0);
737 }
738 let errors = self.joints.get_or_init(|| {
739 let joints = composite.joint_intervals(self.tolerance).ok()?;
740 let starts = composite.starts();
741 Some(
742 joints
743 .iter()
744 .enumerate()
745 .map(|(j, joint)| {
746 let at = starts[j + 1];
747 (at - joint.lower).max(joint.upper - at).max(0.0) * (1.0 + ROUNDING)
748 })
749 .collect(),
750 )
751 });
752 errors.as_ref()?.get(k.checked_sub(1)?).copied()
753 }
754}
755
756#[cfg(test)]
757mod tests {
758 use super::*;
759 use crate::station::{station_section2, station_section3, SectionFrame};
760 use axiolid_core::{Frame2, Point2, Point3};
761 use axiolid_curve::{
762 BankConvention, Banked3, CantLaw, CantPiece, Circle2, Circle3, Elevated3, ElevationLaw,
763 Line2,
764 };
765
766 type Axis = fn(&SectionFrame) -> Vec3;
768
769 fn assert_bounded(
772 section: impl Fn(Scalar) -> SectionFrame,
773 bounds: &FrameLawBounds,
774 lo: Scalar,
775 hi: Scalar,
776 ) {
777 let h = 1e-4 * (hi - lo);
778 for i in 1..20 {
779 let s = lo + (hi - lo) * i as Scalar / 20.0;
780 let (a, b, c) = (section(s - h), section(s), section(s + h));
781 let first = |f: fn(&SectionFrame) -> Vec3| ((f(&c) - f(&a)) / (2.0 * h)).length();
782 let second =
783 |f: fn(&SectionFrame) -> Vec3| ((f(&c) - 2.0 * f(&b) + f(&a)) / (h * h)).length();
784 let slack = |bound: Scalar| bound * (1.0 + 1e-3) + 1e-6;
785 let axes: [(Axis, [Scalar; 2], &str); 4] = [
786 (|f| f.point - Point3::ZERO, bounds.point, "point"),
787 (|f| f.tangent, bounds.tangent, "tangent"),
788 (|f| f.lateral, bounds.lateral, "lateral"),
789 (|f| f.up, bounds.up, "up"),
790 ];
791 for (f, [b1, b2], name) in axes {
792 assert!(first(f) <= slack(b1), "{name}' {} > {b1} at {s}", first(f));
793 assert!(
794 second(f) <= slack(b2),
795 "{name}'' {} > {b2} at {s}",
796 second(f)
797 );
798 }
799 }
800 }
801
802 fn whole3(curve: &Curve3, lo: Scalar, hi: Scalar) -> StationPiece<'_> {
803 StationPiece::between(StationCurve::Three(curve), lo, hi).unwrap()
804 }
805
806 #[test]
807 fn a_plan_circle_bounds_its_frame() {
808 let curve = Curve2::Circle(Circle2 {
809 frame: Frame2 {
810 origin: Point2::new(1.0, 2.0),
811 x: Vec2::X,
812 y: Vec2::Y,
813 },
814 radius: 3.0,
815 });
816 let piece = StationPiece::between(StationCurve::Two(&curve), 1.0, 5.0).unwrap();
817 let bounds = piece_frame_bounds(&piece, 0.0, 4.0, FrameReading::Section).unwrap();
818 assert_bounded(
819 |s| station_section2(&curve, 1.0 + s).unwrap(),
820 &bounds,
821 0.0,
822 4.0,
823 );
824 assert!((bounds.point[1] - 1.0 / 3.0).abs() <= 1e-12);
825 }
826
827 #[test]
828 fn a_tilted_circle_bounds_its_reference_up_frame() {
829 let x = Vec3::new(1.0, 0.0, 0.0);
830 let y = Vec3::new(0.0, 0.8, 0.6);
831 let curve = Curve3::Circle(Circle3 {
832 frame: axiolid_core::Frame3 {
833 origin: Point3::ZERO,
834 x,
835 y,
836 z: x.cross(y),
837 },
838 radius: 5.0,
839 });
840 let piece = whole3(&curve, 0.5, 2.5);
842 for reading in [FrameReading::Section, FrameReading::Plan] {
843 let bounds = piece_frame_bounds(&piece, 0.0, 2.0, reading).unwrap();
844 assert_bounded(
845 |s| {
846 let f = station_section3(&curve, 0.5 + s).unwrap();
847 match reading {
848 FrameReading::Section => f,
849 FrameReading::Plan => f.plan().unwrap(),
850 }
851 },
852 &bounds,
853 0.0,
854 2.0,
855 );
856 }
857 }
858
859 #[test]
860 fn a_vertical_curve_bounds_its_leaning_frame() {
861 let curve = Curve3::Elevated(Elevated3::new(
862 Curve2::Circle(Circle2 {
863 frame: Frame2 {
864 origin: Point2::ZERO,
865 x: Vec2::X,
866 y: Vec2::Y,
867 },
868 radius: 40.0,
869 }),
870 ElevationLaw::Polynomial {
871 coefficients: vec![1.0, 0.03, -0.0004],
872 },
873 ));
874 let piece = whole3(&curve, 0.0, 30.0);
875 let bounds = piece_frame_bounds(&piece, 2.0, 28.0, FrameReading::Section).unwrap();
876 assert_bounded(|s| station_section3(&curve, s).unwrap(), &bounds, 2.0, 28.0);
877 assert_eq!(bounds.measure, 0.0);
878 }
879
880 #[test]
881 fn a_banked_transition_bounds_its_rolled_frame() {
882 let cases = [
885 (BankConvention::TangentRotation, 60.0, 10.0, 50.0),
886 (BankConvention::VerticalRise, 60.0, 10.0, 50.0),
887 (BankConvention::TangentRotation, 8.0, 1.0, 7.0),
888 (BankConvention::VerticalRise, 8.0, 1.0, 7.0),
889 ];
890 for (convention, length, lo, hi) in cases {
891 let curve = Curve3::Banked(Banked3::new(
892 Elevated3::new(
893 Curve2::Circle(Circle2 {
894 frame: Frame2 {
895 origin: Point2::ZERO,
896 x: Vec2::X,
897 y: Vec2::Y,
898 },
899 radius: 300.0,
900 }),
901 ElevationLaw::constant_grade(60.0, 0.02),
902 ),
903 CantLaw::new(vec![CantPiece::cosine(length, 0.0, 0.15)]),
904 CantLaw::zero(length),
905 1.5,
906 convention,
907 ));
908 let piece = whole3(&curve, 0.0, length);
909 let bounds = piece_frame_bounds(&piece, lo, hi, FrameReading::Section).unwrap();
910 assert_bounded(|s| station_section3(&curve, s).unwrap(), &bounds, lo, hi);
911 }
912 }
913
914 #[test]
915 fn lines_are_straight_and_numerical_families_unbounded() {
916 let line = Curve2::Line(Line2 {
917 origin: Point2::ZERO,
918 direction: Vec2::new(3.0, 4.0),
919 });
920 let piece = StationPiece::between(StationCurve::Two(&line), 0.0, 10.0).unwrap();
921 let bounds = piece_frame_bounds(&piece, 0.0, 10.0, FrameReading::Section).unwrap();
922 assert!(bounds.is_straight());
923 let ellipse = Curve3::Ellipse(axiolid_curve::Ellipse3 {
924 frame: axiolid_core::Frame3 {
925 origin: Point3::ZERO,
926 x: Vec3::X,
927 y: Vec3::Y,
928 z: Vec3::Z,
929 },
930 semi_axis_x: 2.0,
931 semi_axis_y: 1.0,
932 });
933 let piece = whole3(&ellipse, 0.0, 1.0);
934 assert!(piece_frame_bounds(&piece, 0.0, 1.0, FrameReading::Section).is_none());
935 }
936}