Skip to main content

ifc_lite_processing/
stream_meta.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//! Shared streaming pre-pass meta resolution.
6//!
7//! The browser pre-passes (`buildPrePassOnce` / `buildPrePassStreaming` in
8//! `wasm-bindings`) each need the same bundle of load-time metadata before
9//! workers can start meshing: the length/plane-angle unit scales, the RTC
10//! (relative-to-centre) offset plus its needs-shift flag, and the building
11//! rotation from `IfcSite`. This module is the single home for that
12//! resolution logic so the three call sites can no longer drift.
13//!
14//! Only the RESOLUTION logic lives here — the wasm side keeps ownership of
15//! WHEN the resulting [`StreamMeta`] is emitted. In particular the streaming
16//! pre-pass still emits its `meta` event MID-SCAN (as soon as
17//! `RTC_SAMPLE_THRESHOLD` geometry jobs are buffered, near the top of the
18//! file) so workers spin up early — the ~17 s → ~3 s time-to-first-geometry
19//! win on a 986 MB file. This helper does not change that timing; it only
20//! factors out the two-vs-three-stage RTC ladder that the two emission sites
21//! previously copied.
22//!
23//! Everything here COMPOSES the existing canonical primitives:
24//! [`resolve_unit_scales`](crate::prepass::resolve_unit_scales),
25//! [`EntityDecoder::seed_unit_scales`],
26//! [`GeometryRouter::with_scale`],
27//! [`GeometryRouter::detect_rtc_offset_from_jobs`],
28//! [`GeometryRouter::detect_rtc_offset_with_fallback`], and the shared
29//! [`LARGE_COORD_THRESHOLD_METERS`](ifc_lite_geometry::LARGE_COORD_THRESHOLD_METERS)
30//! needs-shift constant.
31
32use ifc_lite_core::{EntityDecoder, IfcType};
33use ifc_lite_geometry::{GeometryRouter, LARGE_COORD_THRESHOLD_METERS};
34
35/// A geometry job span as the pre-passes carry it: `(id, start, end, type)`.
36pub type Job = (u32, usize, usize, IfcType);
37
38/// Which RTC-detection ladder [`resolve_stream_meta`] should run.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum MetaMode {
41    /// Streaming early-meta: the caller's `decoder` sees only a PARTIAL entity
42    /// index (the file head scanned so far), so RTC detection runs the 3-stage
43    /// fallback ladder — partial-index detect → full-index re-detect (triggered
44    /// when no large offset was found AND either the `IfcSite` has not been
45    /// scanned yet OR the partial index resolved no usable placement chain) →
46    /// placement-bounds last resort — instead of silently defaulting to
47    /// no-shift and rendering f32 vertex jitter on models whose world offset
48    /// lives in late spatial placements.
49    StreamingPartial,
50    /// The caller's `decoder` already sees the FULL entity index (the
51    /// small-file streaming tail, or the single-pass `buildPrePassOnce`), so a
52    /// single [`GeometryRouter::detect_rtc_offset_with_fallback`] is correct.
53    SmallFileSingle,
54}
55
56/// The load-time metadata both pre-passes emit before workers start meshing.
57#[derive(Debug, Clone, Copy, PartialEq)]
58pub struct StreamMeta {
59    /// IFC length unit → metres.
60    pub length_unit_scale: f64,
61    /// IFC plane-angle unit → radians.
62    pub plane_angle_to_radians: f64,
63    /// World-space RTC offset to subtract before the f32 cast.
64    pub rtc_offset: (f64, f64, f64),
65    /// True when [`Self::rtc_offset`] exceeds the large-coordinate threshold
66    /// and the model must be re-based.
67    pub needs_shift: bool,
68    /// Z-rotation of the `IfcSite` placement, if any.
69    pub building_rotation: Option<f64>,
70}
71
72/// True when any component of the offset exceeds the shared large-coordinate
73/// threshold — the single needs-shift predicate, sharing
74/// [`LARGE_COORD_THRESHOLD_METERS`] with the router's own sampling so both
75/// sides make the identical decision.
76#[inline]
77pub fn coord_is_large(offset: (f64, f64, f64)) -> bool {
78    offset.0.abs() > LARGE_COORD_THRESHOLD_METERS
79        || offset.1.abs() > LARGE_COORD_THRESHOLD_METERS
80        || offset.2.abs() > LARGE_COORD_THRESHOLD_METERS
81}
82
83/// Resolve the full [`StreamMeta`] bundle for one pre-pass emission point.
84///
85/// Seeds the caller's `decoder` with the resolved unit scales (so nothing
86/// downstream re-pays the `IFCPROJECT` hunt) and leaves it seeded on return.
87/// The caller owns emission — this only computes.
88pub fn resolve_stream_meta(
89    mode: MetaMode,
90    content: &[u8],
91    project_id: Option<u32>,
92    site_position: Option<(u32, usize, usize)>,
93    jobs: &[Job],
94    decoder: &mut EntityDecoder,
95) -> StreamMeta {
96    // Unit scales via the shared resolver (handles a missing project-id hint
97    // and partial-index chains internally), then seed the decoder.
98    let unit_scales = crate::prepass::resolve_unit_scales(content, project_id, decoder);
99    let length_unit_scale = unit_scales.length_unit_scale;
100    decoder.seed_unit_scales(length_unit_scale, unit_scales.plane_angle_to_radians);
101
102    let router = GeometryRouter::with_scale(length_unit_scale);
103
104    let rtc_offset = match mode {
105        MetaMode::StreamingPartial => resolve_partial_rtc(
106            &router,
107            content,
108            site_position,
109            jobs,
110            decoder,
111            length_unit_scale,
112        ),
113        MetaMode::SmallFileSingle => {
114            router.detect_rtc_offset_with_fallback(jobs, decoder, content)
115        }
116    };
117    let needs_shift = coord_is_large(rtc_offset);
118
119    let building_rotation =
120        site_position.and_then(|pos| resolve_building_rotation(pos, &router, decoder));
121
122    StreamMeta {
123        length_unit_scale,
124        plane_angle_to_radians: unit_scales.plane_angle_to_radians,
125        rtc_offset,
126        needs_shift,
127        building_rotation,
128    }
129}
130
131/// The streaming early-meta 3-stage RTC ladder against a PARTIAL index.
132///
133/// 1. Detect from the buffered jobs on the partial index.
134/// 2. If no large offset was found AND either the `IfcSite` hasn't been
135///    scanned yet OR the partial index resolved NO usable placement samples,
136///    re-detect against a freshly built FULL index. A successful "no shift"
137///    (0,0,0) that DID resolve samples must not pay for this.
138/// 3. Last resort: only when NO detection (partial or full) found any usable
139///    placement translation, fall back to the raw placement-bounds scan
140///    (unit-scaled to metres).
141///
142/// Mirrors the server needs-shift decision so a browser and the native
143/// pipeline re-base a given model identically.
144fn resolve_partial_rtc(
145    router: &GeometryRouter,
146    content: &[u8],
147    site_position: Option<(u32, usize, usize)>,
148    jobs: &[Job],
149    decoder: &mut EntityDecoder,
150    length_unit_scale: f64,
151) -> (f64, f64, f64) {
152    let detected_rtc = router.detect_rtc_offset_from_jobs(jobs, decoder);
153    let mut rtc_offset = detected_rtc.unwrap_or((0.0, 0.0, 0.0));
154    // True once ANY detection (partial OR the full re-detect below) resolved
155    // usable placement samples — even if it concluded "no shift" (0,0,0). The
156    // placement-bounds fallback must NOT override a successful "no shift".
157    let mut detection_succeeded = detected_rtc.is_some();
158
159    if !coord_is_large(rtc_offset) && (site_position.is_none() || !detection_succeeded) {
160        let full_index = crate::build_entity_index_parallel(content);
161        let mut full_decoder = EntityDecoder::with_index(content, full_index);
162        if let Some(full_rtc) = router.detect_rtc_offset_from_jobs(jobs, &mut full_decoder) {
163            // The full index resolved the placement chain — a successful
164            // detection whether it shifts (large) or not.
165            detection_succeeded = true;
166            if coord_is_large(full_rtc) {
167                rtc_offset = full_rtc;
168            }
169        }
170    }
171
172    if !detection_succeeded && !coord_is_large(rtc_offset) {
173        let raw = ifc_lite_core::scan_placement_bounds(content).rtc_offset();
174        // scan_placement_bounds reads raw IfcCartesianPoint values (FILE
175        // units); the detection path is unit-scaled to metres.
176        rtc_offset = (
177            raw.0 * length_unit_scale,
178            raw.1 * length_unit_scale,
179            raw.2 * length_unit_scale,
180        );
181    }
182    rtc_offset
183}
184
185/// Building rotation = Z-rotation of the `IfcSite` scaled placement, composing
186/// the router's placement resolution with the shared rotation extractor.
187fn resolve_building_rotation(
188    site_pos: (u32, usize, usize),
189    router: &GeometryRouter,
190    decoder: &mut EntityDecoder,
191) -> Option<f64> {
192    let (site_id, start, end) = site_pos;
193    let site_entity = decoder.decode_at_with_id(site_id, start, end).ok()?;
194    let matrix = router.resolve_scaled_placement(&site_entity, decoder).ok()?;
195    ifc_lite_geometry::rotation_angle_about_z(&matrix)
196}
197
198#[cfg(test)]
199#[path = "stream_meta_tests.rs"]
200mod tests;