Skip to main content

ifc_lite_geometry/
transform.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//! Shared transform utilities for IFC geometry processing
6//!
7//! Provides unified implementations for parsing IFC placement and direction entities,
8//! eliminating code duplication across processors.
9
10use crate::error::{Error, Result};
11use crate::mesh::Mesh;
12use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcType};
13use nalgebra::{Matrix4, Point3, Vector3};
14
15/// Parse IfcAxis2Placement3D into transformation matrix
16///
17/// IfcAxis2Placement3D attributes:
18/// - 0: Location (IfcCartesianPoint)
19/// - 1: Axis (IfcDirection, optional)
20/// - 2: RefDirection (IfcDirection, optional)
21///
22/// Returns a 4x4 transformation matrix that transforms from local coordinates
23/// to parent coordinates.
24pub fn parse_axis2_placement_3d(
25    placement: &DecodedEntity,
26    decoder: &mut EntityDecoder,
27) -> Result<Matrix4<f64>> {
28    // Get location (attribute 0)
29    let location = parse_cartesian_point(placement, decoder, 0)?;
30
31    // Get Z axis (attribute 1) - defaults to (0, 0, 1)
32    let z_axis = if let Some(axis_attr) = placement.get(1) {
33        if !axis_attr.is_null() {
34            if let Some(axis_entity) = decoder.resolve_ref(axis_attr)? {
35                parse_direction(&axis_entity)?
36            } else {
37                Vector3::new(0.0, 0.0, 1.0)
38            }
39        } else {
40            Vector3::new(0.0, 0.0, 1.0)
41        }
42    } else {
43        Vector3::new(0.0, 0.0, 1.0)
44    };
45
46    // Get X axis (attribute 2: RefDirection) - defaults to (1, 0, 0)
47    let x_axis = if let Some(ref_dir_attr) = placement.get(2) {
48        if !ref_dir_attr.is_null() {
49            if let Some(ref_dir_entity) = decoder.resolve_ref(ref_dir_attr)? {
50                parse_direction(&ref_dir_entity)?
51            } else {
52                Vector3::new(1.0, 0.0, 0.0)
53            }
54        } else {
55            Vector3::new(1.0, 0.0, 0.0)
56        }
57    } else {
58        Vector3::new(1.0, 0.0, 0.0)
59    };
60
61    // Normalize axes
62    let z_axis_final = z_axis.normalize();
63    let x_axis_normalized = x_axis.normalize();
64
65    // Ensure X is orthogonal to Z (project X onto plane perpendicular to Z)
66    let dot_product = x_axis_normalized.dot(&z_axis_final);
67    let x_axis_orthogonal = x_axis_normalized - z_axis_final * dot_product;
68    let x_axis_final = if x_axis_orthogonal.norm() > 1e-6 {
69        x_axis_orthogonal.normalize()
70    } else {
71        // X and Z are parallel or nearly parallel - use a default perpendicular direction
72        if z_axis_final.z.abs() < 0.9 {
73            Vector3::new(0.0, 0.0, 1.0).cross(&z_axis_final).normalize()
74        } else {
75            Vector3::new(1.0, 0.0, 0.0).cross(&z_axis_final).normalize()
76        }
77    };
78
79    // Y axis is cross product of Z and X (right-hand rule: Y = Z × X)
80    let y_axis = z_axis_final.cross(&x_axis_final).normalize();
81
82    // Build transformation matrix
83    // Columns represent world-space directions of local axes
84    let mut transform = Matrix4::identity();
85    transform[(0, 0)] = x_axis_final.x;
86    transform[(1, 0)] = x_axis_final.y;
87    transform[(2, 0)] = x_axis_final.z;
88    transform[(0, 1)] = y_axis.x;
89    transform[(1, 1)] = y_axis.y;
90    transform[(2, 1)] = y_axis.z;
91    transform[(0, 2)] = z_axis_final.x;
92    transform[(1, 2)] = z_axis_final.y;
93    transform[(2, 2)] = z_axis_final.z;
94    transform[(0, 3)] = location.x;
95    transform[(1, 3)] = location.y;
96    transform[(2, 3)] = location.z;
97
98    Ok(transform)
99}
100
101/// Parse IfcCartesianPoint from an entity attribute
102///
103/// Attempts fast-path extraction first, falls back to full decode if needed.
104pub fn parse_cartesian_point(
105    parent: &DecodedEntity,
106    decoder: &mut EntityDecoder,
107    attr_index: usize,
108) -> Result<Point3<f64>> {
109    let point_attr = parent
110        .get(attr_index)
111        .ok_or_else(|| Error::geometry("Missing cartesian point".to_string()))?;
112
113    // Try fast path first
114    if let Some(point_id) = point_attr.as_entity_ref() {
115        if let Some((x, y, z)) = decoder.get_cartesian_point_fast(point_id) {
116            return Ok(Point3::new(x, y, z));
117        }
118    }
119
120    // Fallback to full decode
121    let point_entity = decoder
122        .resolve_ref(point_attr)?
123        .ok_or_else(|| Error::geometry("Failed to resolve cartesian point".to_string()))?;
124
125    if point_entity.ifc_type != IfcType::IfcCartesianPoint {
126        return Err(Error::geometry(format!(
127            "Expected IfcCartesianPoint, got {}",
128            point_entity.ifc_type
129        )));
130    }
131
132    // Get coordinates list (attribute 0)
133    let coords_attr = point_entity
134        .get(0)
135        .ok_or_else(|| Error::geometry("IfcCartesianPoint missing coordinates".to_string()))?;
136
137    let coords = coords_attr
138        .as_list()
139        .ok_or_else(|| Error::geometry("Expected coordinate list".to_string()))?;
140
141    let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
142    let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
143    let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
144
145    Ok(Point3::new(x, y, z))
146}
147
148/// Parse IfcCartesianPoint from entity ID (fast-path variant)
149///
150/// Uses fast-path extraction when available.
151pub fn parse_cartesian_point_from_id(
152    point_id: u32,
153    decoder: &mut EntityDecoder,
154) -> Result<Point3<f64>> {
155    // Try fast path first
156    if let Some((x, y, z)) = decoder.get_cartesian_point_fast(point_id) {
157        return Ok(Point3::new(x, y, z));
158    }
159
160    // Fallback to full decode
161    let point_entity = decoder.decode_by_id(point_id)?;
162
163    if point_entity.ifc_type != IfcType::IfcCartesianPoint {
164        return Err(Error::geometry(format!(
165            "Expected IfcCartesianPoint, got {}",
166            point_entity.ifc_type
167        )));
168    }
169
170    // Get coordinates list (attribute 0)
171    let coords_attr = point_entity
172        .get(0)
173        .ok_or_else(|| Error::geometry("IfcCartesianPoint missing coordinates".to_string()))?;
174
175    let coords = coords_attr
176        .as_list()
177        .ok_or_else(|| Error::geometry("Expected coordinate list".to_string()))?;
178
179    let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
180    let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
181    let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
182
183    Ok(Point3::new(x, y, z))
184}
185
186/// Parse IfcDirection from entity ID (fast-path variant)
187///
188/// Uses fast-path extraction when available.
189pub fn parse_direction_from_id(dir_id: u32, decoder: &mut EntityDecoder) -> Result<Vector3<f64>> {
190    let dir = decoder.decode_by_id(dir_id)?;
191    parse_direction(&dir)
192}
193
194/// Parse IfcAxis2Placement3D from entity ID (fast-path variant)
195///
196/// Uses fast-path extraction when available for location and directions.
197pub fn parse_axis2_placement_3d_from_id(
198    placement_id: u32,
199    decoder: &mut EntityDecoder,
200) -> Result<Matrix4<f64>> {
201    let placement = decoder.decode_by_id(placement_id)?;
202
203    // Get location using fast path if available
204    let location = if let Some(loc_attr) = placement.get(0) {
205        if let Some(loc_id) = loc_attr.as_entity_ref() {
206            parse_cartesian_point_from_id(loc_id, decoder)?
207        } else {
208            Point3::new(0.0, 0.0, 0.0)
209        }
210    } else {
211        Point3::new(0.0, 0.0, 0.0)
212    };
213
214    // Get Z axis (attribute 1)
215    let z_axis = if let Some(axis_attr) = placement.get(1) {
216        if !axis_attr.is_null() {
217            if let Some(axis_id) = axis_attr.as_entity_ref() {
218                parse_direction_from_id(axis_id, decoder)?
219            } else {
220                Vector3::new(0.0, 0.0, 1.0)
221            }
222        } else {
223            Vector3::new(0.0, 0.0, 1.0)
224        }
225    } else {
226        Vector3::new(0.0, 0.0, 1.0)
227    };
228
229    // Get X axis (attribute 2: RefDirection)
230    let x_axis = if let Some(ref_dir_attr) = placement.get(2) {
231        if !ref_dir_attr.is_null() {
232            if let Some(ref_dir_id) = ref_dir_attr.as_entity_ref() {
233                parse_direction_from_id(ref_dir_id, decoder)?
234            } else {
235                Vector3::new(1.0, 0.0, 0.0)
236            }
237        } else {
238            Vector3::new(1.0, 0.0, 0.0)
239        }
240    } else {
241        Vector3::new(1.0, 0.0, 0.0)
242    };
243
244    // Normalize axes
245    let z_axis_final = z_axis.normalize();
246    let x_axis_normalized = x_axis.normalize();
247
248    // Ensure X is orthogonal to Z (project X onto plane perpendicular to Z)
249    let dot_product = x_axis_normalized.dot(&z_axis_final);
250    let x_axis_orthogonal = x_axis_normalized - z_axis_final * dot_product;
251    let x_axis_final = if x_axis_orthogonal.norm() > 1e-6 {
252        x_axis_orthogonal.normalize()
253    } else {
254        // X and Z are parallel or nearly parallel - use a default perpendicular direction
255        if z_axis_final.z.abs() < 0.9 {
256            Vector3::new(0.0, 0.0, 1.0).cross(&z_axis_final).normalize()
257        } else {
258            Vector3::new(1.0, 0.0, 0.0).cross(&z_axis_final).normalize()
259        }
260    };
261
262    // Y axis is cross product of Z and X (right-hand rule: Y = Z × X)
263    let y_axis = z_axis_final.cross(&x_axis_final).normalize();
264
265    // Build transformation matrix using Matrix4::new constructor (column-major)
266    Ok(Matrix4::new(
267        x_axis_final.x,
268        y_axis.x,
269        z_axis_final.x,
270        location.x,
271        x_axis_final.y,
272        y_axis.y,
273        z_axis_final.y,
274        location.y,
275        x_axis_final.z,
276        y_axis.z,
277        z_axis_final.z,
278        location.z,
279        0.0,
280        0.0,
281        0.0,
282        1.0,
283    ))
284}
285
286/// Parse IfcDirection entity
287///
288/// Extracts direction ratios from IfcDirection (attribute 0).
289pub fn parse_direction(direction_entity: &DecodedEntity) -> Result<Vector3<f64>> {
290    if direction_entity.ifc_type != IfcType::IfcDirection {
291        return Err(Error::geometry(format!(
292            "Expected IfcDirection, got {}",
293            direction_entity.ifc_type
294        )));
295    }
296
297    // Get direction ratios (attribute 0)
298    let ratios_attr = direction_entity
299        .get(0)
300        .ok_or_else(|| Error::geometry("IfcDirection missing ratios".to_string()))?;
301
302    let ratios = ratios_attr
303        .as_list()
304        .ok_or_else(|| Error::geometry("Expected ratio list".to_string()))?;
305
306    let x = ratios.first().and_then(|v| v.as_float()).unwrap_or(0.0);
307    let y = ratios.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
308    let z = ratios.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
309
310    Ok(Vector3::new(x, y, z))
311}
312
313/// Apply RTC offset to mesh vertices
314///
315/// Subtracts the RTC offset from all vertex positions to shift coordinates
316/// from world space to RTC-shifted space.
317pub fn apply_rtc_offset(mesh: &mut Mesh, rtc: (f64, f64, f64)) {
318    let (rtc_x, rtc_y, rtc_z) = rtc;
319    mesh.positions.chunks_exact_mut(3).for_each(|chunk| {
320        chunk[0] = (chunk[0] as f64 - rtc_x) as f32;
321        chunk[1] = (chunk[1] as f64 - rtc_y) as f32;
322        chunk[2] = (chunk[2] as f64 - rtc_z) as f32;
323    });
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    #[test]
331    fn test_parse_direction() {
332        // This would require a mock decoder, so we'll test integration-style
333        // in the processor tests instead
334    }
335}