ifc_lite_geometry/alignment_axis.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! The one answer to "which curve is this `IfcAlignment`'s directrix?",
6//! shared by the alignment processor (`processors/alignment.rs`) and the
7//! wasm centerline overlay (`wasm-bindings/src/api/alignment_lines.rs`).
8//! Two private copies of this lookup used to exist and could silently
9//! disagree about what an alignment's axis is (#5327).
10
11use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcType};
12
13/// Resolve an `IfcAlignment`'s directrix curve.
14///
15/// IFC4X1 puts `Axis` at attribute 7; some publishers reuse `Representation`
16/// (6) or hang it at 8. Accept the first ref that resolves to an
17/// `IfcAlignmentCurve` or `IfcPolyline` (the curves `AlignmentCurve::parse`
18/// understands). A ref that fails to resolve is skipped, not fatal.
19/// Otherwise IFC4x3 `Representation` (attr 6) supplies the `'Axis'` shape
20/// representation's curve (`IfcGradientCurve`; an
21/// `IfcSegmentedReferenceCurve` contributes its gradient `BaseCurve`), or
22/// the `'FootPrint'` 2D curve. `None` when neither path yields a curve.
23pub fn locate_axis_curve(entity: &DecodedEntity, decoder: &mut EntityDecoder) -> Option<DecodedEntity> {
24 let alignment_curve = IfcType::from_str("IFCALIGNMENTCURVE");
25 for idx in [7usize, 8, 6] {
26 let Some(attr) = entity.get(idx) else { continue };
27 if attr.is_null() {
28 continue;
29 }
30 if let Ok(Some(resolved)) = decoder.resolve_ref(attr) {
31 if resolved.ifc_type == alignment_curve || resolved.ifc_type == IfcType::IfcPolyline {
32 return Some(resolved);
33 }
34 }
35 }
36 representation_axis_curve(entity, decoder)
37}
38
39/// IFC4x3 path of [`locate_axis_curve`]: prefer the `'Axis'` representation
40/// (3D, carries the vertical profile) over `'FootPrint'` (plan only).
41fn representation_axis_curve(entity: &DecodedEntity, decoder: &mut EntityDecoder) -> Option<DecodedEntity> {
42 let shape = decoder.decode_by_id(entity.get_ref(6)?).ok()?;
43 if shape.ifc_type != IfcType::IfcProductDefinitionShape {
44 return None;
45 }
46 let mut footprint = None;
47 for rep_id in shape.get_refs(2)? {
48 let Ok(rep) = decoder.decode_by_id(rep_id) else { continue };
49 let identifier = rep.get(1).and_then(|v| v.as_string()).unwrap_or("").to_ascii_uppercase();
50 for item_id in rep.get_refs(3).unwrap_or_default() {
51 let Ok(item) = decoder.decode_by_id(item_id) else { continue };
52 let curve = if item.ifc_type == IfcType::IfcSegmentedReferenceCurve {
53 // IfcSegmentedReferenceCurve: 0 Segments, 1 SelfIntersect, 2 BaseCurve
54 match item.get_ref(2).and_then(|id| decoder.decode_by_id(id).ok()) {
55 Some(base) => base,
56 None => continue,
57 }
58 } else {
59 item
60 };
61 let usable = matches!(
62 curve.ifc_type,
63 IfcType::IfcGradientCurve | IfcType::IfcPolyline | IfcType::IfcCompositeCurve
64 );
65 if !usable {
66 continue;
67 }
68 if identifier == "AXIS" {
69 return Some(curve);
70 }
71 if identifier == "FOOTPRINT" && footprint.is_none() {
72 footprint = Some(curve);
73 }
74 }
75 }
76 footprint
77}