ifc_alignment/horizontal/
segment.rs1use axiolid_core::Point2;
4use ifc_model::value::Value;
5use ifc_model::{Entity, EntityId, Model};
6
7use crate::error::{AlignmentError, AlignmentResult};
8
9#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct AlignmentUnits {
12 pub length_to_metres: f64,
14 pub angle_to_radians: f64,
16}
17
18impl AlignmentUnits {
19 fn validate(self) -> AlignmentResult<Self> {
20 if !self.length_to_metres.is_finite() || self.length_to_metres <= 0.0 {
21 return Err(AlignmentError::InvalidUnits {
22 detail: "length factor must be finite and positive",
23 });
24 }
25 if !self.angle_to_radians.is_finite() || self.angle_to_radians <= 0.0 {
26 return Err(AlignmentError::InvalidUnits {
27 detail: "angle factor must be finite and positive",
28 });
29 }
30 Ok(self)
31 }
32}
33
34#[non_exhaustive]
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum HorizontalSegmentType {
38 Line,
40 CircularArc,
43 Transition(String),
47 UserDefined,
49 NotDefined,
51}
52
53impl HorizontalSegmentType {
54 pub fn source_name(&self) -> &str {
56 match self {
57 Self::Line => "LINE",
58 Self::CircularArc => "CIRCULARARC",
59 Self::Transition(name) => name,
60 Self::UserDefined => "USERDEFINED",
61 Self::NotDefined => "NOTDEFINED",
62 }
63 }
64}
65
66#[derive(Debug, Clone, PartialEq)]
68#[non_exhaustive]
69pub struct HorizontalSegment {
70 pub entity: EntityId,
72 pub start_point: Point2,
75 pub start_direction: f64,
77 pub start_radius: f64,
79 pub end_radius: f64,
81 pub segment_length: f64,
86 pub gravity_center_line_height: Option<f64>,
89 pub segment_type: HorizontalSegmentType,
91}
92
93use crate::slot::horizontal::{
96 END_RADIUS, GRAVITY_CENTER_LINE_HEIGHT, PREDEFINED_TYPE, SEGMENT_LENGTH, START_DIRECTION,
97 START_POINT, START_RADIUS,
98};
99
100pub fn read_horizontal_segment(
113 model: &Model,
114 id: EntityId,
115 units: AlignmentUnits,
116) -> AlignmentResult<HorizontalSegment> {
117 let units = units.validate()?;
118 let entity = model
119 .get(id)
120 .ok_or(AlignmentError::MissingEntity { entity: id })?;
121 if !entity.is_type("IFCALIGNMENTHORIZONTALSEGMENT") {
122 return Err(AlignmentError::WrongType {
123 entity: id,
124 expected: "IFCALIGNMENTHORIZONTALSEGMENT",
125 actual: entity.type_name.to_string(),
126 });
127 }
128 let point_ref = required_ref(entity, id, START_POINT, "StartPoint")?;
129 let point = model
130 .get(point_ref)
131 .ok_or(AlignmentError::MissingEntity { entity: point_ref })?;
132 if !point.is_type("IFCCARTESIANPOINT") {
133 return Err(AlignmentError::WrongType {
134 entity: point_ref,
135 expected: "IFCCARTESIANPOINT",
136 actual: point.type_name.to_string(),
137 });
138 }
139 let coordinates =
140 point
141 .attribute(0)
142 .and_then(Value::as_list)
143 .ok_or(AlignmentError::InvalidAttribute {
144 entity: point_ref,
145 index: 0,
146 name: "Coordinates",
147 })?;
148 if coordinates.len() < 2 {
149 return Err(AlignmentError::InvalidAttribute {
150 entity: point_ref,
151 index: 0,
152 name: "Coordinates",
153 });
154 }
155 let x = coordinates[0]
156 .unwrap_typed()
157 .as_f64()
158 .ok_or(AlignmentError::InvalidAttribute {
159 entity: point_ref,
160 index: 0,
161 name: "Coordinates",
162 })?
163 * units.length_to_metres;
164 let y = coordinates[1]
165 .unwrap_typed()
166 .as_f64()
167 .ok_or(AlignmentError::InvalidAttribute {
168 entity: point_ref,
169 index: 0,
170 name: "Coordinates",
171 })?
172 * units.length_to_metres;
173
174 let start_direction =
175 required_number(entity, id, START_DIRECTION, "StartDirection")? * units.angle_to_radians;
176 let start_radius = required_number(entity, id, START_RADIUS, "StartRadiusOfCurvature")?
177 * units.length_to_metres;
178 let end_radius =
179 required_number(entity, id, END_RADIUS, "EndRadiusOfCurvature")? * units.length_to_metres;
180 let segment_length =
181 required_number(entity, id, SEGMENT_LENGTH, "SegmentLength")? * units.length_to_metres;
182 let gravity_center_line_height = optional_number(
183 entity,
184 id,
185 GRAVITY_CENTER_LINE_HEIGHT,
186 "GravityCenterLineHeight",
187 )?
188 .map(|value| value * units.length_to_metres);
189 let source_type = required_enum(entity, id, PREDEFINED_TYPE, "PredefinedType")?;
190 let segment_type = match source_type {
191 "LINE" => HorizontalSegmentType::Line,
192 "CIRCULARARC" => HorizontalSegmentType::CircularArc,
193 "USERDEFINED" => HorizontalSegmentType::UserDefined,
194 "NOTDEFINED" => HorizontalSegmentType::NotDefined,
195 other => HorizontalSegmentType::Transition(other.to_owned()),
196 };
197
198 let values = [
199 x,
200 y,
201 start_direction,
202 start_radius,
203 end_radius,
204 segment_length,
205 ];
206 if values.iter().any(|value| !value.is_finite())
207 || gravity_center_line_height.is_some_and(|value| !value.is_finite())
208 || segment_length < 0.0
209 {
210 return Err(AlignmentError::InvalidSegment {
211 entity: id,
212 detail: "coordinates and parameters must be finite and SegmentLength non-negative",
213 });
214 }
215
216 Ok(HorizontalSegment {
217 entity: id,
218 start_point: Point2::new(x, y),
219 start_direction,
220 start_radius,
221 end_radius,
222 segment_length,
223 gravity_center_line_height,
224 segment_type,
225 })
226}
227
228fn required_ref(
229 entity: &Entity,
230 id: EntityId,
231 index: usize,
232 name: &'static str,
233) -> AlignmentResult<EntityId> {
234 match entity.attribute(index) {
235 None | Some(Value::Null) => Err(AlignmentError::MissingAttribute {
236 entity: id,
237 index,
238 name,
239 }),
240 Some(Value::Ref(reference)) => Ok(*reference),
241 _ => Err(AlignmentError::InvalidAttribute {
242 entity: id,
243 index,
244 name,
245 }),
246 }
247}
248
249fn required_number(
250 entity: &Entity,
251 id: EntityId,
252 index: usize,
253 name: &'static str,
254) -> AlignmentResult<f64> {
255 match entity.attribute(index) {
256 None | Some(Value::Null) => Err(AlignmentError::MissingAttribute {
257 entity: id,
258 index,
259 name,
260 }),
261 Some(value) => value
262 .unwrap_typed()
263 .as_f64()
264 .ok_or(AlignmentError::InvalidAttribute {
265 entity: id,
266 index,
267 name,
268 }),
269 }
270}
271
272fn optional_number(
273 entity: &Entity,
274 id: EntityId,
275 index: usize,
276 name: &'static str,
277) -> AlignmentResult<Option<f64>> {
278 match entity.attribute(index) {
279 None | Some(Value::Null) => Ok(None),
280 Some(value) => {
281 value
282 .unwrap_typed()
283 .as_f64()
284 .map(Some)
285 .ok_or(AlignmentError::InvalidAttribute {
286 entity: id,
287 index,
288 name,
289 })
290 }
291 }
292}
293
294fn required_enum<'a>(
295 entity: &'a Entity,
296 id: EntityId,
297 index: usize,
298 name: &'static str,
299) -> AlignmentResult<&'a str> {
300 match entity.attribute(index) {
301 Some(Value::Enum(value)) => Ok(value),
302 None | Some(Value::Null) => Err(AlignmentError::MissingAttribute {
303 entity: id,
304 index,
305 name,
306 }),
307 _ => Err(AlignmentError::InvalidAttribute {
308 entity: id,
309 index,
310 name,
311 }),
312 }
313}