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::curve::terminal::split_closing;
15use crate::error::{AlignmentError, AlignmentResult};
16use crate::horizontal::AlignmentUnits;
17use crate::view::AlignmentView;
18
19#[derive(Debug, Clone, PartialEq)]
21#[non_exhaustive]
22pub struct CantLayout {
23 pub entity: EntityId,
25 pub rail_head_distance: f64,
28 segments: Vec<CantSegment>,
29}
30
31impl CantLayout {
32 pub fn resolve(
41 model: &Model,
42 entity: EntityId,
43 units: AlignmentUnits,
44 ) -> AlignmentResult<Self> {
45 let view = AlignmentView::for_model(model)?;
46 let cant_entity = model
47 .get(entity)
48 .ok_or(AlignmentError::MissingEntity { entity })?;
49 if !view.schema.is_a(&cant_entity.type_name, "IfcAlignmentCant") {
50 return Err(AlignmentError::WrongType {
51 entity,
52 expected: "IfcAlignmentCant",
53 actual: cant_entity.type_name.to_string(),
54 });
55 }
56 let rail_head_distance = cant_entity
60 .attributes
61 .last()
62 .and_then(|value| value.unwrap_typed().as_f64())
63 .ok_or(AlignmentError::InvalidAttribute {
64 entity,
65 index: cant_entity.attributes.len().saturating_sub(1),
66 name: "RailHeadDistance",
67 })?
68 * units.length_to_metres;
69 if !(rail_head_distance.is_finite() && rail_head_distance > 0.0) {
70 return Err(AlignmentError::InvalidAttribute {
71 entity,
72 index: cant_entity.attributes.len().saturating_sub(1),
73 name: "RailHeadDistance",
74 });
75 }
76
77 let ids = view.segment_chain(entity, "IfcAlignmentCantSegment")?;
78 if ids.is_empty() {
79 return Err(AlignmentError::SemanticViolation {
80 entity: Some(entity),
81 rule: "IfcAlignmentCant must nest at least one IfcAlignmentSegment",
82 });
83 }
84 let mut segments = Vec::with_capacity(ids.len());
85 for id in ids {
86 segments.push(read_cant_segment(model, id, units)?);
87 }
88 split_closing(&segments, |s| s.horizontal_length, |s| s.entity)?;
92
93 for pair in segments.windows(2) {
98 let [previous, next] = pair else {
99 unreachable!()
100 };
101 let previous_end = previous.start_dist_along + previous.horizontal_length;
102 if (next.start_dist_along - previous_end).abs() > 1e-6 {
103 return Err(AlignmentError::SemanticViolation {
104 entity: Some(next.entity),
105 rule: "cant segments must be contiguous in StartDistAlong order",
106 });
107 }
108 let previous_end_left = previous.end_cant_left.unwrap_or(previous.start_cant_left);
109 let previous_end_right = previous.end_cant_right.unwrap_or(previous.start_cant_right);
110 if (previous_end_left - next.start_cant_left).abs() > 1e-9
111 || (previous_end_right - next.start_cant_right).abs() > 1e-9
112 {
113 return Err(AlignmentError::SemanticViolation {
114 entity: Some(next.entity),
115 rule: "cant segments must be C0 continuous: end cant must equal the next segment's start cant",
116 });
117 }
118 }
119
120 Ok(Self {
121 entity,
122 rail_head_distance,
123 segments,
124 })
125 }
126
127 pub fn for_alignment(
141 model: &Model,
142 alignment: EntityId,
143 units: AlignmentUnits,
144 ) -> AlignmentResult<Self> {
145 let view = AlignmentView::for_model(model)?;
146 let entity = model
147 .get(alignment)
148 .ok_or(AlignmentError::MissingEntity { entity: alignment })?;
149 if !view.schema.is_a(&entity.type_name, "IfcAlignment") {
150 return Err(AlignmentError::WrongType {
151 entity: alignment,
152 expected: "IfcAlignment",
153 actual: entity.type_name.to_string(),
154 });
155 }
156 match view
157 .nested_children(alignment, "IfcAlignmentCant")?
158 .as_slice()
159 {
160 [only] => Self::resolve(model, *only, units),
161 [] => Err(AlignmentError::SemanticViolation {
162 entity: Some(alignment),
163 rule: "the alignment nests no IfcAlignmentCant layout",
164 }),
165 _ => Err(AlignmentError::SemanticViolation {
166 entity: Some(alignment),
167 rule: "an alignment with several cant layouts is ambiguous to compose",
168 }),
169 }
170 }
171
172 #[must_use]
174 pub fn segments(&self) -> &[CantSegment] {
175 &self.segments
176 }
177
178 #[must_use]
180 pub fn length(&self) -> f64 {
181 self.segments
182 .last()
183 .map(|last| {
184 last.start_dist_along + last.horizontal_length - self.segments[0].start_dist_along
185 })
186 .unwrap_or(0.0)
187 }
188
189 pub fn cant_at_distance(&self, distance_along: f64) -> AlignmentResult<CantAtStation> {
191 if !distance_along.is_finite() {
192 return Err(AlignmentError::InvalidUnits {
193 detail: "distance along must be finite",
194 });
195 }
196 let segment = self
197 .segments
198 .iter()
199 .find(|segment| {
200 let start = segment.start_dist_along;
201 let end = start + segment.horizontal_length;
202 distance_along >= start - 1e-9 && distance_along <= end + 1e-9
203 })
204 .ok_or(AlignmentError::InvalidUnits {
205 detail: "distance along lies outside the cant profile's span",
206 })?;
207 let xi = if segment.horizontal_length > 0.0 {
208 ((distance_along - segment.start_dist_along) / segment.horizontal_length)
209 .clamp(0.0, 1.0)
210 } else {
211 0.0
212 };
213 cant_at(segment, xi, Some(self.rail_head_distance))
214 }
215}