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