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
// 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/.
//! #1623 Phase 2 "don't-bake" router helpers: eligibility of a mapped source for
//! instancing, and the one-time source mesh into the shared registry that backs the
//! finalize's orphan recovery. The don't-bake decision itself lives at the top-level
//! mapped-item branch of `collect_submeshes_from_item_inner` (see `processing.rs`).
use super::GeometryRouter;
use crate::Mesh;
use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcType};
use std::sync::Arc;
impl GeometryRouter {
/// #1623 Phase 2 eligibility: if this `MappedRepresentation` resolves to exactly
/// ONE direct (non-mapped) geometry item, return that item's express id, else
/// `None`. The don't-bake template↔instance model represents an occurrence with a
/// SINGLE placeholder / a SINGLE re-tagged sub-mesh, so it only applies when the
/// source is one solid. Multi-item sources (each carrying its own per-item
/// colour/rep_identity) and nested-mapped sources fall through to the normal flat
/// materialize — never instanced, never lost. The returned id is used as the
/// placeholder's `geometry_id` so colour resolves EXACTLY as the flat/template
/// sub-mesh does (both key on the nested solid's id).
pub(super) fn mapped_source_single_item(
&self,
mapped_repr: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Option<u32> {
let items_attr = mapped_repr.get(3)?;
let items = decoder.resolve_ref_list(items_attr).ok()?;
match items.as_slice() {
[only] if only.ifc_type != IfcType::IfcMappedItem => Some(only.id),
_ => None,
}
}
/// #1623 Phase 2: mesh a mapped source ONCE into the shared registry (source
/// coords, pre-`MappingTarget`, pre-placement), if not already present. Called on
/// the don't-bake instance path so the streaming finalize can recover an
/// occurrence's geometry from the registry in the (effectively unreachable) case
/// that the template occurrence never materialized — never a silent geometry loss.
/// The meshing runs OUTSIDE the brief lock (no join held under lock).
///
/// It shares `process_mapped_item_cached`'s insert guards — empty mesh and #1257
/// budget trip — because both write the same model-wide cache, but deliberately
/// NOT its recursion into nested mapped items: this is a flat walk. The only
/// caller reaches here having passed `mapped_source_single_item`, so the source
/// is exactly ONE non-mapped item and a nested one cannot occur. Should that ever
/// change, the walk below bails without inserting rather than registering a mesh
/// short of the source's real geometry — a truncated source under a key that does
/// not encode the truncation is served to every later occurrence.
pub(super) fn ensure_shared_mapped_source(
&self,
mapped_repr: &DecodedEntity,
source_id: u32,
decoder: &mut EntityDecoder,
) {
let Some(shared) = self.shared_mapped_item_cache.as_ref() else {
// Without a shared registry there is nothing to recover from; callers
// that arm output instancing always enable the shared cache too.
return;
};
if shared
.lock()
.unwrap_or_else(|e| e.into_inner())
.contains_key(&source_id)
{
return;
}
let mut mesh = Mesh::new();
if let Some(items_attr) = mapped_repr.get(3) {
if let Ok(items) = decoder.resolve_ref_list(items_attr) {
for sub_item in items {
if sub_item.ifc_type == IfcType::IfcMappedItem {
// Unreachable via the only caller (see the doc above).
// Skipping it would leave `mesh` short of the source's real
// geometry, and the insert below would publish that model-wide.
return;
}
if let Some(processor) = self.processors.get(&sub_item.ifc_type) {
if let Ok(mut sub_mesh) = processor.process(
&sub_item,
decoder,
&self.schema,
self.tessellation_quality,
) {
sub_mesh.validate_indices();
self.scale_mesh(&mut sub_mesh);
mesh.merge(&sub_mesh);
}
}
}
}
}
if !mesh.positions.is_empty() && !crate::kernel::budget::tripped() {
shared
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(source_id, Arc::new(mesh));
}
}
}
#[cfg(test)]
mod tests {
use crate::router::GeometryRouter;
use ifc_lite_core::EntityDecoder;
fn fixture() -> String {
std::fs::read_to_string("tests/fixtures/nested_mapped_item.ifc")
.expect("read tests/fixtures/nested_mapped_item.ifc")
}
/// `ensure_shared_mapped_source` walks the source's items FLAT — it has no
/// depth/visited bound, so it cannot recurse into a nested `IfcMappedItem` the
/// way `process_mapped_item_cached` now does. Handed a nested source anyway, it
/// must publish NOTHING: the shared cache is keyed on the source id, so a mesh
/// missing the nested contribution would be served to every later occurrence,
/// including through `process_mapped_item_cached`'s own lookup.
///
/// `#20` is the outer map's representation: its own solid `#16` plus a nested
/// mapped item `#19` on map `#14`.
#[test]
fn nested_source_is_not_registered_flat() {
let content = fixture();
let entity_index = ifc_lite_core::build_entity_index(&content);
let mut decoder = EntityDecoder::with_index(&content, entity_index);
let mut router = GeometryRouter::with_units(&content, &mut decoder);
router.enable_shared_mapped_item_cache(GeometryRouter::new_mapped_item_cache());
let nested_rep = decoder.decode_by_id(20).expect("decode #20");
router.ensure_shared_mapped_source(&nested_rep, 21, &mut decoder);
assert_eq!(
router.mapped_shared_unique_count(),
0,
"a flat walk of a nested source must not be registered"
);
// Negative half: the inner map's representation `#13` is a single solid,
// the shape the caller actually reaches here, and it IS registered.
let flat_rep = decoder.decode_by_id(13).expect("decode #13");
router.ensure_shared_mapped_source(&flat_rep, 14, &mut decoder);
assert_eq!(router.mapped_shared_unique_count(), 1);
}
/// The empty-mesh guard: a source that decodes fine but walks to ZERO items
/// (`#40` is a well-formed `IfcShapeRepresentation` with an empty items list)
/// must not be published into the shared registry — an empty/truncated source
/// cached under `source_id` would be served to every later occurrence sharing
/// that `IfcRepresentationMap`, per the doc on `ensure_shared_mapped_source`.
///
/// The second half proves the guard doesn't poison the id going forward: a
/// later call with the SAME `source_id` but a real single-solid source (`#13`)
/// must still register, because the aborted empty attempt inserted nothing.
#[test]
fn empty_mesh_source_not_registered_then_real_source_still_is() {
let content = fixture();
let entity_index = ifc_lite_core::build_entity_index(&content);
let mut decoder = EntityDecoder::with_index(&content, entity_index);
let mut router = GeometryRouter::with_units(&content, &mut decoder);
router.enable_shared_mapped_item_cache(GeometryRouter::new_mapped_item_cache());
let empty_rep = decoder.decode_by_id(40).expect("decode #40");
router.ensure_shared_mapped_source(&empty_rep, 999, &mut decoder);
assert_eq!(
router.mapped_shared_unique_count(),
0,
"a source that meshes to zero positions must not be cached"
);
let flat_rep = decoder.decode_by_id(13).expect("decode #13");
router.ensure_shared_mapped_source(&flat_rep, 999, &mut decoder);
assert_eq!(
router.mapped_shared_unique_count(),
1,
"a real mesh for the same source id must still register after an \
aborted empty attempt didn't poison the entry"
);
}
}