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