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