Skip to main content

ifc_lite_geometry/processors/
extrusion.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//! ExtrudedAreaSolid processor - extrusion of 2D profiles.
6
7use crate::{
8    extrusion::{apply_transform, extrude_profile},
9    profiles::ProfileProcessor,
10    Error, Mesh, Result, TessellationQuality, Vector3,
11};
12use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcSchema, IfcType};
13use nalgebra::Matrix4;
14
15use super::helpers::parse_axis2_placement_3d;
16use crate::router::GeometryProcessor;
17use crate::scalar::{magnitude_squared3, GeomScalar};
18
19/// Local (pre-`Position`) transform an `IfcExtrudedAreaSolid` needs for its
20/// `ExtrudedDirection`, generic over the scalar (B4.4).
21///
22/// `direction` is the raw (unnormalised) ratio triple; callers reject the
23/// zero-length case before calling.
24///
25/// ExtrudedDirection is in the LOCAL coordinate system (before Position transform).
26/// We need to determine when to add an extrusion rotation vs. letting Position handle it.
27///
28/// Two key cases:
29/// 1. Opening: local_direction=(0,0,-1), Position rotates local Z to world Y
30///    -> local_direction IS along Z, so no rotation needed; Position handles orientation
31/// 2. Roof slab: local_direction=(0,-0.5,0.866), Position tilts the profile
32///    -> world_direction = Position.rotation * local_direction = (0,0,1) (along world Z!)
33///    -> No extra rotation needed; Position handles the tilt
34#[inline]
35pub(crate) fn extrusion_local_transform<S: GeomScalar>(
36    direction: &Vector3<S>,
37    depth: S,
38) -> Option<Matrix4<S>> {
39    let zero = S::from_f64(0.0);
40    let one = S::from_f64(1.0);
41    // `Vector3::normalize` == `unscale(norm())`.
42    let norm = magnitude_squared3(direction).sqrt();
43    let local_direction = Vector3::new(
44        direction.x / norm,
45        direction.y / norm,
46        direction.z / norm,
47    );
48
49    // Check if local direction is along Z axis
50    // Note: We only check local direction because extrusion happens in LOCAL coordinates
51    // before the Position transform is applied. What the direction becomes in world
52    // space is irrelevant to the extrusion operation.
53    let is_local_z_aligned =
54        local_direction.x.abs().value() < 0.001 && local_direction.y.abs().value() < 0.001;
55
56    if is_local_z_aligned {
57        // Local direction is along Z - no extra rotation needed.
58        // Position transform will handle the correct orientation.
59        // Only need translation if extruding in negative direction.
60        if local_direction.z.value() < 0.0 {
61            // Downward extrusion: shift the extrusion down by depth
62            #[rustfmt::skip]
63            let m = Matrix4::new(
64                one,  zero, zero, zero,
65                zero, one,  zero, zero,
66                zero, zero, one,  -depth,
67                zero, zero, zero, one,
68            );
69            Some(m)
70        } else {
71            None
72        }
73    } else {
74        // Local direction is NOT along Z - use SHEAR matrix (not rotation!)
75        // A shear preserves the profile plane orientation while redirecting extrusion.
76        //
77        // For ExtrudedDirection (dx, dy, dz), the shear matrix is:
78        // | 1    0    dx |
79        // | 0    1    dy |
80        // | 0    0    dz |
81        //
82        // This transforms (x, y, depth) to (x + dx*depth, y + dy*depth, dz*depth)
83        // while keeping (x, y, 0) unchanged.
84        #[rustfmt::skip]
85        let shear_mat = Matrix4::new(
86            one,  zero, local_direction.x, zero,
87            zero, one,  local_direction.y, zero,
88            zero, zero, local_direction.z, zero,
89            zero, zero, zero,              one,
90        );
91        Some(shear_mat)
92    }
93}
94
95/// ExtrudedAreaSolid processor (P0)
96/// Handles IfcExtrudedAreaSolid - extrusion of 2D profiles
97pub struct ExtrudedAreaSolidProcessor {
98    profile_processor: ProfileProcessor,
99}
100
101impl ExtrudedAreaSolidProcessor {
102    /// Create new processor
103    pub fn new(schema: IfcSchema) -> Self {
104        Self {
105            profile_processor: ProfileProcessor::new(schema),
106        }
107    }
108}
109
110impl GeometryProcessor for ExtrudedAreaSolidProcessor {
111    fn process(
112        &self,
113        entity: &DecodedEntity,
114        decoder: &mut EntityDecoder,
115        _schema: &IfcSchema,
116        quality: TessellationQuality,
117    ) -> Result<Mesh> {
118        // IfcExtrudedAreaSolid attributes:
119        // 0: SweptArea (IfcProfileDef)
120        // 1: Position (IfcAxis2Placement3D)
121        // 2: ExtrudedDirection (IfcDirection)
122        // 3: Depth (IfcPositiveLengthMeasure)
123
124        // Get profile
125        let profile_attr = entity
126            .get(0)
127            .ok_or_else(|| Error::geometry("ExtrudedAreaSolid missing SweptArea".to_string()))?;
128
129        let profile_entity = decoder
130            .resolve_ref(profile_attr)?
131            .ok_or_else(|| Error::geometry("Failed to resolve SweptArea".to_string()))?;
132
133        let profile = self
134            .profile_processor
135            .process(&profile_entity, decoder, quality)?;
136
137        if profile.outer.is_empty() {
138            return Ok(Mesh::new());
139        }
140
141        // Get extrusion direction
142        let direction_attr = entity.get(2).ok_or_else(|| {
143            Error::geometry("ExtrudedAreaSolid missing ExtrudedDirection".to_string())
144        })?;
145
146        let direction_entity = decoder
147            .resolve_ref(direction_attr)?
148            .ok_or_else(|| Error::geometry("Failed to resolve ExtrudedDirection".to_string()))?;
149
150        if direction_entity.ifc_type != IfcType::IfcDirection {
151            return Err(Error::geometry(format!(
152                "Expected IfcDirection, got {}",
153                direction_entity.ifc_type
154            )));
155        }
156
157        // Parse direction
158        let ratios_attr = direction_entity
159            .get(0)
160            .ok_or_else(|| Error::geometry("IfcDirection missing ratios".to_string()))?;
161
162        let ratios = ratios_attr
163            .as_list()
164            .ok_or_else(|| Error::geometry("Expected ratio list".to_string()))?;
165
166        use ifc_lite_core::AttributeValue;
167        let dir_x = ratios
168            .first()
169            .and_then(|v: &AttributeValue| v.as_float())
170            .unwrap_or(0.0);
171        let dir_y = ratios
172            .get(1)
173            .and_then(|v: &AttributeValue| v.as_float())
174            .unwrap_or(0.0);
175        let dir_z = ratios
176            .get(2)
177            .and_then(|v: &AttributeValue| v.as_float())
178            .unwrap_or(1.0);
179
180        let direction = Vector3::new(dir_x, dir_y, dir_z);
181        if direction.norm_squared() <= f64::EPSILON {
182            return Err(Error::geometry(
183                "ExtrudedAreaSolid has zero-length ExtrudedDirection".to_string(),
184            ));
185        }
186
187        // Get depth
188        let depth = entity
189            .get_float(3)
190            .ok_or_else(|| Error::geometry("ExtrudedAreaSolid missing Depth".to_string()))?;
191
192        // Parse Position transform first (attribute 1: IfcAxis2Placement3D)
193        // We need Position's rotation to transform ExtrudedDirection to world coordinates
194        let pos_transform = if let Some(pos_attr) = entity.get(1) {
195            if !pos_attr.is_null() {
196                if let Some(pos_entity) = decoder.resolve_ref(pos_attr)? {
197                    if pos_entity.ifc_type == IfcType::IfcAxis2Placement3D {
198                        Some(parse_axis2_placement_3d(&pos_entity, decoder)?)
199                    } else {
200                        None
201                    }
202                } else {
203                    None
204                }
205            } else {
206                None
207            }
208        } else {
209            None
210        };
211
212        let transform = extrusion_local_transform(&direction, depth);
213
214        // Extrude the profile
215        let mut mesh = extrude_profile(&profile, depth, transform)?;
216
217        // Apply Position transform
218        if let Some(pos) = pos_transform {
219            apply_transform(&mut mesh, &pos);
220        }
221
222        Ok(mesh)
223    }
224
225    fn supported_types(&self) -> Vec<IfcType> {
226        vec![IfcType::IfcExtrudedAreaSolid]
227    }
228}