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}