Skip to main content

ifc_alignment/placement/
linear.rs

1//! `IfcLinearPlacement` and `IfcPointByDistanceExpression`.
2//!
3//! IFC4X3 constrains `IfcAxis2PlacementLinear.Location` (WR1) to always be an
4//! `IfcPointByDistanceExpression`: linear placement is defined entirely in
5//! terms of distance-along a basis curve plus lateral/vertical/longitudinal
6//! offsets, never a bare Cartesian point. Resolving it therefore means
7//! resolving the distance expression, not a coordinate triple.
8
9use ifc_model::value::Value;
10use ifc_model::{EntityId, Model};
11
12use crate::error::{AlignmentError, AlignmentResult};
13use crate::horizontal::AlignmentUnits;
14
15/// `DistanceAlong` may be an absolute length or a normalized parameter; the
16/// two are never comparable without knowing which was authored.
17#[derive(Debug, Clone, Copy, PartialEq)]
18pub enum CurveMeasure {
19    /// `IfcLengthMeasure`: absolute distance along the basis curve.
20    Length(f64),
21    /// `IfcParameterValue`: dimensionless curve parameter.
22    Parameter(f64),
23}
24
25/// Resolved `IfcPointByDistanceExpression`.
26#[derive(Debug, Clone, PartialEq)]
27pub struct PointByDistance {
28    /// The `IfcPointByDistanceExpression` entity this was read from.
29    pub entity: EntityId,
30    /// `DistanceAlong`: absolute length or normalized parameter, per
31    /// `CurveMeasure`.
32    pub distance_along: CurveMeasure,
33    /// `OffsetLateral`, when authored.
34    pub offset_lateral: Option<f64>,
35    /// `OffsetVertical`, when authored.
36    pub offset_vertical: Option<f64>,
37    /// `OffsetLongitudinal`, when authored.
38    pub offset_longitudinal: Option<f64>,
39    /// `BasisCurve`: the curve `distance_along` is measured against.
40    pub basis_curve: EntityId,
41}
42
43/// Resolved `IfcLinearPlacement`.
44#[derive(Debug, Clone, PartialEq)]
45pub struct LinearPlacement {
46    /// The `IfcLinearPlacement` entity this was read from.
47    pub entity: EntityId,
48    /// `RelativePlacement.Location`: the resolved `IfcPointByDistanceExpression`.
49    pub relative_placement: PointByDistance,
50    /// `IfcAxis2PlacementLinear.Axis`/`RefDirection`, if the source stated an
51    /// explicit orientation rather than deriving one from the basis curve.
52    pub has_explicit_axes: bool,
53}
54
55/// Read one `IfcPointByDistanceExpression` referenced by `id`.
56///
57/// Fails if `id` is missing, is not an `IfcPointByDistanceExpression`, or
58/// any attribute is missing or holds a value of the wrong kind.
59pub fn resolve_point_by_distance(
60    model: &Model,
61    id: EntityId,
62    units: AlignmentUnits,
63) -> AlignmentResult<PointByDistance> {
64    let entity = model
65        .get(id)
66        .ok_or(AlignmentError::MissingEntity { entity: id })?;
67    if !entity.is_type("IFCPOINTBYDISTANCEEXPRESSION") {
68        return Err(AlignmentError::WrongType {
69            entity: id,
70            expected: "IFCPOINTBYDISTANCEEXPRESSION",
71            actual: entity.type_name.to_string(),
72        });
73    }
74    // IFC4X3_ADD2: `IfcPoint` and `IfcGeometricRepresentationItem` and
75    // `IfcRepresentationItem` contribute no attributes ahead of this
76    // declaration's own slots 0..4: DistanceAlong, OffsetLateral,
77    // OffsetVertical, OffsetLongitudinal, BasisCurve.
78    let values = &entity.attributes;
79    let distance_along = curve_measure(values, id, 0, "DistanceAlong", units)?;
80    let offset_lateral = optional_number(values, id, 1, "OffsetLateral")?.map(|v| length(v, units));
81    let offset_vertical =
82        optional_number(values, id, 2, "OffsetVertical")?.map(|v| length(v, units));
83    let offset_longitudinal =
84        optional_number(values, id, 3, "OffsetLongitudinal")?.map(|v| length(v, units));
85    let basis_curve = reference(values, id, 4, "BasisCurve")?;
86    // A distance along a curve that is not in the file is not a position.
87    // Checked here rather than at each call site, so every consumer of a
88    // `PointByDistance` can rely on the target existing.
89    if model.get(basis_curve).is_none() {
90        return Err(AlignmentError::DanglingReference {
91            entity: id,
92            attribute: "BasisCurve",
93            target: basis_curve,
94        });
95    }
96
97    Ok(PointByDistance {
98        entity: id,
99        distance_along,
100        offset_lateral,
101        offset_vertical,
102        offset_longitudinal,
103        basis_curve,
104    })
105}
106
107/// Resolve one `IfcLinearPlacement` referenced by `id` to its distance-along
108/// point on a basis curve.
109///
110/// Fails if `id` is missing or not an `IfcLinearPlacement`, `RelativePlacement`
111/// does not resolve to an `IfcAxis2PlacementLinear` whose `Location` is an
112/// `IfcPointByDistanceExpression` (WR1), or `Axis`/`RefDirection` are supplied
113/// one without the other (WR2).
114pub fn resolve_linear_placement(
115    model: &Model,
116    id: EntityId,
117    units: AlignmentUnits,
118) -> AlignmentResult<LinearPlacement> {
119    let entity = model
120        .get(id)
121        .ok_or(AlignmentError::MissingEntity { entity: id })?;
122    if !entity.is_type("IFCLINEARPLACEMENT") {
123        return Err(AlignmentError::WrongType {
124            entity: id,
125            expected: "IFCLINEARPLACEMENT",
126            actual: entity.type_name.to_string(),
127        });
128    }
129    // IFC4X3_ADD2: inherited PlacementRelTo is slot 0; RelativePlacement and
130    // CartesianPosition are this declaration's own slots 1..2.
131    let values = &entity.attributes;
132    let relative_placement_id = reference(values, id, 1, "RelativePlacement")?;
133    let placement_entity =
134        model
135            .get(relative_placement_id)
136            .ok_or(AlignmentError::DanglingReference {
137                entity: id,
138                attribute: "RelativePlacement",
139                target: relative_placement_id,
140            })?;
141    if !placement_entity.is_type("IFCAXIS2PLACEMENTLINEAR") {
142        return Err(AlignmentError::WrongType {
143            entity: relative_placement_id,
144            expected: "IFCAXIS2PLACEMENTLINEAR",
145            actual: placement_entity.type_name.to_string(),
146        });
147    }
148    // IFC4X3_ADD2: inherited Location is slot 0; Axis/RefDirection are this
149    // declaration's own slots 1..2. WR1 requires Location to be a
150    // IfcPointByDistanceExpression -- enforced here, not assumed.
151    let placement_values = &placement_entity.attributes;
152    let location_id = reference(placement_values, relative_placement_id, 0, "Location")?;
153    let relative_placement = resolve_point_by_distance(model, location_id, units)?;
154    let has_explicit_axes = matches!(placement_values.get(1), Some(Value::Ref(_)))
155        || matches!(placement_values.get(2), Some(Value::Ref(_)));
156    // WR2: Axis and RefDirection are both-or-neither.
157    let axis_present = matches!(placement_values.get(1), Some(Value::Ref(_)));
158    let ref_direction_present = matches!(placement_values.get(2), Some(Value::Ref(_)));
159    if axis_present != ref_direction_present {
160        return Err(AlignmentError::InvalidSegment {
161            entity: relative_placement_id,
162            detail: "IfcAxis2PlacementLinear.Axis and RefDirection must both be present or both absent (WR2)",
163        });
164    }
165
166    Ok(LinearPlacement {
167        entity: id,
168        relative_placement,
169        has_explicit_axes,
170    })
171}
172
173fn curve_measure(
174    values: &[Value],
175    id: EntityId,
176    slot: usize,
177    name: &'static str,
178    units: AlignmentUnits,
179) -> AlignmentResult<CurveMeasure> {
180    // `IfcCurveMeasureSelect = SELECT (IfcLengthMeasure, IfcParameterValue)`.
181    // Both wrap a bare real; the codec preserves which was declared as the
182    // `Typed` wrapper's type name, and losing that distinction here would
183    // make a normalized parameter silently readable as an absolute length.
184    match values.get(slot) {
185        Some(Value::Typed { type_name, value }) => {
186            let raw = value.as_f64().ok_or(AlignmentError::InvalidAttribute {
187                entity: id,
188                index: slot,
189                name,
190            })?;
191            if type_name.eq_ignore_ascii_case("IFCLENGTHMEASURE") {
192                Ok(CurveMeasure::Length(raw * units.length_to_metres))
193            } else if type_name.eq_ignore_ascii_case("IFCPARAMETERVALUE") {
194                Ok(CurveMeasure::Parameter(raw))
195            } else {
196                Err(AlignmentError::InvalidAttribute {
197                    entity: id,
198                    index: slot,
199                    name,
200                })
201            }
202        }
203        Some(Value::Real(value)) => Ok(CurveMeasure::Length(value * units.length_to_metres)),
204        Some(Value::Integer(value)) => {
205            Ok(CurveMeasure::Length(*value as f64 * units.length_to_metres))
206        }
207        _ => Err(AlignmentError::InvalidAttribute {
208            entity: id,
209            index: slot,
210            name,
211        }),
212    }
213}
214
215fn optional_number(
216    values: &[Value],
217    id: EntityId,
218    slot: usize,
219    name: &'static str,
220) -> AlignmentResult<Option<f64>> {
221    match values.get(slot) {
222        Some(Value::Null) => Ok(None),
223        Some(value) => {
224            value
225                .unwrap_typed()
226                .as_f64()
227                .map(Some)
228                .ok_or(AlignmentError::InvalidAttribute {
229                    entity: id,
230                    index: slot,
231                    name,
232                })
233        }
234        None => Err(AlignmentError::InvalidAttribute {
235            entity: id,
236            index: slot,
237            name,
238        }),
239    }
240}
241
242fn reference(
243    values: &[Value],
244    id: EntityId,
245    slot: usize,
246    name: &'static str,
247) -> AlignmentResult<EntityId> {
248    values
249        .get(slot)
250        .and_then(|value| value.as_ref_id())
251        .ok_or(AlignmentError::InvalidAttribute {
252            entity: id,
253            index: slot,
254            name,
255        })
256}
257
258fn length(value: f64, units: AlignmentUnits) -> f64 {
259    value * units.length_to_metres
260}