ifc_alignment/cant/
layout.rs1use ifc_model::{EntityId, Model};
11
12use crate::cant::evaluate::{cant_at, CantAtStation};
13use crate::cant::segment::{read_cant_segment, CantSegment};
14use crate::error::{AlignmentError, AlignmentResult};
15use crate::horizontal::AlignmentUnits;
16use crate::view::AlignmentView;
17
18#[derive(Debug, Clone, PartialEq)]
20pub struct CantLayout {
21 pub entity: EntityId,
23 pub rail_head_distance: f64,
26 segments: Vec<CantSegment>,
27}
28
29impl CantLayout {
30 pub fn resolve(
33 model: &Model,
34 entity: EntityId,
35 units: AlignmentUnits,
36 ) -> AlignmentResult<Self> {
37 let view = AlignmentView::for_model(model)?;
38 let cant_entity = model
39 .get(entity)
40 .ok_or(AlignmentError::MissingEntity { entity })?;
41 if !view.schema.is_a(&cant_entity.type_name, "IfcAlignmentCant") {
42 return Err(AlignmentError::WrongType {
43 entity,
44 expected: "IfcAlignmentCant",
45 actual: cant_entity.type_name.to_string(),
46 });
47 }
48 let rail_head_distance = cant_entity
52 .attributes
53 .last()
54 .and_then(|value| value.as_f64())
55 .ok_or(AlignmentError::InvalidAttribute {
56 entity,
57 index: cant_entity.attributes.len().saturating_sub(1),
58 name: "RailHeadDistance",
59 })?
60 * units.length_to_metres;
61 if !(rail_head_distance.is_finite() && rail_head_distance > 0.0) {
62 return Err(AlignmentError::InvalidAttribute {
63 entity,
64 index: cant_entity.attributes.len().saturating_sub(1),
65 name: "RailHeadDistance",
66 });
67 }
68
69 let ids = view.segment_chain(entity, "IfcAlignmentCantSegment")?;
70 if ids.is_empty() {
71 return Err(AlignmentError::SemanticViolation {
72 entity: Some(entity),
73 rule: "IfcAlignmentCant must nest at least one IfcAlignmentSegment",
74 });
75 }
76 let mut segments = Vec::with_capacity(ids.len());
77 for id in ids {
78 segments.push(read_cant_segment(model, id, units)?);
79 }
80
81 for pair in segments.windows(2) {
86 let [previous, next] = pair else {
87 unreachable!()
88 };
89 let previous_end = previous.start_dist_along + previous.horizontal_length;
90 if (next.start_dist_along - previous_end).abs() > 1e-6 {
91 return Err(AlignmentError::SemanticViolation {
92 entity: Some(next.entity),
93 rule: "cant segments must be contiguous in StartDistAlong order",
94 });
95 }
96 let previous_end_left = previous.end_cant_left.unwrap_or(previous.start_cant_left);
97 let previous_end_right = previous.end_cant_right.unwrap_or(previous.start_cant_right);
98 if (previous_end_left - next.start_cant_left).abs() > 1e-9
99 || (previous_end_right - next.start_cant_right).abs() > 1e-9
100 {
101 return Err(AlignmentError::SemanticViolation {
102 entity: Some(next.entity),
103 rule: "cant segments must be C0 continuous: end cant must equal the next segment's start cant",
104 });
105 }
106 }
107
108 Ok(Self {
109 entity,
110 rail_head_distance,
111 segments,
112 })
113 }
114
115 #[must_use]
117 pub fn segments(&self) -> &[CantSegment] {
118 &self.segments
119 }
120
121 #[must_use]
123 pub fn length(&self) -> f64 {
124 self.segments
125 .last()
126 .map(|last| {
127 last.start_dist_along + last.horizontal_length - self.segments[0].start_dist_along
128 })
129 .unwrap_or(0.0)
130 }
131
132 pub fn cant_at_distance(&self, distance_along: f64) -> AlignmentResult<CantAtStation> {
134 if !distance_along.is_finite() {
135 return Err(AlignmentError::InvalidUnits {
136 detail: "distance along must be finite",
137 });
138 }
139 let segment = self
140 .segments
141 .iter()
142 .find(|segment| {
143 let start = segment.start_dist_along;
144 let end = start + segment.horizontal_length;
145 distance_along >= start - 1e-9 && distance_along <= end + 1e-9
146 })
147 .ok_or(AlignmentError::InvalidUnits {
148 detail: "distance along lies outside the cant profile's span",
149 })?;
150 let xi = if segment.horizontal_length > 0.0 {
151 ((distance_along - segment.start_dist_along) / segment.horizontal_length)
152 .clamp(0.0, 1.0)
153 } else {
154 0.0
155 };
156 cant_at(segment, xi, Some(self.rail_head_distance))
157 }
158}