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(
190    dir_id: u32,
191    decoder: &mut EntityDecoder,
192) -> Result<Vector3<f64>> {
193    let dir = decoder.decode_by_id(dir_id)?;
194    parse_direction(&dir)
195}
196
197/// Parse IfcAxis2Placement3D from entity ID (fast-path variant)
198///
199/// Uses fast-path extraction when available for location and directions.
200pub fn parse_axis2_placement_3d_from_id(
201    placement_id: u32,
202    decoder: &mut EntityDecoder,
203) -> Result<Matrix4<f64>> {
204    let placement = decoder.decode_by_id(placement_id)?;
205    
206    // Get location using fast path if available
207    let location = if let Some(loc_attr) = placement.get(0) {
208        if let Some(loc_id) = loc_attr.as_entity_ref() {
209            parse_cartesian_point_from_id(loc_id, decoder)?
210        } else {
211            Point3::new(0.0, 0.0, 0.0)
212        }
213    } else {
214        Point3::new(0.0, 0.0, 0.0)
215    };
216
217    // Get Z axis (attribute 1)
218    let z_axis = if let Some(axis_attr) = placement.get(1) {
219        if !axis_attr.is_null() {
220            if let Some(axis_id) = axis_attr.as_entity_ref() {
221                parse_direction_from_id(axis_id, decoder)?
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    } else {
229        Vector3::new(0.0, 0.0, 1.0)
230    };
231
232    // Get X axis (attribute 2: RefDirection)
233    let x_axis = if let Some(ref_dir_attr) = placement.get(2) {
234        if !ref_dir_attr.is_null() {
235            if let Some(ref_dir_id) = ref_dir_attr.as_entity_ref() {
236                parse_direction_from_id(ref_dir_id, decoder)?
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    } else {
244        Vector3::new(1.0, 0.0, 0.0)
245    };
246
247    // Normalize axes
248    let z_axis_final = z_axis.normalize();
249    let x_axis_normalized = x_axis.normalize();
250
251    // Ensure X is orthogonal to Z (project X onto plane perpendicular to Z)
252    let dot_product = x_axis_normalized.dot(&z_axis_final);
253    let x_axis_orthogonal = x_axis_normalized - z_axis_final * dot_product;
254    let x_axis_final = if x_axis_orthogonal.norm() > 1e-6 {
255        x_axis_orthogonal.normalize()
256    } else {
257        // X and Z are parallel or nearly parallel - use a default perpendicular direction
258        if z_axis_final.z.abs() < 0.9 {
259            Vector3::new(0.0, 0.0, 1.0).cross(&z_axis_final).normalize()
260        } else {
261            Vector3::new(1.0, 0.0, 0.0).cross(&z_axis_final).normalize()
262        }
263    };
264
265    // Y axis is cross product of Z and X (right-hand rule: Y = Z × X)
266    let y_axis = z_axis_final.cross(&x_axis_final).normalize();
267
268    // Build transformation matrix using Matrix4::new constructor (column-major)
269    Ok(Matrix4::new(
270        x_axis_final.x, y_axis.x, z_axis_final.x, location.x,
271        x_axis_final.y, y_axis.y, z_axis_final.y, location.y,
272        x_axis_final.z, y_axis.z, z_axis_final.z, location.z,
273        0.0, 0.0, 0.0, 1.0,
274    ))
275}
276
277/// Parse IfcDirection entity
278///
279/// Extracts direction ratios from IfcDirection (attribute 0).
280pub fn parse_direction(direction_entity: &DecodedEntity) -> Result<Vector3<f64>> {
281    if direction_entity.ifc_type != IfcType::IfcDirection {
282        return Err(Error::geometry(format!(
283            "Expected IfcDirection, got {}",
284            direction_entity.ifc_type
285        )));
286    }
287
288    // Get direction ratios (attribute 0)
289    let ratios_attr = direction_entity
290        .get(0)
291        .ok_or_else(|| Error::geometry("IfcDirection missing ratios".to_string()))?;
292
293    let ratios = ratios_attr
294        .as_list()
295        .ok_or_else(|| Error::geometry("Expected ratio list".to_string()))?;
296
297    let x = ratios.first().and_then(|v| v.as_float()).unwrap_or(0.0);
298    let y = ratios.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
299    let z = ratios.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
300
301    Ok(Vector3::new(x, y, z))
302}
303
304/// Apply RTC offset to mesh vertices
305///
306/// Subtracts the RTC offset from all vertex positions to shift coordinates
307/// from world space to RTC-shifted space.
308pub fn apply_rtc_offset(mesh: &mut Mesh, rtc: (f64, f64, f64)) {
309    let (rtc_x, rtc_y, rtc_z) = rtc;
310    mesh.positions.chunks_exact_mut(3).for_each(|chunk| {
311        chunk[0] = (chunk[0] as f64 - rtc_x) as f32;
312        chunk[1] = (chunk[1] as f64 - rtc_y) as f32;
313        chunk[2] = (chunk[2] as f64 - rtc_z) as f32;
314    });
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    #[test]
322    fn test_parse_direction() {
323        // This would require a mock decoder, so we'll test integration-style
324        // in the processor tests instead
325    }
326}