1use ifc_lite_core::{AttributeValue, DecodedEntity, EntityDecoder, IfcType};
30use nalgebra::{Point3, Vector3};
31use std::sync::OnceLock;
32
33use crate::gradient::GradientProfile;
34use crate::profiles::ProfileProcessor;
35use crate::{Error, Result, TessellationQuality};
36use ifc_lite_core::IfcSchema;
37
38macro_rules! ifc_type_fn {
41 ($name:ident, $literal:expr) => {
42 fn $name() -> IfcType {
43 static T: OnceLock<IfcType> = OnceLock::new();
44 T.get_or_init(|| IfcType::from_str($literal)).clone()
45 }
46 };
47}
48
49ifc_type_fn!(t_alignment_curve, "IFCALIGNMENTCURVE");
50ifc_type_fn!(t_alignment_2d_horizontal, "IFCALIGNMENT2DHORIZONTAL");
51ifc_type_fn!(t_alignment_2d_horizontal_segment, "IFCALIGNMENT2DHORIZONTALSEGMENT");
52ifc_type_fn!(t_alignment_2d_vertical, "IFCALIGNMENT2DVERTICAL");
53ifc_type_fn!(t_line_segment_2d, "IFCLINESEGMENT2D");
54ifc_type_fn!(t_circular_arc_segment_2d, "IFCCIRCULARARCSEGMENT2D");
55ifc_type_fn!(t_transition_curve_segment_2d, "IFCTRANSITIONCURVESEGMENT2D");
56ifc_type_fn!(t_ver_seg_line, "IFCALIGNMENT2DVERSEGLINE");
57ifc_type_fn!(t_ver_seg_parabolic, "IFCALIGNMENT2DVERSEGPARABOLICARC");
58ifc_type_fn!(t_ver_seg_circular, "IFCALIGNMENT2DVERSEGCIRCULARARC");
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
123fn read_bool(attr: Option<&AttributeValue>) -> bool {
126 attr.and_then(|v| v.as_enum()).map(|s| s == "T").unwrap_or(false)
127}
128
129#[derive(Debug, Clone, Copy)]
131enum HSeg {
132 Line {
133 sx: f64,
134 sy: f64,
135 heading: f64,
136 length: f64,
137 cum_start: f64,
138 },
139 Arc {
140 sx: f64,
141 sy: f64,
142 heading: f64,
143 radius: f64,
144 length: f64,
145 ccw: bool,
146 cum_start: f64,
147 },
148 Transition {
154 sx: f64,
155 sy: f64,
156 heading: f64,
157 length: f64,
158 start_curv: f64,
159 end_curv: f64,
160 kind: TransitionKind,
161 cum_start: f64,
162 },
163}
164
165#[derive(Debug, Clone, Copy)]
166enum VSeg {
167 Line {
168 start: f64,
169 length: f64,
170 h0: f64,
171 g0: f64,
172 },
173 Parabolic {
174 start: f64,
175 length: f64,
176 h0: f64,
177 g0: f64,
178 parabola_constant: f64,
179 is_convex: bool,
180 },
181 CircularArc {
186 start: f64,
187 length: f64,
188 h0: f64,
189 g0: f64,
190 radius: f64,
191 is_convex: bool,
192 },
193}
194
195#[derive(Debug, Clone, Copy)]
197pub struct AlignmentFrame {
198 pub origin: Point3<f64>,
200 pub right: Vector3<f64>,
204 pub up: Vector3<f64>,
206 pub tangent: Vector3<f64>,
209}
210
211pub struct AlignmentCurve {
214 horizontal: Vec<HSeg>,
215 vertical: Vec<VSeg>,
216 gradient: Option<GradientProfile>,
219}
220
221impl AlignmentCurve {
222 pub fn parse(directrix: &DecodedEntity, decoder: &mut EntityDecoder) -> Result<Option<Self>> {
239 Self::parse_with_policy(directrix, decoder, false)
240 }
241
242 pub(crate) fn parse_for_sampling(directrix: &DecodedEntity, decoder: &mut EntityDecoder) -> Result<Option<Self>> {
243 Self::parse_with_policy(directrix, decoder, true)
244 }
245
246 fn parse_with_policy(directrix: &DecodedEntity, decoder: &mut EntityDecoder, strict: bool) -> Result<Option<Self>> {
247 if directrix.ifc_type == IfcType::IfcPolyline {
248 return Self::from_polyline(directrix, decoder, strict).map(Some);
249 }
250 if directrix.ifc_type == IfcType::IfcGradientCurve {
251 if strict {
252 crate::alignment_sampling_curve::validate_composite(directrix, decoder)?;
253 for attr in directrix.get_list(0).ok_or_else(|| Error::geometry("GradientCurve missing Segments"))? {
254 let id = attr.as_entity_ref().ok_or_else(|| Error::geometry("Gradient segment must be a reference"))?;
255 let segment = decoder.decode_by_id(id)?;
256 crate::alignment_sampling_curve::validate_segment_placement(&segment, decoder)?;
257 }
258 }
259 return Self::from_gradient_curve(directrix, decoder);
260 }
261 if directrix.ifc_type == IfcType::IfcCompositeCurve {
262 if strict { crate::alignment_sampling_curve::validate_composite(directrix, decoder)?; }
263 return Self::from_sampled_curve(directrix, decoder);
264 }
265 if directrix.ifc_type != t_alignment_curve() {
266 return Ok(None);
267 }
268
269 let angle_scale = decoder.plane_angle_to_radians();
270
271 let h_id = directrix.get_ref(0).ok_or_else(|| {
273 Error::geometry("IfcAlignmentCurve missing Horizontal".to_string())
274 })?;
275 let (horizontal, horizontal_base) = parse_horizontal(h_id, decoder, angle_scale, strict)?;
283
284 let vertical = match directrix.get(1) {
286 Some(v) if !v.is_null() => match v.as_entity_ref() {
287 Some(v_id) => parse_vertical(v_id, decoder, horizontal_base, strict)?,
288 None if strict => return Err(Error::geometry("Vertical must be a reference")),
289 None => Vec::new(),
290 },
291 _ => Vec::new(),
292 };
293
294 Ok(Some(Self { horizontal, vertical, gradient: None }))
295 }
296
297 pub(crate) fn uses_approximation(&self) -> bool {
298 self.gradient.is_some() || self.horizontal.iter().any(|segment| matches!(segment,
299 HSeg::Transition { kind: TransitionKind::CubicParabola | TransitionKind::BiquadraticParabola, .. }))
300 }
301
302 pub fn horizontal_length(&self) -> f64 {
304 self.horizontal
305 .last()
306 .map(|s| h_cum_start(s) + h_length(s))
307 .unwrap_or(0.0)
308 }
309
310 pub(crate) fn station_breaks(&self) -> Vec<f64> {
313 let mut breaks = vec![0.0, self.horizontal_length()];
314 for seg in &self.horizontal {
315 breaks.push(h_cum_start(seg));
316 breaks.push(h_cum_start(seg) + h_length(seg));
317 }
318 for seg in &self.vertical {
319 breaks.push(v_start(seg));
320 breaks.push(v_start(seg) + v_length(seg));
321 }
322 if let Some(profile) = &self.gradient {
323 breaks.extend(profile.station_breaks().map(|s| s - profile.first_station()));
324 }
325 breaks.retain(|s| s.is_finite() && *s >= 0.0 && *s <= self.horizontal_length());
326 breaks.sort_by(f64::total_cmp);
327 breaks.dedup_by(|a, b| (*a - *b).abs() < 1e-9);
328 breaks
329 }
330
331 fn from_polyline(curve: &DecodedEntity, decoder: &mut EntityDecoder, strict: bool) -> Result<Self> {
336 let points_attr = curve
337 .get(0)
338 .ok_or_else(|| Error::geometry("IfcPolyline missing Points".to_string()))?;
339 let point_refs = points_attr
340 .as_list()
341 .ok_or_else(|| Error::geometry("IfcPolyline Points is not a list".to_string()))?;
342 if point_refs.len() < 2 {
343 return Err(Error::geometry(
344 "IfcPolyline directrix needs ≥ 2 points".to_string(),
345 ));
346 }
347 let mut pts: Vec<(f64, f64, f64)> = Vec::with_capacity(point_refs.len());
348 for r in point_refs {
349 let pid = r
350 .as_entity_ref()
351 .ok_or_else(|| Error::geometry("Polyline point is not an entity ref".to_string()))?;
352 let p = decoder.decode_by_id(pid)?;
353 if strict && p.ifc_type != IfcType::IfcCartesianPoint {
354 return Err(Error::geometry(format!("Polyline point #{} is not IfcCartesianPoint", pid)));
355 }
356 let coords = p
357 .get_list(0)
358 .ok_or_else(|| Error::geometry("CartesianPoint missing Coordinates".to_string()))?;
359 if strict && (!(2..=3).contains(&coords.len()) || coords.iter().any(|v| v.as_float().is_none_or(|n| !n.is_finite()))) {
360 return Err(Error::geometry(format!("Malformed CartesianPoint #{}", pid)));
361 }
362 let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
363 let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
364 let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
365 pts.push((x, y, z));
366 }
367 if strict && pts.windows(2).any(|p| (p[1].0 - p[0].0).hypot(p[1].1 - p[0].1) < 1e-12 && (p[1].2 - p[0].2).abs() > 1e-12) {
368 return Err(Error::geometry("A vertical polyline edge cannot use horizontal-distance stationing"));
369 }
370 Self::from_points(&pts)
371 }
372
373 fn from_gradient_curve(curve: &DecodedEntity, decoder: &mut EntityDecoder) -> Result<Option<Self>> {
380 let Some(profile) = GradientProfile::from_curve(curve, decoder) else {
381 return Ok(None);
382 };
383 let Some(base_id) = curve.get_ref(2) else {
384 return Ok(None);
385 };
386 let base = decoder.decode_by_id(base_id)?;
387 let samples = ProfileProcessor::new(IfcSchema::new()).get_curve_points(
388 &base,
389 decoder,
390 TessellationQuality::Medium,
391 )?;
392 if samples.len() < 2 {
393 return Ok(None);
394 }
395 let pts: Vec<(f64, f64, f64)> = samples.iter().map(|p| (p.x, p.y, 0.0)).collect();
396 let mut alignment = Self::from_points(&pts)?;
397 alignment.vertical.clear();
398 alignment.gradient = Some(profile);
399 Ok(Some(alignment))
400 }
401
402 fn from_sampled_curve(curve: &DecodedEntity, decoder: &mut EntityDecoder) -> Result<Option<Self>> {
406 let samples = ProfileProcessor::new(IfcSchema::new()).get_curve_points(
407 curve,
408 decoder,
409 TessellationQuality::Medium,
410 )?;
411 if samples.len() < 2 {
412 return Ok(None);
413 }
414 let pts: Vec<(f64, f64, f64)> = samples.iter().map(|p| (p.x, p.y, p.z)).collect();
415 Self::from_points(&pts).map(Some)
416 }
417
418 fn from_points(pts: &[(f64, f64, f64)]) -> Result<Self> {
421 let mut horizontal: Vec<HSeg> = Vec::with_capacity(pts.len() - 1);
422 let mut vertical: Vec<VSeg> = Vec::with_capacity(pts.len() - 1);
423 let mut cum_xy = 0.0;
424 for w in pts.windows(2) {
425 let (x0, y0, z0) = w[0];
426 let (x1, y1, z1) = w[1];
427 let dx = x1 - x0;
428 let dy = y1 - y0;
429 let dz = z1 - z0;
430 let len_xy = (dx * dx + dy * dy).sqrt();
431 if len_xy < 1e-12 {
432 continue;
436 }
437 let heading = dy.atan2(dx);
438 horizontal.push(HSeg::Line {
439 sx: x0,
440 sy: y0,
441 heading,
442 length: len_xy,
443 cum_start: cum_xy,
444 });
445 let gradient = dz / len_xy;
446 vertical.push(VSeg::Line {
447 start: cum_xy,
448 length: len_xy,
449 h0: z0,
450 g0: gradient,
451 });
452 cum_xy += len_xy;
453 }
454 if horizontal.is_empty() {
455 return Err(Error::geometry(
456 "IfcPolyline directrix degenerated to zero horizontal length".to_string(),
457 ));
458 }
459 Ok(Self { horizontal, vertical, gradient: None })
460 }
461
462 pub fn evaluate(&self, station: f64) -> AlignmentFrame {
466 let (x, y, heading) = self.evaluate_horizontal(station);
467 let (z, slope) = self.evaluate_vertical_frame(station);
468 let cos_h = heading.cos();
469 let sin_h = heading.sin();
470 let right = Vector3::new(sin_h, -cos_h, 0.0);
473 let up = Vector3::new(0.0, 0.0, 1.0);
474 let inv_norm = (1.0 + slope * slope).sqrt();
478 let tangent = Vector3::new(cos_h / inv_norm, sin_h / inv_norm, slope / inv_norm);
479 AlignmentFrame {
480 origin: Point3::new(x, y, z),
481 right,
482 up,
483 tangent,
484 }
485 }
486
487 pub fn cant_angle(&self, _station: f64) -> f64 {
495 0.0
496 }
497
498 fn evaluate_vertical_frame(&self, station: f64) -> (f64, f64) {
514 if let Some(profile) = &self.gradient {
515 return profile.evaluate(station + profile.first_station());
516 }
517 let Some(first) = self.vertical.first() else {
518 return (0.0, 0.0);
519 };
520 let mut before: Option<&VSeg> = None;
522 for seg in &self.vertical {
523 let start = v_start(seg);
524 let length = v_length(seg);
525 if station >= start - 1e-9 && station <= start + length + 1e-9 {
526 let local = (station - start).max(0.0).min(length);
527 return v_eval(seg, local);
528 }
529 if start + length < station {
530 before = Some(seg);
531 }
532 }
533 let after = self.vertical.iter().find(|seg| v_start(seg) > station);
536 let seg = match (before, after) {
537 (Some(b), Some(a)) => {
538 let gap_before = station - (v_start(b) + v_length(b));
539 let gap_after = v_start(a) - station;
540 if gap_after < gap_before { a } else { b }
541 }
542 (Some(b), None) => b,
543 (None, Some(a)) => a,
544 (None, None) => first,
545 };
546 if v_start(seg) > station {
547 let (z0, g0) = v_eval(seg, 0.0);
549 (z0 + g0 * (station - v_start(seg)), g0)
550 } else {
551 let length = v_length(seg);
553 let (z_end, slope) = v_eval(seg, length);
554 (z_end + slope * (station - (v_start(seg) + length)), slope)
555 }
556 }
557
558 fn evaluate_horizontal(&self, station: f64) -> (f64, f64, f64) {
559 if self.horizontal.is_empty() {
560 return (0.0, 0.0, 0.0);
561 }
562 let index = self.horizontal.partition_point(|seg| {
572 !(station <= h_cum_start(seg) + h_length(seg) + 1e-9)
573 });
574 if let Some(seg) = self.horizontal.get(index) {
575 let len = h_length(seg);
576 let local = (station - h_cum_start(seg)).max(0.0).min(len);
577 return h_eval(seg, local);
578 }
579 let last = self.horizontal.last().unwrap();
581 let len = h_length(last);
582 let (x, y, h) = h_eval(last, len);
583 let extra = station - (h_cum_start(last) + len);
584 (x + extra * h.cos(), y + extra * h.sin(), h)
585 }
586
587}
588
589fn parse_horizontal(
595 h_id: u32,
596 decoder: &mut EntityDecoder,
597 angle_scale: f64,
598 strict: bool,
599) -> Result<(Vec<HSeg>, f64)> {
600 let h_entity = decoder.decode_by_id(h_id)?;
601 if h_entity.ifc_type != t_alignment_2d_horizontal() {
602 return Err(Error::geometry(format!(
603 "AlignmentCurve.Horizontal #{} is not IfcAlignment2DHorizontal",
604 h_id,
605 )));
606 }
607 let start_dist_along = h_entity.get_float(0).unwrap_or(0.0);
609 if strict && h_entity.get(0).is_some_and(|a| !a.is_null() && a.as_float().is_none_or(|v| !v.is_finite())) {
610 return Err(Error::geometry("Invalid horizontal StartDistAlong"));
611 }
612 let segs_attr = h_entity
613 .get(1)
614 .ok_or_else(|| Error::geometry("IfcAlignment2DHorizontal missing Segments".to_string()))?;
615 let seg_refs = segs_attr
616 .as_list()
617 .ok_or_else(|| Error::geometry("Horizontal Segments must be a list".to_string()))?;
618
619 let mut segments = Vec::with_capacity(seg_refs.len());
620 let mut cumulative = 0.0;
621 for seg_ref in seg_refs {
622 let seg_id = seg_ref.as_entity_ref().ok_or_else(|| {
623 Error::geometry("Horizontal segment ref is not an entity reference".to_string())
624 })?;
625 let seg = decoder.decode_by_id(seg_id)?;
626 if seg.ifc_type != t_alignment_2d_horizontal_segment() {
627 return Err(Error::geometry(format!(
628 "#{} is not IfcAlignment2DHorizontalSegment",
629 seg_id,
630 )));
631 }
632 let curve_id = seg.get_ref(3).ok_or_else(|| {
634 Error::geometry(format!(
635 "IfcAlignment2DHorizontalSegment #{} missing CurveGeometry",
636 seg_id,
637 ))
638 })?;
639 let curve = decoder.decode_by_id(curve_id)?;
640
641 let sp_id = curve.get_ref(0).ok_or_else(|| {
646 Error::geometry(format!("CurveSegment #{} missing StartPoint", curve_id))
647 })?;
648 let sp = decoder.decode_by_id(sp_id)?;
649 let coords = sp
650 .get_list(0)
651 .ok_or_else(|| Error::geometry("StartPoint missing Coordinates".to_string()))?;
652 if strict && (sp.ifc_type != IfcType::IfcCartesianPoint || coords.len() != 2 || coords.iter().any(|v| v.as_float().is_none_or(|n| !n.is_finite()))) {
653 return Err(Error::geometry("Horizontal StartPoint must contain two finite CartesianPoint coordinates"));
654 }
655 let sx = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
656 let sy = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
657 let heading_raw = curve.get_float(1).ok_or_else(|| {
658 Error::geometry(format!(
659 "CurveSegment #{} missing StartDirection",
660 curve_id,
661 ))
662 })?;
663 let heading = heading_raw * angle_scale;
664 if strict && !heading.is_finite() { return Err(Error::geometry("Nonfinite horizontal heading")); }
665 let length = curve.get_float(2).ok_or_else(|| {
666 Error::geometry(format!("CurveSegment #{} missing SegmentLength", curve_id))
667 })?;
668 if !length.is_finite() || length < 0.0 {
672 return Err(Error::geometry(format!(
673 "CurveSegment #{} has negative/non-finite SegmentLength {}",
674 curve_id, length
675 )));
676 }
677
678 let hseg = if curve.ifc_type == t_line_segment_2d() {
679 HSeg::Line {
680 sx,
681 sy,
682 heading,
683 length,
684 cum_start: cumulative,
685 }
686 } else if curve.ifc_type == t_circular_arc_segment_2d() {
687 let radius = curve.get_float(3).ok_or_else(|| {
689 Error::geometry(format!("CircularArcSegment2D #{} missing Radius", curve_id))
690 })?;
691 if radius < 1e-12 {
692 return Err(Error::geometry(format!(
693 "CircularArcSegment2D #{} has non-positive radius {}",
694 curve_id, radius,
695 )));
696 }
697 let ccw = read_bool(curve.get(4));
698 HSeg::Arc {
699 sx,
700 sy,
701 heading,
702 radius,
703 length,
704 ccw,
705 cum_start: cumulative,
706 }
707 } else if curve.ifc_type == t_transition_curve_segment_2d() {
708 if strict && !matches!(curve.get(7).and_then(|v| v.as_enum()), Some("CLOTHOIDCURVE" | "BLOSSCURVE" | "COSINECURVE" | "SINECURVE" | "CUBICPARABOLA" | "BIQUADRATICPARABOLA")) {
715 return Err(Error::geometry("Unknown TransitionCurveType"));
716 }
717 let start_radius = curve.get_float(3);
718 let end_radius = curve.get_float(4);
719 let start_ccw = read_bool(curve.get(5));
720 let end_ccw = read_bool(curve.get(6));
721 let start_curv = match start_radius {
722 Some(r) if r.abs() > 1e-12 => (if start_ccw { 1.0 } else { -1.0 }) / r,
723 _ => 0.0,
724 };
725 let end_curv = match end_radius {
726 Some(r) if r.abs() > 1e-12 => (if end_ccw { 1.0 } else { -1.0 }) / r,
727 _ => 0.0,
728 };
729 let kind = curve
730 .get(7)
731 .and_then(|v| v.as_enum())
732 .map(TransitionKind::from_enum)
733 .unwrap_or(TransitionKind::Clothoid);
734 HSeg::Transition {
735 sx,
736 sy,
737 heading,
738 length,
739 start_curv,
740 end_curv,
741 kind,
742 cum_start: cumulative,
743 }
744 } else {
745 return Err(Error::geometry(format!(
746 "Unsupported horizontal curve geometry at #{}: {}",
747 curve_id, curve.ifc_type,
748 )));
749 };
750 cumulative += length;
751 segments.push(hseg);
752 }
753 Ok((segments, start_dist_along))
754}
755
756fn parse_vertical(
764 v_id: u32,
765 decoder: &mut EntityDecoder,
766 horizontal_base: f64,
767 strict: bool,
768) -> Result<Vec<VSeg>> {
769 let v_entity = decoder.decode_by_id(v_id)?;
770 if v_entity.ifc_type != t_alignment_2d_vertical() {
771 return Err(Error::geometry(format!(
772 "AlignmentCurve.Vertical #{} is not IfcAlignment2DVertical",
773 v_id,
774 )));
775 }
776 let segs_attr = v_entity
778 .get(0)
779 .ok_or_else(|| Error::geometry("IfcAlignment2DVertical missing Segments".to_string()))?;
780 let seg_refs = segs_attr
781 .as_list()
782 .ok_or_else(|| Error::geometry("Vertical Segments must be a list".to_string()))?;
783
784 let mut segments = Vec::with_capacity(seg_refs.len());
785 for seg_ref in seg_refs {
786 let seg_id = seg_ref.as_entity_ref().ok_or_else(|| {
787 Error::geometry("Vertical segment ref is not an entity reference".to_string())
788 })?;
789 let seg = decoder.decode_by_id(seg_id)?;
790 let start = seg.get_float(3).ok_or_else(|| {
801 Error::geometry(format!(
802 "VerticalSegment #{} missing StartDistAlong",
803 seg_id,
804 ))
805 })? - horizontal_base;
806 let length = seg.get_float(4).ok_or_else(|| {
807 Error::geometry(format!(
808 "VerticalSegment #{} missing HorizontalLength",
809 seg_id,
810 ))
811 })?;
812 if !length.is_finite() || length < 0.0 {
814 return Err(Error::geometry(format!(
815 "VerticalSegment #{} has negative/non-finite HorizontalLength {}",
816 seg_id, length
817 )));
818 }
819 let h0 = seg
820 .get_float(5)
821 .ok_or_else(|| Error::geometry(format!("VerticalSegment #{} missing StartHeight", seg_id)))?;
822 let g0 = seg.get_float(6).ok_or_else(|| {
823 Error::geometry(format!(
824 "VerticalSegment #{} missing StartGradient",
825 seg_id,
826 ))
827 })?;
828
829 let vseg = if seg.ifc_type == t_ver_seg_line() {
830 VSeg::Line {
831 start,
832 length,
833 h0,
834 g0,
835 }
836 } else if seg.ifc_type == t_ver_seg_parabolic() {
837 let parabola_constant = seg.get_float(7).ok_or_else(|| {
839 Error::geometry(format!(
840 "ParabolicVerSeg #{} missing ParabolaConstant",
841 seg_id,
842 ))
843 })?;
844 if strict && (!parabola_constant.is_finite() || parabola_constant <= 0.0) {
845 return Err(Error::geometry(format!(
846 "ParabolicVerSeg #{} has invalid ParabolaConstant", seg_id
847 )));
848 }
849 let is_convex = read_bool(seg.get(8));
850 VSeg::Parabolic {
851 start,
852 length,
853 h0,
854 g0,
855 parabola_constant,
856 is_convex,
857 }
858 } else if seg.ifc_type == t_ver_seg_circular() {
859 let radius = seg.get_float(7).ok_or_else(|| {
861 Error::geometry(format!("CircularVerSeg #{} missing Radius", seg_id))
862 })?;
863 if strict && (!radius.is_finite() || radius <= 0.0) {
864 return Err(Error::geometry(format!(
865 "CircularVerSeg #{} has invalid Radius", seg_id
866 )));
867 }
868 let is_convex = read_bool(seg.get(8));
869 VSeg::CircularArc {
870 start,
871 length,
872 h0,
873 g0,
874 radius,
875 is_convex,
876 }
877 } else {
878 if strict {
879 return Err(Error::geometry(format!("Unsupported vertical segment #{}: {}", seg_id, seg.ifc_type)));
880 }
881 crate::diag::diag_warn!(
888 { vertical_segment = seg_id, ifc_type = %seg.ifc_type,
889 "alignment: unknown vertical segment subtype, degrading to a straight gradient" }
890 else {
891 eprintln!(
892 "[ifc-lite][alignment] vertical segment #{} has unsupported subtype {}; \
893 degrading to a straight gradient segment",
894 seg_id, seg.ifc_type,
895 );
896 }
897 );
898 VSeg::Line {
899 start,
900 length,
901 h0,
902 g0,
903 }
904 };
905 segments.push(vseg);
906 }
907 Ok(segments)
908}
909
910fn h_cum_start(seg: &HSeg) -> f64 {
913 match seg {
914 HSeg::Line { cum_start, .. }
915 | HSeg::Arc { cum_start, .. }
916 | HSeg::Transition { cum_start, .. } => *cum_start,
917 }
918}
919
920fn h_length(seg: &HSeg) -> f64 {
921 match seg {
922 HSeg::Line { length, .. }
923 | HSeg::Arc { length, .. }
924 | HSeg::Transition { length, .. } => *length,
925 }
926}
927
928fn h_eval(seg: &HSeg, s: f64) -> (f64, f64, f64) {
931 match seg {
932 HSeg::Line {
933 sx,
934 sy,
935 heading,
936 ..
937 } => (sx + s * heading.cos(), sy + s * heading.sin(), *heading),
938 HSeg::Arc {
939 sx,
940 sy,
941 heading,
942 radius,
943 ccw,
944 ..
945 } => {
946 let sign = if *ccw { 1.0 } else { -1.0 };
947 let theta = s / radius;
948 let new_heading = heading + sign * theta;
949 let (nx, ny) = if *ccw {
953 (-heading.sin(), heading.cos())
954 } else {
955 (heading.sin(), -heading.cos())
956 };
957 let cx = sx + radius * nx;
958 let cy = sy + radius * ny;
959 let start_angle = (-ny).atan2(-nx);
961 let new_angle = start_angle + sign * theta;
962 (
963 cx + radius * new_angle.cos(),
964 cy + radius * new_angle.sin(),
965 new_heading,
966 )
967 }
968 HSeg::Transition {
969 sx,
970 sy,
971 heading,
972 length,
973 start_curv,
974 end_curv,
975 kind,
976 ..
977 } => {
978 if *length <= 0.0 {
984 return (*sx, *sy, *heading);
985 }
986 let n = ((s.abs() * 0.5).ceil() as usize).max(16).min(4096);
998 let ds = s / n as f64;
999 let mut x = *sx;
1000 let mut y = *sy;
1001 let mut prev_cos = heading.cos();
1002 let mut prev_sin = heading.sin();
1003 for i in 1..=n {
1004 let u = i as f64 * ds;
1005 let h = *heading
1006 + start_curv * u
1007 + (end_curv - start_curv) * length * kind.heading_integral(u / length);
1008 let cs = h.cos();
1009 let sn = h.sin();
1010 x += 0.5 * ds * (prev_cos + cs);
1011 y += 0.5 * ds * (prev_sin + sn);
1012 prev_cos = cs;
1013 prev_sin = sn;
1014 }
1015 let final_h = *heading
1016 + start_curv * s
1017 + (end_curv - start_curv) * length * kind.heading_integral(s / length);
1018 (x, y, final_h)
1019 }
1020 }
1021}
1022
1023fn v_start(seg: &VSeg) -> f64 {
1024 match seg {
1025 VSeg::Line { start, .. }
1026 | VSeg::Parabolic { start, .. }
1027 | VSeg::CircularArc { start, .. } => *start,
1028 }
1029}
1030
1031fn v_length(seg: &VSeg) -> f64 {
1032 match seg {
1033 VSeg::Line { length, .. }
1034 | VSeg::Parabolic { length, .. }
1035 | VSeg::CircularArc { length, .. } => *length,
1036 }
1037}
1038
1039fn v_eval(seg: &VSeg, s: f64) -> (f64, f64) {
1042 match seg {
1043 VSeg::Line { h0, g0, .. } => (h0 + g0 * s, *g0),
1044 VSeg::Parabolic {
1045 h0,
1046 g0,
1047 parabola_constant,
1048 is_convex,
1049 ..
1050 } => {
1051 let sign = if *is_convex { -1.0 } else { 1.0 };
1055 let k = parabola_constant.abs().max(1e-12);
1056 let z = h0 + g0 * s + sign * (s * s) / (2.0 * k);
1057 let slope = g0 + sign * s / k;
1058 (z, slope)
1059 }
1060 VSeg::CircularArc {
1061 h0,
1062 g0,
1063 radius,
1064 is_convex,
1065 ..
1066 } => {
1067 let sign = if *is_convex { -1.0 } else { 1.0 };
1070 let r = radius.abs().max(1e-12);
1071 let z = h0 + g0 * s + sign * (s * s) / (2.0 * r);
1072 let slope = g0 + sign * s / r;
1073 (z, slope)
1074 }
1075 }
1076}
1077
1078#[cfg(test)]
1079#[path = "alignment_tests.rs"]
1080mod tests;