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