Skip to main content

ifc_alignment/horizontal/
segment.rs

1//! IFC4x3 horizontal alignment segment parameters.
2
3use axiolid_core::Point2;
4use ifc_model::value::Value;
5use ifc_model::{Entity, EntityId, Model};
6
7use crate::error::{AlignmentError, AlignmentResult};
8
9/// Project unit conversion applied exactly once while reading a segment.
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct AlignmentUnits {
12    /// Factor converting a length attribute's stored value to metres.
13    pub length_to_metres: f64,
14    /// Factor converting an angle attribute's stored value to radians.
15    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/// IFC4x3 horizontal segment kind, preserving the declaration rather than guessing.
35#[non_exhaustive]
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum HorizontalSegmentType {
38    /// `LINE`: a straight tangent segment (zero curvature throughout).
39    Line,
40    /// `CIRCULARARC`: constant-radius arc; `StartRadiusOfCurvature` and
41    /// `EndRadiusOfCurvature` are equal.
42    CircularArc,
43    /// A transition-spiral `PredefinedType` token (e.g. `CLOTHOID`,
44    /// `CUBIC`, `BLOSS`), preserved verbatim -- which spiral laws this crate
45    /// can lower exactly is decided downstream, not by this type.
46    Transition(String),
47    /// `USERDEFINED`: an author-supplied law outside the enumerated set.
48    UserDefined,
49    /// `NOTDEFINED`: no horizontal segment kind was declared.
50    NotDefined,
51}
52
53impl HorizontalSegmentType {
54    /// The `PredefinedType` enumeration token this variant was read from.
55    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/// Resolved IFC4x3 `IfcAlignmentHorizontalSegment` parameters in SI units.
67#[derive(Debug, Clone, PartialEq)]
68pub struct HorizontalSegment {
69    /// The `IfcAlignmentHorizontalSegment` entity this was read from.
70    pub entity: EntityId,
71    /// `StartPoint`: the segment's start, resolved from the referenced
72    /// `IfcCartesianPoint`.
73    pub start_point: Point2,
74    /// `StartDirection`: bearing at the segment start, in radians.
75    pub start_direction: f64,
76    /// `StartRadiusOfCurvature`.
77    pub start_radius: f64,
78    /// `EndRadiusOfCurvature`.
79    pub end_radius: f64,
80    /// `SegmentLength`: arc length along the segment. Must be positive.
81    pub segment_length: f64,
82    /// `GravityCenterLineHeight`, when authored (rail/cant-related, not
83    /// used by exact lowering).
84    pub gravity_center_line_height: Option<f64>,
85    /// `PredefinedType`: which horizontal segment kind this is.
86    pub segment_type: HorizontalSegmentType,
87}
88
89// Slots live in `crate::slot` so the authoring side indexes the same
90// definitions these readers do (ADR 0011).
91use crate::slot::horizontal::{
92    END_RADIUS, GRAVITY_CENTER_LINE_HEIGHT, PREDEFINED_TYPE, SEGMENT_LENGTH, START_DIRECTION,
93    START_POINT, START_RADIUS,
94};
95
96/// Read one `IfcAlignmentHorizontalSegment` referenced by `id`.
97///
98/// Fails if `units` is non-finite or non-positive, `id` is missing or not an
99/// `IfcAlignmentHorizontalSegment`, `StartPoint` does not resolve to an
100/// `IfcCartesianPoint` with at least two coordinates, an attribute is
101/// missing or the wrong kind, any value is non-finite, or `SegmentLength`
102/// is not positive.
103pub 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}