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,
83 pub gravity_center_line_height: Option<f64>,
86 pub segment_type: HorizontalSegmentType,
88}
89
90use crate::slot::horizontal::{
93 END_RADIUS, GRAVITY_CENTER_LINE_HEIGHT, PREDEFINED_TYPE, SEGMENT_LENGTH, START_DIRECTION,
94 START_POINT, START_RADIUS,
95};
96
97pub fn read_horizontal_segment(
105 model: &Model,
106 id: EntityId,
107 units: AlignmentUnits,
108) -> AlignmentResult<HorizontalSegment> {
109 let units = units.validate()?;
110 let entity = model
111 .get(id)
112 .ok_or(AlignmentError::MissingEntity { entity: id })?;
113 if !entity.is_type("IFCALIGNMENTHORIZONTALSEGMENT") {
114 return Err(AlignmentError::WrongType {
115 entity: id,
116 expected: "IFCALIGNMENTHORIZONTALSEGMENT",
117 actual: entity.type_name.to_string(),
118 });
119 }
120 let point_ref = required_ref(entity, id, START_POINT, "StartPoint")?;
121 let point = model
122 .get(point_ref)
123 .ok_or(AlignmentError::MissingEntity { entity: point_ref })?;
124 if !point.is_type("IFCCARTESIANPOINT") {
125 return Err(AlignmentError::WrongType {
126 entity: point_ref,
127 expected: "IFCCARTESIANPOINT",
128 actual: point.type_name.to_string(),
129 });
130 }
131 let coordinates =
132 point
133 .attribute(0)
134 .and_then(Value::as_list)
135 .ok_or(AlignmentError::InvalidAttribute {
136 entity: point_ref,
137 index: 0,
138 name: "Coordinates",
139 })?;
140 if coordinates.len() < 2 {
141 return Err(AlignmentError::InvalidAttribute {
142 entity: point_ref,
143 index: 0,
144 name: "Coordinates",
145 });
146 }
147 let x = coordinates[0]
148 .unwrap_typed()
149 .as_f64()
150 .ok_or(AlignmentError::InvalidAttribute {
151 entity: point_ref,
152 index: 0,
153 name: "Coordinates",
154 })?
155 * units.length_to_metres;
156 let y = coordinates[1]
157 .unwrap_typed()
158 .as_f64()
159 .ok_or(AlignmentError::InvalidAttribute {
160 entity: point_ref,
161 index: 0,
162 name: "Coordinates",
163 })?
164 * units.length_to_metres;
165
166 let start_direction =
167 required_number(entity, id, START_DIRECTION, "StartDirection")? * units.angle_to_radians;
168 let start_radius = required_number(entity, id, START_RADIUS, "StartRadiusOfCurvature")?
169 * units.length_to_metres;
170 let end_radius =
171 required_number(entity, id, END_RADIUS, "EndRadiusOfCurvature")? * units.length_to_metres;
172 let segment_length =
173 required_number(entity, id, SEGMENT_LENGTH, "SegmentLength")? * units.length_to_metres;
174 let gravity_center_line_height = optional_number(
175 entity,
176 id,
177 GRAVITY_CENTER_LINE_HEIGHT,
178 "GravityCenterLineHeight",
179 )?
180 .map(|value| value * units.length_to_metres);
181 let source_type = required_enum(entity, id, PREDEFINED_TYPE, "PredefinedType")?;
182 let segment_type = match source_type {
183 "LINE" => HorizontalSegmentType::Line,
184 "CIRCULARARC" => HorizontalSegmentType::CircularArc,
185 "USERDEFINED" => HorizontalSegmentType::UserDefined,
186 "NOTDEFINED" => HorizontalSegmentType::NotDefined,
187 other => HorizontalSegmentType::Transition(other.to_owned()),
188 };
189
190 let values = [
191 x,
192 y,
193 start_direction,
194 start_radius,
195 end_radius,
196 segment_length,
197 ];
198 if values.iter().any(|value| !value.is_finite())
199 || gravity_center_line_height.is_some_and(|value| !value.is_finite())
200 || segment_length <= 0.0
201 {
202 return Err(AlignmentError::InvalidSegment {
203 entity: id,
204 detail: "coordinates and parameters must be finite and SegmentLength positive",
205 });
206 }
207
208 Ok(HorizontalSegment {
209 entity: id,
210 start_point: Point2::new(x, y),
211 start_direction,
212 start_radius,
213 end_radius,
214 segment_length,
215 gravity_center_line_height,
216 segment_type,
217 })
218}
219
220fn required_ref(
221 entity: &Entity,
222 id: EntityId,
223 index: usize,
224 name: &'static str,
225) -> AlignmentResult<EntityId> {
226 match entity.attribute(index) {
227 None | Some(Value::Null) => Err(AlignmentError::MissingAttribute {
228 entity: id,
229 index,
230 name,
231 }),
232 Some(Value::Ref(reference)) => Ok(*reference),
233 _ => Err(AlignmentError::InvalidAttribute {
234 entity: id,
235 index,
236 name,
237 }),
238 }
239}
240
241fn required_number(
242 entity: &Entity,
243 id: EntityId,
244 index: usize,
245 name: &'static str,
246) -> AlignmentResult<f64> {
247 match entity.attribute(index) {
248 None | Some(Value::Null) => Err(AlignmentError::MissingAttribute {
249 entity: id,
250 index,
251 name,
252 }),
253 Some(value) => value
254 .unwrap_typed()
255 .as_f64()
256 .ok_or(AlignmentError::InvalidAttribute {
257 entity: id,
258 index,
259 name,
260 }),
261 }
262}
263
264fn optional_number(
265 entity: &Entity,
266 id: EntityId,
267 index: usize,
268 name: &'static str,
269) -> AlignmentResult<Option<f64>> {
270 match entity.attribute(index) {
271 None | Some(Value::Null) => Ok(None),
272 Some(value) => {
273 value
274 .unwrap_typed()
275 .as_f64()
276 .map(Some)
277 .ok_or(AlignmentError::InvalidAttribute {
278 entity: id,
279 index,
280 name,
281 })
282 }
283 }
284}
285
286fn required_enum<'a>(
287 entity: &'a Entity,
288 id: EntityId,
289 index: usize,
290 name: &'static str,
291) -> AlignmentResult<&'a str> {
292 match entity.attribute(index) {
293 Some(Value::Enum(value)) => Ok(value),
294 None | Some(Value::Null) => Err(AlignmentError::MissingAttribute {
295 entity: id,
296 index,
297 name,
298 }),
299 _ => Err(AlignmentError::InvalidAttribute {
300 entity: id,
301 index,
302 name,
303 }),
304 }
305}