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}