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