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