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. Non-negative
82    /// (`IfcNonNegativeLengthMeasure`); zero only for the closing segment
83    /// IFC4.3 requires at the end of a layout, which the layout paths
84    /// accept in last place alone.
85    pub segment_length: f64,
86    /// `GravityCenterLineHeight`, when authored (rail/cant-related, not
87    /// used by exact lowering).
88    pub gravity_center_line_height: Option<f64>,
89    /// `PredefinedType`: which horizontal segment kind this is.
90    pub segment_type: HorizontalSegmentType,
91}
92
93// Slots live in `crate::slot` so the authoring side indexes the same
94// definitions these readers do (ADR 0011).
95use crate::slot::horizontal::{
96    END_RADIUS, GRAVITY_CENTER_LINE_HEIGHT, PREDEFINED_TYPE, SEGMENT_LENGTH, START_DIRECTION,
97    START_POINT, START_RADIUS,
98};
99
100/// Read one `IfcAlignmentHorizontalSegment` referenced by `id`.
101///
102/// Fails if `units` is non-finite or non-positive, `id` is missing or not an
103/// `IfcAlignmentHorizontalSegment`, `StartPoint` does not resolve to an
104/// `IfcCartesianPoint` with at least two coordinates, an attribute is
105/// missing or the wrong kind, any value is non-finite, or `SegmentLength`
106/// is negative.
107///
108/// A zero `SegmentLength` is read: IFC4.3 (concept template *Alignment
109/// Layout - Horizontal, Vertical and Cant*) requires a zero-length segment
110/// at the end of every layout. Where it may stand is a layout question, so
111/// the layout paths decide it, not this reader.
112pub 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}