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)]
68#[non_exhaustive]
69pub struct HorizontalSegment {
70    /// The `IfcAlignmentHorizontalSegment` entity this was read from.
71    pub entity: EntityId,
72    /// `StartPoint`: the segment's start, resolved from the referenced
73    /// `IfcCartesianPoint`.
74    pub start_point: Point2,
75    /// `StartDirection`: bearing at the segment start, in radians.
76    pub start_direction: f64,
77    /// `StartRadiusOfCurvature`.
78    pub start_radius: f64,
79    /// `EndRadiusOfCurvature`.
80    pub end_radius: f64,
81    /// `SegmentLength`: arc length along the segment. Must be positive.
82    pub segment_length: f64,
83    /// `GravityCenterLineHeight`, when authored (rail/cant-related, not
84    /// used by exact lowering).
85    pub gravity_center_line_height: Option<f64>,
86    /// `PredefinedType`: which horizontal segment kind this is.
87    pub segment_type: HorizontalSegmentType,
88}
89
90// Slots live in `crate::slot` so the authoring side indexes the same
91// definitions these readers do (ADR 0011).
92use crate::slot::horizontal::{
93    END_RADIUS, GRAVITY_CENTER_LINE_HEIGHT, PREDEFINED_TYPE, SEGMENT_LENGTH, START_DIRECTION,
94    START_POINT, START_RADIUS,
95};
96
97/// Read one `IfcAlignmentHorizontalSegment` referenced by `id`.
98///
99/// Fails if `units` is non-finite or non-positive, `id` is missing or not an
100/// `IfcAlignmentHorizontalSegment`, `StartPoint` does not resolve to an
101/// `IfcCartesianPoint` with at least two coordinates, an attribute is
102/// missing or the wrong kind, any value is non-finite, or `SegmentLength`
103/// is not positive.
104pub 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}