Skip to main content

ifc_lite_geometry/processors/
swept.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//! Swept geometry processors - SweptDiskSolid and RevolvedAreaSolid.
6
7use crate::{profiles::ProfileProcessor, Error, Mesh, Point3, Result, Vector3};
8use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcSchema, IfcType};
9
10use crate::router::GeometryProcessor;
11
12/// SweptDiskSolid processor
13/// Handles IfcSweptDiskSolid - sweeps a circular profile along a curve
14pub struct SweptDiskSolidProcessor {
15    profile_processor: ProfileProcessor,
16}
17
18impl SweptDiskSolidProcessor {
19    pub fn new(schema: IfcSchema) -> Self {
20        Self {
21            profile_processor: ProfileProcessor::new(schema),
22        }
23    }
24}
25
26impl GeometryProcessor for SweptDiskSolidProcessor {
27    fn process(
28        &self,
29        entity: &DecodedEntity,
30        decoder: &mut EntityDecoder,
31        _schema: &IfcSchema,
32    ) -> Result<Mesh> {
33        // IfcSweptDiskSolid attributes:
34        // 0: Directrix (IfcCurve) - the path to sweep along
35        // 1: Radius (IfcPositiveLengthMeasure) - outer radius
36        // 2: InnerRadius (optional) - inner radius for hollow tubes
37        // 3: StartParam (optional)
38        // 4: EndParam (optional)
39
40        let directrix_attr = entity
41            .get(0)
42            .ok_or_else(|| Error::geometry("SweptDiskSolid missing Directrix".to_string()))?;
43
44        let radius = entity
45            .get_float(1)
46            .ok_or_else(|| Error::geometry("SweptDiskSolid missing Radius".to_string()))?;
47
48        // Get inner radius if hollow
49        let _inner_radius = entity.get_float(2);
50
51        // Resolve the directrix curve
52        let directrix = decoder
53            .resolve_ref(directrix_attr)?
54            .ok_or_else(|| Error::geometry("Failed to resolve Directrix".to_string()))?;
55
56        // Get points along the curve
57        let curve_points = self
58            .profile_processor
59            .get_curve_points(&directrix, decoder)?;
60
61        if curve_points.len() < 2 {
62            return Ok(Mesh::new()); // Not enough points
63        }
64
65        // Generate tube mesh by sweeping circle along curve
66        let segments = 24; // Number of segments around the circle
67        let mut positions = Vec::new();
68        let mut indices = Vec::new();
69
70        // For each point on the curve, create a ring of vertices
71        for i in 0..curve_points.len() {
72            let p = curve_points[i];
73
74            // Calculate tangent direction
75            let tangent = if i == 0 {
76                (curve_points[1] - curve_points[0]).normalize()
77            } else if i == curve_points.len() - 1 {
78                (curve_points[i] - curve_points[i - 1]).normalize()
79            } else {
80                ((curve_points[i + 1] - curve_points[i - 1]) / 2.0).normalize()
81            };
82
83            // Create perpendicular vectors using cross product
84            // First, find a vector not parallel to tangent
85            let up = if tangent.x.abs() < 0.9 {
86                Vector3::new(1.0, 0.0, 0.0)
87            } else {
88                Vector3::new(0.0, 1.0, 0.0)
89            };
90
91            let perp1 = tangent.cross(&up).normalize();
92            let perp2 = tangent.cross(&perp1).normalize();
93
94            // Create ring of vertices
95            for j in 0..segments {
96                let angle = 2.0 * std::f64::consts::PI * j as f64 / segments as f64;
97                let offset = perp1 * (radius * angle.cos()) + perp2 * (radius * angle.sin());
98                let vertex = p + offset;
99
100                positions.push(vertex.x as f32);
101                positions.push(vertex.y as f32);
102                positions.push(vertex.z as f32);
103            }
104
105            // Create triangles connecting this ring to the next
106            if i < curve_points.len() - 1 {
107                let base = (i * segments) as u32;
108                let next_base = ((i + 1) * segments) as u32;
109
110                for j in 0..segments {
111                    let j_next = (j + 1) % segments;
112
113                    // Two triangles per quad
114                    indices.push(base + j as u32);
115                    indices.push(next_base + j as u32);
116                    indices.push(next_base + j_next as u32);
117
118                    indices.push(base + j as u32);
119                    indices.push(next_base + j_next as u32);
120                    indices.push(base + j_next as u32);
121                }
122            }
123        }
124
125        // Add end caps
126        // Start cap
127        let center_idx = (positions.len() / 3) as u32;
128        let start = curve_points[0];
129        positions.push(start.x as f32);
130        positions.push(start.y as f32);
131        positions.push(start.z as f32);
132
133        for j in 0..segments {
134            let j_next = (j + 1) % segments;
135            indices.push(center_idx);
136            indices.push(j_next as u32);
137            indices.push(j as u32);
138        }
139
140        // End cap
141        let end_center_idx = (positions.len() / 3) as u32;
142        let end_base = ((curve_points.len() - 1) * segments) as u32;
143        let end = curve_points[curve_points.len() - 1];
144        positions.push(end.x as f32);
145        positions.push(end.y as f32);
146        positions.push(end.z as f32);
147
148        for j in 0..segments {
149            let j_next = (j + 1) % segments;
150            indices.push(end_center_idx);
151            indices.push(end_base + j as u32);
152            indices.push(end_base + j_next as u32);
153        }
154
155        Ok(Mesh {
156            positions,
157            normals: Vec::new(),
158            indices,
159            rtc_applied: false,
160        })
161    }
162
163    fn supported_types(&self) -> Vec<IfcType> {
164        vec![IfcType::IfcSweptDiskSolid]
165    }
166}
167
168impl Default for SweptDiskSolidProcessor {
169    fn default() -> Self {
170        Self::new(IfcSchema::new())
171    }
172}
173
174/// RevolvedAreaSolid processor
175/// Handles IfcRevolvedAreaSolid - rotates a 2D profile around an axis
176pub struct RevolvedAreaSolidProcessor {
177    profile_processor: ProfileProcessor,
178}
179
180impl RevolvedAreaSolidProcessor {
181    pub fn new(schema: IfcSchema) -> Self {
182        Self {
183            profile_processor: ProfileProcessor::new(schema),
184        }
185    }
186}
187
188impl GeometryProcessor for RevolvedAreaSolidProcessor {
189    fn process(
190        &self,
191        entity: &DecodedEntity,
192        decoder: &mut EntityDecoder,
193        _schema: &IfcSchema,
194    ) -> Result<Mesh> {
195        // IfcRevolvedAreaSolid attributes:
196        // 0: SweptArea (IfcProfileDef) - the 2D profile to revolve
197        // 1: Position (IfcAxis2Placement3D) - placement of the solid
198        // 2: Axis (IfcAxis1Placement) - the axis of revolution
199        // 3: Angle (IfcPlaneAngleMeasure) - revolution angle in radians
200
201        let profile_attr = entity
202            .get(0)
203            .ok_or_else(|| Error::geometry("RevolvedAreaSolid missing SweptArea".to_string()))?;
204
205        let profile = decoder
206            .resolve_ref(profile_attr)?
207            .ok_or_else(|| Error::geometry("Failed to resolve SweptArea".to_string()))?;
208
209        // Get axis placement (attribute 2)
210        let axis_attr = entity
211            .get(2)
212            .ok_or_else(|| Error::geometry("RevolvedAreaSolid missing Axis".to_string()))?;
213
214        let axis_placement = decoder
215            .resolve_ref(axis_attr)?
216            .ok_or_else(|| Error::geometry("Failed to resolve Axis".to_string()))?;
217
218        // Get angle (attribute 3)
219        let angle = entity
220            .get_float(3)
221            .ok_or_else(|| Error::geometry("RevolvedAreaSolid missing Angle".to_string()))?;
222
223        // Get the 2D profile points
224        let profile_2d = self.profile_processor.process(&profile, decoder)?;
225        if profile_2d.outer.is_empty() {
226            return Ok(Mesh::new());
227        }
228
229        // Parse axis placement to get axis point and direction
230        // IfcAxis1Placement: Location, Axis (optional)
231        let axis_location = {
232            let loc_attr = axis_placement
233                .get(0)
234                .ok_or_else(|| Error::geometry("Axis1Placement missing Location".to_string()))?;
235            let loc = decoder
236                .resolve_ref(loc_attr)?
237                .ok_or_else(|| Error::geometry("Failed to resolve axis location".to_string()))?;
238            let coords = loc
239                .get(0)
240                .and_then(|v| v.as_list())
241                .ok_or_else(|| Error::geometry("Axis location missing coordinates".to_string()))?;
242            Point3::new(
243                coords.first().and_then(|v| v.as_float()).unwrap_or(0.0),
244                coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0),
245                coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0),
246            )
247        };
248
249        let axis_direction = {
250            if let Some(dir_attr) = axis_placement.get(1) {
251                if !dir_attr.is_null() {
252                    let dir = decoder.resolve_ref(dir_attr)?.ok_or_else(|| {
253                        Error::geometry("Failed to resolve axis direction".to_string())
254                    })?;
255                    let coords = dir.get(0).and_then(|v| v.as_list()).ok_or_else(|| {
256                        Error::geometry("Axis direction missing coordinates".to_string())
257                    })?;
258                    Vector3::new(
259                        coords.first().and_then(|v| v.as_float()).unwrap_or(0.0),
260                        coords.get(1).and_then(|v| v.as_float()).unwrap_or(1.0),
261                        coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0),
262                    )
263                    .normalize()
264                } else {
265                    Vector3::new(0.0, 1.0, 0.0) // Default Y axis
266                }
267            } else {
268                Vector3::new(0.0, 1.0, 0.0) // Default Y axis
269            }
270        };
271
272        // Generate revolved mesh
273        // Number of segments depends on angle
274        let full_circle = angle.abs() >= std::f64::consts::PI * 1.99;
275        let segments = if full_circle {
276            24 // Full revolution
277        } else {
278            ((angle.abs() / std::f64::consts::PI * 12.0).ceil() as usize).max(4)
279        };
280
281        let profile_points = &profile_2d.outer;
282        let num_profile_points = profile_points.len();
283
284        let mut positions = Vec::new();
285        let mut indices = Vec::new();
286
287        // For each segment around the revolution
288        for i in 0..=segments {
289            let t = if full_circle && i == segments {
290                0.0 // Close the loop exactly
291            } else {
292                angle * i as f64 / segments as f64
293            };
294
295            // Rotation matrix around axis
296            let cos_t = t.cos();
297            let sin_t = t.sin();
298            let (ax, ay, az) = (axis_direction.x, axis_direction.y, axis_direction.z);
299
300            // Rodrigues' rotation formula components
301            let k_matrix = |v: Vector3<f64>| -> Vector3<f64> {
302                Vector3::new(
303                    ay * v.z - az * v.y,
304                    az * v.x - ax * v.z,
305                    ax * v.y - ay * v.x,
306                )
307            };
308
309            // For each point in the profile
310            for (j, p2d) in profile_points.iter().enumerate() {
311                // Profile point in 3D (assume profile is in XY plane, rotated around Y axis)
312                // The 2D profile X becomes distance from axis, Y becomes height along axis
313                let radius = p2d.x;
314                let height = p2d.y;
315
316                // Initial position before rotation (in the plane containing the axis)
317                let v = Vector3::new(radius, 0.0, 0.0);
318
319                // Rodrigues' rotation: v_rot = v*cos(t) + (k x v)*sin(t) + k*(k.v)*(1-cos(t))
320                let k_cross_v = k_matrix(v);
321                let k_dot_v = ax * v.x + ay * v.y + az * v.z;
322
323                let v_rot =
324                    v * cos_t + k_cross_v * sin_t + axis_direction * k_dot_v * (1.0 - cos_t);
325
326                // Final position = axis_location + height along axis + rotated radius
327                let pos = axis_location + axis_direction * height + v_rot;
328
329                positions.push(pos.x as f32);
330                positions.push(pos.y as f32);
331                positions.push(pos.z as f32);
332
333                // Create triangles (except for the last segment if it connects back)
334                if i < segments && j < num_profile_points - 1 {
335                    let current = (i * num_profile_points + j) as u32;
336                    let next_seg = ((i + 1) * num_profile_points + j) as u32;
337                    let current_next = current + 1;
338                    let next_seg_next = next_seg + 1;
339
340                    // Two triangles per quad
341                    indices.push(current);
342                    indices.push(next_seg);
343                    indices.push(next_seg_next);
344
345                    indices.push(current);
346                    indices.push(next_seg_next);
347                    indices.push(current_next);
348                }
349            }
350        }
351
352        // Add end caps if not a full revolution
353        if !full_circle {
354            // Start cap
355            let start_center_idx = (positions.len() / 3) as u32;
356            let start_center = axis_location
357                + axis_direction
358                    * (profile_points.iter().map(|p| p.y).sum::<f64>()
359                        / profile_points.len() as f64);
360            positions.push(start_center.x as f32);
361            positions.push(start_center.y as f32);
362            positions.push(start_center.z as f32);
363
364            for j in 0..num_profile_points - 1 {
365                indices.push(start_center_idx);
366                indices.push(j as u32 + 1);
367                indices.push(j as u32);
368            }
369
370            // End cap
371            let end_center_idx = (positions.len() / 3) as u32;
372            let end_base = (segments * num_profile_points) as u32;
373            positions.push(start_center.x as f32);
374            positions.push(start_center.y as f32);
375            positions.push(start_center.z as f32);
376
377            for j in 0..num_profile_points - 1 {
378                indices.push(end_center_idx);
379                indices.push(end_base + j as u32);
380                indices.push(end_base + j as u32 + 1);
381            }
382        }
383
384        Ok(Mesh {
385            positions,
386            normals: Vec::new(),
387            indices,
388            rtc_applied: false,
389        })
390    }
391
392    fn supported_types(&self) -> Vec<IfcType> {
393        vec![IfcType::IfcRevolvedAreaSolid]
394    }
395}
396
397impl Default for RevolvedAreaSolidProcessor {
398    fn default() -> Self {
399        Self::new(IfcSchema::new())
400    }
401}