1use axiolid_contracts::{GeomError, GeomResult};
58use axiolid_core::{Point3, Scalar, Transform3, Vec3};
59use axiolid_curve::{Curve2, Curve3, OffsetFrame, OffsetLaw, SeamSide};
60use axiolid_curve_evaluate_contract::DistanceConvention;
61
62use super::composite::{StationCurve, StationPiece};
63use super::{invalid, reference_up_frame, SectionFrame};
64use crate::arc_parameter::{NODES, WEIGHTS};
65
66pub const OFFSET_TOLERANCE: Scalar = 1e-9;
70
71pub const CUSP_TOLERANCE: Scalar = 1e-6;
75
76const MAX_OFFSET_PANELS: usize = 1 << 12;
78
79const MAX_ITERATIONS: usize = 100;
81
82fn degenerate(detail: String) -> GeomError {
83 GeomError::Degenerate(detail)
84}
85
86#[derive(Debug, Clone, Copy, PartialEq)]
88enum BaseCurve<'c> {
89 Two(&'c Curve2),
90 Three(&'c Curve3),
91}
92
93#[non_exhaustive]
96#[derive(Debug, Clone, Copy, PartialEq)]
97pub struct StationOffset<'c> {
98 curve: BaseCurve<'c>,
99 start: Scalar,
100 end: Scalar,
101 reversed: bool,
102 placement: Option<Transform3>,
103 placement_exact: bool,
104 law: OffsetLaw,
105}
106
107#[derive(Debug, Clone, PartialEq)]
109pub(crate) struct OffsetTable {
110 closed: Option<Scalar>,
112 knots: Vec<Scalar>,
114 sums: Vec<Scalar>,
116}
117
118impl OffsetTable {
119 pub(crate) fn length(&self, base: Scalar) -> Scalar {
121 match self.closed {
122 Some(scale) => scale * base,
123 None => self.sums[self.sums.len() - 1],
124 }
125 }
126}
127
128impl<'c> StationOffset<'c> {
129 pub fn new(base: StationPiece<'c>, law: OffsetLaw) -> GeomResult<Self> {
137 let curve =
138 match base.curve {
139 StationCurve::Two(curve) => BaseCurve::Two(curve),
140 StationCurve::Three(curve) => BaseCurve::Three(curve),
141 _ => return Err(invalid(
142 "station: an offset of an offset as a station basis: offset the base once, \
143 by the summed law"
144 .into(),
145 )),
146 };
147 match law {
148 OffsetLaw::Planar { distance } => {
149 if !distance.is_finite() {
150 return Err(invalid("station: an offset distance is not finite".into()));
151 }
152 if matches!(curve, BaseCurve::Three(_)) {
153 return Err(invalid(
154 "station: a planar (2D) offset of a 3D curve: a 3D offset needs a \
155 reference direction"
156 .into(),
157 ));
158 }
159 }
160 OffsetLaw::Directed {
161 distance,
162 reference_direction,
163 } => {
164 if !distance.is_finite()
165 || !reference_direction.is_finite()
166 || reference_direction.length() <= 1e-12
167 {
168 return Err(invalid(
169 "station: a 3D offset's distance or reference direction is not finite, \
170 or the direction is zero"
171 .into(),
172 ));
173 }
174 }
175 OffsetLaw::Linear { start, end, .. } => {
176 let finite = |o: axiolid_curve::PathOffsets| {
177 o.lateral.is_finite() && o.vertical.is_finite() && o.longitudinal.is_finite()
178 };
179 if !(finite(start) && finite(end)) {
180 return Err(invalid(
181 "station: an offset by distances has an offset that is not finite".into(),
182 ));
183 }
184 }
185 _ => {
186 return Err(invalid(
187 "station: an offset law this evaluator does not know".into(),
188 ))
189 }
190 }
191 if !base.seams()?.is_empty() {
192 return Err(invalid(
193 "station: an offset piece whose base crosses a seam of its curve: split the base \
194 there, one offset piece per span between seams"
195 .into(),
196 ));
197 }
198 Ok(Self {
199 curve,
200 start: base.start,
201 end: base.end,
202 reversed: base.reversed,
203 placement: base.placement,
204 placement_exact: base.placement_exact,
205 law,
206 })
207 }
208
209 #[must_use]
211 pub fn base(&self) -> StationPiece<'c> {
212 let curve = match self.curve {
213 BaseCurve::Two(curve) => StationCurve::Two(curve),
214 BaseCurve::Three(curve) => StationCurve::Three(curve),
215 };
216 StationPiece {
217 curve,
218 start: self.start,
219 end: self.end,
220 reversed: self.reversed,
221 placement: self.placement,
222 placement_exact: self.placement_exact,
223 }
224 }
225
226 #[must_use]
228 pub fn law(&self) -> OffsetLaw {
229 self.law
230 }
231
232 #[must_use]
235 pub fn convention(&self) -> DistanceConvention {
236 self.base().convention()
237 }
238
239 pub fn length(&self) -> GeomResult<Scalar> {
246 Ok(self.table()?.length(self.base_length()))
247 }
248
249 pub(super) fn base_length(&self) -> Scalar {
250 self.end - self.start
251 }
252
253 fn plan_measured(&self) -> bool {
254 self.convention() == DistanceConvention::PlanDistance
255 }
256
257 fn banked(&self) -> bool {
258 matches!(self.curve, BaseCurve::Three(Curve3::Banked(_)))
259 }
260
261 pub(crate) fn frame_is_exact(&self) -> bool {
263 self.law.is_constant()
264 && matches!(
265 self.curve,
266 BaseCurve::Two(Curve2::Line(_)) | BaseCurve::Three(Curve3::Line(_))
267 )
268 && self.placement_exact
269 }
270
271 pub(crate) fn measure_is_exact(&self) -> bool {
273 matches!(self.closed_form(), Ok(Some(_)))
274 }
275
276 fn base_section(&self, v: Scalar) -> GeomResult<SectionFrame> {
278 self.base().section_on(v, SeamSide::Outgoing)
279 }
280
281 pub(super) fn displacement(&self, section: &SectionFrame, v: Scalar) -> GeomResult<Vec3> {
283 match self.law {
284 OffsetLaw::Planar { distance } => Ok(distance * section.lateral),
285 OffsetLaw::Directed {
286 distance,
287 reference_direction,
288 } => {
289 let reference = reference_direction.normalize();
290 let normal = reference.cross(section.tangent);
291 let size = normal.length();
292 if !(size.is_finite() && size > 1e-9) {
293 return Err(invalid(
294 "station: a 3D offset whose base tangent is parallel to its reference \
295 direction: the offset direction is undefined there"
296 .into(),
297 ));
298 }
299 Ok(distance / size * normal)
300 }
301 OffsetLaw::Linear { start, end, frame } => {
302 let length = self.base_length();
303 let at = start.lerp(end, (v / length).clamp(0.0, 1.0));
304 let frame = match frame {
305 OffsetFrame::Section => *section,
306 OffsetFrame::Plan => section.plan()?,
307 _ => {
308 return Err(invalid(
309 "station: an offset frame this evaluator does not know".into(),
310 ))
311 }
312 };
313 Ok(at.lateral * frame.lateral
314 + at.vertical * frame.up
315 + at.longitudinal * frame.tangent)
316 }
317 _ => Err(invalid(
318 "station: an offset law this evaluator does not know".into(),
319 )),
320 }
321 }
322
323 fn displacement_at(&self, v: Scalar) -> GeomResult<Vec3> {
325 let section = self.base_section(v)?;
326 self.displacement(§ion, v)
327 }
328
329 fn step(&self) -> Scalar {
331 (self.base_length() / 8.0).min(1e-2)
332 }
333
334 fn velocity(&self, v: Scalar) -> GeomResult<(Vec3, SectionFrame)> {
336 let section = self.base_section(v)?;
337 let rate = if self.plan_measured() {
340 let horizontal = Vec3::new(section.tangent.x, section.tangent.y, 0.0).length();
341 if horizontal <= 1e-12 {
342 return Err(invalid(
343 "station: a plan-measured offset base runs vertically, so it has no plan \
344 rate"
345 .into(),
346 ));
347 }
348 1.0 / horizontal
349 } else {
350 1.0
351 };
352 let length = self.base_length();
353 let h = self.step();
354 let d = |x: Scalar| self.displacement_at(x);
355 let derivative = if v - 2.0 * h < 0.0 {
358 (-25.0 * self.displacement(§ion, v)? + 48.0 * d(v + h)? - 36.0 * d(v + 2.0 * h)?
359 + 16.0 * d(v + 3.0 * h)?
360 - 3.0 * d(v + 4.0 * h)?)
361 / (12.0 * h)
362 } else if v + 2.0 * h > length {
363 (25.0 * self.displacement(§ion, v)? - 48.0 * d(v - h)? + 36.0 * d(v - 2.0 * h)?
364 - 16.0 * d(v - 3.0 * h)?
365 + 3.0 * d(v - 4.0 * h)?)
366 / (12.0 * h)
367 } else {
368 (-d(v + 2.0 * h)? + 8.0 * d(v + h)? - 8.0 * d(v - h)? + d(v - 2.0 * h)?) / (12.0 * h)
369 };
370 Ok((rate * section.tangent + derivative, section))
371 }
372
373 fn speed(&self, v: Scalar) -> GeomResult<Scalar> {
375 let (velocity, section) = self.velocity(v)?;
376 let (along, forward) = if self.plan_measured() {
377 let flat = Vec3::new(velocity.x, velocity.y, 0.0);
378 let heading = Vec3::new(section.tangent.x, section.tangent.y, 0.0).normalize();
379 (flat.length(), flat.dot(heading))
380 } else {
381 (velocity.length(), velocity.dot(section.tangent))
382 };
383 if !(forward.is_finite() && forward > CUSP_TOLERANCE) {
384 return Err(degenerate(format!(
385 "station: an offset with a cusp or turning back at {v} along its base: its speed \
386 in the base's direction is {forward}, the offset reaches the base's radius of \
387 curvature there"
388 )));
389 }
390 Ok(along)
391 }
392
393 fn closed_form(&self) -> GeomResult<Option<Scalar>> {
396 if !self.law.is_constant() {
397 return Ok(None);
398 }
399 let longitudinal = matches!(
400 self.law,
401 OffsetLaw::Linear { start, .. } if start.longitudinal != 0.0
402 );
403 let place_point = |p: Point3| self.placement.map_or(p, |r| r.transform_point3(p));
404 let place_vector = |v: Vec3| self.placement.map_or(v, |r| r.transform_vector3(v));
405 let (centre, axis, radius) = match self.curve {
406 BaseCurve::Two(Curve2::Line(_)) | BaseCurve::Three(Curve3::Line(_)) => {
407 return Ok(Some(1.0))
408 }
409 BaseCurve::Two(Curve2::Circle(circle)) => (
410 Point3::new(circle.frame.origin.x, circle.frame.origin.y, 0.0),
411 Vec3::Z,
412 circle.radius,
413 ),
414 BaseCurve::Three(Curve3::Circle(circle)) => {
415 (circle.frame.origin, circle.frame.z, circle.radius)
416 }
417 _ => return Ok(None),
418 };
419 if longitudinal {
420 return Ok(None);
421 }
422 let (centre, axis) = (place_point(centre), place_vector(axis).normalize());
423 let upright = axis.cross(Vec3::Z).length() <= 1e-12;
424 let invariant = match self.law {
425 OffsetLaw::Directed {
426 reference_direction,
427 ..
428 } => reference_direction.normalize().cross(axis).length() <= 1e-12,
429 _ => upright,
430 };
431 if !invariant {
432 return Ok(None);
433 }
434 let section = self.base_section(0.0)?;
435 let towards = centre - section.point;
436 let inward = towards - towards.dot(axis) * axis;
437 let inward = inward / inward.length();
438 let scale = (radius - self.displacement(§ion, 0.0)?.dot(inward)) / radius;
439 if !(scale.is_finite() && scale > CUSP_TOLERANCE) {
440 return Err(degenerate(format!(
441 "station: an offset of a circle of radius {radius} whose radius collapses to {} \
442 (at or below zero): the offset passes through or beyond the centre",
443 scale * radius
444 )));
445 }
446 Ok(Some(scale))
447 }
448
449 pub(crate) fn table(&self) -> GeomResult<OffsetTable> {
452 if let Some(scale) = self.closed_form()? {
453 return Ok(OffsetTable {
454 closed: Some(scale),
455 knots: Vec::new(),
456 sums: Vec::new(),
457 });
458 }
459 let length = self.base_length();
460 let rule = |a: Scalar, b: Scalar| -> GeomResult<Scalar> {
461 let (half, mid) = (0.5 * (b - a), 0.5 * (a + b));
462 let mut sum = 0.0;
463 for (node, weight) in NODES.iter().zip(WEIGHTS.iter()) {
464 sum += weight * self.speed(mid + half * node)?;
465 }
466 Ok(sum * half)
467 };
468 let coarse = rule(0.0, length)?;
469 let tolerance = OFFSET_TOLERANCE * coarse.abs().max(1.0);
470 let floor = 4.0 * self.step() * 1e-3;
471 let mut knots = vec![0.0];
472 let mut sums = vec![0.0];
473 let mut stack = vec![(0.0, length, coarse)];
474 while let Some((lo, hi, whole)) = stack.pop() {
475 if knots.len() > MAX_OFFSET_PANELS {
476 return Err(GeomError::BudgetExceeded {
477 resource: "offset length quadrature panels",
478 });
479 }
480 let mid = 0.5 * (lo + hi);
481 let (left, right) = (rule(lo, mid)?, rule(mid, hi)?);
482 let share = tolerance * (hi - lo) / length;
483 if (left + right - whole).abs() <= share || hi - lo <= floor || !(mid > lo && mid < hi)
484 {
485 let run = sums[sums.len() - 1];
486 knots.extend([mid, hi]);
487 sums.extend([run + left, run + left + right]);
488 } else {
489 stack.push((mid, hi, right));
490 stack.push((lo, mid, left));
491 }
492 }
493 let table = OffsetTable {
494 closed: None,
495 knots,
496 sums,
497 };
498 self.refuse_self_crossing(&table)?;
499 Ok(table)
500 }
501
502 fn refuse_self_crossing(&self, table: &OffsetTable) -> GeomResult<()> {
505 let mut points = Vec::new();
506 for pair in table.knots.windows(2) {
507 for k in 0..4 {
508 let v = pair[0] + (pair[1] - pair[0]) * Scalar::from(k) / 4.0;
509 points.push(self.point_at_base(v)?);
510 }
511 }
512 points.push(self.point_at_base(self.base_length())?);
513 let z = points[0].z;
514 if points.iter().any(|p| p.z != z) {
515 return Ok(());
516 }
517 let cross =
518 |a: Point3, b: Point3, c: Point3| (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
519 let n = points.len();
520 for i in 0..n.saturating_sub(1) {
521 for j in i + 2..n - 1 {
522 let (a, b, c, d) = (points[i], points[i + 1], points[j], points[j + 1]);
523 let (d1, d2) = (cross(a, b, c), cross(a, b, d));
524 let (d3, d4) = (cross(c, d, a), cross(c, d, b));
525 if d1 * d2 < 0.0 && d3 * d4 < 0.0 {
526 return Err(degenerate(
527 "station: an offset that crosses itself within its piece: the offset \
528 distance exceeds the room the base's curvature leaves"
529 .into(),
530 ));
531 }
532 }
533 }
534 Ok(())
535 }
536
537 pub(super) fn base_measure(&self, m: Scalar, table: &OffsetTable) -> GeomResult<Scalar> {
540 let length = self.base_length();
541 if let Some(scale) = table.closed {
542 return Ok((m / scale).clamp(0.0, length));
543 }
544 let last = table.sums.len() - 1;
545 let m = m.clamp(0.0, table.sums[last]);
546 let i = table.sums[1..last]
547 .partition_point(|sum| *sum <= m)
548 .min(last - 1);
549 let (lo, hi) = (table.knots[i], table.knots[i + 1]);
550 let target = m - table.sums[i];
551 let span = table.sums[i + 1] - table.sums[i];
552 let tolerance = 1e-3 * OFFSET_TOLERANCE * m.max(1.0);
553 let travelled = |v: Scalar| -> GeomResult<Scalar> {
554 let (half, mid) = (0.5 * (v - lo), 0.5 * (lo + v));
555 let mut sum = 0.0;
556 for (node, weight) in NODES.iter().zip(WEIGHTS.iter()) {
557 sum += weight * self.speed(mid + half * node)?;
558 }
559 Ok(sum * half)
560 };
561 let (mut a, mut b) = (lo, hi);
562 let mut v = if span > 0.0 {
563 lo + (hi - lo) * (target / span)
564 } else {
565 lo
566 };
567 for _ in 0..MAX_ITERATIONS {
568 let residual = travelled(v)? - target;
569 if residual.abs() <= tolerance {
570 return Ok(v);
571 }
572 if residual < 0.0 {
573 a = v;
574 } else {
575 b = v;
576 }
577 let newton = v - residual / self.speed(v)?;
578 v = if newton > a && newton < b {
579 newton
580 } else {
581 0.5 * (a + b)
582 };
583 if !(v > a && v < b) {
584 return Ok(v);
585 }
586 }
587 Ok(v)
588 }
589
590 fn point_at_base(&self, v: Scalar) -> GeomResult<Point3> {
592 let section = self.base_section(v)?;
593 Ok(section.point + self.displacement(§ion, v)?)
594 }
595
596 pub(crate) fn point_at(&self, m: Scalar, table: Option<&OffsetTable>) -> GeomResult<Point3> {
599 let built;
600 let table = match table {
601 Some(table) => table,
602 None => {
603 built = self.table()?;
604 &built
605 }
606 };
607 self.point_at_base(self.base_measure(m, table)?)
608 }
609
610 pub(crate) fn section_at(
614 &self,
615 m: Scalar,
616 table: Option<&OffsetTable>,
617 ) -> GeomResult<SectionFrame> {
618 let built;
619 let table = match table {
620 Some(table) => table,
621 None => {
622 built = self.table()?;
623 &built
624 }
625 };
626 let v = self.base_measure(m, table)?;
627 let (point, tangent, base) = if table.closed.is_some() {
628 let section = self.base_section(v)?;
630 let point = section.point + self.displacement(§ion, v)?;
631 (point, section.tangent, section)
632 } else {
633 let (velocity, section) = self.velocity(v)?;
634 let point = section.point + self.displacement(§ion, v)?;
635 (point, velocity, section)
636 };
637 own_frame(point, tangent, &base, self.banked())
638 }
639}
640
641fn own_frame(
646 point: Point3,
647 direction: Vec3,
648 base: &SectionFrame,
649 banked: bool,
650) -> GeomResult<SectionFrame> {
651 let size = direction.length();
652 if !(size.is_finite() && size > 0.0) {
653 return Err(invalid(
654 "station: the offset has no tangent direction there".into(),
655 ));
656 }
657 let tangent = direction / size;
658 if banked {
659 let lateral = base.lateral - base.lateral.dot(tangent) * tangent;
660 let magnitude = lateral.length();
661 if !(magnitude.is_finite() && magnitude > 1e-12) {
662 return Err(invalid(
663 "station: the offset's tangent lies along its banked base's lateral".into(),
664 ));
665 }
666 let lateral = lateral / magnitude;
667 return Ok(SectionFrame {
668 point,
669 tangent,
670 lateral,
671 up: tangent.cross(lateral),
672 });
673 }
674 if tangent.z == 0.0 {
675 return Ok(SectionFrame {
676 point,
677 tangent,
678 lateral: Vec3::new(-tangent.y, tangent.x, 0.0),
679 up: Vec3::Z,
680 });
681 }
682 reference_up_frame(point, tangent)
683}
684
685pub fn offset_pieces<'c>(
701 base: &[StationPiece<'c>],
702 law: OffsetLaw,
703) -> GeomResult<Vec<StationPiece<'c>>> {
704 if base.is_empty() {
705 return Err(invalid("station: an offset of no pieces".into()));
706 }
707 let total: Scalar = base.iter().map(StationPiece::length).sum();
708 let mut out = Vec::new();
709 let mut run = 0.0;
710 for piece in base {
711 let mut cuts = vec![0.0];
712 cuts.extend(piece.seams()?.into_iter().map(|seam| seam.distance));
713 cuts.push(piece.length());
714 for pair in cuts.windows(2) {
715 let (a, b) = (pair[0], pair[1]);
716 let mut span = *piece;
717 if piece.reversed {
718 span.start = piece.end - b;
719 span.end = piece.end - a;
720 } else {
721 span.start = piece.start + a;
722 span.end = piece.start + b;
723 }
724 let law = match law {
725 OffsetLaw::Linear { start, end, frame } => OffsetLaw::Linear {
726 start: start.lerp(end, (run + a) / total),
727 end: start.lerp(end, (run + b) / total),
728 frame,
729 },
730 other => other,
731 };
732 out.push(StationPiece::offset(StationOffset::new(span, law)?)?);
733 }
734 run += piece.length();
735 }
736 Ok(out)
737}