ifc_lite_geometry/router/
processor.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//! The [`GeometryProcessor`] trait: one IFC representation-item family in,
6//! one mesh out, plus the diagnostics a processor accumulated while doing it.
7//!
8//! Split out of `router/mod.rs` so the drain hook below has room to carry its
9//! own rationale (module-size ratchet).
10
11use crate::tessellation::TessellationQuality;
12use crate::{BoolFailure, Mesh, Result};
13use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcSchema, IfcType};
14
15/// Geometry processor trait
16/// Each processor handles one type of IFC representation
17pub trait GeometryProcessor {
18    /// Process entity into mesh.
19    ///
20    /// `quality` selects tessellation detail; processors that approximate
21    /// curves derive their segment counts from it via
22    /// [`crate::tessellation::scale_segments`]. Processors with no curved
23    /// geometry ignore it. [`TessellationQuality::Medium`] reproduces the
24    /// engine's historical hardcoded behavior.
25    fn process(
26        &self,
27        entity: &DecodedEntity,
28        decoder: &mut EntityDecoder,
29        schema: &IfcSchema,
30        quality: TessellationQuality,
31    ) -> Result<Mesh>;
32
33    /// Process a raw-coordinate item in an element-local RTC frame.
34    ///
35    /// `rtc_file_units` is the relative-to-center offset in the file's own
36    /// length unit, already expressed in the item's coordinate frame; a
37    /// processor that reads its coordinates as `f64` subtracts it BEFORE
38    /// narrowing to `f32`, returns the rebased mesh and sets
39    /// `Mesh::rtc_applied`. Default `None`: the processor has no such hook,
40    /// and the router falls back to [`Self::process`], leaving RTC to the
41    /// final f64 world transform — which cannot recover detail its f32 output
42    /// already lost at national-grid magnitudes (#5026 review). Every
43    /// built-in raw-coordinate processor implements this (faces, faceted
44    /// Brep, tessellated face sets and surface models, #5698); a registered
45    /// override that handles large coordinates must too.
46    fn process_in_rtc_frame(
47        &self,
48        _entity: &DecodedEntity,
49        _decoder: &mut EntityDecoder,
50        _schema: &IfcSchema,
51        _quality: TessellationQuality,
52        _rtc_file_units: (f64, f64, f64),
53    ) -> Option<Result<Mesh>> {
54        None
55    }
56
57    /// Get supported IFC types
58    fn supported_types(&self) -> Vec<IfcType>;
59
60    /// Drain the boolean / CSG failures this processor recorded while meshing.
61    ///
62    /// Default: none — most processors record nothing. Overridden by
63    /// [`crate::processors::BooleanClippingProcessor`], whose failure log had
64    /// no route out of the router at all before #3821: `take_failures` was
65    /// called from tests only, so an unsupported operand, an
66    /// `EmptyOperand` cutter and an unknown operator were recorded into a
67    /// buffer that nothing ever read, and the pipeline reported a clean load.
68    ///
69    /// Drained by `GeometryRouter::drain_processor_failures`, which
70    /// `take_csg_failures` calls, so every consumer of the router's CSG
71    /// diagnostics — the native pipeline and the wasm batch path alike — sees
72    /// these without a second opt-in.
73    fn take_bool_failures(&self) -> Vec<BoolFailure> {
74        Vec::new()
75    }
76
77    /// Number of `BoolFailure` records currently buffered (without draining).
78    /// #4083 (double-count half): lets [`crate::GeometryRouter::process_representation_item`]
79    /// snapshot a before/after delta around ONE item's uncached build, so a CSG
80    /// diagnostic can be attributed to that item's `item_dedup_key`. Default:
81    /// 0 — matches [`Self::take_bool_failures`]'s "most processors record
82    /// nothing" default. Overridden by
83    /// [`crate::processors::BooleanClippingProcessor`].
84    fn bool_failure_count(&self) -> usize {
85        0
86    }
87
88    /// Discard every `BoolFailure` recorded after index `since` (a prior
89    /// [`Self::bool_failure_count`]). #4083 (double-count half): the router
90    /// calls this when a racing sibling router already claimed this item's
91    /// `item_dedup_key` for its own diagnostic, so THIS router's redundant
92    /// record of the same logical operation never reaches
93    /// [`Self::take_bool_failures`]. Default: no-op, matching the 0 default
94    /// above (nothing buffered, nothing to discard).
95    fn truncate_bool_failures_to(&self, _since: usize) {}
96}
97
98// The RTC-hook regression lives beside the trait on purpose: a test-only
99// override implementing `process_in_rtc_frame` cannot compile against a tree
100// where the hook does not exist, so a whole-file production revert (the CI
101// revert oracle) must take this test with it rather than break the build.
102#[cfg(test)]
103mod rtc_hook_tests {
104    use super::GeometryProcessor;
105    use crate::router::structural_tests::surface_member;
106    use crate::router::GeometryRouter;
107    use crate::{Mesh, Result, TessellationQuality};
108    use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcSchema, IfcType};
109
110    /// An override that reads its coordinates in f64 and rebases through the
111    /// router's RTC hook, so sub-ULP detail at national-grid magnitude survives.
112    struct RtcAwareFace;
113
114    impl GeometryProcessor for RtcAwareFace {
115        fn process(
116            &self,
117            entity: &DecodedEntity,
118            decoder: &mut EntityDecoder,
119            schema: &IfcSchema,
120            quality: TessellationQuality,
121        ) -> Result<Mesh> {
122            crate::router::structural_tests::RegisteredFace
123                .process(entity, decoder, schema, quality)
124        }
125
126        fn process_in_rtc_frame(
127            &self,
128            _entity: &DecodedEntity,
129            _decoder: &mut EntityDecoder,
130            _schema: &IfcSchema,
131            _quality: TessellationQuality,
132            rtc_file_units: (f64, f64, f64),
133        ) -> Option<Result<Mesh>> {
134            // A 0.125 m triangle at (5,000,000 + 0.0625, 5,000,000): only
135            // representable once the offset is removed in f64.
136            let corners: [[f64; 3]; 3] = [
137                [5_000_000.062_5, 5_000_000.0, 0.0],
138                [5_000_000.187_5, 5_000_000.0, 0.0],
139                [5_000_000.062_5, 5_000_000.125, 0.0],
140            ];
141            let mut mesh = Mesh::new();
142            for corner in corners {
143                mesh.positions.push((corner[0] - rtc_file_units.0) as f32);
144                mesh.positions.push((corner[1] - rtc_file_units.1) as f32);
145                mesh.positions.push((corner[2] - rtc_file_units.2) as f32);
146            }
147            mesh.indices = vec![0, 1, 2];
148            mesh.rtc_applied = true;
149            Some(Ok(mesh))
150        }
151
152        fn supported_types(&self) -> Vec<IfcType> {
153            vec![IfcType::IfcFaceSurface, IfcType::IfcAdvancedFace]
154        }
155    }
156
157    #[test]
158    fn registered_face_override_rebases_in_f64_through_the_rtc_hook() {
159        let source = format!(
160        "{}{}",
161        surface_member(true, false)
162            .replace("(0.,0.,0.)", "(5000000.,5000000.,0.)")
163            .replace("(10.,0.,0.)", "(5000001.,5000000.,0.)")
164            .replace("(10.,10.,0.)", "(5000001.,5000001.,0.)")
165            .replace("(0.,10.,0.)", "(5000000.,5000001.,0.)")
166            .replace("$,$,#17", "$,#33,#17"),
167        "#30=IFCCARTESIANPOINT((0.,0.,0.));#31=IFCDIRECTION((0.,0.,1.));         #32=IFCDIRECTION((0.,1.,0.));#34=IFCAXIS2PLACEMENT3D(#30,#31,#32);         #33=IFCLOCALPLACEMENT($,#34);"
168    );
169        let mut decoder = EntityDecoder::new(&source);
170        let entity = decoder.decode_by_id(18).unwrap();
171        let mut router = GeometryRouter::with_rtc((-5_000_000.0, 5_000_000.0, 0.0));
172        router.register(Box::new(RtcAwareFace));
173        let mesh = router.process_element(&entity, &mut decoder).unwrap();
174
175        // The narrowing to f32 happened AFTER the rebase, so the 0.125 m extent
176        // survives (a plain `process` override would have collapsed all three
177        // corners onto 5,000,000.0 and the router could only subtract from that).
178        let xs: Vec<f32> = mesh.positions.chunks_exact(3).map(|p| p[0]).collect();
179        let ys: Vec<f32> = mesh.positions.chunks_exact(3).map(|p| p[1]).collect();
180        assert!(
181            (xs.iter().cloned().fold(f32::MIN, f32::max)
182                - xs.iter().cloned().fold(f32::MAX, f32::min)
183                - 0.125)
184                .abs()
185                < 1e-6
186                || (ys.iter().cloned().fold(f32::MIN, f32::max)
187                    - ys.iter().cloned().fold(f32::MAX, f32::min)
188                    - 0.125)
189                    .abs()
190                    < 1e-6,
191            "sub-ULP extent must survive an RTC-aware override: {:?}",
192            mesh.positions
193        );
194        assert_eq!(mesh.indices, vec![0, 1, 2]);
195    }
196}