ifc-lite-geometry 2.1.8

Geometry processing and mesh generation for IFC models
Documentation
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
// 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/.

//! Boolean clipping result extraction: half-space planes and profile drilling.

use super::GeometryRouter;
use crate::{Error, Mesh, Point3, Result, Vector3};
use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcType};
use nalgebra::Matrix4;
use rustc_hash::FxHashSet;

/// Maximum IfcBooleanClippingResult chain depth we will follow when extracting a base profile.
const MAX_CLIPPING_DEPTH: usize = 32;

impl GeometryRouter {
    /// Quick check if an element has clipping planes (IfcBooleanClippingResult in representation)
    /// This is much faster than extract_base_profile_and_clips and allows skipping expensive
    /// extraction for the ~95% of elements that don't have clipping.
    #[inline]
    pub(super) fn has_clipping_planes(
        &self,
        element: &DecodedEntity,
        decoder: &mut EntityDecoder,
    ) -> bool {
        // Get representation
        let representation_attr = match element.get(6) {
            Some(attr) => attr,
            None => return false,
        };

        let representation = match decoder.resolve_ref(representation_attr) {
            Ok(Some(r)) if r.ifc_type == IfcType::IfcProductDefinitionShape => r,
            _ => return false,
        };

        // Get representations list
        let representations_attr = match representation.get(2) {
            Some(attr) => attr,
            None => return false,
        };

        let representations = match decoder.resolve_ref_list(representations_attr) {
            Ok(r) => r,
            Err(_) => return false,
        };

        // Check if any representation item is IfcBooleanClippingResult
        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 = match decoder.resolve_ref_list(items_attr) {
                Ok(i) => i,
                Err(_) => continue,
            };

            for item in &items {
                if item.ifc_type == IfcType::IfcBooleanClippingResult {
                    return true;
                }
            }
        }

