Skip to main content

ifc_lite_geometry/router/
voids_2d.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//! 2D void subtraction: profile-level opening processing for extrusions.
6
7use super::GeometryRouter;
8use crate::bool2d::subtract_multiple_2d;
9use crate::csg::ClippingProcessor;
10use crate::profile::{Profile2D, Profile2DWithVoids, VoidInfo};
11use crate::void_analysis::{extract_coplanar_voids, extract_nonplanar_voids, VoidAnalyzer};
12use crate::void_index::VoidIndex;
13use crate::{Error, Mesh, Result, Vector3};
14use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcType};
15use nalgebra::{Matrix4, Point2};
16use rustc_hash::FxHashMap;
17
18impl GeometryRouter {
19    /// Process element with voids using 2D profile-level operations
20    ///
21    /// This is a smarter and more efficient approach that:
22    /// 1. Classifies voids as coplanar (can subtract in 2D) or non-planar (need 3D CSG)
23    /// 2. Subtracts coplanar voids at the 2D profile level before extrusion
24    /// 3. Falls back to 3D CSG only for non-planar voids
25    ///
26    /// Benefits:
27    /// - 10-25x faster than full 3D CSG for most openings
28    /// - More reliable, especially for floors/slabs with many penetrations
29    /// - Cleaner geometry with fewer degenerate triangles
30    #[inline]
31    pub fn process_element_with_voids_2d(
32        &self,
33        element: &DecodedEntity,
34        decoder: &mut EntityDecoder,
35        void_index: &VoidIndex,
36    ) -> Result<Mesh> {
37        // Check if this element has any openings
38        let opening_ids = void_index.get_voids(element.id);
39
40        if opening_ids.is_empty() {
41            // No openings, just process normally
42            return self.process_element(element, decoder);
43        }
44
45        // Try to extract extrusion parameters for 2D void processing
46        // If the element isn't an extrusion, fall back to 3D CSG
47        match self.try_process_extrusion_with_voids_2d(element, decoder, opening_ids) {
48            Ok(Some(mesh)) => Ok(mesh),
49            Ok(None) | Err(_) => {
50                // Fall back to traditional 3D CSG approach
51                let void_map: FxHashMap<u32, Vec<u32>> = [(element.id, opening_ids.to_vec())]
52                    .into_iter()
53                    .collect();
54                self.process_element_with_voids(element, decoder, &void_map)
55            }
56        }
57    }
58
59    /// Try to process an extrusion with 2D void subtraction
60    ///
61    /// Returns Ok(Some(mesh)) if 2D processing was successful,
62    /// Ok(None) if the element is not suitable for 2D processing,
63    /// Err if an error occurred.
64    fn try_process_extrusion_with_voids_2d(
65        &self,
66        element: &DecodedEntity,
67        decoder: &mut EntityDecoder,
68        opening_ids: &[u32],
69    ) -> Result<Option<Mesh>> {
70        // Get representation
71        let representation_attr = match element.get(6) {
72            Some(attr) if !attr.is_null() => attr,
73            _ => return Ok(None),
74        };
75
76        let representation = match decoder.resolve_ref(representation_attr)? {
77            Some(r) => r,
78            None => return Ok(None),
79        };
80
81        if representation.ifc_type != IfcType::IfcProductDefinitionShape {
82            return Ok(None);
83        }
84
85        // Find an IfcExtrudedAreaSolid in the representations
86        let representations_attr = match representation.get(2) {
87            Some(attr) => attr,
88            None => return Ok(None),
89        };
90
91        let representations = decoder.resolve_ref_list(representations_attr)?;
92
93        // Look for extruded area solid
94        for shape_rep in &representations {
95            if shape_rep.ifc_type != IfcType::IfcShapeRepresentation {
96                continue;
97            }
98
99            let items_attr = match shape_rep.get(3) {
100                Some(attr) => attr,
101                None => continue,
102            };
103
104            let items = decoder.resolve_ref_list(items_attr)?;
105
106            for item in &items {
107                if item.ifc_type == IfcType::IfcExtrudedAreaSolid {
108                    // Found an extrusion - try 2D void processing
109                    return self.process_extrusion_with_voids_2d_impl(
110                        element,
111                        item,
112                        decoder,
113                        opening_ids,
114                    );
115                }
116            }
117        }
118
119        Ok(None)
120    }
121
122    /// Implementation of 2D void processing for extrusions
123    fn process_extrusion_with_voids_2d_impl(
124        &self,
125        element: &DecodedEntity,
126        extrusion: &DecodedEntity,
127        decoder: &mut EntityDecoder,
128        opening_ids: &[u32],
129    ) -> Result<Option<Mesh>> {
130        // Extract extrusion parameters
131        // IfcExtrudedAreaSolid: SweptArea, Position, ExtrudedDirection, Depth
132
133        // Get depth (attribute 3)
134        let depth = match extrusion.get_float(3) {
135            Some(d) if d > 0.0 => d,
136            _ => return Ok(None),
137        };
138
139        // Get extrusion direction (attribute 2)
140        let direction_attr = match extrusion.get(2) {
141            Some(attr) if !attr.is_null() => attr,
142            _ => return Ok(None),
143        };
144
145        let direction_entity = match decoder.resolve_ref(direction_attr)? {
146            Some(e) => e,
147            None => return Ok(None),
148        };
149
150        let local_extrusion_direction = self.parse_direction(&direction_entity)?;
151
152        // Get position transform (attribute 1)
153        let position_transform = if let Some(pos_attr) = extrusion.get(1) {
154            if !pos_attr.is_null() {
155                if let Some(pos_entity) = decoder.resolve_ref(pos_attr)? {
156                    self.parse_axis2_placement_3d(&pos_entity, decoder)?
157                } else {
158                    Matrix4::identity()
159                }
160            } else {
161                Matrix4::identity()
162            }
163        } else {
164            Matrix4::identity()
165        };
166
167        // Transform extrusion direction from local to world coordinates
168        // ExtrudedDirection is specified in Position's local coordinate system
169        let extrusion_direction = {
170            let rot_x = Vector3::new(
171                position_transform[(0, 0)],
172                position_transform[(1, 0)],
173                position_transform[(2, 0)],
174            );
175            let rot_y = Vector3::new(
176                position_transform[(0, 1)],
177                position_transform[(1, 1)],
178                position_transform[(2, 1)],
179            );
180            let rot_z = Vector3::new(
181                position_transform[(0, 2)],
182                position_transform[(1, 2)],
183                position_transform[(2, 2)],
184            );
185            (rot_x * local_extrusion_direction.x
186                + rot_y * local_extrusion_direction.y
187                + rot_z * local_extrusion_direction.z)
188                .normalize()
189        };
190
191        // Get element placement transform
192        let element_transform = self.get_placement_transform_from_element(element, decoder)?;
193        let combined_transform = element_transform * position_transform;
194
195        // Get swept area (profile) - attribute 0
196        let profile_attr = match extrusion.get(0) {
197            Some(attr) if !attr.is_null() => attr,
198            _ => return Ok(None),
199        };
200
201        let profile_entity = match decoder.resolve_ref(profile_attr)? {
202            Some(e) => e,
203            None => return Ok(None),
204        };
205
206        // Extract base 2D profile
207        let base_profile = match self.extract_profile_2d(&profile_entity, decoder) {
208            Ok(p) => p,
209            Err(_) => return Ok(None),
210        };
211
212        // Process opening meshes and classify them
213        let mut void_meshes: Vec<Mesh> = Vec::new();
214
215        for &opening_id in opening_ids {
216            let opening_entity = match decoder.decode_by_id(opening_id) {
217                Ok(e) => e,
218                Err(_) => continue,
219            };
220
221            let opening_mesh = match self.process_element(&opening_entity, decoder) {
222                Ok(m) if !m.is_empty() => m,
223                _ => continue,
224            };
225
226            void_meshes.push(opening_mesh);
227        }
228
229        if void_meshes.is_empty() {
230            // No valid openings - just process the extrusion normally
231            let processor = self.processors.get(&IfcType::IfcExtrudedAreaSolid);
232            if let Some(proc) = processor {
233                let mut mesh = proc.process(extrusion, decoder, &self.schema)?;
234                self.scale_mesh(&mut mesh);
235                self.apply_placement(element, decoder, &mut mesh)?;
236                return Ok(Some(mesh));
237            }
238            return Ok(None);
239        }
240
241        // Classify voids
242        // Use unscaled depth since void_meshes are in file units (not yet scaled)
243        let analyzer = VoidAnalyzer::new();
244
245        let classifications: Vec<crate::void_analysis::VoidClassification> = void_meshes
246            .iter()
247            .map(|mesh| {
248                analyzer.classify_void(
249                    mesh,
250                    &combined_transform,
251                    &extrusion_direction.normalize(),
252                    depth,
253                )
254            })
255            .collect();
256
257        // Extract coplanar and non-planar voids
258        let coplanar_voids = extract_coplanar_voids(&classifications);
259        let nonplanar_voids = extract_nonplanar_voids(classifications);
260
261        // Process coplanar voids at 2D level
262        let profile_with_voids = if !coplanar_voids.is_empty() {
263            // Collect through-void contours for 2D subtraction
264            let through_contours: Vec<Vec<Point2<f64>>> = coplanar_voids
265                .iter()
266                .filter(|v| v.is_through)
267                .map(|v| v.contour.clone())
268                .collect();
269
270            // Subtract voids from profile
271            let modified_profile = if !through_contours.is_empty() {
272                match subtract_multiple_2d(&base_profile, &through_contours) {
273                    Ok(p) => p,
274                    Err(_) => base_profile.clone(),
275                }
276            } else {
277                base_profile.clone()
278            };
279
280            // Create profile with partial-depth voids
281            let partial_voids: Vec<VoidInfo> = coplanar_voids
282                .into_iter()
283                .filter(|v| !v.is_through)
284                .map(|v| VoidInfo {
285                    contour: v.contour,
286                    depth_start: v.depth_start,
287                    depth_end: v.depth_end,
288                    is_through: false,
289                })
290                .collect();
291
292            Profile2DWithVoids::new(modified_profile, partial_voids)
293        } else {
294            Profile2DWithVoids::from_profile(base_profile)
295        };
296
297        // Extrude with voids
298        use crate::extrusion::extrude_profile_with_voids;
299
300        let mut mesh = match extrude_profile_with_voids(&profile_with_voids, depth, None) {
301            Ok(m) => m,
302            Err(_) => {
303                // Fall back to normal extrusion
304                let processor = self.processors.get(&IfcType::IfcExtrudedAreaSolid);
305                if let Some(proc) = processor {
306                    proc.process(extrusion, decoder, &self.schema)?
307                } else {
308                    return Ok(None);
309                }
310            }
311        };
312
313        // Apply extrusion position transform (with RTC offset)
314        if position_transform != Matrix4::identity() {
315            self.transform_mesh(&mut mesh, &position_transform);
316        }
317
318        // Scale mesh
319        self.scale_mesh(&mut mesh);
320
321        // Apply element placement
322        self.apply_placement(element, decoder, &mut mesh)?;
323
324        // Handle non-planar voids with 3D CSG
325        if !nonplanar_voids.is_empty() {
326            let clipper = ClippingProcessor::new();
327            mesh = clipper.subtract_meshes_with_fallback(&mesh, &nonplanar_voids);
328        }
329
330        Ok(Some(mesh))
331    }
332
333    /// Extract a 2D profile from an IFC profile entity
334    pub(super) fn extract_profile_2d(
335        &self,
336        profile_entity: &DecodedEntity,
337        decoder: &mut EntityDecoder,
338    ) -> Result<Profile2D> {
339        use crate::profile::create_rectangle;
340
341        match profile_entity.ifc_type {
342            IfcType::IfcRectangleProfileDef => {
343                // Attributes: ProfileType, ProfileName, Position, XDim, YDim
344                let x_dim = profile_entity.get_float(3).unwrap_or(1.0);
345                let y_dim = profile_entity.get_float(4).unwrap_or(1.0);
346                Ok(create_rectangle(x_dim, y_dim))
347            }
348
349            IfcType::IfcCircleProfileDef => {
350                use crate::profile::create_circle;
351                let radius = profile_entity.get_float(3).unwrap_or(1.0);
352                Ok(create_circle(radius, None))
353            }
354
355            IfcType::IfcArbitraryClosedProfileDef => {
356                // Get outer curve and convert to points
357                let curve_attr = profile_entity.get(2).ok_or_else(|| {
358                    Error::geometry("ArbitraryClosedProfileDef missing OuterCurve".to_string())
359                })?;
360
361                let curve = decoder.resolve_ref(curve_attr)?.ok_or_else(|| {
362                    Error::geometry("Failed to resolve OuterCurve".to_string())
363                })?;
364
365                let points = self.extract_curve_points(&curve, decoder)?;
366                Ok(Profile2D::new(points))
367            }
368
369            IfcType::IfcArbitraryProfileDefWithVoids => {
370                // Get outer curve
371                let outer_attr = profile_entity.get(2).ok_or_else(|| {
372                    Error::geometry(
373                        "ArbitraryProfileDefWithVoids missing OuterCurve".to_string(),
374                    )
375                })?;
376
377                let outer_curve = decoder.resolve_ref(outer_attr)?.ok_or_else(|| {
378                    Error::geometry("Failed to resolve OuterCurve".to_string())
379                })?;
380
381                let outer_points = self.extract_curve_points(&outer_curve, decoder)?;
382                let mut profile = Profile2D::new(outer_points);
383
384                // Get inner curves (holes)
385                if let Some(inner_attr) = profile_entity.get(3) {
386                    let inner_curves = decoder.resolve_ref_list(inner_attr)?;
387                    for inner_curve in inner_curves {
388                        if let Ok(hole_points) = self.extract_curve_points(&inner_curve, decoder) {
389                            profile.add_hole(hole_points);
390                        }
391                    }
392                }
393
394                Ok(profile)
395            }
396
397            _ => Err(Error::geometry(format!(
398                "Unsupported profile type for 2D extraction: {}",
399                profile_entity.ifc_type
400            ))),
401        }
402    }
403
404    /// Extract points from a curve entity (IfcPolyline, IfcIndexedPolyCurve, etc.)
405    fn extract_curve_points(
406        &self,
407        curve: &DecodedEntity,
408        decoder: &mut EntityDecoder,
409    ) -> Result<Vec<Point2<f64>>> {
410        match curve.ifc_type {
411            IfcType::IfcPolyline => {
412                // IfcPolyline: Points (list of IfcCartesianPoint)
413                let points_attr = curve
414                    .get(0)
415                    .ok_or_else(|| Error::geometry("IfcPolyline missing Points".to_string()))?;
416
417                let point_entities = decoder.resolve_ref_list(points_attr)?;
418                let mut points = Vec::with_capacity(point_entities.len());
419
420                for (_i, point_entity) in point_entities.iter().enumerate() {
421                    if point_entity.ifc_type == IfcType::IfcCartesianPoint {
422                        if let Some(coords_attr) = point_entity.get(0) {
423                            if let Some(coords) = coords_attr.as_list() {
424                                let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
425                                let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
426                                points.push(Point2::new(x, y));
427                            }
428                        }
429                    }
430                }
431
432                Ok(points)
433            }
434
435            IfcType::IfcIndexedPolyCurve => {
436                // IfcIndexedPolyCurve: Points (IfcCartesianPointList2D), Segments, SelfIntersect
437                let points_attr = curve.get(0).ok_or_else(|| {
438                    Error::geometry("IfcIndexedPolyCurve missing Points".to_string())
439                })?;
440
441                let point_list = decoder.resolve_ref(points_attr)?.ok_or_else(|| {
442                    Error::geometry("Failed to resolve Points".to_string())
443                })?;
444
445                // IfcCartesianPointList2D: CoordList (list of coordinates)
446                if let Some(coord_attr) = point_list.get(0) {
447                    if let Some(coord_list) = coord_attr.as_list() {
448                        let mut points = Vec::with_capacity(coord_list.len());
449
450                        for coord in coord_list {
451                            if let Some(pair) = coord.as_list() {
452                                let x = pair.first().and_then(|v| v.as_float()).unwrap_or(0.0);
453                                let y = pair.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
454                                points.push(Point2::new(x, y));
455                            }
456                        }
457
458                        return Ok(points);
459                    }
460                }
461
462                Err(Error::geometry(
463                    "Failed to extract points from IfcIndexedPolyCurve".to_string(),
464                ))
465            }
466
467            IfcType::IfcCompositeCurve => {
468                // IfcCompositeCurve: Segments (list of IfcCompositeCurveSegment)
469                let segments_attr = curve.get(0).ok_or_else(|| {
470                    Error::geometry("IfcCompositeCurve missing Segments".to_string())
471                })?;
472
473                let segments = decoder.resolve_ref_list(segments_attr)?;
474                let mut all_points = Vec::new();
475
476                for segment in segments {
477                    // IfcCompositeCurveSegment: Transition, SameSense, ParentCurve
478                    if let Some(parent_attr) = segment.get(2) {
479                        if let Some(parent_curve) = decoder.resolve_ref(parent_attr)? {
480                            if let Ok(points) = self.extract_curve_points(&parent_curve, decoder) {
481                                all_points.extend(points);
482                            }
483                        }
484                    }
485                }
486
487                Ok(all_points)
488            }
489
490            _ => Err(Error::geometry(format!(
491                "Unsupported curve type: {}",
492                curve.ifc_type
493            ))),
494        }
495    }
496}