@ifc-lite/renderer 1.41.1 → 1.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/camera-animation.d.ts +20 -32
- package/dist/camera-animation.d.ts.map +1 -1
- package/dist/camera-animation.js +71 -358
- package/dist/camera-animation.js.map +1 -1
- package/dist/camera-controls.d.ts +11 -51
- package/dist/camera-controls.d.ts.map +1 -1
- package/dist/camera-controls.js +140 -12
- package/dist/camera-controls.js.map +1 -1
- package/dist/camera-first-person.d.ts +52 -0
- package/dist/camera-first-person.d.ts.map +1 -0
- package/dist/camera-first-person.js +119 -0
- package/dist/camera-first-person.js.map +1 -0
- package/dist/camera-fit-policy.d.ts.map +1 -1
- package/dist/camera-fit-policy.js +18 -7
- package/dist/camera-fit-policy.js.map +1 -1
- package/dist/camera-framing.d.ts +86 -0
- package/dist/camera-framing.d.ts.map +1 -0
- package/dist/camera-framing.js +230 -0
- package/dist/camera-framing.js.map +1 -0
- package/dist/camera-guards.d.ts +148 -0
- package/dist/camera-guards.d.ts.map +1 -0
- package/dist/camera-guards.js +150 -0
- package/dist/camera-guards.js.map +1 -0
- package/dist/camera-matrices.d.ts +14 -0
- package/dist/camera-matrices.d.ts.map +1 -0
- package/dist/camera-matrices.js +146 -0
- package/dist/camera-matrices.js.map +1 -0
- package/dist/camera-preset-view.d.ts +56 -0
- package/dist/camera-preset-view.d.ts.map +1 -0
- package/dist/camera-preset-view.js +180 -0
- package/dist/camera-preset-view.js.map +1 -0
- package/dist/camera-projection.d.ts +1 -1
- package/dist/camera-projection.d.ts.map +1 -1
- package/dist/camera-projection.js +83 -30
- package/dist/camera-projection.js.map +1 -1
- package/dist/camera-state.d.ts +62 -0
- package/dist/camera-state.d.ts.map +1 -0
- package/dist/camera-state.js +5 -0
- package/dist/camera-state.js.map +1 -0
- package/dist/camera.d.ts +22 -11
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +110 -85
- package/dist/camera.js.map +1 -1
- package/dist/deviation/deviation-pipeline.d.ts.map +1 -1
- package/dist/deviation/deviation-pipeline.js +21 -4
- package/dist/deviation/deviation-pipeline.js.map +1 -1
- package/dist/environment.d.ts.map +1 -1
- package/dist/environment.js +23 -1
- package/dist/environment.js.map +1 -1
- package/dist/index.d.ts +333 -43
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +753 -704
- package/dist/index.js.map +1 -1
- package/dist/math.d.ts +59 -2
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +173 -23
- package/dist/math.js.map +1 -1
- package/dist/model-bounds-tracker.d.ts +96 -0
- package/dist/model-bounds-tracker.d.ts.map +1 -0
- package/dist/model-bounds-tracker.js +147 -0
- package/dist/model-bounds-tracker.js.map +1 -0
- package/dist/pointcloud/point-cloud-node.d.ts.map +1 -1
- package/dist/pointcloud/point-cloud-node.js +24 -4
- package/dist/pointcloud/point-cloud-node.js.map +1 -1
- package/dist/render-degradation.d.ts +77 -0
- package/dist/render-degradation.d.ts.map +1 -0
- package/dist/render-degradation.js +52 -0
- package/dist/render-degradation.js.map +1 -0
- package/dist/render-section-draw.d.ts +51 -0
- package/dist/render-section-draw.d.ts.map +1 -0
- package/dist/render-section-draw.js +66 -0
- package/dist/render-section-draw.js.map +1 -0
- package/dist/render-section-plane.d.ts +128 -0
- package/dist/render-section-plane.d.ts.map +1 -0
- package/dist/render-section-plane.js +298 -0
- package/dist/render-section-plane.js.map +1 -0
- package/dist/renderer-overlays.d.ts +144 -0
- package/dist/renderer-overlays.d.ts.map +1 -0
- package/dist/renderer-overlays.js +257 -0
- package/dist/renderer-overlays.js.map +1 -0
- package/dist/renderer-symbolic-overlays.d.ts +52 -0
- package/dist/renderer-symbolic-overlays.d.ts.map +1 -0
- package/dist/renderer-symbolic-overlays.js +115 -0
- package/dist/renderer-symbolic-overlays.js.map +1 -0
- package/dist/scene-geometry.d.ts +32 -0
- package/dist/scene-geometry.d.ts.map +1 -1
- package/dist/scene-geometry.js +66 -0
- package/dist/scene-geometry.js.map +1 -1
- package/dist/scene.d.ts +86 -1
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +391 -201
- package/dist/scene.js.map +1 -1
- package/dist/section-2d-lift.d.ts +105 -0
- package/dist/section-2d-lift.d.ts.map +1 -0
- package/dist/section-2d-lift.js +162 -0
- package/dist/section-2d-lift.js.map +1 -0
- package/dist/section-2d-line-buffer.d.ts +58 -0
- package/dist/section-2d-line-buffer.d.ts.map +1 -0
- package/dist/section-2d-line-buffer.js +109 -0
- package/dist/section-2d-line-buffer.js.map +1 -0
- package/dist/section-2d-overlay.d.ts +43 -75
- package/dist/section-2d-overlay.d.ts.map +1 -1
- package/dist/section-2d-overlay.js +189 -559
- package/dist/section-2d-overlay.js.map +1 -1
- package/dist/section-plane-basis.d.ts +22 -3
- package/dist/section-plane-basis.d.ts.map +1 -1
- package/dist/section-plane-basis.js +51 -10
- package/dist/section-plane-basis.js.map +1 -1
- package/dist/section-plane.d.ts.map +1 -1
- package/dist/section-plane.js +10 -1
- package/dist/section-plane.js.map +1 -1
- package/dist/shaders/section-2d-overlay.wgsl.d.ts +102 -0
- package/dist/shaders/section-2d-overlay.wgsl.d.ts.map +1 -0
- package/dist/shaders/section-2d-overlay.wgsl.js +281 -0
- package/dist/shaders/section-2d-overlay.wgsl.js.map +1 -0
- package/dist/visual-enhancement.d.ts +33 -0
- package/dist/visual-enhancement.d.ts.map +1 -0
- package/dist/visual-enhancement.js +46 -0
- package/dist/visual-enhancement.js.map +1 -0
- package/package.json +4 -3
package/dist/scene.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { BATCH_CONSTANTS } from './constants.js';
|
|
5
5
|
import { prepareRayDirInv, raycastBoundingBoxes, raycastTriangles, rayIntersectsBox, } from './scene-raycaster.js';
|
|
6
6
|
import { selectBoundingBoxesInRect } from './scene-rect-select.js';
|
|
7
|
-
import { mergeGeometry, splitMeshDataForBufferLimit, colorSaltByte, packEntityLane } from './scene-geometry.js';
|
|
7
|
+
import { mergeGeometry, splitMeshDataForBufferLimit, colorSaltByte, packEntityLane, worldAabbFromPieces } from './scene-geometry.js';
|
|
8
8
|
import { sumResidentGpuBytes } from './render-stats.js';
|
|
9
9
|
import { simplifyIndicesByClustering, lodCellSizeForBounds, LOD_MIN_TRIANGLES } from './lod-simplify.js';
|
|
10
10
|
import { quantizeInterleaved } from './quantize.js';
|
|
@@ -36,10 +36,23 @@ export class Scene {
|
|
|
36
36
|
* Refcounted: entries die when the last referencing mesh is removed / on clear(). */
|
|
37
37
|
sharedTextures = new Map();
|
|
38
38
|
texturedDevice; // #961: cached for textured-mesh re-upload on translate
|
|
39
|
-
|
|
39
|
+
/** GPU-instancing: unique templates + per-occurrence buffers (fed by
|
|
40
|
+
* addInstancedShard). SLOT-STABLE and therefore SPARSE: a per-model removal
|
|
41
|
+
* (`removeInstancedTemplatesForModel`) leaves `undefined` holes instead of
|
|
42
|
+
* splicing, because `InstancedOccurrence.templateIndex` holds the slot by
|
|
43
|
+
* value and a splice would silently repoint every later occurrence at the
|
|
44
|
+
* wrong template. New templates always append at `length`; freed slots are
|
|
45
|
+
* never recycled. Iterate `getInstancedTemplates()` (compacted, cached) for
|
|
46
|
+
* the draw/pick/accounting walks — never this array directly. */
|
|
47
|
+
instancedTemplates = [];
|
|
48
|
+
/** Compacted live view of `instancedTemplates`, rebuilt on mutation. */
|
|
49
|
+
liveInstancedTemplates = [];
|
|
40
50
|
instancedVisible = true; // GPU-instancing: hidden in Types view mode (instanced geometry is class-0 occurrences)
|
|
41
51
|
instancedEntityMap = new Map(); // express_id -> occurrences, for per-instance selection/overlay patching
|
|
42
|
-
|
|
52
|
+
/** Compact CPU geometry per template, slot-aligned with `instancedTemplates`
|
|
53
|
+
* (so equally sparse) for CPU consumers. Also emptied wholesale on geometry
|
|
54
|
+
* release — every reader already tolerates a missing entry. */
|
|
55
|
+
instancedTemplateCpu = [];
|
|
43
56
|
instancedDevice; // cached for per-instance flag/colour writeBuffer updates
|
|
44
57
|
instancedSelected = new Set(); // currently flag-selected instanced express_ids
|
|
45
58
|
instancedHidden = new Set(); // currently hidden instanced express_ids (hide/isolate)
|
|
@@ -1876,94 +1889,156 @@ export class Scene {
|
|
|
1876
1889
|
// synchronously, so settle-sensitive consumers need this flag until the
|
|
1877
1890
|
// time-sliced rebuild swaps the new batch array in.
|
|
1878
1891
|
this.finalizeInProgress = true;
|
|
1879
|
-
|
|
1892
|
+
const scene = this;
|
|
1880
1893
|
const oldFragments = this.streamingFragments;
|
|
1881
1894
|
const oldBatches = this.batchedMeshes;
|
|
1882
1895
|
const fragmentSet = new Set(oldFragments);
|
|
1883
|
-
|
|
1884
|
-
//
|
|
1885
|
-
//
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1896
|
+
const oldBatchSet = new Set(oldBatches);
|
|
1897
|
+
// The chunked rebuild spans multiple event-loop turns via setTimeout, so a
|
|
1898
|
+
// throw inside a LATER chunk is a distinct macrotask — it does NOT reject
|
|
1899
|
+
// the promise below just because that code sits inside its executor (only
|
|
1900
|
+
// a SYNCHRONOUS throw during the executor's own call frame does that per
|
|
1901
|
+
// spec). Every entry point that can fail — the synchronous preamble AND
|
|
1902
|
+
// each chunked continuation — must therefore run under its own try/catch
|
|
1903
|
+
// that explicitly calls `reject`, mirroring finalizeStreamingInner's
|
|
1904
|
+
// try/finally contract: restore oldFragments/oldBatches, free only what
|
|
1905
|
+
// this attempt created, defer dropAllPartialCaches() until success, and
|
|
1906
|
+
// always clear finalizeInProgress.
|
|
1907
|
+
return new Promise((resolve, reject) => {
|
|
1908
|
+
const newBatches = [];
|
|
1909
|
+
// Every batch this attempt creates, paired with the bucket that now owns
|
|
1910
|
+
// it and the value it displaced. `newBatches` alone is not enough to roll
|
|
1911
|
+
// back: processChunk publishes each batch into `bucket.batchedMesh`, so
|
|
1912
|
+
// freeing it without repairing the owner leaves the bucket map pointing
|
|
1913
|
+
// at destroyed GPU resources (use-after-free on the next bucket-driven
|
|
1914
|
+
// access). `previous` is null for the freshly built buckets and, for a
|
|
1915
|
+
// carried COLD bucket a re-grouped meshData landed in, the shell that the
|
|
1916
|
+
// restored `batchedMeshes` still holds — which must be put back, not
|
|
1917
|
+
// nulled.
|
|
1918
|
+
const createdOwned = [];
|
|
1919
|
+
let carriedCold = [];
|
|
1920
|
+
let pendingKeys = [];
|
|
1921
|
+
let keyIdx = 0;
|
|
1922
|
+
function rollback() {
|
|
1923
|
+
// Free ONLY what this attempt created — carried cold shells and
|
|
1924
|
+
// anything already live before the rebuild must be left alone (they
|
|
1925
|
+
// are what the restored arrays point back at). Iterating the owned
|
|
1926
|
+
// pairs rather than `newBatches` also skips the carried cold shells
|
|
1927
|
+
// appended just before the swap, which this attempt did not create.
|
|
1928
|
+
for (const { bucket, previous, batch } of createdOwned) {
|
|
1929
|
+
// Repair the owner BEFORE the free, so no bucket is ever observable
|
|
1930
|
+
// holding a destroyed batch.
|
|
1931
|
+
if (bucket.batchedMesh === batch)
|
|
1932
|
+
bucket.batchedMesh = previous;
|
|
1933
|
+
if (!oldBatchSet.has(batch) && !fragmentSet.has(batch)) {
|
|
1934
|
+
destroyGpuResources(batch);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
scene.streamingFragments = oldFragments;
|
|
1938
|
+
scene.batchedMeshes = oldBatches;
|
|
1939
|
+
scene.finalizeInProgress = false;
|
|
1892
1940
|
}
|
|
1893
|
-
for (const md of bucket.meshData)
|
|
1894
|
-
allMeshData.push(md);
|
|
1895
|
-
}
|
|
1896
|
-
// 2. Clear bucket/batch state
|
|
1897
|
-
this.buckets.clear();
|
|
1898
|
-
this.meshDataBucket = new Map();
|
|
1899
|
-
this.activeBucketKey.clear();
|
|
1900
|
-
this.lastDrawnFrame.clear();
|
|
1901
|
-
this.residencyRestoreQueue.clear();
|
|
1902
|
-
this.pendingBatchKeys.clear();
|
|
1903
|
-
this.dropAllPartialCaches();
|
|
1904
|
-
// Re-seat the carried cold shells in the fresh bucket map.
|
|
1905
|
-
for (const [key, bucket] of carriedCold)
|
|
1906
|
-
this.buckets.set(key, bucket);
|
|
1907
|
-
// 3. Re-group meshData by current color (and grid cell) — fast
|
|
1908
|
-
for (const meshData of allMeshData) {
|
|
1909
|
-
const baseKey = this.bucketBaseKey(meshData);
|
|
1910
|
-
const bucketKey = this.resolveActiveBucket(baseKey, meshData);
|
|
1911
|
-
let bucket = this.buckets.get(bucketKey);
|
|
1912
|
-
if (!bucket) {
|
|
1913
|
-
bucket = { key: bucketKey, meshData: [], batchedMesh: null, vertexBytes: 0 };
|
|
1914
|
-
this.buckets.set(bucketKey, bucket);
|
|
1915
|
-
}
|
|
1916
|
-
bucket.meshData.push(meshData);
|
|
1917
|
-
this.meshDataBucket.set(meshData, bucket);
|
|
1918
|
-
this.pendingBatchKeys.add(bucketKey);
|
|
1919
|
-
}
|
|
1920
|
-
// Build new batches into a temporary array so the old batchedMeshes
|
|
1921
|
-
// (streaming fragments) keep rendering until the swap is complete.
|
|
1922
|
-
const newBatches = [];
|
|
1923
|
-
const pendingKeys = Array.from(this.pendingBatchKeys);
|
|
1924
|
-
this.pendingBatchKeys.clear();
|
|
1925
|
-
// --- Async: rebuild batches in time-sliced chunks ---
|
|
1926
|
-
let keyIdx = 0;
|
|
1927
|
-
const scene = this;
|
|
1928
|
-
return new Promise((resolve) => {
|
|
1929
1941
|
function processChunk() {
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1942
|
+
try {
|
|
1943
|
+
const chunkStart = performance.now();
|
|
1944
|
+
while (keyIdx < pendingKeys.length) {
|
|
1945
|
+
const key = pendingKeys[keyIdx++];
|
|
1946
|
+
const bucket = scene.buckets.get(key);
|
|
1947
|
+
if (!bucket || bucket.meshData.length === 0) {
|
|
1948
|
+
scene.buckets.delete(key);
|
|
1949
|
+
continue;
|
|
1950
|
+
}
|
|
1951
|
+
const color = bucket.meshData[0].color;
|
|
1952
|
+
const previous = bucket.batchedMesh;
|
|
1953
|
+
const batchedMesh = scene.createBatchedMesh(bucket.meshData, color, device, pipeline, key);
|
|
1954
|
+
bucket.batchedMesh = batchedMesh;
|
|
1955
|
+
createdOwned.push({ bucket, previous, batch: batchedMesh });
|
|
1956
|
+
newBatches.push(batchedMesh);
|
|
1957
|
+
// Check time budget — yield if exceeded
|
|
1958
|
+
if (performance.now() - chunkStart >= budgetMs) {
|
|
1959
|
+
setTimeout(processChunk, 0);
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
// Carried cold shells stay drawable-when-restored: keep them in the
|
|
1964
|
+
// flat array (their buffers are already destroyed; the draw loop
|
|
1965
|
+
// skips gpuResident === false and the restore path revives them).
|
|
1966
|
+
for (const [, bucket] of carriedCold) {
|
|
1967
|
+
if (bucket.batchedMesh)
|
|
1968
|
+
newBatches.push(bucket.batchedMesh);
|
|
1937
1969
|
}
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
//
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1970
|
+
// All batches built — atomic swap so renderer never sees an empty array
|
|
1971
|
+
scene.batchedMeshes = newBatches;
|
|
1972
|
+
// Cached partial (filtered-visibility) batches are keyed by their
|
|
1973
|
+
// SOURCE batch, so they only go stale once the replacement batches
|
|
1974
|
+
// are live — dropping them any earlier destroys GPU resources a
|
|
1975
|
+
// mid-rebuild failure could never get back (same rationale as
|
|
1976
|
+
// finalizeStreamingInner).
|
|
1977
|
+
scene.dropAllPartialCaches();
|
|
1978
|
+
// Destroy old fragment/batch GPU resources
|
|
1979
|
+
for (const fragment of oldFragments)
|
|
1980
|
+
destroyGpuResources(fragment);
|
|
1981
|
+
for (const batch of oldBatches) {
|
|
1982
|
+
if (!fragmentSet.has(batch))
|
|
1983
|
+
destroyGpuResources(batch);
|
|
1946
1984
|
}
|
|
1985
|
+
scene.finalizeInProgress = false;
|
|
1986
|
+
resolve();
|
|
1947
1987
|
}
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
for (const [, bucket] of carriedCold) {
|
|
1952
|
-
if (bucket.batchedMesh)
|
|
1953
|
-
newBatches.push(bucket.batchedMesh);
|
|
1988
|
+
catch (err) {
|
|
1989
|
+
rollback();
|
|
1990
|
+
reject(err);
|
|
1954
1991
|
}
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
//
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1992
|
+
}
|
|
1993
|
+
try {
|
|
1994
|
+
// --- Synchronous preamble (fast O(N) bookkeeping) ---
|
|
1995
|
+
scene.streamingFragments = [];
|
|
1996
|
+
// 1. Collect ALL accumulated meshData (cold buckets carried as sealed
|
|
1997
|
+
// shells — see the sync finalize for the rationale)
|
|
1998
|
+
const allMeshData = [];
|
|
1999
|
+
for (const [key, bucket] of scene.buckets) {
|
|
2000
|
+
if (scene.coldBuckets.has(key) && bucket.meshData.length === 0 && bucket.batchedMesh) {
|
|
2001
|
+
carriedCold.push([key, bucket]);
|
|
2002
|
+
continue;
|
|
2003
|
+
}
|
|
2004
|
+
for (const md of bucket.meshData)
|
|
2005
|
+
allMeshData.push(md);
|
|
1963
2006
|
}
|
|
1964
|
-
|
|
1965
|
-
|
|
2007
|
+
// 2. Clear bucket/batch state. dropAllPartialCaches() is deliberately
|
|
2008
|
+
// NOT called here — see the success path above.
|
|
2009
|
+
scene.buckets.clear();
|
|
2010
|
+
scene.meshDataBucket = new Map();
|
|
2011
|
+
scene.activeBucketKey.clear();
|
|
2012
|
+
scene.lastDrawnFrame.clear();
|
|
2013
|
+
scene.residencyRestoreQueue.clear();
|
|
2014
|
+
scene.pendingBatchKeys.clear();
|
|
2015
|
+
// Re-seat the carried cold shells in the fresh bucket map.
|
|
2016
|
+
for (const [key, bucket] of carriedCold)
|
|
2017
|
+
scene.buckets.set(key, bucket);
|
|
2018
|
+
// 3. Re-group meshData by current color (and grid cell) — fast
|
|
2019
|
+
for (const meshData of allMeshData) {
|
|
2020
|
+
const baseKey = scene.bucketBaseKey(meshData);
|
|
2021
|
+
const bucketKey = scene.resolveActiveBucket(baseKey, meshData);
|
|
2022
|
+
let bucket = scene.buckets.get(bucketKey);
|
|
2023
|
+
if (!bucket) {
|
|
2024
|
+
bucket = { key: bucketKey, meshData: [], batchedMesh: null, vertexBytes: 0 };
|
|
2025
|
+
scene.buckets.set(bucketKey, bucket);
|
|
2026
|
+
}
|
|
2027
|
+
bucket.meshData.push(meshData);
|
|
2028
|
+
scene.meshDataBucket.set(meshData, bucket);
|
|
2029
|
+
scene.pendingBatchKeys.add(bucketKey);
|
|
2030
|
+
}
|
|
2031
|
+
// Build new batches into a temporary array so the old batchedMeshes
|
|
2032
|
+
// (streaming fragments) keep rendering until the swap is complete.
|
|
2033
|
+
pendingKeys = Array.from(scene.pendingBatchKeys);
|
|
2034
|
+
scene.pendingBatchKeys.clear();
|
|
2035
|
+
}
|
|
2036
|
+
catch (err) {
|
|
2037
|
+
rollback();
|
|
2038
|
+
reject(err);
|
|
2039
|
+
return;
|
|
1966
2040
|
}
|
|
2041
|
+
// --- Async: rebuild batches in time-sliced chunks ---
|
|
1967
2042
|
// Start first chunk immediately (no setTimeout delay)
|
|
1968
2043
|
processChunk();
|
|
1969
2044
|
});
|
|
@@ -1974,40 +2049,17 @@ export class Scene {
|
|
|
1974
2049
|
return;
|
|
1975
2050
|
}
|
|
1976
2051
|
// Preserve lightweight per-entity bounds so large-model picking and
|
|
1977
|
-
// selection can continue to work after we discard CPU mesh arrays.
|
|
2052
|
+
// selection can continue to work after we discard CPU mesh arrays. An
|
|
2053
|
+
// entity with no usable vertex gets NO entry: after release, the keys of
|
|
2054
|
+
// `boundingBoxes` become the authoritative id set (`getAllMeshDataExpressIds`),
|
|
2055
|
+
// so caching the inverted-empty sentinel here would publish a geometry-less
|
|
2056
|
+
// entity to every CPU consumer with a garbage box (#2480).
|
|
1978
2057
|
for (const [expressId, pieces] of this.meshDataMap) {
|
|
1979
2058
|
if (this.boundingBoxes.has(expressId))
|
|
1980
2059
|
continue;
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
const positions = piece.positions;
|
|
1985
|
-
// world = origin + position (per-element local frame); bake WORLD bbox.
|
|
1986
|
-
const ox = piece.origin ? piece.origin[0] : 0;
|
|
1987
|
-
const oy = piece.origin ? piece.origin[1] : 0;
|
|
1988
|
-
const oz = piece.origin ? piece.origin[2] : 0;
|
|
1989
|
-
for (let i = 0; i < positions.length; i += 3) {
|
|
1990
|
-
const x = positions[i] + ox;
|
|
1991
|
-
const y = positions[i + 1] + oy;
|
|
1992
|
-
const z = positions[i + 2] + oz;
|
|
1993
|
-
if (x < minX)
|
|
1994
|
-
minX = x;
|
|
1995
|
-
if (y < minY)
|
|
1996
|
-
minY = y;
|
|
1997
|
-
if (z < minZ)
|
|
1998
|
-
minZ = z;
|
|
1999
|
-
if (x > maxX)
|
|
2000
|
-
maxX = x;
|
|
2001
|
-
if (y > maxY)
|
|
2002
|
-
maxY = y;
|
|
2003
|
-
if (z > maxZ)
|
|
2004
|
-
maxZ = z;
|
|
2005
|
-
}
|
|
2006
|
-
}
|
|
2007
|
-
this.boundingBoxes.set(expressId, {
|
|
2008
|
-
min: { x: minX, y: minY, z: minZ },
|
|
2009
|
-
max: { x: maxX, y: maxY, z: maxZ },
|
|
2010
|
-
});
|
|
2060
|
+
const bbox = worldAabbFromPieces(pieces);
|
|
2061
|
+
if (bbox)
|
|
2062
|
+
this.boundingBoxes.set(expressId, bbox);
|
|
2011
2063
|
}
|
|
2012
2064
|
this.streamingFragments = [];
|
|
2013
2065
|
this.buckets.clear();
|
|
@@ -2051,40 +2103,15 @@ export class Scene {
|
|
|
2051
2103
|
`Call finalizeStreaming()/rebuildPendingBatches() first.`);
|
|
2052
2104
|
return;
|
|
2053
2105
|
}
|
|
2054
|
-
// 1. Precompute and cache ALL entity bounding boxes before releasing data
|
|
2106
|
+
// 1. Precompute and cache ALL entity bounding boxes before releasing data.
|
|
2107
|
+
// Same rule as `finishEphemeralStreaming`: an entity with no usable vertex
|
|
2108
|
+
// gets no entry rather than the inverted-empty sentinel (#2480).
|
|
2055
2109
|
for (const [expressId, pieces] of this.meshDataMap) {
|
|
2056
2110
|
if (this.boundingBoxes.has(expressId))
|
|
2057
2111
|
continue;
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
const positions = piece.positions;
|
|
2062
|
-
// world = origin + position (per-element local frame); bake WORLD bbox.
|
|
2063
|
-
const ox = piece.origin ? piece.origin[0] : 0;
|
|
2064
|
-
const oy = piece.origin ? piece.origin[1] : 0;
|
|
2065
|
-
const oz = piece.origin ? piece.origin[2] : 0;
|
|
2066
|
-
for (let i = 0; i < positions.length; i += 3) {
|
|
2067
|
-
const x = positions[i] + ox;
|
|
2068
|
-
const y = positions[i + 1] + oy;
|
|
2069
|
-
const z = positions[i + 2] + oz;
|
|
2070
|
-
if (x < minX)
|
|
2071
|
-
minX = x;
|
|
2072
|
-
if (y < minY)
|
|
2073
|
-
minY = y;
|
|
2074
|
-
if (z < minZ)
|
|
2075
|
-
minZ = z;
|
|
2076
|
-
if (x > maxX)
|
|
2077
|
-
maxX = x;
|
|
2078
|
-
if (y > maxY)
|
|
2079
|
-
maxY = y;
|
|
2080
|
-
if (z > maxZ)
|
|
2081
|
-
maxZ = z;
|
|
2082
|
-
}
|
|
2083
|
-
}
|
|
2084
|
-
this.boundingBoxes.set(expressId, {
|
|
2085
|
-
min: { x: minX, y: minY, z: minZ },
|
|
2086
|
-
max: { x: maxX, y: maxY, z: maxZ },
|
|
2087
|
-
});
|
|
2112
|
+
const bbox = worldAabbFromPieces(pieces);
|
|
2113
|
+
if (bbox)
|
|
2114
|
+
this.boundingBoxes.set(expressId, bbox);
|
|
2088
2115
|
}
|
|
2089
2116
|
// 2. Clear the heavy data structures — typed arrays become GC-eligible
|
|
2090
2117
|
this.meshDataMap.clear();
|
|
@@ -2636,7 +2663,8 @@ export class Scene {
|
|
|
2636
2663
|
partialBatches: this.partialBatchCache.values(),
|
|
2637
2664
|
meshes: this.meshes,
|
|
2638
2665
|
textured: this.texturedMeshes,
|
|
2639
|
-
|
|
2666
|
+
// Live templates only — freed slots are holes with destroyed buffers.
|
|
2667
|
+
instanced: this.liveInstancedTemplates,
|
|
2640
2668
|
});
|
|
2641
2669
|
}
|
|
2642
2670
|
/**
|
|
@@ -2653,7 +2681,148 @@ export class Scene {
|
|
|
2653
2681
|
getInstancedTemplates() {
|
|
2654
2682
|
if (!this.instancedVisible)
|
|
2655
2683
|
return EMPTY_INSTANCED_TEMPLATES;
|
|
2656
|
-
return this.
|
|
2684
|
+
return this.liveInstancedTemplates;
|
|
2685
|
+
}
|
|
2686
|
+
/** Rebuild the compacted live-template view after a slot add/remove. */
|
|
2687
|
+
refreshLiveInstancedTemplates() {
|
|
2688
|
+
const live = [];
|
|
2689
|
+
for (const t of this.instancedTemplates) {
|
|
2690
|
+
if (t)
|
|
2691
|
+
live.push(t);
|
|
2692
|
+
}
|
|
2693
|
+
this.liveInstancedTemplates = live;
|
|
2694
|
+
}
|
|
2695
|
+
/** Model indices that currently own at least one instanced template. Unlike
|
|
2696
|
+
* `getInstancedTemplates()` this ignores the Types-view visibility toggle —
|
|
2697
|
+
* hiding the pass does not change who owns what. */
|
|
2698
|
+
getInstancedModelIndices() {
|
|
2699
|
+
const seen = new Set();
|
|
2700
|
+
for (const t of this.instancedTemplates) {
|
|
2701
|
+
if (t)
|
|
2702
|
+
seen.add(t.modelIndex);
|
|
2703
|
+
}
|
|
2704
|
+
return [...seen];
|
|
2705
|
+
}
|
|
2706
|
+
/**
|
|
2707
|
+
* Free every instanced template owned by `modelIndex`: destroy exactly its
|
|
2708
|
+
* own vertex/index/instance buffers, blank its slots (leaving holes, so no
|
|
2709
|
+
* other model's `templateIndex` shifts), prune its occurrences from
|
|
2710
|
+
* `instancedEntityMap`, and drop the per-id selection/hidden/override
|
|
2711
|
+
* bookkeeping for ids that lost their LAST occurrence. Express ids shared
|
|
2712
|
+
* with another model — the federated case before id offsetting — keep the
|
|
2713
|
+
* surviving model's occurrences.
|
|
2714
|
+
*
|
|
2715
|
+
* `boundingBoxes` bookkeeping (#2073): an id that ALSO owns flat geometry is
|
|
2716
|
+
* a mixed id — `removeMeshesForEntity` owns that cache, so it is left alone
|
|
2717
|
+
* here even when the id's last instanced occurrence is pruned. An
|
|
2718
|
+
* instanced-only id that loses its last occurrence has nothing left to keep
|
|
2719
|
+
* a cached box for, so its entry is dropped — otherwise bbox-raycast and
|
|
2720
|
+
* `getBounds()` (post geometry-release) keep finding/sizing an element that
|
|
2721
|
+
* no longer has geometry. An id that keeps SOME occurrences (the shared,
|
|
2722
|
+
* federated case) has its box recomputed from just the survivors, not left
|
|
2723
|
+
* as the stale union that also covered the freed model's occurrences.
|
|
2724
|
+
*
|
|
2725
|
+
* Returns the number of templates removed (0 for an unknown model, which is a
|
|
2726
|
+
* no-op).
|
|
2727
|
+
*/
|
|
2728
|
+
removeInstancedTemplatesForModel(modelIndex) {
|
|
2729
|
+
const freed = new Set();
|
|
2730
|
+
for (let i = 0; i < this.instancedTemplates.length; i++) {
|
|
2731
|
+
const t = this.instancedTemplates[i];
|
|
2732
|
+
if (!t || t.modelIndex !== modelIndex)
|
|
2733
|
+
continue;
|
|
2734
|
+
t.vertexBuffer.destroy();
|
|
2735
|
+
t.indexBuffer.destroy();
|
|
2736
|
+
t.instanceBuffer.destroy();
|
|
2737
|
+
this.instancedTemplates[i] = undefined;
|
|
2738
|
+
this.instancedTemplateCpu[i] = undefined;
|
|
2739
|
+
freed.add(i);
|
|
2740
|
+
}
|
|
2741
|
+
if (freed.size === 0)
|
|
2742
|
+
return 0;
|
|
2743
|
+
for (const [eid, occurrences] of this.instancedEntityMap) {
|
|
2744
|
+
const kept = occurrences.filter((o) => !freed.has(o.templateIndex));
|
|
2745
|
+
if (kept.length === occurrences.length)
|
|
2746
|
+
continue;
|
|
2747
|
+
if (kept.length > 0) {
|
|
2748
|
+
this.instancedEntityMap.set(eid, kept);
|
|
2749
|
+
// Shared id: some occurrences survive in another model. The cached box
|
|
2750
|
+
// is a union that also covered the freed occurrences — recompute it
|
|
2751
|
+
// from exactly the survivors rather than leave it oversized.
|
|
2752
|
+
this.recomputeInstancedWorldAabb(eid, kept);
|
|
2753
|
+
continue;
|
|
2754
|
+
}
|
|
2755
|
+
// Last occurrence gone: the id is no longer instanced at all.
|
|
2756
|
+
this.instancedEntityMap.delete(eid);
|
|
2757
|
+
this.instancedSelected.delete(eid);
|
|
2758
|
+
this.instancedHidden.delete(eid);
|
|
2759
|
+
this.instancedOverridden.delete(eid);
|
|
2760
|
+
// Only drop the cached box when the id has no flat geometry either — a
|
|
2761
|
+
// mixed id's box is owned by the flat-removal path (removeMeshesForEntity),
|
|
2762
|
+
// which clears it on its own schedule.
|
|
2763
|
+
if (!this.meshDataMap.has(eid)) {
|
|
2764
|
+
this.boundingBoxes.delete(eid);
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
this.refreshLiveInstancedTemplates();
|
|
2768
|
+
// Occurrence population changed → force the next visibility pass to
|
|
2769
|
+
// recompute rather than early-return on an unchanged id set.
|
|
2770
|
+
this.lastInstancedVisibilityVersion = -1;
|
|
2771
|
+
this.instancedVisibilityDirty = true;
|
|
2772
|
+
return freed.size;
|
|
2773
|
+
}
|
|
2774
|
+
/**
|
|
2775
|
+
* Recompute `boundingBoxes[eid]` from exactly `occurrences` (REPLACING any
|
|
2776
|
+
* existing entry rather than unioning into it), reading each occurrence's
|
|
2777
|
+
* template-local AABB + packed instance matrix the same way
|
|
2778
|
+
* `unionInstancedWorldAabb` does at upload time. Used when a model-level
|
|
2779
|
+
* template removal (`removeInstancedTemplatesForModel`) prunes some but not
|
|
2780
|
+
* all of a shared id's occurrences — a plain union only ever grows, so it
|
|
2781
|
+
* cannot shrink to reflect occurrences that no longer exist. Deletes the
|
|
2782
|
+
* entry outright if no surviving occurrence has a finite local box (#2073).
|
|
2783
|
+
*/
|
|
2784
|
+
recomputeInstancedWorldAabb(eid, occurrences) {
|
|
2785
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
2786
|
+
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
2787
|
+
let any = false;
|
|
2788
|
+
for (const occ of occurrences) {
|
|
2789
|
+
const cpu = this.instancedTemplateCpu[occ.templateIndex];
|
|
2790
|
+
if (!cpu || !Number.isFinite(cpu.localMin[0]))
|
|
2791
|
+
continue;
|
|
2792
|
+
const dv = new DataView(cpu.instanceData);
|
|
2793
|
+
const [lmnx, lmny, lmnz] = cpu.localMin;
|
|
2794
|
+
const [lmxx, lmxy, lmxz] = cpu.localMax;
|
|
2795
|
+
const b = occ.byteOffset;
|
|
2796
|
+
const m0 = dv.getFloat32(b + 0, true), m1 = dv.getFloat32(b + 4, true), m2 = dv.getFloat32(b + 8, true);
|
|
2797
|
+
const m4 = dv.getFloat32(b + 16, true), m5 = dv.getFloat32(b + 20, true), m6 = dv.getFloat32(b + 24, true);
|
|
2798
|
+
const m8 = dv.getFloat32(b + 32, true), m9 = dv.getFloat32(b + 36, true), m10 = dv.getFloat32(b + 40, true);
|
|
2799
|
+
const m12 = dv.getFloat32(b + 48, true), m13 = dv.getFloat32(b + 52, true), m14 = dv.getFloat32(b + 56, true);
|
|
2800
|
+
for (let c = 0; c < 8; c++) {
|
|
2801
|
+
const x = (c & 1) ? lmxx : lmnx, y = (c & 2) ? lmxy : lmny, z = (c & 4) ? lmxz : lmnz;
|
|
2802
|
+
const wx = m0 * x + m4 * y + m8 * z + m12;
|
|
2803
|
+
const wy = m1 * x + m5 * y + m9 * z + m13;
|
|
2804
|
+
const wz = m2 * x + m6 * y + m10 * z + m14;
|
|
2805
|
+
if (wx < minX)
|
|
2806
|
+
minX = wx;
|
|
2807
|
+
if (wy < minY)
|
|
2808
|
+
minY = wy;
|
|
2809
|
+
if (wz < minZ)
|
|
2810
|
+
minZ = wz;
|
|
2811
|
+
if (wx > maxX)
|
|
2812
|
+
maxX = wx;
|
|
2813
|
+
if (wy > maxY)
|
|
2814
|
+
maxY = wy;
|
|
2815
|
+
if (wz > maxZ)
|
|
2816
|
+
maxZ = wz;
|
|
2817
|
+
}
|
|
2818
|
+
any = true;
|
|
2819
|
+
}
|
|
2820
|
+
if (any) {
|
|
2821
|
+
this.boundingBoxes.set(eid, { min: { x: minX, y: minY, z: minZ }, max: { x: maxX, y: maxY, z: maxZ } });
|
|
2822
|
+
}
|
|
2823
|
+
else {
|
|
2824
|
+
this.boundingBoxes.delete(eid);
|
|
2825
|
+
}
|
|
2657
2826
|
}
|
|
2658
2827
|
/**
|
|
2659
2828
|
* Decode a per-batch IFNS instancing shard (`processGeometryBatchInstanced`)
|
|
@@ -2671,7 +2840,7 @@ export class Scene {
|
|
|
2671
2840
|
* type-only dependency on @ifc-lite/geometry and the Scene stays focused on
|
|
2672
2841
|
* GPU upload.
|
|
2673
2842
|
*/
|
|
2674
|
-
addInstancedShard(device, shard) {
|
|
2843
|
+
addInstancedShard(device, shard, modelIndex = 0) {
|
|
2675
2844
|
this.instancedDevice = device; // cached for per-instance selection/overlay writeBuffer
|
|
2676
2845
|
const prepared = prepareInstancedRender(shard);
|
|
2677
2846
|
// Selected ids whose occurrences arrived in THIS shard (selection recorded
|
|
@@ -2721,8 +2890,10 @@ export class Scene {
|
|
|
2721
2890
|
});
|
|
2722
2891
|
new Uint8Array(instanceBuffer.getMappedRange()).set(new Uint8Array(t.instanceBuffer, 0, instSize));
|
|
2723
2892
|
instanceBuffer.unmap();
|
|
2893
|
+
// Always append: slots are stable identities, never recycled.
|
|
2724
2894
|
const templateIndex = this.instancedTemplates.length;
|
|
2725
2895
|
const template = {
|
|
2896
|
+
modelIndex,
|
|
2726
2897
|
vertexBuffer,
|
|
2727
2898
|
indexBuffer,
|
|
2728
2899
|
indexCount: t.indices.length,
|
|
@@ -2732,7 +2903,7 @@ export class Scene {
|
|
|
2732
2903
|
maxOccRadius: 0,
|
|
2733
2904
|
selectedCount: 0,
|
|
2734
2905
|
};
|
|
2735
|
-
this.instancedTemplates
|
|
2906
|
+
this.instancedTemplates[templateIndex] = template;
|
|
2736
2907
|
// Template-local AABB (used to derive per-occurrence world AABBs cheaply).
|
|
2737
2908
|
let lmnx = Infinity, lmny = Infinity, lmnz = Infinity;
|
|
2738
2909
|
let lmxx = -Infinity, lmxy = -Infinity, lmxz = -Infinity;
|
|
@@ -2754,14 +2925,16 @@ export class Scene {
|
|
|
2754
2925
|
// Retain the compact CPU geometry + the packed instance records (mat4 per
|
|
2755
2926
|
// occurrence) so CPU consumers can reach instanced geometry without a full
|
|
2756
2927
|
// per-occurrence MeshData each. These are references into the decoded shard.
|
|
2757
|
-
|
|
2928
|
+
// Slot-assigned, not pushed: the CPU array is emptied on geometry release
|
|
2929
|
+
// while the GPU slots live on, so `push` would silently misalign the two.
|
|
2930
|
+
this.instancedTemplateCpu[templateIndex] = {
|
|
2758
2931
|
positions: t.positions,
|
|
2759
2932
|
normals: t.normals,
|
|
2760
2933
|
indices: t.indices,
|
|
2761
2934
|
instanceData: t.instanceBuffer,
|
|
2762
2935
|
localMin: [lmnx, lmny, lmnz],
|
|
2763
2936
|
localMax: [lmxx, lmxy, lmxz],
|
|
2764
|
-
}
|
|
2937
|
+
};
|
|
2765
2938
|
// Map each occurrence's express_id -> (template, byte offset, original
|
|
2766
2939
|
// colour) so selection (flag byte) + lens/IDS overlays (colour bytes) can
|
|
2767
2940
|
// patch individual occurrences via writeBuffer without a re-upload. Also fold
|
|
@@ -2805,6 +2978,7 @@ export class Scene {
|
|
|
2805
2978
|
}
|
|
2806
2979
|
}
|
|
2807
2980
|
}
|
|
2981
|
+
this.refreshLiveInstancedTemplates();
|
|
2808
2982
|
// Write the selected flag for ids whose occurrences arrived after the
|
|
2809
2983
|
// selection was recorded (idempotent for their pre-existing occurrences),
|
|
2810
2984
|
// so the highlight shows on late-streamed geometry too.
|
|
@@ -3287,7 +3461,60 @@ export class Scene {
|
|
|
3287
3461
|
this.sharedTextures.delete(tm.sharedTextureKey);
|
|
3288
3462
|
}
|
|
3289
3463
|
}
|
|
3464
|
+
/**
|
|
3465
|
+
* Destroy every GPU-instanced template's buffers and reset all instanced
|
|
3466
|
+
* bookkeeping, regardless of owning model. Shared by `clear()` (full reset)
|
|
3467
|
+
* — `clearFlatGeometry()` deliberately does NOT call this, so a reshape
|
|
3468
|
+
* that still has models present can retain their instanced geometry
|
|
3469
|
+
* (#2073).
|
|
3470
|
+
*/
|
|
3471
|
+
destroyAllInstancedTemplates() {
|
|
3472
|
+
for (const it of this.instancedTemplates) {
|
|
3473
|
+
if (!it)
|
|
3474
|
+
continue;
|
|
3475
|
+
it.vertexBuffer.destroy();
|
|
3476
|
+
it.indexBuffer.destroy();
|
|
3477
|
+
it.instanceBuffer.destroy();
|
|
3478
|
+
}
|
|
3479
|
+
this.instancedTemplates = [];
|
|
3480
|
+
this.liveInstancedTemplates = [];
|
|
3481
|
+
this.instancedTemplateCpu = [];
|
|
3482
|
+
this.instancedEntityMap.clear();
|
|
3483
|
+
this.instancedSelected.clear();
|
|
3484
|
+
this.instancedHidden.clear();
|
|
3485
|
+
this.instancedOverridden.clear();
|
|
3486
|
+
this.instancedHasTransparent = false;
|
|
3487
|
+
// Force the next setInstancedVisibility to recompute against fresh state.
|
|
3488
|
+
this.lastInstancedVisibilityVersion = -1;
|
|
3489
|
+
this.instancedVisibilityDirty = false;
|
|
3490
|
+
this.instancedDevice = undefined;
|
|
3491
|
+
}
|
|
3290
3492
|
clear() {
|
|
3493
|
+
// GPU-instancing templates own their vertex/index/instance buffers.
|
|
3494
|
+
// (Freed slots are holes whose buffers are already destroyed — skip them so
|
|
3495
|
+
// a per-model removal followed by clear() can't double-destroy.)
|
|
3496
|
+
this.destroyAllInstancedTemplates();
|
|
3497
|
+
this.clearFlatGeometry();
|
|
3498
|
+
}
|
|
3499
|
+
/**
|
|
3500
|
+
* Clear flat/batched geometry (meshes, batches, buckets, textured meshes,
|
|
3501
|
+
* colour overlays, streaming state, residency bookkeeping) WITHOUT
|
|
3502
|
+
* touching GPU-instanced templates (#2073). A reshape that still has at
|
|
3503
|
+
* least one model present should call this instead of `clear()`, then
|
|
3504
|
+
* reconcile instanced ownership with `removeInstancedTemplatesForModel`
|
|
3505
|
+
* for any model that did NOT survive — that way a still-loaded model's
|
|
3506
|
+
* repeated geometry (windows, doors, bolts, ...) stays resident across a
|
|
3507
|
+
* visibility toggle / in-place content mutation / federated model add
|
|
3508
|
+
* instead of silently vanishing (nothing re-uploads instanced shard bytes
|
|
3509
|
+
* after their one-time drain).
|
|
3510
|
+
*
|
|
3511
|
+
* Bounding boxes are only dropped for ids with NO surviving instanced
|
|
3512
|
+
* occurrence — an instanced-only id's box must outlive this call so
|
|
3513
|
+
* raycast / measure / section keep working for the geometry that was
|
|
3514
|
+
* just retained; a flat-only id's box is stale the moment its mesh data
|
|
3515
|
+
* is gone, so it is dropped like everything else here.
|
|
3516
|
+
*/
|
|
3517
|
+
clearFlatGeometry() {
|
|
3291
3518
|
for (const mesh of this.meshes)
|
|
3292
3519
|
destroyGpuResources(mesh);
|
|
3293
3520
|
for (const batch of this.batchedMeshes)
|
|
@@ -3304,23 +3531,6 @@ export class Scene {
|
|
|
3304
3531
|
for (const entry of this.sharedTextures.values())
|
|
3305
3532
|
entry.texture.destroy();
|
|
3306
3533
|
this.sharedTextures.clear();
|
|
3307
|
-
// GPU-instancing templates own their vertex/index/instance buffers.
|
|
3308
|
-
for (const it of this.instancedTemplates) {
|
|
3309
|
-
it.vertexBuffer.destroy();
|
|
3310
|
-
it.indexBuffer.destroy();
|
|
3311
|
-
it.instanceBuffer.destroy();
|
|
3312
|
-
}
|
|
3313
|
-
this.instancedTemplates = [];
|
|
3314
|
-
this.instancedTemplateCpu = [];
|
|
3315
|
-
this.instancedEntityMap.clear();
|
|
3316
|
-
this.instancedSelected.clear();
|
|
3317
|
-
this.instancedHidden.clear();
|
|
3318
|
-
this.instancedOverridden.clear();
|
|
3319
|
-
this.instancedHasTransparent = false;
|
|
3320
|
-
// Force the next setInstancedVisibility to recompute against fresh state.
|
|
3321
|
-
this.lastInstancedVisibilityVersion = -1;
|
|
3322
|
-
this.instancedVisibilityDirty = false;
|
|
3323
|
-
this.instancedDevice = undefined;
|
|
3324
3534
|
// Clear partial batch cache (destroys buffers + drops all cache maps)
|
|
3325
3535
|
this.dropAllPartialCaches();
|
|
3326
3536
|
this.colorOverrideGeneration++;
|
|
@@ -3328,14 +3538,21 @@ export class Scene {
|
|
|
3328
3538
|
this.streamingFragments = [];
|
|
3329
3539
|
this.destroyOverrideBatches();
|
|
3330
3540
|
this.colorOverrides = null;
|
|
3331
|
-
// Reset the shared frame origin so the next model picks its own.
|
|
3541
|
+
// Reset the shared frame origin so the next model picks its own. Retained
|
|
3542
|
+
// instanced templates are unaffected — their per-occurrence transforms are
|
|
3543
|
+
// already baked to absolute world coordinates at upload time, not relative
|
|
3544
|
+
// to this origin.
|
|
3332
3545
|
this.sharedFrameOrigin = null;
|
|
3333
3546
|
this.meshes = [];
|
|
3334
3547
|
this.batchedMeshes = [];
|
|
3335
3548
|
this.buckets.clear();
|
|
3336
3549
|
this.meshDataBucket = new Map();
|
|
3337
3550
|
this.meshDataMap.clear();
|
|
3338
|
-
this.boundingBoxes.
|
|
3551
|
+
for (const eid of [...this.boundingBoxes.keys()]) {
|
|
3552
|
+
if (!this.instancedEntityMap.has(eid)) {
|
|
3553
|
+
this.boundingBoxes.delete(eid);
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3339
3556
|
this.activeBucketKey.clear();
|
|
3340
3557
|
this.lastDrawnFrame.clear();
|
|
3341
3558
|
this.residencyRestoreQueue.clear();
|
|
@@ -3448,41 +3665,14 @@ export class Scene {
|
|
|
3448
3665
|
const cached = this.boundingBoxes.get(expressId);
|
|
3449
3666
|
if (cached)
|
|
3450
3667
|
return cached;
|
|
3451
|
-
// Compute from mesh data
|
|
3452
|
-
|
|
3453
|
-
|
|
3668
|
+
// Compute from mesh data. `null` covers both "no pieces at all" and
|
|
3669
|
+
// "pieces with no vertex a box can be built from" — and, critically, is
|
|
3670
|
+
// NOT cached (#2480): a transient empty piece must not poison the entry
|
|
3671
|
+
// for an entity that later gains real geometry, and this cache has no
|
|
3672
|
+
// invalidation tied to that.
|
|
3673
|
+
const bbox = worldAabbFromPieces(this.meshDataMap.get(expressId));
|
|
3674
|
+
if (!bbox)
|
|
3454
3675
|
return null;
|
|
3455
|
-
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
3456
|
-
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
3457
|
-
for (const piece of pieces) {
|
|
3458
|
-
const positions = piece.positions;
|
|
3459
|
-
// world = origin + position (per-element local frame); origin absent/[0,0,0]
|
|
3460
|
-
// for legacy absolute meshes.
|
|
3461
|
-
const ox = piece.origin ? piece.origin[0] : 0;
|
|
3462
|
-
const oy = piece.origin ? piece.origin[1] : 0;
|
|
3463
|
-
const oz = piece.origin ? piece.origin[2] : 0;
|
|
3464
|
-
for (let i = 0; i < positions.length; i += 3) {
|
|
3465
|
-
const x = positions[i] + ox;
|
|
3466
|
-
const y = positions[i + 1] + oy;
|
|
3467
|
-
const z = positions[i + 2] + oz;
|
|
3468
|
-
if (x < minX)
|
|
3469
|
-
minX = x;
|
|
3470
|
-
if (y < minY)
|
|
3471
|
-
minY = y;
|
|
3472
|
-
if (z < minZ)
|
|
3473
|
-
minZ = z;
|
|
3474
|
-
if (x > maxX)
|
|
3475
|
-
maxX = x;
|
|
3476
|
-
if (y > maxY)
|
|
3477
|
-
maxY = y;
|
|
3478
|
-
if (z > maxZ)
|
|
3479
|
-
maxZ = z;
|
|
3480
|
-
}
|
|
3481
|
-
}
|
|
3482
|
-
const bbox = {
|
|
3483
|
-
min: { x: minX, y: minY, z: minZ },
|
|
3484
|
-
max: { x: maxX, y: maxY, z: maxZ },
|
|
3485
|
-
};
|
|
3486
3676
|
this.boundingBoxes.set(expressId, bbox);
|
|
3487
3677
|
return bbox;
|
|
3488
3678
|
}
|