Skip to main content

ifc_lite_geometry/processors/
tessellated.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//! Tessellated geometry processors - pre-tessellated/polygon meshes.
6//!
7//! Handles IfcTriangulatedFaceSet (explicit triangle meshes) and
8//! IfcPolygonalFaceSet (polygon meshes requiring triangulation).
9
10use crate::{Error, Mesh, Result};
11use ifc_lite_core::{AttributeValue, DecodedEntity, EntityDecoder, IfcSchema, IfcType};
12
13use crate::router::GeometryProcessor;
14
15/// TriangulatedFaceSet processor (P0)
16/// Handles IfcTriangulatedFaceSet - explicit triangle meshes
17pub struct TriangulatedFaceSetProcessor;
18
19impl TriangulatedFaceSetProcessor {
20    pub fn new() -> Self {
21        Self
22    }
23}
24
25impl GeometryProcessor for TriangulatedFaceSetProcessor {
26    #[inline]
27    fn process(
28        &self,
29        entity: &DecodedEntity,
30        decoder: &mut EntityDecoder,
31        _schema: &IfcSchema,
32    ) -> Result<Mesh> {
33        // IfcTriangulatedFaceSet attributes:
34        // 0: Coordinates (IfcCartesianPointList3D)
35        // 1: Normals (optional)
36        // 2: Closed (optional)
37        // 3: CoordIndex (list of list of IfcPositiveInteger)
38
39        // Get coordinate entity reference
40        let coords_attr = entity.get(0).ok_or_else(|| {
41            Error::geometry("TriangulatedFaceSet missing Coordinates".to_string())
42        })?;
43
44        let coord_entity_id = coords_attr.as_entity_ref().ok_or_else(|| {
45            Error::geometry("Expected entity reference for Coordinates".to_string())
46        })?;
47
48        // FAST PATH: Try direct parsing of raw bytes (3-5x faster)
49        // This bypasses Token/AttributeValue allocations entirely
50        use ifc_lite_core::{extract_coordinate_list_from_entity, parse_indices_direct};
51
52        let positions = if let Some(raw_bytes) = decoder.get_raw_bytes(coord_entity_id) {
53            // Fast path: parse coordinates directly from raw bytes
54            // Use extract_coordinate_list_from_entity to skip entity header (#N=IFCTYPE...)
55            extract_coordinate_list_from_entity(raw_bytes).unwrap_or_default()
56        } else {
57            // Fallback path: use standard decoding
58            let coords_entity = decoder.decode_by_id(coord_entity_id)?;
59
60            let coord_list_attr = coords_entity.get(0).ok_or_else(|| {
61                Error::geometry("CartesianPointList3D missing CoordList".to_string())
62            })?;
63
64            let coord_list = coord_list_attr
65                .as_list()
66                .ok_or_else(|| Error::geometry("Expected coordinate list".to_string()))?;
67
68            use ifc_lite_core::AttributeValue;
69            AttributeValue::parse_coordinate_list_3d(coord_list)
70        };
71
72        // Get face indices - try fast path first
73        let indices_attr = entity
74            .get(3)
75            .ok_or_else(|| Error::geometry("TriangulatedFaceSet missing CoordIndex".to_string()))?;
76
77        // For indices, we need to extract from the main entity's raw bytes
78        // Fast path: parse directly if we can get the raw CoordIndex section
79        let indices = if let Some(raw_entity_bytes) = decoder.get_raw_bytes(entity.id) {
80            // Find the CoordIndex attribute (4th attribute, index 3)
81            // and parse directly
82            if let Some(coord_index_bytes) = super::extract_coord_index_bytes(raw_entity_bytes) {
83                parse_indices_direct(coord_index_bytes)
84            } else {
85                // Fallback to standard parsing
86                let face_list = indices_attr
87                    .as_list()
88                    .ok_or_else(|| Error::geometry("Expected face index list".to_string()))?;
89                use ifc_lite_core::AttributeValue;
90                AttributeValue::parse_index_list(face_list)
91            }
92        } else {
93            let face_list = indices_attr
94                .as_list()
95                .ok_or_else(|| Error::geometry("Expected face index list".to_string()))?;
96            use ifc_lite_core::AttributeValue;
97            AttributeValue::parse_index_list(face_list)
98        };
99
100        // Create mesh (normals will be computed later)
101        Ok(Mesh {
102            positions,
103            normals: Vec::new(),
104            indices,
105        })
106    }
107
108    fn supported_types(&self) -> Vec<IfcType> {
109        vec![IfcType::IfcTriangulatedFaceSet]
110    }
111}
112
113impl Default for TriangulatedFaceSetProcessor {
114    fn default() -> Self {
115        Self::new()
116    }
117}
118
119/// Handles IfcPolygonalFaceSet - explicit polygon meshes that need triangulation
120/// Unlike IfcTriangulatedFaceSet, faces can be arbitrary polygons (not just triangles)
121pub struct PolygonalFaceSetProcessor;
122
123impl PolygonalFaceSetProcessor {
124    pub fn new() -> Self {
125        Self
126    }
127
128    #[inline]
129    fn parse_index_loop(indices: &[AttributeValue], pn_index: Option<&[u32]>) -> Vec<u32> {
130        indices
131            .iter()
132            .filter_map(|value| {
133                let idx = value.as_int()?;
134                if idx <= 0 {
135                    return None;
136                }
137                let idx = idx as usize;
138
139                if let Some(remap) = pn_index {
140                    remap.get(idx - 1).copied().filter(|mapped| *mapped > 0)
141                } else {
142                    Some(idx as u32)
143                }
144            })
145            .collect()
146    }
147
148    /// Triangulate a polygon (optionally with holes) using ear-clipping (earcutr)
149    /// This works correctly for both convex and concave polygons
150    /// IFC indices are 1-based, so we subtract 1 to get 0-based indices
151    /// positions is flattened [x0, y0, z0, x1, y1, z1, ...]
152    fn triangulate_polygon(
153        outer_indices: &[u32],
154        inner_indices: &[Vec<u32>],
155        positions: &[f32],
156        output: &mut Vec<u32>,
157    ) {
158        if outer_indices.len() < 3 {
159            return;
160        }
161
162        // Helper to get 3D position from flattened array
163        let get_pos = |idx: u32| -> Option<(f32, f32, f32)> {
164            if idx == 0 {
165                return None;
166            }
167            let base = ((idx - 1) * 3) as usize;
168            if base + 2 < positions.len() {
169                Some((positions[base], positions[base + 1], positions[base + 2]))
170            } else {
171                None
172            }
173        };
174
175        // For complex polygons (5+ vertices), use ear-clipping triangulation
176        // This handles concave polygons correctly (like opening cutouts)
177
178        // Extract 2D coordinates by projecting to best-fit plane
179        // Find dominant normal direction to choose projection plane
180        let mut sum_x = 0.0f64;
181        let mut sum_y = 0.0f64;
182        let mut sum_z = 0.0f64;
183
184        // Calculate centroid-based normal approximation using Newell's method
185        for i in 0..outer_indices.len() {
186            let v0 = match get_pos(outer_indices[i]) {
187                Some(p) => p,
188                None => {
189                    // Fallback to fan triangulation if indices are invalid
190                    let first = outer_indices[0] - 1;
191                    for j in 1..outer_indices.len() - 1 {
192                        output.push(first);
193                        output.push(outer_indices[j] - 1);
194                        output.push(outer_indices[j + 1] - 1);
195                    }
196                    return;
197                }
198            };
199            let v1 = match get_pos(outer_indices[(i + 1) % outer_indices.len()]) {
200                Some(p) => p,
201                None => {
202                    let first = outer_indices[0] - 1;
203                    for j in 1..outer_indices.len() - 1 {
204                        output.push(first);
205                        output.push(outer_indices[j] - 1);
206                        output.push(outer_indices[j + 1] - 1);
207                    }
208                    return;
209                }
210            };
211
212            sum_x += (v0.1 - v1.1) as f64 * (v0.2 + v1.2) as f64;
213            sum_y += (v0.2 - v1.2) as f64 * (v0.0 + v1.0) as f64;
214            sum_z += (v0.0 - v1.0) as f64 * (v0.1 + v1.1) as f64;
215        }
216        let expected_normal = (sum_x, sum_y, sum_z);
217
218        let mut push_oriented_triangle = |a: u32, b: u32, c: u32| {
219            if a == 0 || b == 0 || c == 0 {
220                return;
221            }
222            let i0 = a - 1;
223            let mut i1 = b - 1;
224            let mut i2 = c - 1;
225
226            if expected_normal.0.abs() + expected_normal.1.abs() + expected_normal.2.abs() > 1e-12 {
227                if let (Some(p0), Some(p1), Some(p2)) = (get_pos(a), get_pos(b), get_pos(c)) {
228                    let e1 = (
229                        (p1.0 - p0.0) as f64,
230                        (p1.1 - p0.1) as f64,
231                        (p1.2 - p0.2) as f64,
232                    );
233                    let e2 = (
234                        (p2.0 - p0.0) as f64,
235                        (p2.1 - p0.1) as f64,
236                        (p2.2 - p0.2) as f64,
237                    );
238                    let tri_normal = (
239                        e1.1 * e2.2 - e1.2 * e2.1,
240                        e1.2 * e2.0 - e1.0 * e2.2,
241                        e1.0 * e2.1 - e1.1 * e2.0,
242                    );
243                    let dot = tri_normal.0 * expected_normal.0
244                        + tri_normal.1 * expected_normal.1
245                        + tri_normal.2 * expected_normal.2;
246                    if dot < 0.0 {
247                        std::mem::swap(&mut i1, &mut i2);
248                    }
249                }
250            }
251
252            output.push(i0);
253            output.push(i1);
254            output.push(i2);
255        };
256
257        // For triangles, no triangulation needed (but still enforce orientation)
258        if inner_indices.is_empty() && outer_indices.len() == 3 {
259            push_oriented_triangle(outer_indices[0], outer_indices[1], outer_indices[2]);
260            return;
261        }
262
263        // For quads, use fan triangulation with orientation correction
264        if inner_indices.is_empty() && outer_indices.len() == 4 {
265            push_oriented_triangle(outer_indices[0], outer_indices[1], outer_indices[2]);
266            push_oriented_triangle(outer_indices[0], outer_indices[2], outer_indices[3]);
267            return;
268        }
269
270        // Choose projection plane based on dominant axis
271        let abs_x = sum_x.abs();
272        let abs_y = sum_y.abs();
273        let abs_z = sum_z.abs();
274
275        let valid_holes: Vec<&[u32]> = inner_indices
276            .iter()
277            .filter(|loop_indices| loop_indices.len() >= 3)
278            .map(|loop_indices| loop_indices.as_slice())
279            .collect();
280
281        // Flatten all loops for earcut (outer ring first, then holes)
282        let total_vertices =
283            outer_indices.len() + valid_holes.iter().map(|loop_indices| loop_indices.len()).sum::<usize>();
284        let mut coords_2d: Vec<f64> = Vec::with_capacity(total_vertices * 2);
285        let mut flattened_indices: Vec<u32> = Vec::with_capacity(total_vertices);
286        let mut hole_starts: Vec<usize> = Vec::with_capacity(valid_holes.len());
287
288        for &idx in outer_indices {
289            let Some(p) = get_pos(idx) else {
290                let first = outer_indices[0];
291                for i in 1..outer_indices.len() - 1 {
292                    push_oriented_triangle(first, outer_indices[i], outer_indices[i + 1]);
293                }
294                return;
295            };
296            flattened_indices.push(idx);
297
298            // Project to 2D based on dominant normal axis
299            if abs_z >= abs_x && abs_z >= abs_y {
300                // XY plane (Z is dominant)
301                coords_2d.push(p.0 as f64);
302                coords_2d.push(p.1 as f64);
303            } else if abs_y >= abs_x {
304                // XZ plane (Y is dominant)
305                coords_2d.push(p.0 as f64);
306                coords_2d.push(p.2 as f64);
307            } else {
308                // YZ plane (X is dominant)
309                coords_2d.push(p.1 as f64);
310                coords_2d.push(p.2 as f64);
311            }
312        }
313
314        for hole in valid_holes {
315            hole_starts.push(flattened_indices.len());
316            for &idx in hole {
317                let Some(p) = get_pos(idx) else {
318                    let first = outer_indices[0];
319                    for i in 1..outer_indices.len() - 1 {
320                        push_oriented_triangle(first, outer_indices[i], outer_indices[i + 1]);
321                    }
322                    return;
323                };
324                flattened_indices.push(idx);
325
326                // Project to 2D based on dominant normal axis
327                if abs_z >= abs_x && abs_z >= abs_y {
328                    // XY plane (Z is dominant)
329                    coords_2d.push(p.0 as f64);
330                    coords_2d.push(p.1 as f64);
331                } else if abs_y >= abs_x {
332                    // XZ plane (Y is dominant)
333                    coords_2d.push(p.0 as f64);
334                    coords_2d.push(p.2 as f64);
335                } else {
336                    // YZ plane (X is dominant)
337                    coords_2d.push(p.1 as f64);
338                    coords_2d.push(p.2 as f64);
339                }
340            }
341        }
342
343        if flattened_indices.len() < 3 {
344            return;
345        }
346
347        // Run ear-clipping triangulation
348        match earcutr::earcut(&coords_2d, &hole_starts, 2) {
349            Ok(tri_indices) => {
350                for tri in tri_indices.chunks(3) {
351                    if tri.len() != 3
352                        || tri[0] >= flattened_indices.len()
353                        || tri[1] >= flattened_indices.len()
354                        || tri[2] >= flattened_indices.len()
355                    {
356                        continue;
357                    }
358                    push_oriented_triangle(
359                        flattened_indices[tri[0]],
360                        flattened_indices[tri[1]],
361                        flattened_indices[tri[2]],
362                    );
363                }
364            }
365            Err(_) => {
366                // Fallback to fan triangulation on the outer loop
367                let first = outer_indices[0];
368                for i in 1..outer_indices.len() - 1 {
369                    push_oriented_triangle(first, outer_indices[i], outer_indices[i + 1]);
370                }
371            }
372        }
373    }
374
375    #[inline]
376    fn parse_face_inner_indices(face_entity: &DecodedEntity, pn_index: Option<&[u32]>) -> Vec<Vec<u32>> {
377        if face_entity.ifc_type != IfcType::IfcIndexedPolygonalFaceWithVoids {
378            return Vec::new();
379        }
380
381        let Some(inner_attr) = face_entity.get(1).and_then(|a| a.as_list()) else {
382            return Vec::new();
383        };
384
385        let mut result = Vec::with_capacity(inner_attr.len());
386        for loop_attr in inner_attr {
387            let Some(loop_values) = loop_attr.as_list() else {
388                continue;
389            };
390            let parsed = Self::parse_index_loop(loop_values, pn_index);
391            if parsed.len() >= 3 {
392                result.push(parsed);
393            }
394        }
395
396        result
397    }
398
399    #[inline]
400    fn orient_closed_shell_outward(positions: &[f32], indices: &mut [u32]) {
401        if indices.len() < 3 || positions.len() < 9 {
402            return;
403        }
404
405        let vertex_count = positions.len() / 3;
406        if vertex_count == 0 {
407            return;
408        }
409
410        // Mesh centroid
411        let mut cx = 0.0f64;
412        let mut cy = 0.0f64;
413        let mut cz = 0.0f64;
414        for p in positions.chunks_exact(3) {
415            cx += p[0] as f64;
416            cy += p[1] as f64;
417            cz += p[2] as f64;
418        }
419        let inv_n = 1.0 / vertex_count as f64;
420        cx *= inv_n;
421        cy *= inv_n;
422        cz *= inv_n;
423
424        let mut sign_accum = 0.0f64;
425        for tri in indices.chunks_exact(3) {
426            let i0 = tri[0] as usize;
427            let i1 = tri[1] as usize;
428            let i2 = tri[2] as usize;
429            if i0 >= vertex_count || i1 >= vertex_count || i2 >= vertex_count {
430                continue;
431            }
432
433            let p0 = (
434                positions[i0 * 3] as f64,
435                positions[i0 * 3 + 1] as f64,
436                positions[i0 * 3 + 2] as f64,
437            );
438            let p1 = (
439                positions[i1 * 3] as f64,
440                positions[i1 * 3 + 1] as f64,
441                positions[i1 * 3 + 2] as f64,
442            );
443            let p2 = (
444                positions[i2 * 3] as f64,
445                positions[i2 * 3 + 1] as f64,
446                positions[i2 * 3 + 2] as f64,
447            );
448
449            let e1 = (p1.0 - p0.0, p1.1 - p0.1, p1.2 - p0.2);
450            let e2 = (p2.0 - p0.0, p2.1 - p0.1, p2.2 - p0.2);
451            let n = (
452                e1.1 * e2.2 - e1.2 * e2.1,
453                e1.2 * e2.0 - e1.0 * e2.2,
454                e1.0 * e2.1 - e1.1 * e2.0,
455            );
456
457            let tc = (
458                (p0.0 + p1.0 + p2.0) / 3.0,
459                (p0.1 + p1.1 + p2.1) / 3.0,
460                (p0.2 + p1.2 + p2.2) / 3.0,
461            );
462            let out = (tc.0 - cx, tc.1 - cy, tc.2 - cz);
463            sign_accum += n.0 * out.0 + n.1 * out.1 + n.2 * out.2;
464        }
465
466        // If most triangles point inward, flip all winding.
467        if sign_accum < 0.0 {
468            for tri in indices.chunks_exact_mut(3) {
469                tri.swap(1, 2);
470            }
471        }
472    }
473
474    #[inline]
475    fn build_flat_shaded_mesh(positions: &[f32], indices: &[u32]) -> Mesh {
476        let mut flat_positions: Vec<f32> = Vec::with_capacity(indices.len() * 3);
477        let mut flat_normals: Vec<f32> = Vec::with_capacity(indices.len() * 3);
478        let mut flat_indices: Vec<u32> = Vec::with_capacity(indices.len());
479
480        let vertex_count = positions.len() / 3;
481        let mut next_index: u32 = 0;
482
483        for tri in indices.chunks_exact(3) {
484            let i0 = tri[0] as usize;
485            let i1 = tri[1] as usize;
486            let i2 = tri[2] as usize;
487            if i0 >= vertex_count || i1 >= vertex_count || i2 >= vertex_count {
488                continue;
489            }
490
491            let p0 = (
492                positions[i0 * 3] as f64,
493                positions[i0 * 3 + 1] as f64,
494                positions[i0 * 3 + 2] as f64,
495            );
496            let p1 = (
497                positions[i1 * 3] as f64,
498                positions[i1 * 3 + 1] as f64,
499                positions[i1 * 3 + 2] as f64,
500            );
501            let p2 = (
502                positions[i2 * 3] as f64,
503                positions[i2 * 3 + 1] as f64,
504                positions[i2 * 3 + 2] as f64,
505            );
506
507            let e1 = (p1.0 - p0.0, p1.1 - p0.1, p1.2 - p0.2);
508            let e2 = (p2.0 - p0.0, p2.1 - p0.1, p2.2 - p0.2);
509            let nx = e1.1 * e2.2 - e1.2 * e2.1;
510            let ny = e1.2 * e2.0 - e1.0 * e2.2;
511            let nz = e1.0 * e2.1 - e1.1 * e2.0;
512            let len = (nx * nx + ny * ny + nz * nz).sqrt();
513            let (nx, ny, nz) = if len > 1e-12 {
514                (nx / len, ny / len, nz / len)
515            } else {
516                (0.0, 0.0, 1.0)
517            };
518
519            for &idx in &[i0, i1, i2] {
520                flat_positions.push(positions[idx * 3]);
521                flat_positions.push(positions[idx * 3 + 1]);
522                flat_positions.push(positions[idx * 3 + 2]);
523                flat_normals.push(nx as f32);
524                flat_normals.push(ny as f32);
525                flat_normals.push(nz as f32);
526                flat_indices.push(next_index);
527                next_index += 1;
528            }
529        }
530
531        Mesh {
532            positions: flat_positions,
533            normals: flat_normals,
534            indices: flat_indices,
535        }
536    }
537}
538
539impl GeometryProcessor for PolygonalFaceSetProcessor {
540    fn process(
541        &self,
542        entity: &DecodedEntity,
543        decoder: &mut EntityDecoder,
544        _schema: &IfcSchema,
545    ) -> Result<Mesh> {
546        // IfcPolygonalFaceSet attributes:
547        // 0: Coordinates (IfcCartesianPointList3D)
548        // 1: Closed (optional BOOLEAN)
549        // 2: Faces (LIST of IfcIndexedPolygonalFace)
550        // 3: PnIndex (optional - point index remapping)
551
552        // Get coordinate entity reference
553        let coords_attr = entity.get(0).ok_or_else(|| {
554            Error::geometry("PolygonalFaceSet missing Coordinates".to_string())
555        })?;
556
557        let coord_entity_id = coords_attr.as_entity_ref().ok_or_else(|| {
558            Error::geometry("Expected entity reference for Coordinates".to_string())
559        })?;
560
561        // Parse coordinates - try fast path first
562        use ifc_lite_core::extract_coordinate_list_from_entity;
563
564        let positions = if let Some(raw_bytes) = decoder.get_raw_bytes(coord_entity_id) {
565            extract_coordinate_list_from_entity(raw_bytes).unwrap_or_default()
566        } else {
567            // Fallback path
568            let coords_entity = decoder.decode_by_id(coord_entity_id)?;
569            let coord_list_attr = coords_entity.get(0).ok_or_else(|| {
570                Error::geometry("CartesianPointList3D missing CoordList".to_string())
571            })?;
572            let coord_list = coord_list_attr
573                .as_list()
574                .ok_or_else(|| Error::geometry("Expected coordinate list".to_string()))?;
575            AttributeValue::parse_coordinate_list_3d(coord_list)
576        };
577
578        if positions.is_empty() {
579            return Ok(Mesh::new());
580        }
581
582        // Get faces list (attribute 2)
583        let faces_attr = entity.get(2).ok_or_else(|| {
584            Error::geometry("PolygonalFaceSet missing Faces".to_string())
585        })?;
586
587        let face_refs = faces_attr
588            .as_list()
589            .ok_or_else(|| Error::geometry("Expected faces list".to_string()))?;
590
591        // Optional point remapping list for IfcPolygonalFaceSet.
592        // CoordIndex values refer to this list when present.
593        let pn_index = entity
594            .get(3)
595            .and_then(|attr| attr.as_list())
596            .map(|list| {
597                list.iter()
598                    .filter_map(|value| value.as_int())
599                    .filter(|v| *v > 0)
600                    .map(|v| v as u32)
601                    .collect::<Vec<u32>>()
602            });
603
604        // Pre-allocate indices - estimate 2 triangles per face average
605        let mut indices = Vec::with_capacity(face_refs.len() * 6);
606
607        // Process each face
608        for face_ref in face_refs {
609            let face_id = face_ref.as_entity_ref().ok_or_else(|| {
610                Error::geometry("Expected entity reference for face".to_string())
611            })?;
612
613            let face_entity = decoder.decode_by_id(face_id)?;
614
615            // IfcIndexedPolygonalFace has CoordIndex at attribute 0
616            // IfcIndexedPolygonalFaceWithVoids has CoordIndex at 0 and InnerCoordIndices at 1
617            let coord_index_attr = face_entity.get(0).ok_or_else(|| {
618                Error::geometry("IndexedPolygonalFace missing CoordIndex".to_string())
619            })?;
620
621            let coord_indices = coord_index_attr
622                .as_list()
623                .ok_or_else(|| Error::geometry("Expected coord index list".to_string()))?;
624
625            // Parse face indices (1-based in IFC), with optional PnIndex remapping.
626            let face_indices = Self::parse_index_loop(coord_indices, pn_index.as_deref());
627            if face_indices.len() < 3 {
628                continue;
629            }
630
631            // Parse optional inner loops for IfcIndexedPolygonalFaceWithVoids.
632            let inner_indices = Self::parse_face_inner_indices(&face_entity, pn_index.as_deref());
633
634            // Triangulate the polygon face (including holes when present).
635            Self::triangulate_polygon(&face_indices, &inner_indices, &positions, &mut indices);
636        }
637
638        // Closed shells from some exporters may be consistently inward.
639        // Flip globally to outward winding when needed.
640        let is_closed = entity
641            .get(1)
642            .and_then(|a| a.as_enum())
643            .map(|v| v == "T")
644            .unwrap_or(false);
645        if is_closed {
646            Self::orient_closed_shell_outward(&positions, &mut indices);
647        }
648
649        Ok(Self::build_flat_shaded_mesh(&positions, &indices))
650    }
651
652    fn supported_types(&self) -> Vec<IfcType> {
653        vec![IfcType::IfcPolygonalFaceSet]
654    }
655}
656
657impl Default for PolygonalFaceSetProcessor {
658    fn default() -> Self {
659        Self::new()
660    }
661}