1use ifc_lite_core::{AttributeValue, DecodedEntity, EntityDecoder, 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");
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61enum TransitionKind {
62 Clothoid,
64 Bloss,
66 Cosine,
68 Sine,
70 CubicParabola,
75 BiquadraticParabola,
80}
81
82impl TransitionKind {
83 fn from_enum(name: &str) -> Self {
84 match name {
85 "CLOTHOIDCURVE" => Self::Clothoid,
86 "BLOSSCURVE" => Self::Bloss,
87 "COSINECURVE" => Self::Cosine,
88 "SINECURVE" => Self::Sine,
89 "CUBICPARABOLA" => Self::CubicParabola,
90 "BIQUADRATICPARABOLA" => Self::BiquadraticParabola,
91 _ => Self::Clothoid,
93 }
94 }
95
96 fn heading_integral(self, u: f64) -> f64 {
100 let u = u.clamp(0.0, 1.0);
101 match self {
102 Self::Clothoid | Self::CubicParabola => 0.5 * u * u,
103 Self::Bloss | Self::BiquadraticParabola => {
104 u * u * u - 0.5 * u * u * u * u
106 }
107 Self::Cosine => {
108 0.5 * u - (std::f64::consts::PI * u).sin() / (2.0 * std::f64::consts::PI)
110 }
111 Self::Sine => {
112 let two_pi = 2.0 * std::f64::consts::PI;
114 0.5 * u * u + ((two_pi * u).cos() - 1.0) / (4.0 * std::f64::consts::PI.powi(2))
115 }
116 }
117 }
118}
119
120fn read_bool(attr: Option<&AttributeValue>) -> bool {
123 attr.and_then(|v| v.as_enum()).map(|s| s == "T").unwrap_or(false)
124}
125
126#[derive(Debug, Clone, Copy)]
128enum HSeg {
129 Line {
130 sx: f64,
131 sy: f64,
132 heading: f64,
133 length: f64,
134 cum_start: f64,
135 },
136 Arc {
137 sx: f64,
138 sy: f64,
139 heading: f64,
140 radius: f64,
141 length: f64,
142 ccw: bool,
143 cum_start: f64,
144 },
145 Transition {
151 sx: f64,
152 sy: f64,
153 heading: f64,
154 length: f64,
155 start_curv: f64,
156 end_curv: f64,
157 kind: TransitionKind,
158 cum_start: f64,
159 },
160}
161
162#[derive(Debug, Clone, Copy)]
163enum VSeg {
164 Line {
165 start: f64,
166 length: f64,
167 h0: f64,
168 g0: f64,
169 },
170 Parabolic {
171 start: f64,
172 length: f64,
173 h0: f64,
174 g0: f64,
175 parabola_constant: f64,
176 is_convex: bool,
177 },
178 CircularArc {
183 start: f64,
184 length: f64,
185 h0: f64,
186 g0: f64,
187 radius: f64,
188 is_convex: bool,
189 },
190}
191
192#[derive(Debug, Clone, Copy)]
194pub struct AlignmentFrame {
195 pub origin: Point3<f64>,
197 pub right: Vector3<f64>,
201 pub up: Vector3<f64>,
203 pub tangent: Vector3<f64>,
206}
207
208pub struct AlignmentCurve {
211 horizontal: Vec<HSeg>,
212 vertical: Vec<VSeg>,
213}
214
215impl AlignmentCurve {
216 pub fn parse(directrix: &DecodedEntity, decoder: &mut EntityDecoder) -> Result<Option<Self>> {
231 if directrix.ifc_type == IfcType::IfcPolyline {
232 return Self::from_polyline(directrix, decoder).map(Some);
233 }
234 if directrix.ifc_type != t_alignment_curve() {
235 return Ok(None);
236 }
237
238 let angle_scale = decoder.plane_angle_to_radians();
239
240 let h_id = directrix.get_ref(0).ok_or_else(|| {
242 Error::geometry("IfcAlignmentCurve missing Horizontal".to_string())
243 })?;
244 let (horizontal, horizontal_base) = parse_horizontal(h_id, decoder, angle_scale)?;
252
253 let vertical = match directrix.get(1) {
255 Some(v) if !v.is_null() => match v.as_entity_ref() {
256 Some(v_id) => parse_vertical(v_id, decoder, horizontal_base)?,
257 None => Vec::new(),
258 },
259 _ => Vec::new(),
260 };
261
262 Ok(Some(Self { horizontal, vertical }))
263 }
264
265 pub fn horizontal_length(&self) -> f64 {
267 self.horizontal
268 .last()
269 .map(|s| h_cum_start(s) + h_length(s))
270 .unwrap_or(0.0)
271 }
272
273 fn from_polyline(curve: &DecodedEntity, decoder: &mut EntityDecoder) -> Result<Self> {
278 let points_attr = curve
279 .get(0)
280 .ok_or_else(|| Error::geometry("IfcPolyline missing Points".to_string()))?;
281 let point_refs = points_attr
282 .as_list()
283 .ok_or_else(|| Error::geometry("IfcPolyline Points is not a list".to_string()))?;
284 if point_refs.len() < 2 {
285 return Err(Error::geometry(
286 "IfcPolyline directrix needs ≥ 2 points".to_string(),
287 ));
288 }
289 let mut pts: Vec<(f64, f64, f64)> = Vec::with_capacity(point_refs.len());
290 for r in point_refs {
291 let pid = r
292 .as_entity_ref()
293 .ok_or_else(|| Error::geometry("Polyline point is not an entity ref".to_string()))?;
294 let p = decoder.decode_by_id(pid)?;
295 let coords = p
296 .get_list(0)
297 .ok_or_else(|| Error::geometry("CartesianPoint missing Coordinates".to_string()))?;
298 let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
299 let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
300 let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
301 pts.push((x, y, z));
302 }
303
304 let mut horizontal: Vec<HSeg> = Vec::with_capacity(pts.len() - 1);
305 let mut vertical: Vec<VSeg> = Vec::with_capacity(pts.len() - 1);
306 let mut cum_xy = 0.0;
307 for w in pts.windows(2) {
308 let (x0, y0, z0) = w[0];
309 let (x1, y1, z1) = w[1];
310 let dx = x1 - x0;
311 let dy = y1 - y0;
312 let dz = z1 - z0;
313 let len_xy = (dx * dx + dy * dy).sqrt();
314 if len_xy < 1e-12 {
315 continue;
319 }
320 let heading = dy.atan2(dx);
321 horizontal.push(HSeg::Line {
322 sx: x0,
323 sy: y0,
324 heading,
325 length: len_xy,
326 cum_start: cum_xy,
327 });
328 let gradient = dz / len_xy;
329 vertical.push(VSeg::Line {
330 start: cum_xy,
331 length: len_xy,
332 h0: z0,
333 g0: gradient,
334 });
335 cum_xy += len_xy;
336 }
337 if horizontal.is_empty() {
338 return Err(Error::geometry(
339 "IfcPolyline directrix degenerated to zero horizontal length".to_string(),
340 ));
341 }
342 Ok(Self { horizontal, vertical })
343 }
344
345 pub fn evaluate(&self, station: f64) -> AlignmentFrame {
349 let (x, y, heading) = self.evaluate_horizontal(station);
350 let z = self.evaluate_vertical(station);
351 let slope = self.evaluate_vertical_slope(station);
352 let cos_h = heading.cos();
353 let sin_h = heading.sin();
354 let right = Vector3::new(sin_h, -cos_h, 0.0);
357 let up = Vector3::new(0.0, 0.0, 1.0);
358 let inv_norm = (1.0 + slope * slope).sqrt();
362 let tangent = Vector3::new(cos_h / inv_norm, sin_h / inv_norm, slope / inv_norm);
363 AlignmentFrame {
364 origin: Point3::new(x, y, z),
365 right,
366 up,
367 tangent,
368 }
369 }
370
371 pub fn cant_angle(&self, _station: f64) -> f64 {
379 0.0
380 }
381
382 fn evaluate_vertical_slope(&self, station: f64) -> f64 {
383 if self.vertical.is_empty() {
384 return 0.0;
385 }
386 for seg in &self.vertical {
387 let start = v_start(seg);
388 let length = v_length(seg);
389 if station >= start - 1e-9 && station <= start + length + 1e-9 {
395 let local = (station - start).max(0.0).min(length);
396 return v_eval(seg, local).1;
397 }
398 }
399 let last = self.vertical.last().unwrap();
400 v_eval(last, v_length(last)).1
401 }
402
403 fn evaluate_horizontal(&self, station: f64) -> (f64, f64, f64) {
404 if self.horizontal.is_empty() {
405 return (0.0, 0.0, 0.0);
406 }
407 for seg in &self.horizontal {
413 let len = h_length(seg);
414 let cum = h_cum_start(seg);
415 if station <= cum + len + 1e-9 {
416 let local = (station - cum).max(0.0).min(len);
417 return h_eval(seg, local);
418 }
419 }
420 let last = self.horizontal.last().unwrap();
422 let len = h_length(last);
423 let (x, y, h) = h_eval(last, len);
424 let extra = station - (h_cum_start(last) + len);
425 (x + extra * h.cos(), y + extra * h.sin(), h)
426 }
427
428 fn evaluate_vertical(&self, station: f64) -> f64 {
429 if self.vertical.is_empty() {
430 return 0.0;
431 }
432 for seg in &self.vertical {
433 let start = v_start(seg);
434 let length = v_length(seg);
435 if station >= start - 1e-9 && station <= start + length + 1e-9 {
440 let local = (station - start).max(0.0).min(length);
441 return v_eval_height(seg, local);
442 }
443 }
444 let last = self.vertical.last().unwrap();
446 let length = v_length(last);
447 let (z_end, slope) = v_eval(last, length);
448 let extra = station - (v_start(last) + length);
449 z_end + slope * extra
450 }
451}
452
453fn parse_horizontal(
459 h_id: u32,
460 decoder: &mut EntityDecoder,
461 angle_scale: f64,
462) -> Result<(Vec<HSeg>, f64)> {
463 let h_entity = decoder.decode_by_id(h_id)?;
464 if h_entity.ifc_type != t_alignment_2d_horizontal() {
465 return Err(Error::geometry(format!(
466 "AlignmentCurve.Horizontal #{} is not IfcAlignment2DHorizontal",
467 h_id,
468 )));
469 }
470 let start_dist_along = h_entity.get_float(0).unwrap_or(0.0);
472 let segs_attr = h_entity
473 .get(1)
474 .ok_or_else(|| Error::geometry("IfcAlignment2DHorizontal missing Segments".to_string()))?;
475 let seg_refs = segs_attr
476 .as_list()
477 .ok_or_else(|| Error::geometry("Horizontal Segments must be a list".to_string()))?;
478
479 let mut segments = Vec::with_capacity(seg_refs.len());
480 let mut cumulative = 0.0;
481 for seg_ref in seg_refs {
482 let seg_id = seg_ref.as_entity_ref().ok_or_else(|| {
483 Error::geometry("Horizontal segment ref is not an entity reference".to_string())
484 })?;
485 let seg = decoder.decode_by_id(seg_id)?;
486 if seg.ifc_type != t_alignment_2d_horizontal_segment() {
487 return Err(Error::geometry(format!(
488 "#{} is not IfcAlignment2DHorizontalSegment",
489 seg_id,
490 )));
491 }
492 let curve_id = seg.get_ref(3).ok_or_else(|| {
494 Error::geometry(format!(
495 "IfcAlignment2DHorizontalSegment #{} missing CurveGeometry",
496 seg_id,
497 ))
498 })?;
499 let curve = decoder.decode_by_id(curve_id)?;
500
501 let sp_id = curve.get_ref(0).ok_or_else(|| {
506 Error::geometry(format!("CurveSegment #{} missing StartPoint", curve_id))
507 })?;
508 let sp = decoder.decode_by_id(sp_id)?;
509 let coords = sp
510 .get_list(0)
511 .ok_or_else(|| Error::geometry("StartPoint missing Coordinates".to_string()))?;
512 let sx = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
513 let sy = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
514 let heading_raw = curve.get_float(1).ok_or_else(|| {
515 Error::geometry(format!(
516 "CurveSegment #{} missing StartDirection",
517 curve_id,
518 ))
519 })?;
520 let heading = heading_raw * angle_scale;
521 let length = curve.get_float(2).ok_or_else(|| {
522 Error::geometry(format!("CurveSegment #{} missing SegmentLength", curve_id))
523 })?;
524 if !length.is_finite() || length < 0.0 {
528 return Err(Error::geometry(format!(
529 "CurveSegment #{} has negative/non-finite SegmentLength {}",
530 curve_id, length
531 )));
532 }
533
534 let hseg = if curve.ifc_type == t_line_segment_2d() {
535 HSeg::Line {
536 sx,
537 sy,
538 heading,
539 length,
540 cum_start: cumulative,
541 }
542 } else if curve.ifc_type == t_circular_arc_segment_2d() {
543 let radius = curve.get_float(3).ok_or_else(|| {
545 Error::geometry(format!("CircularArcSegment2D #{} missing Radius", curve_id))
546 })?;
547 if radius < 1e-12 {
548 return Err(Error::geometry(format!(
549 "CircularArcSegment2D #{} has non-positive radius {}",
550 curve_id, radius,
551 )));
552 }
553 let ccw = read_bool(curve.get(4));
554 HSeg::Arc {
555 sx,
556 sy,
557 heading,
558 radius,
559 length,
560 ccw,
561 cum_start: cumulative,
562 }
563 } else if curve.ifc_type == t_transition_curve_segment_2d() {
564 let start_radius = curve.get_float(3);
571 let end_radius = curve.get_float(4);
572 let start_ccw = read_bool(curve.get(5));
573 let end_ccw = read_bool(curve.get(6));
574 let start_curv = match start_radius {
575 Some(r) if r.abs() > 1e-12 => (if start_ccw { 1.0 } else { -1.0 }) / r,
576 _ => 0.0,
577 };
578 let end_curv = match end_radius {
579 Some(r) if r.abs() > 1e-12 => (if end_ccw { 1.0 } else { -1.0 }) / r,
580 _ => 0.0,
581 };
582 let kind = curve
583 .get(7)
584 .and_then(|v| v.as_enum())
585 .map(TransitionKind::from_enum)
586 .unwrap_or(TransitionKind::Clothoid);
587 HSeg::Transition {
588 sx,
589 sy,
590 heading,
591 length,
592 start_curv,
593 end_curv,
594 kind,
595 cum_start: cumulative,
596 }
597 } else {
598 return Err(Error::geometry(format!(
599 "Unsupported horizontal curve geometry at #{}: {}",
600 curve_id, curve.ifc_type,
601 )));
602 };
603 cumulative += length;
604 segments.push(hseg);
605 }
606 Ok((segments, start_dist_along))
607}
608
609fn parse_vertical(
617 v_id: u32,
618 decoder: &mut EntityDecoder,
619 horizontal_base: f64,
620) -> Result<Vec<VSeg>> {
621 let v_entity = decoder.decode_by_id(v_id)?;
622 if v_entity.ifc_type != t_alignment_2d_vertical() {
623 return Err(Error::geometry(format!(
624 "AlignmentCurve.Vertical #{} is not IfcAlignment2DVertical",
625 v_id,
626 )));
627 }
628 let segs_attr = v_entity
630 .get(0)
631 .ok_or_else(|| Error::geometry("IfcAlignment2DVertical missing Segments".to_string()))?;
632 let seg_refs = segs_attr
633 .as_list()
634 .ok_or_else(|| Error::geometry("Vertical Segments must be a list".to_string()))?;
635
636 let mut segments = Vec::with_capacity(seg_refs.len());
637 for seg_ref in seg_refs {
638 let seg_id = seg_ref.as_entity_ref().ok_or_else(|| {
639 Error::geometry("Vertical segment ref is not an entity reference".to_string())
640 })?;
641 let seg = decoder.decode_by_id(seg_id)?;
642 let start = seg.get_float(3).ok_or_else(|| {
653 Error::geometry(format!(
654 "VerticalSegment #{} missing StartDistAlong",
655 seg_id,
656 ))
657 })? - horizontal_base;
658 let length = seg.get_float(4).ok_or_else(|| {
659 Error::geometry(format!(
660 "VerticalSegment #{} missing HorizontalLength",
661 seg_id,
662 ))
663 })?;
664 if !length.is_finite() || length < 0.0 {
666 return Err(Error::geometry(format!(
667 "VerticalSegment #{} has negative/non-finite HorizontalLength {}",
668 seg_id, length
669 )));
670 }
671 let h0 = seg
672 .get_float(5)
673 .ok_or_else(|| Error::geometry(format!("VerticalSegment #{} missing StartHeight", seg_id)))?;
674 let g0 = seg.get_float(6).ok_or_else(|| {
675 Error::geometry(format!(
676 "VerticalSegment #{} missing StartGradient",
677 seg_id,
678 ))
679 })?;
680
681 let vseg = if seg.ifc_type == t_ver_seg_line() {
682 VSeg::Line {
683 start,
684 length,
685 h0,
686 g0,
687 }
688 } else if seg.ifc_type == t_ver_seg_parabolic() {
689 let parabola_constant = seg.get_float(7).ok_or_else(|| {
691 Error::geometry(format!(
692 "ParabolicVerSeg #{} missing ParabolaConstant",
693 seg_id,
694 ))
695 })?;
696 let is_convex = read_bool(seg.get(8));
697 VSeg::Parabolic {
698 start,
699 length,
700 h0,
701 g0,
702 parabola_constant,
703 is_convex,
704 }
705 } else if seg.ifc_type == t_ver_seg_circular() {
706 let radius = seg.get_float(7).ok_or_else(|| {
708 Error::geometry(format!("CircularVerSeg #{} missing Radius", seg_id))
709 })?;
710 let is_convex = read_bool(seg.get(8));
711 VSeg::CircularArc {
712 start,
713 length,
714 h0,
715 g0,
716 radius,
717 is_convex,
718 }
719 } else {
720 crate::diag::diag_warn!(
727 { vertical_segment = seg_id, ifc_type = %seg.ifc_type,
728 "alignment: unknown vertical segment subtype, degrading to a straight gradient" }
729 else {
730 eprintln!(
731 "[ifc-lite][alignment] vertical segment #{} has unsupported subtype {}; \
732 degrading to a straight gradient segment",
733 seg_id, seg.ifc_type,
734 );
735 }
736 );
737 VSeg::Line {
738 start,
739 length,
740 h0,
741 g0,
742 }
743 };
744 segments.push(vseg);
745 }
746 Ok(segments)
747}
748
749fn h_cum_start(seg: &HSeg) -> f64 {
752 match seg {
753 HSeg::Line { cum_start, .. }
754 | HSeg::Arc { cum_start, .. }
755 | HSeg::Transition { cum_start, .. } => *cum_start,
756 }
757}
758
759fn h_length(seg: &HSeg) -> f64 {
760 match seg {
761 HSeg::Line { length, .. }
762 | HSeg::Arc { length, .. }
763 | HSeg::Transition { length, .. } => *length,
764 }
765}
766
767fn h_eval(seg: &HSeg, s: f64) -> (f64, f64, f64) {
770 match seg {
771 HSeg::Line {
772 sx,
773 sy,
774 heading,
775 ..
776 } => (sx + s * heading.cos(), sy + s * heading.sin(), *heading),
777 HSeg::Arc {
778 sx,
779 sy,
780 heading,
781 radius,
782 ccw,
783 ..
784 } => {
785 let sign = if *ccw { 1.0 } else { -1.0 };
786 let theta = s / radius;
787 let new_heading = heading + sign * theta;
788 let (nx, ny) = if *ccw {
792 (-heading.sin(), heading.cos())
793 } else {
794 (heading.sin(), -heading.cos())
795 };
796 let cx = sx + radius * nx;
797 let cy = sy + radius * ny;
798 let start_angle = (-ny).atan2(-nx);
800 let new_angle = start_angle + sign * theta;
801 (
802 cx + radius * new_angle.cos(),
803 cy + radius * new_angle.sin(),
804 new_heading,
805 )
806 }
807 HSeg::Transition {
808 sx,
809 sy,
810 heading,
811 length,
812 start_curv,
813 end_curv,
814 kind,
815 ..
816 } => {
817 let n = ((s.abs() * 0.5).ceil() as usize).max(16).min(4096);
829 let ds = s / n as f64;
830 let mut x = *sx;
831 let mut y = *sy;
832 let mut prev_cos = heading.cos();
833 let mut prev_sin = heading.sin();
834 for i in 1..=n {
835 let u = i as f64 * ds;
836 let h = *heading
837 + start_curv * u
838 + (end_curv - start_curv) * length * kind.heading_integral(u / length);
839 let cs = h.cos();
840 let sn = h.sin();
841 x += 0.5 * ds * (prev_cos + cs);
842 y += 0.5 * ds * (prev_sin + sn);
843 prev_cos = cs;
844 prev_sin = sn;
845 }
846 let final_h = *heading
847 + start_curv * s
848 + (end_curv - start_curv) * length * kind.heading_integral(s / length);
849 (x, y, final_h)
850 }
851 }
852}
853
854fn v_start(seg: &VSeg) -> f64 {
855 match seg {
856 VSeg::Line { start, .. }
857 | VSeg::Parabolic { start, .. }
858 | VSeg::CircularArc { start, .. } => *start,
859 }
860}
861
862fn v_length(seg: &VSeg) -> f64 {
863 match seg {
864 VSeg::Line { length, .. }
865 | VSeg::Parabolic { length, .. }
866 | VSeg::CircularArc { length, .. } => *length,
867 }
868}
869
870fn v_eval(seg: &VSeg, s: f64) -> (f64, f64) {
873 match seg {
874 VSeg::Line { h0, g0, .. } => (h0 + g0 * s, *g0),
875 VSeg::Parabolic {
876 h0,
877 g0,
878 parabola_constant,
879 is_convex,
880 ..
881 } => {
882 let sign = if *is_convex { -1.0 } else { 1.0 };
886 let k = parabola_constant.abs().max(1e-12);
887 let z = h0 + g0 * s + sign * (s * s) / (2.0 * k);
888 let slope = g0 + sign * s / k;
889 (z, slope)
890 }
891 VSeg::CircularArc {
892 h0,
893 g0,
894 radius,
895 is_convex,
896 ..
897 } => {
898 let sign = if *is_convex { -1.0 } else { 1.0 };
901 let r = radius.abs().max(1e-12);
902 let z = h0 + g0 * s + sign * (s * s) / (2.0 * r);
903 let slope = g0 + sign * s / r;
904 (z, slope)
905 }
906 }
907}
908
909fn v_eval_height(seg: &VSeg, s: f64) -> f64 {
910 v_eval(seg, s).0
911}
912
913#[cfg(test)]
914mod tests {
915 use super::*;
916
917 #[test]
920 fn line_segment_evaluation() {
921 let seg = HSeg::Line {
922 sx: 0.0,
923 sy: 0.0,
924 heading: 0.0,
925 length: 10.0,
926 cum_start: 0.0,
927 };
928 let (x, y, h) = h_eval(&seg, 10.0);
929 assert!((x - 10.0).abs() < 1e-9);
930 assert!(y.abs() < 1e-9);
931 assert!(h.abs() < 1e-9);
932 }
933
934 #[test]
939 fn fixture_828_arc_endpoint() {
940 let seg = HSeg::Arc {
941 sx: 0.0,
942 sy: 0.0,
943 heading: 13.35833333_f64.to_radians(),
944 radius: 9279.0,
945 length: 2965.68,
946 ccw: false,
947 cum_start: 0.0,
948 };
949 let (x, y, _) = h_eval(&seg, 2965.68);
950 assert!((x - 2945.13).abs() < 5.0, "x = {} expected ~2945.13", x);
953 assert!((y - 216.39).abs() < 5.0, "y = {} expected ~216.39", y);
954 }
955
956 #[test]
961 fn base_frame_axes_and_cant_stub() {
962 let curve = AlignmentCurve {
964 horizontal: vec![HSeg::Line {
965 sx: 0.0,
966 sy: 0.0,
967 heading: 0.0,
968 length: 100.0,
969 cum_start: 0.0,
970 }],
971 vertical: vec![],
972 };
973 let frame = curve.evaluate(50.0);
974 assert!((frame.right.x).abs() < 1e-9);
975 assert!((frame.right.y + 1.0).abs() < 1e-9);
976 assert!((frame.up.z - 1.0).abs() < 1e-9);
977 assert!(curve.cant_angle(50.0).abs() < 1e-9);
979 assert!(curve.cant_angle(150.0).abs() < 1e-9);
980 }
981
982 #[test]
986 fn polyline_directrix_evaluates_piecewise() {
987 let curve = AlignmentCurve {
993 horizontal: vec![
994 HSeg::Line {
995 sx: 0.0,
996 sy: 0.0,
997 heading: 0.0,
998 length: 10.0,
999 cum_start: 0.0,
1000 },
1001 HSeg::Line {
1002 sx: 10.0,
1003 sy: 0.0,
1004 heading: std::f64::consts::FRAC_PI_2,
1005 length: 10.0,
1006 cum_start: 10.0,
1007 },
1008 ],
1009 vertical: vec![
1010 VSeg::Line {
1011 start: 0.0,
1012 length: 10.0,
1013 h0: 0.0,
1014 g0: 0.1,
1015 },
1016 VSeg::Line {
1017 start: 10.0,
1018 length: 10.0,
1019 h0: 1.0,
1020 g0: 0.1,
1021 },
1022 ],
1023 };
1024 let f1 = curve.evaluate(5.0);
1026 assert!((f1.origin.x - 5.0).abs() < 1e-9);
1027 assert!((f1.origin.y).abs() < 1e-9);
1028 assert!((f1.origin.z - 0.5).abs() < 1e-9);
1029 let f2 = curve.evaluate(15.0);
1031 assert!((f2.origin.x - 10.0).abs() < 1e-9);
1032 assert!((f2.origin.y - 5.0).abs() < 1e-9);
1033 assert!((f2.origin.z - 1.5).abs() < 1e-9);
1034 }
1035
1036 #[test]
1037 fn transition_kind_heading_integral_normalised() {
1038 for kind in [
1042 TransitionKind::Clothoid,
1043 TransitionKind::Bloss,
1044 TransitionKind::Cosine,
1045 TransitionKind::Sine,
1046 TransitionKind::CubicParabola,
1047 TransitionKind::BiquadraticParabola,
1048 ] {
1049 assert!(kind.heading_integral(0.0).abs() < 1e-12, "{:?}", kind);
1050 let mid = kind.heading_integral(0.5);
1051 assert!(mid > 0.0 && mid < 0.5, "{:?} mid={}", kind, mid);
1052 let end = kind.heading_integral(1.0);
1055 assert!(end > 0.0 && end < 1.0, "{:?} end={}", kind, end);
1056 }
1057 }
1058
1059 #[test]
1060 fn parabolic_vertical_segment() {
1061 let seg = VSeg::Parabolic {
1066 start: 3600.0,
1067 length: 3685.68,
1068 h0: 399.0,
1069 g0: 0.0579,
1070 parabola_constant: 36000.0,
1071 is_convex: false,
1072 };
1073 let (z, slope) = v_eval(&seg, 1680.0);
1074 assert!((z - 535.472).abs() < 0.01, "z = {}", z);
1075 assert!((slope - 0.1046).abs() < 1e-3, "slope = {}", slope);
1076 }
1077
1078 #[test]
1095 fn nonzero_start_dist_along_rebases_vertical_to_horizontal() {
1096 let content = "\
1097ISO-10303-21;
1098HEADER;
1099FILE_DESCRIPTION((''),'2;1');
1100FILE_NAME('','',(''),(''),'','','');
1101FILE_SCHEMA(('IFC4X1'));
1102ENDSEC;
1103DATA;
1104#10=IFCCARTESIANPOINT((0.,0.));
1105#11=IFCLINESEGMENT2D(#10,0.,100.);
1106#12=IFCALIGNMENT2DHORIZONTALSEGMENT($,$,$,#11);
1107#13=IFCALIGNMENT2DHORIZONTAL(1000.,(#12));
1108#14=IFCALIGNMENT2DVERSEGLINE($,$,$,1000.,100.,50.,0.1);
1109#15=IFCALIGNMENT2DVERTICAL((#14));
1110#16=IFCALIGNMENTCURVE(#13,#15,$);
1111ENDSEC;
1112END-ISO-10303-21;
1113";
1114 let entity_index = ifc_lite_core::build_entity_index(&content);
1115 let mut decoder = EntityDecoder::with_index(&content, entity_index);
1116 let directrix = decoder.decode_by_id(16).expect("decode IfcAlignmentCurve");
1117 let curve = AlignmentCurve::parse(&directrix, &mut decoder)
1118 .expect("parse alignment")
1119 .expect("directrix recognised as alignment");
1120
1121 let f0 = curve.evaluate(0.0);
1123 assert!((f0.origin.x - 0.0).abs() < 1e-6, "start x = {}", f0.origin.x);
1124 assert!((f0.origin.z - 50.0).abs() < 1e-6, "start z = {}", f0.origin.z);
1125
1126 let f_mid = curve.evaluate(50.0);
1129 assert!((f_mid.origin.x - 50.0).abs() < 1e-6, "mid x = {}", f_mid.origin.x);
1130 assert!(
1131 (f_mid.origin.z - 55.0).abs() < 1e-6,
1132 "mid z = {} (expected 55; main desyncs vertical and returns ~50)",
1133 f_mid.origin.z,
1134 );
1135
1136 let f_end = curve.evaluate(100.0);
1138 assert!((f_end.origin.x - 100.0).abs() < 1e-6, "end x = {}", f_end.origin.x);
1139 assert!((f_end.origin.z - 60.0).abs() < 1e-6, "end z = {}", f_end.origin.z);
1140 }
1141
1142 #[test]
1146 fn negative_segment_length_errors_not_nan() {
1147 let content = "\
1148ISO-10303-21;
1149HEADER;
1150FILE_DESCRIPTION((''),'2;1');
1151FILE_NAME('','',(''),(''),'','','');
1152FILE_SCHEMA(('IFC4X1'));
1153ENDSEC;
1154DATA;
1155#10=IFCCARTESIANPOINT((0.,0.));
1156#11=IFCLINESEGMENT2D(#10,0.,-100.);
1157#12=IFCALIGNMENT2DHORIZONTALSEGMENT($,$,$,#11);
1158#13=IFCALIGNMENT2DHORIZONTAL(0.,(#12));
1159#14=IFCALIGNMENT2DVERSEGLINE($,$,$,0.,100.,50.,0.1);
1160#15=IFCALIGNMENT2DVERTICAL((#14));
1161#16=IFCALIGNMENTCURVE(#13,#15,$);
1162ENDSEC;
1163END-ISO-10303-21;
1164";
1165 let entity_index = ifc_lite_core::build_entity_index(&content);
1166 let mut decoder = EntityDecoder::with_index(&content, entity_index);
1167 let directrix = decoder.decode_by_id(16).expect("decode IfcAlignmentCurve");
1168 assert!(AlignmentCurve::parse(&directrix, &mut decoder).is_err());
1169 }
1170}