1use ifc_lite_core::{AttributeValue, DecodedEntity, EntityDecoder, EntityScanner, IfcType};
30use nalgebra::{Point3, Vector3};
31use std::sync::OnceLock;
32
33use crate::{Error, Result};
34
35macro_rules! ifc_type_fn {
38 ($name:ident, $literal:expr) => {
39 fn $name() -> IfcType {
40 static T: OnceLock<IfcType> = OnceLock::new();
41 *T.get_or_init(|| IfcType::from_str($literal))
42 }
43 };
44}
45
46ifc_type_fn!(t_alignment_curve, "IFCALIGNMENTCURVE");
47ifc_type_fn!(t_alignment_2d_horizontal, "IFCALIGNMENT2DHORIZONTAL");
48ifc_type_fn!(t_alignment_2d_horizontal_segment, "IFCALIGNMENT2DHORIZONTALSEGMENT");
49ifc_type_fn!(t_alignment_2d_vertical, "IFCALIGNMENT2DVERTICAL");
50ifc_type_fn!(t_line_segment_2d, "IFCLINESEGMENT2D");
51ifc_type_fn!(t_circular_arc_segment_2d, "IFCCIRCULARARCSEGMENT2D");
52ifc_type_fn!(t_transition_curve_segment_2d, "IFCTRANSITIONCURVESEGMENT2D");
53ifc_type_fn!(t_ver_seg_line, "IFCALIGNMENT2DVERSEGLINE");
54ifc_type_fn!(t_ver_seg_parabolic, "IFCALIGNMENT2DVERSEGPARABOLICARC");
55ifc_type_fn!(t_ver_seg_circular, "IFCALIGNMENT2DVERSEGCIRCULARARC");
56ifc_type_fn!(t_alignment_2d_cant, "IFCALIGNMENT2DCANT");
57ifc_type_fn!(t_cant_seg_const, "IFCALIGNMENT2DCANTSEGLINE");
58ifc_type_fn!(t_cant_seg_transition, "IFCALIGNMENT2DCANTSEGTRANSITION");
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64enum TransitionKind {
65 Clothoid,
67 Bloss,
69 Cosine,
71 Sine,
73 CubicParabola,
78 BiquadraticParabola,
83}
84
85impl TransitionKind {
86 fn from_enum(name: &str) -> Self {
87 match name {
88 "CLOTHOIDCURVE" => Self::Clothoid,
89 "BLOSSCURVE" => Self::Bloss,
90 "COSINECURVE" => Self::Cosine,
91 "SINECURVE" => Self::Sine,
92 "CUBICPARABOLA" => Self::CubicParabola,
93 "BIQUADRATICPARABOLA" => Self::BiquadraticParabola,
94 _ => Self::Clothoid,
96 }
97 }
98
99 fn heading_integral(self, u: f64) -> f64 {
103 let u = u.clamp(0.0, 1.0);
104 match self {
105 Self::Clothoid | Self::CubicParabola => 0.5 * u * u,
106 Self::Bloss | Self::BiquadraticParabola => {
107 u * u * u - 0.5 * u * u * u * u
109 }
110 Self::Cosine => {
111 0.5 * u - (std::f64::consts::PI * u).sin() / (2.0 * std::f64::consts::PI)
113 }
114 Self::Sine => {
115 let two_pi = 2.0 * std::f64::consts::PI;
117 0.5 * u * u + ((two_pi * u).cos() - 1.0) / (4.0 * std::f64::consts::PI.powi(2))
118 }
119 }
120 }
121}
122
123#[derive(Debug, Clone, Copy)]
127pub struct CantSegSpec {
128 pub start: f64,
131 pub length: f64,
133 pub roll_start: f64,
135 pub roll_end: f64,
138}
139
140#[derive(Debug, Clone, Copy)]
144enum CantSeg {
145 Const { start: f64, length: f64, roll: f64 },
148 Linear {
151 start: f64,
152 length: f64,
153 roll_start: f64,
154 roll_end: f64,
155 },
156}
157
158impl CantSeg {
159 fn from_spec(spec: CantSegSpec) -> Self {
160 if (spec.roll_end - spec.roll_start).abs() < 1e-12 {
161 CantSeg::Const {
162 start: spec.start,
163 length: spec.length,
164 roll: spec.roll_start,
165 }
166 } else {
167 CantSeg::Linear {
168 start: spec.start,
169 length: spec.length,
170 roll_start: spec.roll_start,
171 roll_end: spec.roll_end,
172 }
173 }
174 }
175}
176
177pub fn parse_cant(entity: &DecodedEntity, decoder: &mut EntityDecoder) -> Result<Vec<CantSegSpec>> {
203 if entity.ifc_type != t_alignment_2d_cant() {
204 return Err(Error::geometry(format!(
205 "#{} is not IfcAlignment2DCant",
206 entity.id,
207 )));
208 }
209 let rail_head_distance = entity.get_float(0).unwrap_or(1.435); let segs_attr = entity
211 .get(1)
212 .ok_or_else(|| Error::geometry("IfcAlignment2DCant missing Segments".to_string()))?;
213 let seg_refs = segs_attr
214 .as_list()
215 .ok_or_else(|| Error::geometry("Cant Segments must be a list".to_string()))?;
216
217 let mut specs = Vec::with_capacity(seg_refs.len());
218 for r in seg_refs {
219 let sid = r.as_entity_ref().ok_or_else(|| {
220 Error::geometry("Cant segment ref is not an entity reference".to_string())
221 })?;
222 let seg = decoder.decode_by_id(sid)?;
223 let start = seg.get_float(3).ok_or_else(|| {
224 Error::geometry(format!("CantSegment #{} missing StartDistAlong", sid))
225 })?;
226 let length = seg.get_float(4).ok_or_else(|| {
227 Error::geometry(format!("CantSegment #{} missing HorizontalLength", sid))
228 })?;
229 let start_left = seg.get_float(5).unwrap_or(0.0);
230 let start_right = seg.get_float(6).unwrap_or(0.0);
231 let roll_start = ((start_right - start_left) / rail_head_distance.max(1e-9)).atan();
232 let is_transition = seg.ifc_type == t_cant_seg_transition();
239 let is_const = seg.ifc_type == t_cant_seg_const();
240 let (roll_end_left, roll_end_right) = if is_transition {
241 (
242 seg.get_float(7).unwrap_or(start_left),
243 seg.get_float(8).unwrap_or(start_right),
244 )
245 } else if is_const {
246 (start_left, start_right)
247 } else {
248 (start_left, start_right)
250 };
251 let roll_end = ((roll_end_right - roll_end_left) / rail_head_distance.max(1e-9)).atan();
252 specs.push(CantSegSpec {
253 start,
254 length,
255 roll_start,
256 roll_end,
257 });
258 }
259 Ok(specs)
260}
261
262fn read_bool(attr: Option<&AttributeValue>) -> bool {
265 attr.and_then(|v| v.as_enum()).map(|s| s == "T").unwrap_or(false)
266}
267
268#[derive(Debug, Clone, Copy)]
270enum HSeg {
271 Line {
272 sx: f64,
273 sy: f64,
274 heading: f64,
275 length: f64,
276 cum_start: f64,
277 },
278 Arc {
279 sx: f64,
280 sy: f64,
281 heading: f64,
282 radius: f64,
283 length: f64,
284 ccw: bool,
285 cum_start: f64,
286 },
287 Transition {
293 sx: f64,
294 sy: f64,
295 heading: f64,
296 length: f64,
297 start_curv: f64,
298 end_curv: f64,
299 kind: TransitionKind,
300 cum_start: f64,
301 },
302}
303
304#[derive(Debug, Clone, Copy)]
305enum VSeg {
306 Line {
307 start: f64,
308 length: f64,
309 h0: f64,
310 g0: f64,
311 },
312 Parabolic {
313 start: f64,
314 length: f64,
315 h0: f64,
316 g0: f64,
317 parabola_constant: f64,
318 is_convex: bool,
319 },
320 CircularArc {
325 start: f64,
326 length: f64,
327 h0: f64,
328 g0: f64,
329 radius: f64,
330 is_convex: bool,
331 },
332}
333
334#[derive(Debug, Clone, Copy)]
336pub struct AlignmentFrame {
337 pub origin: Point3<f64>,
339 pub right: Vector3<f64>,
343 pub up: Vector3<f64>,
345 pub tangent: Vector3<f64>,
348}
349
350pub struct AlignmentCurve {
353 horizontal: Vec<HSeg>,
354 vertical: Vec<VSeg>,
355 cant: Vec<CantSeg>,
356}
357
358impl AlignmentCurve {
359 pub fn parse(directrix: &DecodedEntity, decoder: &mut EntityDecoder) -> Result<Option<Self>> {
375 if directrix.ifc_type == IfcType::IfcPolyline {
376 return Self::from_polyline(directrix, decoder).map(Some);
377 }
378 if directrix.ifc_type != t_alignment_curve() {
379 return Ok(None);
380 }
381
382 let angle_scale = decoder.plane_angle_to_radians();
383
384 let h_id = directrix.get_ref(0).ok_or_else(|| {
386 Error::geometry("IfcAlignmentCurve missing Horizontal".to_string())
387 })?;
388 let horizontal = parse_horizontal(h_id, decoder, angle_scale)?;
389
390 let vertical = match directrix.get(1) {
392 Some(v) if !v.is_null() => match v.as_entity_ref() {
393 Some(v_id) => parse_vertical(v_id, decoder)?,
394 None => Vec::new(),
395 },
396 _ => Vec::new(),
397 };
398
399 let cant = Vec::new();
406
407 Ok(Some(Self {
408 horizontal,
409 vertical,
410 cant,
411 }))
412 }
413
414 pub fn with_cant_segments(mut self, segments: Vec<CantSegSpec>) -> Self {
420 self.cant = segments.into_iter().map(CantSeg::from_spec).collect();
421 self
422 }
423
424 pub fn auto_attach_cant(self, content: &str, decoder: &mut EntityDecoder) -> Result<Self> {
431 let mut scanner = EntityScanner::new(content);
432 while let Some((id, type_name, _, _)) = scanner.next_entity() {
433 if type_name == "IFCALIGNMENT2DCANT" {
434 let entity = decoder.decode_by_id(id)?;
435 let specs = parse_cant(&entity, decoder)?;
436 return Ok(self.with_cant_segments(specs));
437 }
438 }
439 Ok(self)
440 }
441
442 pub fn horizontal_length(&self) -> f64 {
444 self.horizontal
445 .last()
446 .map(|s| h_cum_start(s) + h_length(s))
447 .unwrap_or(0.0)
448 }
449
450 fn from_polyline(curve: &DecodedEntity, decoder: &mut EntityDecoder) -> Result<Self> {
455 let points_attr = curve
456 .get(0)
457 .ok_or_else(|| Error::geometry("IfcPolyline missing Points".to_string()))?;
458 let point_refs = points_attr
459 .as_list()
460 .ok_or_else(|| Error::geometry("IfcPolyline Points is not a list".to_string()))?;
461 if point_refs.len() < 2 {
462 return Err(Error::geometry(
463 "IfcPolyline directrix needs ≥ 2 points".to_string(),
464 ));
465 }
466 let mut pts: Vec<(f64, f64, f64)> = Vec::with_capacity(point_refs.len());
467 for r in point_refs {
468 let pid = r
469 .as_entity_ref()
470 .ok_or_else(|| Error::geometry("Polyline point is not an entity ref".to_string()))?;
471 let p = decoder.decode_by_id(pid)?;
472 let coords = p
473 .get_list(0)
474 .ok_or_else(|| Error::geometry("CartesianPoint missing Coordinates".to_string()))?;
475 let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
476 let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
477 let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
478 pts.push((x, y, z));
479 }
480
481 let mut horizontal: Vec<HSeg> = Vec::with_capacity(pts.len() - 1);
482 let mut vertical: Vec<VSeg> = Vec::with_capacity(pts.len() - 1);
483 let mut cum_xy = 0.0;
484 for w in pts.windows(2) {
485 let (x0, y0, z0) = w[0];
486 let (x1, y1, z1) = w[1];
487 let dx = x1 - x0;
488 let dy = y1 - y0;
489 let dz = z1 - z0;
490 let len_xy = (dx * dx + dy * dy).sqrt();
491 if len_xy < 1e-12 {
492 continue;
496 }
497 let heading = dy.atan2(dx);
498 horizontal.push(HSeg::Line {
499 sx: x0,
500 sy: y0,
501 heading,
502 length: len_xy,
503 cum_start: cum_xy,
504 });
505 let gradient = dz / len_xy;
506 vertical.push(VSeg::Line {
507 start: cum_xy,
508 length: len_xy,
509 h0: z0,
510 g0: gradient,
511 });
512 cum_xy += len_xy;
513 }
514 if horizontal.is_empty() {
515 return Err(Error::geometry(
516 "IfcPolyline directrix degenerated to zero horizontal length".to_string(),
517 ));
518 }
519 Ok(Self {
520 horizontal,
521 vertical,
522 cant: Vec::new(),
523 })
524 }
525
526 pub fn evaluate(&self, station: f64) -> AlignmentFrame {
530 let (x, y, heading) = self.evaluate_horizontal(station);
531 let z = self.evaluate_vertical(station);
532 let slope = self.evaluate_vertical_slope(station);
533 let cos_h = heading.cos();
534 let sin_h = heading.sin();
535 let right = Vector3::new(sin_h, -cos_h, 0.0);
538 let up = Vector3::new(0.0, 0.0, 1.0);
539 let inv_norm = (1.0 + slope * slope).sqrt();
543 let tangent = Vector3::new(cos_h / inv_norm, sin_h / inv_norm, slope / inv_norm);
544 AlignmentFrame {
545 origin: Point3::new(x, y, z),
546 right,
547 up,
548 tangent,
549 }
550 }
551
552 pub fn cant_angle(&self, station: f64) -> f64 {
555 for seg in &self.cant {
556 match seg {
557 CantSeg::Const {
558 start,
559 length,
560 roll,
561 } => {
562 if station >= *start - 1e-9 && station <= start + length + 1e-9 {
563 return *roll;
564 }
565 }
566 CantSeg::Linear {
567 start,
568 length,
569 roll_start,
570 roll_end,
571 } => {
572 if station >= *start - 1e-9 && station <= start + length + 1e-9 {
573 let t = ((station - start) / length.max(1e-12)).clamp(0.0, 1.0);
574 return roll_start * (1.0 - t) + roll_end * t;
575 }
576 }
577 }
578 }
579 0.0
580 }
581
582 fn evaluate_vertical_slope(&self, station: f64) -> f64 {
583 if self.vertical.is_empty() {
584 return 0.0;
585 }
586 for seg in &self.vertical {
587 let start = v_start(seg);
588 let length = v_length(seg);
589 if station <= start + length + 1e-9 {
590 let local = (station - start).max(0.0).min(length);
591 return v_eval(seg, local).1;
592 }
593 }
594 let last = self.vertical.last().unwrap();
595 v_eval(last, v_length(last)).1
596 }
597
598 fn evaluate_horizontal(&self, station: f64) -> (f64, f64, f64) {
599 if self.horizontal.is_empty() {
600 return (0.0, 0.0, 0.0);
601 }
602 for seg in &self.horizontal {
608 let len = h_length(seg);
609 let cum = h_cum_start(seg);
610 if station <= cum + len + 1e-9 {
611 let local = (station - cum).max(0.0).min(len);
612 return h_eval(seg, local);
613 }
614 }
615 let last = self.horizontal.last().unwrap();
617 let len = h_length(last);
618 let (x, y, h) = h_eval(last, len);
619 let extra = station - (h_cum_start(last) + len);
620 (x + extra * h.cos(), y + extra * h.sin(), h)
621 }
622
623 fn evaluate_vertical(&self, station: f64) -> f64 {
624 if self.vertical.is_empty() {
625 return 0.0;
626 }
627 for seg in &self.vertical {
628 let start = v_start(seg);
629 let length = v_length(seg);
630 if station <= start + length + 1e-9 {
631 let local = (station - start).max(0.0).min(length);
632 return v_eval_height(seg, local);
633 }
634 }
635 let last = self.vertical.last().unwrap();
637 let length = v_length(last);
638 let (z_end, slope) = v_eval(last, length);
639 let extra = station - (v_start(last) + length);
640 z_end + slope * extra
641 }
642}
643
644fn parse_horizontal(
645 h_id: u32,
646 decoder: &mut EntityDecoder,
647 angle_scale: f64,
648) -> Result<Vec<HSeg>> {
649 let h_entity = decoder.decode_by_id(h_id)?;
650 if h_entity.ifc_type != t_alignment_2d_horizontal() {
651 return Err(Error::geometry(format!(
652 "AlignmentCurve.Horizontal #{} is not IfcAlignment2DHorizontal",
653 h_id,
654 )));
655 }
656 let _start_dist_along = h_entity.get_float(0).unwrap_or(0.0);
658 let segs_attr = h_entity
659 .get(1)
660 .ok_or_else(|| Error::geometry("IfcAlignment2DHorizontal missing Segments".to_string()))?;
661 let seg_refs = segs_attr
662 .as_list()
663 .ok_or_else(|| Error::geometry("Horizontal Segments must be a list".to_string()))?;
664
665 let mut segments = Vec::with_capacity(seg_refs.len());
666 let mut cumulative = 0.0;
667 for seg_ref in seg_refs {
668 let seg_id = seg_ref.as_entity_ref().ok_or_else(|| {
669 Error::geometry("Horizontal segment ref is not an entity reference".to_string())
670 })?;
671 let seg = decoder.decode_by_id(seg_id)?;
672 if seg.ifc_type != t_alignment_2d_horizontal_segment() {
673 return Err(Error::geometry(format!(
674 "#{} is not IfcAlignment2DHorizontalSegment",
675 seg_id,
676 )));
677 }
678 let curve_id = seg.get_ref(3).ok_or_else(|| {
680 Error::geometry(format!(
681 "IfcAlignment2DHorizontalSegment #{} missing CurveGeometry",
682 seg_id,
683 ))
684 })?;
685 let curve = decoder.decode_by_id(curve_id)?;
686
687 let sp_id = curve.get_ref(0).ok_or_else(|| {
692 Error::geometry(format!("CurveSegment #{} missing StartPoint", curve_id))
693 })?;
694 let sp = decoder.decode_by_id(sp_id)?;
695 let coords = sp
696 .get_list(0)
697 .ok_or_else(|| Error::geometry("StartPoint missing Coordinates".to_string()))?;
698 let sx = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
699 let sy = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
700 let heading_raw = curve.get_float(1).ok_or_else(|| {
701 Error::geometry(format!(
702 "CurveSegment #{} missing StartDirection",
703 curve_id,
704 ))
705 })?;
706 let heading = heading_raw * angle_scale;
707 let length = curve.get_float(2).ok_or_else(|| {
708 Error::geometry(format!("CurveSegment #{} missing SegmentLength", curve_id))
709 })?;
710
711 let hseg = if curve.ifc_type == t_line_segment_2d() {
712 HSeg::Line {
713 sx,
714 sy,
715 heading,
716 length,
717 cum_start: cumulative,
718 }
719 } else if curve.ifc_type == t_circular_arc_segment_2d() {
720 let radius = curve.get_float(3).ok_or_else(|| {
722 Error::geometry(format!("CircularArcSegment2D #{} missing Radius", curve_id))
723 })?;
724 if radius < 1e-12 {
725 return Err(Error::geometry(format!(
726 "CircularArcSegment2D #{} has non-positive radius {}",
727 curve_id, radius,
728 )));
729 }
730 let ccw = read_bool(curve.get(4));
731 HSeg::Arc {
732 sx,
733 sy,
734 heading,
735 radius,
736 length,
737 ccw,
738 cum_start: cumulative,
739 }
740 } else if curve.ifc_type == t_transition_curve_segment_2d() {
741 let start_radius = curve.get_float(3);
748 let end_radius = curve.get_float(4);
749 let start_ccw = read_bool(curve.get(5));
750 let end_ccw = read_bool(curve.get(6));
751 let start_curv = match start_radius {
752 Some(r) if r.abs() > 1e-12 => (if start_ccw { 1.0 } else { -1.0 }) / r,
753 _ => 0.0,
754 };
755 let end_curv = match end_radius {
756 Some(r) if r.abs() > 1e-12 => (if end_ccw { 1.0 } else { -1.0 }) / r,
757 _ => 0.0,
758 };
759 let kind = curve
760 .get(7)
761 .and_then(|v| v.as_enum())
762 .map(TransitionKind::from_enum)
763 .unwrap_or(TransitionKind::Clothoid);
764 HSeg::Transition {
765 sx,
766 sy,
767 heading,
768 length,
769 start_curv,
770 end_curv,
771 kind,
772 cum_start: cumulative,
773 }
774 } else {
775 return Err(Error::geometry(format!(
776 "Unsupported horizontal curve geometry at #{}: {}",
777 curve_id, curve.ifc_type,
778 )));
779 };
780 cumulative += length;
781 segments.push(hseg);
782 }
783 Ok(segments)
784}
785
786fn parse_vertical(v_id: u32, decoder: &mut EntityDecoder) -> Result<Vec<VSeg>> {
787 let v_entity = decoder.decode_by_id(v_id)?;
788 if v_entity.ifc_type != t_alignment_2d_vertical() {
789 return Err(Error::geometry(format!(
790 "AlignmentCurve.Vertical #{} is not IfcAlignment2DVertical",
791 v_id,
792 )));
793 }
794 let segs_attr = v_entity
796 .get(0)
797 .ok_or_else(|| Error::geometry("IfcAlignment2DVertical missing Segments".to_string()))?;
798 let seg_refs = segs_attr
799 .as_list()
800 .ok_or_else(|| Error::geometry("Vertical Segments must be a list".to_string()))?;
801
802 let mut segments = Vec::with_capacity(seg_refs.len());
803 for seg_ref in seg_refs {
804 let seg_id = seg_ref.as_entity_ref().ok_or_else(|| {
805 Error::geometry("Vertical segment ref is not an entity reference".to_string())
806 })?;
807 let seg = decoder.decode_by_id(seg_id)?;
808 let start = seg.get_float(3).ok_or_else(|| {
817 Error::geometry(format!(
818 "VerticalSegment #{} missing StartDistAlong",
819 seg_id,
820 ))
821 })?;
822 let length = seg.get_float(4).ok_or_else(|| {
823 Error::geometry(format!(
824 "VerticalSegment #{} missing HorizontalLength",
825 seg_id,
826 ))
827 })?;
828 let h0 = seg
829 .get_float(5)
830 .ok_or_else(|| Error::geometry(format!("VerticalSegment #{} missing StartHeight", seg_id)))?;
831 let g0 = seg.get_float(6).ok_or_else(|| {
832 Error::geometry(format!(
833 "VerticalSegment #{} missing StartGradient",
834 seg_id,
835 ))
836 })?;
837
838 let vseg = if seg.ifc_type == t_ver_seg_line() {
839 VSeg::Line {
840 start,
841 length,
842 h0,
843 g0,
844 }
845 } else if seg.ifc_type == t_ver_seg_parabolic() {
846 let parabola_constant = seg.get_float(7).ok_or_else(|| {
848 Error::geometry(format!(
849 "ParabolicVerSeg #{} missing ParabolaConstant",
850 seg_id,
851 ))
852 })?;
853 let is_convex = read_bool(seg.get(8));
854 VSeg::Parabolic {
855 start,
856 length,
857 h0,
858 g0,
859 parabola_constant,
860 is_convex,
861 }
862 } else if seg.ifc_type == t_ver_seg_circular() {
863 let radius = seg.get_float(7).ok_or_else(|| {
865 Error::geometry(format!("CircularVerSeg #{} missing Radius", seg_id))
866 })?;
867 let is_convex = read_bool(seg.get(8));
868 VSeg::CircularArc {
869 start,
870 length,
871 h0,
872 g0,
873 radius,
874 is_convex,
875 }
876 } else {
877 VSeg::Line {
881 start,
882 length,
883 h0,
884 g0,
885 }
886 };
887 segments.push(vseg);
888 }
889 Ok(segments)
890}
891
892fn h_cum_start(seg: &HSeg) -> f64 {
895 match seg {
896 HSeg::Line { cum_start, .. }
897 | HSeg::Arc { cum_start, .. }
898 | HSeg::Transition { cum_start, .. } => *cum_start,
899 }
900}
901
902fn h_length(seg: &HSeg) -> f64 {
903 match seg {
904 HSeg::Line { length, .. }
905 | HSeg::Arc { length, .. }
906 | HSeg::Transition { length, .. } => *length,
907 }
908}
909
910fn h_eval(seg: &HSeg, s: f64) -> (f64, f64, f64) {
913 match seg {
914 HSeg::Line {
915 sx,
916 sy,
917 heading,
918 ..
919 } => (sx + s * heading.cos(), sy + s * heading.sin(), *heading),
920 HSeg::Arc {
921 sx,
922 sy,
923 heading,
924 radius,
925 ccw,
926 ..
927 } => {
928 let sign = if *ccw { 1.0 } else { -1.0 };
929 let theta = s / radius;
930 let new_heading = heading + sign * theta;
931 let (nx, ny) = if *ccw {
935 (-heading.sin(), heading.cos())
936 } else {
937 (heading.sin(), -heading.cos())
938 };
939 let cx = sx + radius * nx;
940 let cy = sy + radius * ny;
941 let start_angle = (-ny).atan2(-nx);
943 let new_angle = start_angle + sign * theta;
944 (
945 cx + radius * new_angle.cos(),
946 cy + radius * new_angle.sin(),
947 new_heading,
948 )
949 }
950 HSeg::Transition {
951 sx,
952 sy,
953 heading,
954 length,
955 start_curv,
956 end_curv,
957 kind,
958 ..
959 } => {
960 let n = ((s.abs() * 0.5).ceil() as usize).max(16).min(4096);
972 let ds = s / n as f64;
973 let mut x = *sx;
974 let mut y = *sy;
975 let mut prev_cos = heading.cos();
976 let mut prev_sin = heading.sin();
977 for i in 1..=n {
978 let u = i as f64 * ds;
979 let h = *heading
980 + start_curv * u
981 + (end_curv - start_curv) * length * kind.heading_integral(u / length);
982 let cs = h.cos();
983 let sn = h.sin();
984 x += 0.5 * ds * (prev_cos + cs);
985 y += 0.5 * ds * (prev_sin + sn);
986 prev_cos = cs;
987 prev_sin = sn;
988 }
989 let final_h = *heading
990 + start_curv * s
991 + (end_curv - start_curv) * length * kind.heading_integral(s / length);
992 (x, y, final_h)
993 }
994 }
995}
996
997fn v_start(seg: &VSeg) -> f64 {
998 match seg {
999 VSeg::Line { start, .. }
1000 | VSeg::Parabolic { start, .. }
1001 | VSeg::CircularArc { start, .. } => *start,
1002 }
1003}
1004
1005fn v_length(seg: &VSeg) -> f64 {
1006 match seg {
1007 VSeg::Line { length, .. }
1008 | VSeg::Parabolic { length, .. }
1009 | VSeg::CircularArc { length, .. } => *length,
1010 }
1011}
1012
1013fn v_eval(seg: &VSeg, s: f64) -> (f64, f64) {
1016 match seg {
1017 VSeg::Line { h0, g0, .. } => (h0 + g0 * s, *g0),
1018 VSeg::Parabolic {
1019 h0,
1020 g0,
1021 parabola_constant,
1022 is_convex,
1023 ..
1024 } => {
1025 let sign = if *is_convex { -1.0 } else { 1.0 };
1029 let k = parabola_constant.abs().max(1e-12);
1030 let z = h0 + g0 * s + sign * (s * s) / (2.0 * k);
1031 let slope = g0 + sign * s / k;
1032 (z, slope)
1033 }
1034 VSeg::CircularArc {
1035 h0,
1036 g0,
1037 radius,
1038 is_convex,
1039 ..
1040 } => {
1041 let sign = if *is_convex { -1.0 } else { 1.0 };
1044 let r = radius.abs().max(1e-12);
1045 let z = h0 + g0 * s + sign * (s * s) / (2.0 * r);
1046 let slope = g0 + sign * s / r;
1047 (z, slope)
1048 }
1049 }
1050}
1051
1052fn v_eval_height(seg: &VSeg, s: f64) -> f64 {
1053 v_eval(seg, s).0
1054}
1055
1056#[cfg(test)]
1057mod tests {
1058 use super::*;
1059
1060 #[test]
1063 fn line_segment_evaluation() {
1064 let seg = HSeg::Line {
1065 sx: 0.0,
1066 sy: 0.0,
1067 heading: 0.0,
1068 length: 10.0,
1069 cum_start: 0.0,
1070 };
1071 let (x, y, h) = h_eval(&seg, 10.0);
1072 assert!((x - 10.0).abs() < 1e-9);
1073 assert!(y.abs() < 1e-9);
1074 assert!(h.abs() < 1e-9);
1075 }
1076
1077 #[test]
1082 fn fixture_828_arc_endpoint() {
1083 let seg = HSeg::Arc {
1084 sx: 0.0,
1085 sy: 0.0,
1086 heading: 13.35833333_f64.to_radians(),
1087 radius: 9279.0,
1088 length: 2965.68,
1089 ccw: false,
1090 cum_start: 0.0,
1091 };
1092 let (x, y, _) = h_eval(&seg, 2965.68);
1093 assert!((x - 2945.13).abs() < 5.0, "x = {} expected ~2945.13", x);
1096 assert!((y - 216.39).abs() < 5.0, "y = {} expected ~216.39", y);
1097 }
1098
1099 #[test]
1104 fn cant_rotates_frame_axes() {
1105 let curve = AlignmentCurve {
1107 horizontal: vec![HSeg::Line {
1108 sx: 0.0,
1109 sy: 0.0,
1110 heading: 0.0,
1111 length: 100.0,
1112 cum_start: 0.0,
1113 }],
1114 vertical: vec![],
1115 cant: vec![],
1116 };
1117 let frame_no_cant = curve.evaluate(50.0);
1118 assert!((frame_no_cant.right.x).abs() < 1e-9);
1120 assert!((frame_no_cant.right.y + 1.0).abs() < 1e-9);
1121 assert!((frame_no_cant.up.z - 1.0).abs() < 1e-9);
1122
1123 let with_cant = curve.with_cant_segments(vec![CantSegSpec {
1124 start: 0.0,
1125 length: 100.0,
1126 roll_start: std::f64::consts::FRAC_PI_2,
1127 roll_end: std::f64::consts::FRAC_PI_2,
1128 }]);
1129 let angle = with_cant.cant_angle(50.0);
1132 assert!((angle - std::f64::consts::FRAC_PI_2).abs() < 1e-9);
1133 assert!(with_cant.cant_angle(150.0).abs() < 1e-9);
1135 }
1136
1137 #[test]
1141 fn polyline_directrix_evaluates_piecewise() {
1142 let curve = AlignmentCurve {
1148 horizontal: vec![
1149 HSeg::Line {
1150 sx: 0.0,
1151 sy: 0.0,
1152 heading: 0.0,
1153 length: 10.0,
1154 cum_start: 0.0,
1155 },
1156 HSeg::Line {
1157 sx: 10.0,
1158 sy: 0.0,
1159 heading: std::f64::consts::FRAC_PI_2,
1160 length: 10.0,
1161 cum_start: 10.0,
1162 },
1163 ],
1164 vertical: vec![
1165 VSeg::Line {
1166 start: 0.0,
1167 length: 10.0,
1168 h0: 0.0,
1169 g0: 0.1,
1170 },
1171 VSeg::Line {
1172 start: 10.0,
1173 length: 10.0,
1174 h0: 1.0,
1175 g0: 0.1,
1176 },
1177 ],
1178 cant: vec![],
1179 };
1180 let f1 = curve.evaluate(5.0);
1182 assert!((f1.origin.x - 5.0).abs() < 1e-9);
1183 assert!((f1.origin.y).abs() < 1e-9);
1184 assert!((f1.origin.z - 0.5).abs() < 1e-9);
1185 let f2 = curve.evaluate(15.0);
1187 assert!((f2.origin.x - 10.0).abs() < 1e-9);
1188 assert!((f2.origin.y - 5.0).abs() < 1e-9);
1189 assert!((f2.origin.z - 1.5).abs() < 1e-9);
1190 }
1191
1192 #[test]
1193 fn transition_kind_heading_integral_normalised() {
1194 for kind in [
1198 TransitionKind::Clothoid,
1199 TransitionKind::Bloss,
1200 TransitionKind::Cosine,
1201 TransitionKind::Sine,
1202 TransitionKind::CubicParabola,
1203 TransitionKind::BiquadraticParabola,
1204 ] {
1205 assert!(kind.heading_integral(0.0).abs() < 1e-12, "{:?}", kind);
1206 let mid = kind.heading_integral(0.5);
1207 assert!(mid > 0.0 && mid < 0.5, "{:?} mid={}", kind, mid);
1208 let end = kind.heading_integral(1.0);
1211 assert!(end > 0.0 && end < 1.0, "{:?} end={}", kind, end);
1212 }
1213 }
1214
1215 #[test]
1216 fn parabolic_vertical_segment() {
1217 let seg = VSeg::Parabolic {
1222 start: 3600.0,
1223 length: 3685.68,
1224 h0: 399.0,
1225 g0: 0.0579,
1226 parabola_constant: 36000.0,
1227 is_convex: false,
1228 };
1229 let (z, slope) = v_eval(&seg, 1680.0);
1230 assert!((z - 535.472).abs() < 0.01, "z = {}", z);
1231 assert!((slope - 0.1046).abs() < 1e-3, "slope = {}", slope);
1232 }
1233}