        false
    }

    /// Extract base wall profile, depth, axis info, Position transform, and clipping planes
    ///
    /// Drills through IfcBooleanClippingResult to find the base extruded solid,
    /// extracts its actual 2D profile (preserving chamfered corners), and collects clipping planes.
    /// Returns: (profile, depth, thickness_axis, wall_origin, position_transform, clipping_planes)
    pub(super) fn extract_base_profile_and_clips(
        &self,
        element: &DecodedEntity,
        decoder: &mut EntityDecoder,
    ) -> Result<(
        crate::profile::Profile2D,
        f64,
        u8,
        f64,
        Option<Matrix4<f64>>,
        Vec<(Point3<f64>, Vector3<f64>, bool)>,
    )> {
        use nalgebra::Vector3;

        let mut clipping_planes: Vec<(Point3<f64>, Vector3<f64>, bool)> = Vec::new();

        // Get representation
        let representation_attr = element
            .get(6)
            .ok_or_else(|| Error::geometry("Element missing representation".to_string()))?;

        let representation = decoder
            .resolve_ref(representation_attr)?
            .ok_or_else(|| Error::geometry("Failed to resolve representation".to_string()))?;

        if representation.ifc_type != IfcType::IfcProductDefinitionShape {
            // Fallback: can't extract profile, return error
            return Err(Error::geometry(
                "Element representation is not ProductDefinitionShape".to_string(),
            ));
        }

        // Get representations list
        let representations_attr = representation
            .get(2)
            .ok_or_else(|| Error::geometry("Missing representations".to_string()))?;

        let representations = decoder.resolve_ref_list(representations_attr)?;

        // Find the shape representation with geometry
        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 {
                // Check if this is a IfcBooleanClippingResult (wall clipped by roof)
                if item.ifc_type == IfcType::IfcBooleanClippingResult {
                    // Recursively extract base solid and collect clipping planes
                    let (profile, depth, axis, origin, transform, clips) =
                        self.extract_profile_from_boolean_result(item, decoder)?;
                    clipping_planes.extend(clips);
                    return Ok((profile, depth, axis, origin, transform, clipping_planes));
                }

                // If it's a simple extruded solid, extract profile directly
                if item.ifc_type == IfcType::IfcExtrudedAreaSolid {
                    let (profile, depth, axis, origin, transform) =
                        self.extract_profile_from_extruded_solid(item, decoder)?;
                    return Ok((profile, depth, axis, origin, transform, clipping_planes));
                }
            }
        }

        // Fallback: couldn't find extruded solid
        Err(Error::geometry(
            "Could not find IfcExtrudedAreaSolid in representation".to_string(),
        ))
    }

    /// Extract profile from IfcBooleanClippingResult recursively
    fn extract_profile_from_boolean_result(
        &self,
        boolean_result: &DecodedEntity,
        decoder: &mut EntityDecoder,
    ) -> Result<(
        crate::profile::Profile2D,
        f64,
        u8,
        f64,
        Option<Matrix4<f64>>,
        Vec<(Point3<f64>, Vector3<f64>, bool)>,
    )> {
        use nalgebra::Vector3;

        let mut clipping_planes: Vec<(Point3<f64>, Vector3<f64>, bool)> = Vec::new();
        let mut current = boolean_result.clone();
        let mut visited = FxHashSet::default();

        for _depth in 0..MAX_CLIPPING_DEPTH {
            if !visited.insert(current.id) {
                return Err(Error::geometry(format!(
                    "Detected cyclic IfcBooleanClippingResult reference at #{}",
                    current.id
                )));
            }

            // Get SecondOperand (the clipping solid - usually IfcHalfSpaceSolid)
            if let Some(second_operand_attr) = current.get(2) {
                if let Ok(Some(second_operand)) = decoder.resolve_ref(second_operand_attr) {
                    if let Some(clip) = self.extract_half_space_plane(&second_operand, decoder) {
                        clipping_planes.push(clip);
                    }
                }
            }

            // Get FirstOperand (the base geometry or another boolean result)
            let first_operand_attr = current
                .get(1)
                .ok_or_else(|| Error::geometry("BooleanResult missing FirstOperand".to_string()))?;

            let first_operand = decoder
                .resolve_ref(first_operand_attr)?
                .ok_or_else(|| Error::geometry("Failed to resolve FirstOperand".to_string()))?;

            if first_operand.ifc_type == IfcType::IfcBooleanClippingResult {
                current = first_operand;
                continue;
            }

            // FirstOperand should be IfcExtrudedAreaSolid
            if first_operand.ifc_type == IfcType::IfcExtrudedAreaSolid {
                let (profile, depth, axis, origin, transform) =
                    self.extract_profile_from_extruded_solid(&first_operand, decoder)?;
                return Ok((profile, depth, axis, origin, transform, clipping_planes));
            }

            return Err(Error::geometry(format!(
                "Unsupported base solid type in boolean result: {:?}",
                first_operand.ifc_type
            )));
        }

        Err(Error::geometry(format!(
            "IfcBooleanClippingResult nesting exceeded maximum depth of {} at #{}",
            MAX_CLIPPING_DEPTH, current.id
        )))
    }

    /// Extract profile, depth, axis, origin, and Position transform from IfcExtrudedAreaSolid
    fn extract_profile_from_extruded_solid(
        &self,
        extruded_solid: &DecodedEntity,
        decoder: &mut EntityDecoder,
    ) -> Result<(
        crate::profile::Profile2D,
        f64,
        u8,
        f64,
        Option<Matrix4<f64>>,
    )> {
        // Get SweptArea (attribute 0: IfcProfileDef)
        let swept_area_attr = extruded_solid
            .get(0)
            .ok_or_else(|| Error::geometry("ExtrudedAreaSolid missing SweptArea".to_string()))?;

        let profile_entity = decoder
            .resolve_ref(swept_area_attr)?
            .ok_or_else(|| Error::geometry("Failed to resolve SweptArea".to_string()))?;

        // Extract the actual 2D profile (preserves chamfered corners!)
        let profile = self.extract_profile_2d(&profile_entity, decoder)?;

        // Get depth (attribute 3: Depth)
        let depth = extruded_solid
            .get_float(3)
            .ok_or_else(|| Error::geometry("ExtrudedAreaSolid missing Depth".to_string()))?;

        // Get ExtrudedDirection (attribute 2: IfcDirection)
        // This tells us which axis is the thickness axis
        let direction_attr = extruded_solid.get(2).ok_or_else(|| {
            Error::geometry("ExtrudedAreaSolid missing ExtrudedDirection".to_string())
        })?;

        let direction_entity = decoder
            .resolve_ref(direction_attr)?
            .ok_or_else(|| Error::geometry("Failed to resolve ExtrudedDirection".to_string()))?;

        // Get direction coordinates (attribute 0: DirectionRatios)
        let ratios_attr = direction_entity
            .get(0)
            .ok_or_else(|| Error::geometry("Direction missing DirectionRatios".to_string()))?;

        let ratios = ratios_attr
            .as_list()
            .ok_or_else(|| Error::geometry("DirectionRatios is not a list".to_string()))?;

        let dx = ratios.get(0).and_then(|v| v.as_float()).unwrap_or(0.0);
        let dy = ratios.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
        let dz = ratios.get(2).and_then(|v| v.as_float()).unwrap_or(1.0);

        // Determine thickness axis from direction (which component is largest)
        let thickness_axis = if dx.abs() >= dy.abs() && dx.abs() >= dz.abs() {
            0 // X axis
        } else if dy.abs() >= dz.abs() {
            1 // Y axis
        } else {
            2 // Z axis
        };

        // For wall origin, we'll need to get it from the element's placement
        // For now, use 0.0 - it will be adjusted when we transform coordinates
        let wall_origin = 0.0;

        // Extract Position transform (attribute 1: IfcAxis2Placement3D)
        let position_transform = if let Some(pos_attr) = extruded_solid.get(1) {
            if !pos_attr.is_null() {
                if let Ok(Some(pos_entity)) = decoder.resolve_ref(pos_attr) {
                    if pos_entity.ifc_type == IfcType::IfcAxis2Placement3D {
                        match self.parse_axis2_placement_3d(&pos_entity, decoder) {
                            Ok(transform) => Some(transform),
                            Err(_) => None,
                        }
                    } else {
                        None
                    }
                } else {
                    None
                }
            } else {
                None
            }
        } else {
            None
        };

        Ok((
            profile,
            depth,
            thickness_axis,
            wall_origin,
            position_transform,
        ))
    }

    /// Extract base mesh from IfcBooleanClippingResult and collect clipping planes
    #[allow(dead_code)] // Used internally for recursive boolean result processing
    fn extract_base_from_boolean_result(
        &self,
        boolean_result: &DecodedEntity,
        decoder: &mut EntityDecoder,
    ) -> Result<(Mesh, Vec<(Point3<f64>, Vector3<f64>, bool)>)> {
        use nalgebra::Vector3;

        let mut clipping_planes: Vec<(Point3<f64>, Vector3<f64>, bool)> = Vec::new();

        // Get FirstOperand (the base geometry or another boolean result)
        let first_operand_attr = boolean_result
            .get(1)
            .ok_or_else(|| Error::geometry("BooleanResult missing FirstOperand".to_string()))?;

        let first_operand = decoder
            .resolve_ref(first_operand_attr)?
            .ok_or_else(|| Error::geometry("Failed to resolve FirstOperand".to_string()))?;

        // Get SecondOperand (the clipping solid - usually IfcHalfSpaceSolid)
        if let Some(second_operand_attr) = boolean_result.get(2) {
            if let Ok(Some(second_operand)) = decoder.resolve_ref(second_operand_attr) {
                if let Some(clip) = self.extract_half_space_plane(&second_operand, decoder) {
                    clipping_planes.push(clip);
                }
            }
        }

        // Process FirstOperand
        if first_operand.ifc_type == IfcType::IfcBooleanClippingResult {
            // Recursively process nested boolean results
            let (base_mesh, nested_clips) =
                self.extract_base_from_boolean_result(&first_operand, decoder)?;
            clipping_planes.extend(nested_clips);
            return Ok((base_mesh, clipping_planes));
        }

        // FirstOperand is the base solid (IfcExtrudedAreaSolid, etc.)
        if let Some(processor) = self.processors.get(&first_operand.ifc_type) {
            let mut mesh = processor.process(&first_operand, decoder, &self.schema)?;
            self.scale_mesh(&mut mesh);
            // Note: placement is applied in the main function
            return Ok((mesh, clipping_planes));
        }

        Err(Error::geometry(format!(
            "Unsupported base solid type: {:?}",
            first_operand.ifc_type
        )))
    }

    /// Extract plane parameters from IfcHalfSpaceSolid or IfcPolygonalBoundedHalfSpace
    fn extract_half_space_plane(
        &self,
        half_space: &DecodedEntity,
        decoder: &mut EntityDecoder,
    ) -> Option<(Point3<f64>, Vector3<f64>, bool)> {
        use nalgebra::Vector3;

        if half_space.ifc_type != IfcType::IfcHalfSpaceSolid
            && half_space.ifc_type != IfcType::IfcPolygonalBoundedHalfSpace
        {
            return None;
        }

        // Get BaseSurface (should be IfcPlane)
        let base_surface_attr = half_space.get(0)?;
        let base_surface = decoder.resolve_ref(base_surface_attr).ok()??;

        if base_surface.ifc_type != IfcType::IfcPlane {
            return None;
        }

        // Get Position (IfcAxis2Placement3D)
        let position_attr = base_surface.get(0)?;
        let position = decoder.resolve_ref(position_attr).ok()??;

        // Get Location (point on plane)
        let location_attr = position.get(0)?;
        let location = decoder.resolve_ref(location_attr).ok()??;

        let coords_attr = location.get(0)?;
        let coords = coords_attr.as_list()?;
        let px = coords.first()?.as_float()?;
        let py = coords.get(1)?.as_float()?;
        let pz = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
        let plane_point = Point3::new(px, py, pz);

        // Get Axis (normal direction) - default to Z if not specified
        let plane_normal = if let Some(axis_attr) = position.get(1) {
            if !axis_attr.is_null() {
                if let Ok(Some(axis)) = decoder.resolve_ref(axis_attr) {
                    if let Some(dir_attr) = axis.get(0) {
                        if let Some(dir) = dir_attr.as_list() {
                            let nx = dir.first().and_then(|v| v.as_float()).unwrap_or(0.0);
                            let ny = dir.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
                            let nz = dir.get(2).and_then(|v| v.as_float()).unwrap_or(1.0);
                            Vector3::new(nx, ny, nz).normalize()
                        } else {
                            Vector3::new(0.0, 0.0, 1.0)
                        }
                    } else {
                        Vector3::new(0.0, 0.0, 1.0)
                    }
                } else {
                    Vector3::new(0.0, 0.0, 1.0)
                }
            } else {
                Vector3::new(0.0, 0.0, 1.0)
            }
        } else {
            Vector3::new(0.0, 0.0, 1.0)
        };

        // Get AgreementFlag - stored as Enum "T" or "F"
        let agreement = half_space
            .get(1)
            .map(|v| match v {
                ifc_lite_core::AttributeValue::Enum(e) => e != "F" && e != ".F.",
                _ => true,
            })
            .unwrap_or(true);

        Some((plane_point, plane_normal, agreement))
    }
}