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