ifc_lite_geometry/router/transforms/
mapped.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//! `IfcMappedItem` placement: the map's `MappingOrigin` composed under the
6//! item's `MappingTarget`.
7
8use super::super::GeometryRouter;
9use crate::{Error, Result};
10use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcType};
11use nalgebra::Matrix4;
12
13impl GeometryRouter {
14    /// Resolve an `IfcMappedItem` placement in metre coordinates. The returned
15    /// column-major matrix retains the authored linear part; only translation
16    /// receives the IFC length-unit scale.
17    pub fn resolve_scaled_mapped_item_transform(
18        &self,
19        item: &DecodedEntity,
20        source: &DecodedEntity,
21        decoder: &mut EntityDecoder,
22    ) -> Result<Option<[f64; 16]>> {
23        self.resolve_scaled_mapped_item_transform_with_origin_loader(item, decoder,
24            |decoder| self.mapping_origin_transform(source, decoder))
25    }
26
27    /// Resolve a mapped target with a caller-supplied source-origin loader.
28    /// The loader runs after target parsing, preserving the standard error
29    /// precedence, and may reuse a parsed origin for repeated source maps.
30    pub fn resolve_scaled_mapped_item_transform_with_origin_loader<F>(
31        &self,
32        item: &DecodedEntity,
33        decoder: &mut EntityDecoder,
34        origin: F,
35    ) -> Result<Option<[f64; 16]>>
36    where F: FnOnce(&mut EntityDecoder) -> Result<Option<Matrix4<f64>>> {
37        let target = self.mapping_target_transform(item, decoder)?;
38        let origin = origin(decoder)?;
39        let Some(mut matrix) = Self::compose_mapped_transform(target, origin) else {
40            return Ok(None);
41        };
42        self.scale_transform(&mut matrix);
43        Ok(Some(std::array::from_fn(|i| matrix.as_slice()[i])))
44    }
45
46    /// The full `IfcMappedItem` transform: `MappingTarget · MappingOrigin`.
47    ///
48    /// `item` is the `IfcMappedItem` (attr 1 = MappingTarget), `source` its
49    /// `IfcRepresentationMap` (attr 0 = MappingOrigin, an `IfcAxis2Placement`).
50    /// The mapped representation's items are authored in the mapping source's
51    /// coordinate system, whose placement within the map IS `MappingOrigin`, so
52    /// it applies INNERMOST — the same composition IfcOpenShell performs
53    /// (`gtrsf(MappingTarget).Multiply(trsf(MappingOrigin))`).
54    ///
55    /// Before #1985 `MappingOrigin` was dropped everywhere, so any map whose
56    /// origin was not the identity placed its geometry at the wrong spot
57    /// (scaled by the target, so a Scale=1000 target multiplied the error
58    /// 1000-fold). Returns `None` when neither attribute contributes, keeping
59    /// the "no transform to apply" fast path for the overwhelmingly common
60    /// identity-origin file.
61    pub(crate) fn mapped_item_transform(
62        &self,
63        item: &DecodedEntity,
64        source: &DecodedEntity,
65        decoder: &mut EntityDecoder,
66    ) -> Result<Option<Matrix4<f64>>> {
67        let target = self.mapping_target_transform(item, decoder)?;
68        let origin = self.mapping_origin_transform(source, decoder)?;
69        Ok(Self::compose_mapped_transform(target, origin))
70    }
71
72    fn mapping_target_transform(
73        &self, item: &DecodedEntity, decoder: &mut EntityDecoder,
74    ) -> Result<Option<Matrix4<f64>>> {
75        match item.get(1) {
76            Some(attr) if !attr.is_null() => match decoder.resolve_ref(attr)? {
77                Some(entity) => Ok(Some(self.parse_cartesian_transformation_operator(&entity, decoder)?)),
78                None => Ok(None),
79            },
80            _ => Ok(None),
81        }
82    }
83
84    fn compose_mapped_transform(
85        target: Option<Matrix4<f64>>, origin: Option<Matrix4<f64>>,
86    ) -> Option<Matrix4<f64>> {
87        match (target, origin) {
88            (Some(t), Some(o)) => Some(t * o),
89            (Some(t), None) => Some(t),
90            (None, Some(o)) => Some(o),
91            (None, None) => None,
92        }
93    }
94
95    /// `IfcRepresentationMap.MappingOrigin` (attr 0) as a 4x4, or `None` when it
96    /// is absent or null (nothing to compose), or the identity.
97    ///
98    /// A PRESENT origin that cannot be turned into a placement — a dangling
99    /// reference, or an entity that is not an `IfcAxis2Placement` — is an ERROR,
100    /// not a silent identity. Every other consumer of a broken mapped-item
101    /// transform now behaves that way: the `MappingTarget` operator parse has
102    /// always propagated (a non-`IfcDirection` axis errors), the void fast-path
103    /// probes defer the opening to the exact kernel, and the 2D drawing extractor
104    /// abandons the profile. Substituting the identity here would render the item
105    /// at a position nothing else agrees with. #1985
106    pub fn mapping_origin_transform(
107        &self,
108        source: &DecodedEntity,
109        decoder: &mut EntityDecoder,
110    ) -> Result<Option<Matrix4<f64>>> {
111        let Some(origin_attr) = source.get(0).filter(|a| !a.is_null()) else {
112            return Ok(None);
113        };
114        let Some(origin) = decoder.resolve_ref(origin_attr)? else {
115            return Err(Error::geometry(
116                "RepresentationMap MappingOrigin does not resolve".to_string(),
117            ));
118        };
119        let m = match origin.ifc_type {
120            IfcType::IfcAxis2Placement3D => self.parse_axis2_placement_3d(&origin, decoder)?,
121            // A 2D mapping origin places a 2D representation in the XY plane.
122            IfcType::IfcAxis2Placement2D => self.parse_axis2_placement_2d(&origin, decoder)?,
123            other => {
124                return Err(Error::geometry(format!(
125                    "RepresentationMap MappingOrigin is {other}, not an IfcAxis2Placement"
126                )))
127            }
128        };
129        Ok(if m.is_identity(1e-12) { None } else { Some(m) })
130    }
131
132    /// Parse IfcAxis2Placement2D (Location, RefDirection) into a 4x4 acting in
133    /// the XY plane (Z untouched).
134    #[inline]
135    pub(crate) fn parse_axis2_placement_2d(
136        &self,
137        placement: &DecodedEntity,
138        decoder: &mut EntityDecoder,
139    ) -> Result<Matrix4<f64>> {
140        crate::transform::parse_axis2_placement_2d(placement, decoder)
141    }
142}
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn issue_5786_cached_origin_uses_standard_mapped_composition() {
149        let content = b"#1=IFCCARTESIANPOINT((10.,20.,0.));\n\
150            #2=IFCAXIS2PLACEMENT3D(#1,$,$);\n\
151            #3=IFCREPRESENTATIONMAP(#2,#9);\n\
152            #4=IFCCARTESIANPOINT((100.,0.,0.));\n\
153            #5=IFCCARTESIANTRANSFORMATIONOPERATOR3D($,$,#4,2.,$);\n\
154            #6=IFCMAPPEDITEM(#3,#5);";
155        let router = GeometryRouter::with_scale(0.001);
156        let mut decoder = EntityDecoder::new(content);
157        let item = decoder.decode_by_id(6).unwrap();
158        let source = decoder.decode_by_id(3).unwrap();
159        let standard = router.resolve_scaled_mapped_item_transform(&item, &source, &mut decoder).unwrap();
160        let origin = router.mapping_origin_transform(&source, &mut decoder).unwrap();
161        let cached = router.resolve_scaled_mapped_item_transform_with_origin_loader(
162            &item, &mut decoder, |_| Ok(origin)).unwrap();
163        // Target(100, 0) · Scale(2) · Origin(10, 20) = (120, 40) mm.
164        // Pin both public routes to that authored result independently.
165        for transform in [standard, cached] {
166            let matrix = transform.unwrap();
167            assert_eq!(matrix[0], 2.0);
168            assert_eq!(matrix[5], 2.0);
169            assert_eq!(matrix[10], 2.0);
170            assert!((matrix[12] - 0.12).abs() < 1e-12);
171            assert!((matrix[13] - 0.04).abs() < 1e-12);
172        }
173    }
174}