@ifc-lite/renderer 3.0.1 → 3.2.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/README.md +22 -0
- package/dist/appearance-preview.d.ts +2 -0
- package/dist/appearance-preview.d.ts.map +1 -1
- package/dist/appearance-preview.js +4 -0
- package/dist/appearance-preview.js.map +1 -1
- package/dist/camera-animation-immediate.d.ts +5 -0
- package/dist/camera-animation-immediate.d.ts.map +1 -0
- package/dist/camera-animation-immediate.js +16 -0
- package/dist/camera-animation-immediate.js.map +1 -0
- package/dist/camera-animation.d.ts +3 -9
- package/dist/camera-animation.d.ts.map +1 -1
- package/dist/camera-animation.js +14 -9
- package/dist/camera-animation.js.map +1 -1
- package/dist/device-recovery.d.ts +60 -0
- package/dist/device-recovery.d.ts.map +1 -0
- package/dist/device-recovery.js +153 -0
- package/dist/device-recovery.js.map +1 -0
- package/dist/device.d.ts +2 -0
- package/dist/device.d.ts.map +1 -1
- package/dist/device.js +12 -0
- package/dist/device.js.map +1 -1
- package/dist/gpu-upload-guard.d.ts +106 -0
- package/dist/gpu-upload-guard.d.ts.map +1 -0
- package/dist/gpu-upload-guard.js +102 -0
- package/dist/gpu-upload-guard.js.map +1 -0
- package/dist/index.d.ts +44 -21
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +156 -139
- package/dist/index.js.map +1 -1
- package/dist/instanced-override-color.d.ts +8 -0
- package/dist/instanced-override-color.d.ts.map +1 -1
- package/dist/instanced-override-color.js +10 -0
- package/dist/instanced-override-color.js.map +1 -1
- package/dist/merged-mesh-extract.d.ts.map +1 -1
- package/dist/merged-mesh-extract.js +5 -0
- package/dist/merged-mesh-extract.js.map +1 -1
- package/dist/mesh-entity-hosting.d.ts +14 -0
- package/dist/mesh-entity-hosting.d.ts.map +1 -0
- package/dist/mesh-entity-hosting.js +25 -0
- package/dist/mesh-entity-hosting.js.map +1 -0
- package/dist/model-translation.d.ts +39 -1
- package/dist/model-translation.d.ts.map +1 -1
- package/dist/model-translation.js +103 -16
- package/dist/model-translation.js.map +1 -1
- package/dist/picking-manager.d.ts +2 -1
- package/dist/picking-manager.d.ts.map +1 -1
- package/dist/picking-manager.js +11 -2
- package/dist/picking-manager.js.map +1 -1
- package/dist/reference-images.d.ts +2 -0
- package/dist/reference-images.d.ts.map +1 -1
- package/dist/reference-images.js +2 -0
- package/dist/reference-images.js.map +1 -1
- package/dist/renderer-overlays.d.ts +3 -0
- package/dist/renderer-overlays.d.ts.map +1 -1
- package/dist/renderer-overlays.js +13 -0
- package/dist/renderer-overlays.js.map +1 -1
- package/dist/renderer-symbolic-overlays.d.ts +1 -0
- package/dist/renderer-symbolic-overlays.d.ts.map +1 -1
- package/dist/renderer-symbolic-overlays.js +3 -0
- package/dist/renderer-symbolic-overlays.js.map +1 -1
- package/dist/scene-appearance-preview.d.ts +2 -0
- package/dist/scene-appearance-preview.d.ts.map +1 -1
- package/dist/scene-appearance-preview.js +9 -0
- package/dist/scene-appearance-preview.js.map +1 -1
- package/dist/scene-authored-owner.d.ts +10 -0
- package/dist/scene-authored-owner.d.ts.map +1 -1
- package/dist/scene-authored-owner.js +44 -5
- package/dist/scene-authored-owner.js.map +1 -1
- package/dist/scene-batch-rebuild.d.ts +18 -0
- package/dist/scene-batch-rebuild.d.ts.map +1 -0
- package/dist/scene-batch-rebuild.js +49 -0
- package/dist/scene-batch-rebuild.js.map +1 -0
- package/dist/scene-batch-upload.d.ts +7 -2
- package/dist/scene-batch-upload.d.ts.map +1 -1
- package/dist/scene-batch-upload.js +26 -1
- package/dist/scene-batch-upload.js.map +1 -1
- package/dist/scene-bucket-routing.d.ts +21 -0
- package/dist/scene-bucket-routing.d.ts.map +1 -0
- package/dist/scene-bucket-routing.js +44 -0
- package/dist/scene-bucket-routing.js.map +1 -0
- package/dist/scene-contents.d.ts +8 -0
- package/dist/scene-contents.d.ts.map +1 -1
- package/dist/scene-derived-batches.d.ts +84 -0
- package/dist/scene-derived-batches.d.ts.map +1 -0
- package/dist/scene-derived-batches.js +73 -0
- package/dist/scene-derived-batches.js.map +1 -0
- package/dist/scene-derived-mesh-provenance.d.ts +13 -0
- package/dist/scene-derived-mesh-provenance.d.ts.map +1 -0
- package/dist/scene-derived-mesh-provenance.js +30 -0
- package/dist/scene-derived-mesh-provenance.js.map +1 -0
- package/dist/scene-device-recovery.d.ts +103 -0
- package/dist/scene-device-recovery.d.ts.map +1 -0
- package/dist/scene-device-recovery.js +299 -0
- package/dist/scene-device-recovery.js.map +1 -0
- package/dist/scene-geometry.d.ts +1 -0
- package/dist/scene-geometry.d.ts.map +1 -1
- package/dist/scene-geometry.js +3 -23
- package/dist/scene-geometry.js.map +1 -1
- package/dist/scene-model-translation.d.ts +11 -1
- package/dist/scene-model-translation.d.ts.map +1 -1
- package/dist/scene-model-translation.js +22 -0
- package/dist/scene-model-translation.js.map +1 -1
- package/dist/scene-precision.d.ts +16 -0
- package/dist/scene-precision.d.ts.map +1 -0
- package/dist/scene-precision.js +73 -0
- package/dist/scene-precision.js.map +1 -0
- package/dist/scene.d.ts +46 -42
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +211 -208
- package/dist/scene.js.map +1 -1
- package/package.json +2 -2
package/dist/scene.js
CHANGED
|
@@ -4,29 +4,34 @@
|
|
|
4
4
|
import { materializeInstances } from './scene-instance-materialization.js';
|
|
5
5
|
import { InstanceSuppression } from './scene-instance-suppression.js';
|
|
6
6
|
import { createSceneBatch } from './scene-batch-upload.js';
|
|
7
|
-
import { createSceneAppearancePreview } from './scene-appearance-preview.js';
|
|
7
|
+
import { createSceneAppearancePreview, rebindSceneAppearanceAccess } from './scene-appearance-preview.js';
|
|
8
8
|
import { AppearanceBuckets } from './scene-appearance-buckets.js';
|
|
9
|
-
import { prepareSceneAuthoredOwner } from './scene-authored-owner.js';
|
|
9
|
+
import { AuthoredPreparationRegistry, prepareSceneAuthoredOwner } from './scene-authored-owner.js';
|
|
10
10
|
import { interleaveTexturedVertices } from './textured-vertices.js';
|
|
11
11
|
import { RgbaTexturePool } from './rgba-texture-pool.js';
|
|
12
12
|
import { splitMeshForStreaming } from './scene-stream-split.js';
|
|
13
|
+
import { hostsOtherEntities } from './mesh-entity-hosting.js';
|
|
13
14
|
import { BATCH_CONSTANTS } from './constants.js';
|
|
14
15
|
import { prepareRayDirInv, raycastBoundingBoxes, raycastTriangles, rayIntersectsBox, } from './scene-raycaster.js';
|
|
15
16
|
import { selectBoundingBoxesInRect } from './scene-rect-select.js';
|
|
16
|
-
import { splitMeshDataForBufferLimit, cachedWorldAabb, worldAabbFromPieces, destroyGpuResources } from './scene-geometry.js';
|
|
17
|
+
import { splitMeshDataForBufferLimit, cachedWorldAabb, worldAabbFromPieces, destroyGpuResources, topologySafeBatchOrigin } from './scene-geometry.js';
|
|
18
|
+
import { resolvePrecisionBucket } from './scene-bucket-routing.js';
|
|
17
19
|
import { sumResidentGpuBytes } from './render-stats.js';
|
|
18
|
-
import { composeInstancedOverrideColor } from './instanced-override-color.js';
|
|
20
|
+
import { composeInstancedOverrideColor, writeOriginalInstancedColors } from './instanced-override-color.js';
|
|
19
21
|
import { bucketBaseKeyFor } from './chunk-grid.js';
|
|
22
|
+
import { inheritedQuantization, groupOverridePieces, cloneOverrides, overlaysInvalidatedBy } from './scene-derived-batches.js';
|
|
20
23
|
import { VisibilityEpochTracker } from './visibility-epoch.js';
|
|
21
24
|
import { isEntityVisible } from './entity-visibility.js';
|
|
22
25
|
import { planInstancedGhosting } from './instanced-ghost-plan.js';
|
|
23
26
|
import { selectEvictions } from './residency.js';
|
|
24
27
|
import { OPAQUE_ALPHA_CUTOFF } from './overlay-routing.js';
|
|
25
|
-
import { translateSceneModel, releaseInstanceVertices, refreshTexturedBounds } from './scene-model-translation.js';
|
|
28
|
+
import { translateSceneModel, rotateSceneModelInstances, releaseInstanceVertices, refreshTexturedBounds } from './scene-model-translation.js';
|
|
26
29
|
import { ModelTranslations } from './model-translation.js';
|
|
27
|
-
import {
|
|
30
|
+
import { DerivedMeshProvenance } from './scene-derived-mesh-provenance.js';
|
|
31
|
+
import { rebuildSceneBatches } from './scene-batch-rebuild.js';
|
|
28
32
|
import { dropAllPartialCaches as dropAllPartialCachesIn, dropPartialCacheForBatch as dropPartialCacheForBatchIn, retireUnusedAlphaSlots as retireUnusedAlphaSlotsIn, } from './partial-batch-cache.js';
|
|
29
33
|
import { prepareInstancedRender, foldOccurrenceWorldBox, INSTANCE_STRIDE_BYTES, INSTANCE_COLOR_OFFSET, INSTANCE_FLAGS_OFFSET, INSTANCE_FLAG_SELECTED, INSTANCE_FLAG_HIDDEN, } from './instanced-render.js';
|
|
34
|
+
import { discardSceneGpuResourcesForRecovery, prepareSceneDeviceRecovery, repartitionHydratedRecoveryBucket, restoreSceneGpuResourcesAfterRecovery } from './scene-device-recovery.js';
|
|
30
35
|
const EMPTY_INSTANCED_TEMPLATES = [];
|
|
31
36
|
/**
|
|
32
37
|
* Pure helper: compute the exclusive end index of the next flushPending()
|
|
@@ -82,6 +87,7 @@ export class Scene {
|
|
|
82
87
|
batchedMeshes = []; // flat render array (rebuilt from buckets)
|
|
83
88
|
buckets = new Map(); // bucketKey -> consolidated bucket state
|
|
84
89
|
meshDataBucket = new Map(); // reverse lookup: MeshData -> owning bucket
|
|
90
|
+
derivedMeshProvenance = new DerivedMeshProvenance();
|
|
85
91
|
modelTranslations = new ModelTranslations();
|
|
86
92
|
meshDataMap = new Map(); // Map expressId -> MeshData[] (for lazy buffer creation, accumulates multiple pieces)
|
|
87
93
|
boundingBoxes = new Map(); // Map expressId -> bounding box (computed lazily)
|
|
@@ -93,7 +99,9 @@ export class Scene {
|
|
|
93
99
|
rgbaTexturePool = new RgbaTexturePool();
|
|
94
100
|
appearanceController;
|
|
95
101
|
appearanceBuckets;
|
|
102
|
+
appearanceAccessState;
|
|
96
103
|
authoredGeneration = 0;
|
|
104
|
+
authoredPreparations = new AuthoredPreparationRegistry();
|
|
97
105
|
appearanceAccess(device, pipeline) {
|
|
98
106
|
return {
|
|
99
107
|
meshes: () => this.texturedMeshes, data: this.meshDataMap,
|
|
@@ -133,24 +141,23 @@ export class Scene {
|
|
|
133
141
|
},
|
|
134
142
|
};
|
|
135
143
|
}
|
|
144
|
+
bindAppearanceAccess(device, pipeline) { return this.appearanceAccessState = rebindSceneAppearanceAccess(this.appearanceAccessState, this.appearanceAccess(device, pipeline)); }
|
|
136
145
|
sharedAppearanceBuckets(access) {
|
|
137
146
|
return this.appearanceBuckets ??= new AppearanceBuckets(access.buckets, id => this.meshDataMap.get(id));
|
|
138
147
|
}
|
|
139
148
|
appearancePreview(device, pipeline) {
|
|
140
|
-
const access = this.
|
|
149
|
+
const access = this.bindAppearanceAccess(device, pipeline);
|
|
141
150
|
return this.appearanceController ??= createSceneAppearancePreview(access, this.sharedAppearanceBuckets(access));
|
|
142
151
|
}
|
|
143
|
-
/** Place canonical native appearance source geometry in this model's live frame. */
|
|
144
152
|
placeAppearanceSource(mesh) { return this.modelTranslations.placeMesh(mesh); }
|
|
145
|
-
/** Retain model-local source coordinates when publishing a placed appearance mesh. */
|
|
146
153
|
appearanceSourceMesh(mesh) { return this.modelTranslations.sourceFromPlaced(mesh); }
|
|
147
154
|
/** Stage one new IFC owner with all its coloured or textured geometry parts. */
|
|
148
155
|
prepareAuthoredOwner(parts, device, pipeline) {
|
|
149
|
-
const access = this.
|
|
150
|
-
return prepareSceneAuthoredOwner(access, this.sharedAppearanceBuckets(access), parts, () => {
|
|
156
|
+
const access = this.bindAppearanceAccess(device, pipeline), generation = this.authoredGeneration;
|
|
157
|
+
return this.authoredPreparations.track(prepareSceneAuthoredOwner(access, this.sharedAppearanceBuckets(access), parts, () => {
|
|
151
158
|
if (generation !== this.authoredGeneration)
|
|
152
159
|
throw new Error('The scene changed while preparing the object.');
|
|
153
|
-
});
|
|
160
|
+
}));
|
|
154
161
|
}
|
|
155
162
|
/** Compatibility entry point for an image-backed single-part owner. */
|
|
156
163
|
prepareTexturedOwner(mesh, device, pipeline) {
|
|
@@ -279,6 +286,7 @@ export class Scene {
|
|
|
279
286
|
// Overlay batches render on top using depthCompare 'equal', so they only
|
|
280
287
|
// paint where original geometry already wrote depth. Clearing is instant.
|
|
281
288
|
overrideBatches = [];
|
|
289
|
+
overlaySources = new Map(); // overridden piece -> bucket key its overlay inherited from (#4832)
|
|
282
290
|
// Defensively-typed: the renderer is the sole writer (via setColorOverrides),
|
|
283
291
|
// external readers go through getColorOverrides() and get a ReadonlyMap.
|
|
284
292
|
colorOverrides = null;
|
|
@@ -298,12 +306,11 @@ export class Scene {
|
|
|
298
306
|
// the queue via flushPending() with a per-frame time budget.
|
|
299
307
|
meshQueue = [];
|
|
300
308
|
meshQueueReadIndex = 0;
|
|
301
|
-
// ─── GPU-resident mode ──────────────────────────────────────────────
|
|
302
|
-
// After releaseGeometryData(), JS-side typed arrays are freed.
|
|
303
|
-
// Only lightweight metadata is retained for operations that don't need
|
|
304
|
-
// raw vertex data (bounding boxes, color key lookups, expressId sets).
|
|
305
309
|
geometryReleased = false;
|
|
306
310
|
ephemeralStreamingMode = false;
|
|
311
|
+
prepareDeviceRecovery() { return prepareSceneDeviceRecovery(this); }
|
|
312
|
+
discardGpuResourcesForRecovery() { discardSceneGpuResourcesForRecovery(this); }
|
|
313
|
+
restoreGpuResourcesAfterRecovery(device, pipeline) { restoreSceneGpuResourcesAfterRecovery(this, device, pipeline); }
|
|
307
314
|
/**
|
|
308
315
|
* Add mesh to scene
|
|
309
316
|
*/
|
|
@@ -326,8 +333,10 @@ export class Scene {
|
|
|
326
333
|
/** The shared local-frame origin all batches relativize against (null until
|
|
327
334
|
* the first batch is built). Per-mesh highlight/picker VBOs replicate the
|
|
328
335
|
* batch's exact f32 path against this so they render bit-coincident. */
|
|
329
|
-
getSharedFrameOrigin(modelIndex = 0) {
|
|
330
|
-
|
|
336
|
+
getSharedFrameOrigin(modelIndex = 0, meshData) {
|
|
337
|
+
const placed = meshData ? this.derivedMeshProvenance.placedSourceFor(meshData, this.modelTranslations) : undefined;
|
|
338
|
+
const bucket = placed ? this.meshDataBucket.get(placed) : undefined;
|
|
339
|
+
return bucket?.batchedMesh?.origin ?? bucket?.frameOrigin ?? this.modelTranslations.frameOrigin(this.sharedFrameOrigins.get(modelIndex) ?? null, modelIndex) ?? null;
|
|
331
340
|
}
|
|
332
341
|
/**
|
|
333
342
|
* Enable/disable spatial chunk bucketing (issue #1682 phase 2). When set,
|
|
@@ -425,6 +434,7 @@ export class Scene {
|
|
|
425
434
|
}
|
|
426
435
|
const rebuilt = this.createBatchedMesh(bucket.meshData, bucket.meshData[0].color, device, pipeline, key);
|
|
427
436
|
bucket.batchedMesh = rebuilt;
|
|
437
|
+
bucket.frameOrigin = rebuilt.origin;
|
|
428
438
|
const idx = this.batchedMeshes.indexOf(old);
|
|
429
439
|
if (idx >= 0)
|
|
430
440
|
this.batchedMeshes[idx] = rebuilt;
|
|
@@ -458,28 +468,21 @@ export class Scene {
|
|
|
458
468
|
* Enable 12-byte lattice-quantized batch vertices (issue #1682 phase 6).
|
|
459
469
|
* ONLY call after the renderer verified its quantized pipeline variants
|
|
460
470
|
* exist (see Renderer.enableQuantizedBatches) — quantized buffers are
|
|
461
|
-
* undrawable without them. Applies to batches built from now on
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
*
|
|
471
|
+
* undrawable without them. Applies to batches built from now on: bucket
|
|
472
|
+
* batches and fragments quantize onto the SAME 2^-10 lattice when their
|
|
473
|
+
* extent fits the u16 range (else f32); partial + override batches INHERIT
|
|
474
|
+
* their source batch's decision (#4832, `scene-derived-batches.ts`), so
|
|
475
|
+
* depth-equal overlay matching and cross-batch coincidence stay bit-exact.
|
|
466
476
|
*/
|
|
467
477
|
setQuantizedBatches(enabled) {
|
|
468
478
|
this.quantizedBatchesEnabled = enabled;
|
|
469
479
|
}
|
|
470
|
-
/**
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
* fell back to f32, e.g. >64m extent, must NOT snap). Falls back to the
|
|
474
|
-
* global flag when the mesh isn't bucketed (mid-stream hydration).
|
|
475
|
-
*/
|
|
480
|
+
/** Whether THIS mesh's source batch renders quantized — drives the hydrated-mesh
|
|
481
|
+
* lattice snap in createMeshFromData (an f32 batch must NOT snap). Same rule
|
|
482
|
+
* as overlay/partial batches; global flag while unbucketed. */
|
|
476
483
|
isMeshQuantized(meshData) {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const bucket = this.meshDataBucket.get(meshData);
|
|
480
|
-
if (bucket?.batchedMesh)
|
|
481
|
-
return bucket.batchedMesh.quantized !== undefined;
|
|
482
|
-
return true;
|
|
484
|
+
const source = this.derivedMeshProvenance.placedSourceFor(meshData, this.modelTranslations);
|
|
485
|
+
return inheritedQuantization(this.quantizedBatchesEnabled, this.meshDataBucket.get(source)?.batchedMesh) !== 'off';
|
|
483
486
|
}
|
|
484
487
|
/** Set (or clear) the HOST budget in bytes for bucket CPU geometry. */
|
|
485
488
|
setHostResidencyBudget(bytes) {
|
|
@@ -634,10 +637,10 @@ export class Scene {
|
|
|
634
637
|
this.meshDataBucket.set(m, bucket);
|
|
635
638
|
this.addMeshData(m);
|
|
636
639
|
}
|
|
637
|
-
this.coldBuckets.delete(key);
|
|
638
640
|
if (members.length > 0) {
|
|
639
|
-
|
|
640
|
-
this
|
|
641
|
+
this.coldBuckets.delete(key);
|
|
642
|
+
for (const restoredKey of repartitionHydratedRecoveryBucket(this, key, bucket, shell))
|
|
643
|
+
this.residencyRestoreQueue.add(restoredKey);
|
|
641
644
|
}
|
|
642
645
|
else {
|
|
643
646
|
console.warn(`[Scene] cold restore for ${key} found no members — bucket stays a shell`);
|
|
@@ -669,6 +672,7 @@ export class Scene {
|
|
|
669
672
|
continue;
|
|
670
673
|
const rebuilt = this.createBatchedMesh(bucket.meshData, bucket.meshData[0].color, device, pipeline, bucket.key);
|
|
671
674
|
bucket.batchedMesh = rebuilt;
|
|
675
|
+
bucket.frameOrigin = rebuilt.origin;
|
|
672
676
|
const idx = this.batchedMeshes.indexOf(old);
|
|
673
677
|
if (idx >= 0)
|
|
674
678
|
this.batchedMeshes[idx] = rebuilt;
|
|
@@ -868,7 +872,7 @@ export class Scene {
|
|
|
868
872
|
// this expressId so selection highlighting is per-entity, not the
|
|
869
873
|
// entire merged batch.
|
|
870
874
|
if (single.entityIds) {
|
|
871
|
-
return
|
|
875
|
+
return this.derivedMeshProvenance.extract(single, expressId);
|
|
872
876
|
}
|
|
873
877
|
return single;
|
|
874
878
|
}
|
|
@@ -878,7 +882,7 @@ export class Scene {
|
|
|
878
882
|
const extracted = [];
|
|
879
883
|
for (const piece of pieces) {
|
|
880
884
|
if (piece.entityIds) {
|
|
881
|
-
const ex =
|
|
885
|
+
const ex = this.derivedMeshProvenance.extract(piece, expressId);
|
|
882
886
|
if (ex)
|
|
883
887
|
extracted.push(ex);
|
|
884
888
|
}
|
|
@@ -893,6 +897,18 @@ export class Scene {
|
|
|
893
897
|
pieces = extracted;
|
|
894
898
|
// Fall through to the normal multi-piece merge below
|
|
895
899
|
}
|
|
900
|
+
// A cross-bucket singular result keeps the historical representative merge
|
|
901
|
+
// (all triangles, no provenance); precise callers use the pieces accessor.
|
|
902
|
+
const firstOrigin = pieces[0].origin;
|
|
903
|
+
const firstSource = this.derivedMeshProvenance.placedSourceFor(pieces[0], this.modelTranslations);
|
|
904
|
+
const firstBucket = this.meshDataBucket.get(firstSource);
|
|
905
|
+
const sameBucket = pieces.every(piece => this.meshDataBucket.get(this.derivedMeshProvenance.placedSourceFor(piece, this.modelTranslations)) === firstBucket);
|
|
906
|
+
const sameFrame = pieces.every(piece => piece.origin?.[0] === firstOrigin?.[0]
|
|
907
|
+
&& piece.origin?.[1] === firstOrigin?.[1] && piece.origin?.[2] === firstOrigin?.[2]);
|
|
908
|
+
const precisionPlaceable = sameBucket || (!firstBucket && sameFrame);
|
|
909
|
+
const mergedOrigin = precisionPlaceable
|
|
910
|
+
? firstBucket?.batchedMesh?.origin ?? firstBucket?.frameOrigin ?? firstOrigin ?? [0, 0, 0]
|
|
911
|
+
: undefined;
|
|
896
912
|
// Check if all pieces have the same color (within tolerance)
|
|
897
913
|
// This handles multi-material elements like windows (frame vs glass)
|
|
898
914
|
const firstColor = pieces[0].color;
|
|
@@ -926,8 +942,17 @@ export class Scene {
|
|
|
926
942
|
let idxOffset = 0;
|
|
927
943
|
let vertexOffset = 0;
|
|
928
944
|
for (const piece of pieces) {
|
|
929
|
-
|
|
930
|
-
|
|
945
|
+
if (mergedOrigin) {
|
|
946
|
+
const ox = (piece.origin?.[0] ?? 0) - mergedOrigin[0], oy = (piece.origin?.[1] ?? 0) - mergedOrigin[1], oz = (piece.origin?.[2] ?? 0) - mergedOrigin[2];
|
|
947
|
+
for (let i = 0; i < piece.positions.length; i += 3) {
|
|
948
|
+
mergedPositions[posOffset + i] = piece.positions[i] + ox;
|
|
949
|
+
mergedPositions[posOffset + i + 1] = piece.positions[i + 1] + oy;
|
|
950
|
+
mergedPositions[posOffset + i + 2] = piece.positions[i + 2] + oz;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
else {
|
|
954
|
+
mergedPositions.set(piece.positions, posOffset);
|
|
955
|
+
}
|
|
931
956
|
mergedNormals.set(piece.normals, posOffset);
|
|
932
957
|
// Copy indices with offset
|
|
933
958
|
for (let i = 0; i < piece.indices.length; i++) {
|
|
@@ -938,7 +963,7 @@ export class Scene {
|
|
|
938
963
|
vertexOffset += piece.positions.length / 3;
|
|
939
964
|
}
|
|
940
965
|
// Return merged MeshData (all pieces have same color)
|
|
941
|
-
|
|
966
|
+
const merged = {
|
|
942
967
|
expressId,
|
|
943
968
|
modelIndex: pieces[0].modelIndex, // Preserve modelIndex for multi-model support
|
|
944
969
|
positions: mergedPositions,
|
|
@@ -946,7 +971,15 @@ export class Scene {
|
|
|
946
971
|
indices: mergedIndices,
|
|
947
972
|
color: firstColor,
|
|
948
973
|
ifcType: pieces[0].ifcType,
|
|
974
|
+
...(mergedOrigin ? { origin: mergedOrigin } : {}),
|
|
949
975
|
};
|
|
976
|
+
// The common frame is explicit above. Keep a source only when every part
|
|
977
|
+
// belongs to the same live bucket; an arbitrary source would give a merged
|
|
978
|
+
// result the wrong quantization/frame after a re-batch.
|
|
979
|
+
if (firstBucket && precisionPlaceable) {
|
|
980
|
+
this.derivedMeshProvenance.remember(merged, firstSource);
|
|
981
|
+
}
|
|
982
|
+
return merged;
|
|
950
983
|
}
|
|
951
984
|
/**
|
|
952
985
|
* Check if MeshData exists for an expressId
|
|
@@ -1018,7 +1051,7 @@ export class Scene {
|
|
|
1018
1051
|
const extracted = [];
|
|
1019
1052
|
for (const piece of pieces) {
|
|
1020
1053
|
if (piece.entityIds) {
|
|
1021
|
-
const ex =
|
|
1054
|
+
const ex = this.derivedMeshProvenance.extract(piece, expressId);
|
|
1022
1055
|
if (ex)
|
|
1023
1056
|
extracted.push(ex);
|
|
1024
1057
|
}
|
|
@@ -1054,10 +1087,20 @@ export class Scene {
|
|
|
1054
1087
|
* when streaming completes to do one O(N) full merge.
|
|
1055
1088
|
*/
|
|
1056
1089
|
appendToBatches(meshDataArray, device, pipeline, isStreaming = false) {
|
|
1090
|
+
meshDataArray = meshDataArray.map((mesh) => this.modelTranslations.placeMesh(mesh));
|
|
1091
|
+
// Validate every input before cancelling appearance work, publishing a
|
|
1092
|
+
// bucket, or touching GPU state. A single impossible f32 frame must leave
|
|
1093
|
+
// an already-valid scene usable and allow the next safe append.
|
|
1094
|
+
for (const meshData of meshDataArray) {
|
|
1095
|
+
const modelIndex = meshData.modelIndex ?? 0;
|
|
1096
|
+
const shared = this.modelTranslations.frameOrigin(this.sharedFrameOrigins.get(modelIndex) ?? null, modelIndex);
|
|
1097
|
+
if (!topologySafeBatchOrigin([meshData], undefined, shared)) {
|
|
1098
|
+
throw new Error('Unable to resolve a topology-safe GPU frame for mesh geometry.');
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1057
1101
|
if (this.appearanceController)
|
|
1058
1102
|
for (const part of meshDataArray)
|
|
1059
1103
|
this.appearanceController.cancelFor(part.expressId);
|
|
1060
|
-
meshDataArray = meshDataArray.map((mesh) => this.modelTranslations.placeMesh(mesh));
|
|
1061
1104
|
// Cache max buffer size on first call
|
|
1062
1105
|
if (this.cachedMaxBufferSize === 0) {
|
|
1063
1106
|
this.cachedMaxBufferSize = this.getMaxBufferSize(device);
|
|
@@ -1086,6 +1129,9 @@ export class Scene {
|
|
|
1086
1129
|
for (const meshData of renderable) {
|
|
1087
1130
|
const baseKey = this.bucketBaseKey(meshData);
|
|
1088
1131
|
const bucketKey = this.resolveActiveBucket(baseKey, meshData);
|
|
1132
|
+
const resolvedBucket = this.buckets.get(bucketKey);
|
|
1133
|
+
if (resolvedBucket)
|
|
1134
|
+
this.meshDataBucket.set(meshData, resolvedBucket);
|
|
1089
1135
|
if (retainStreamingGeometry || !isStreaming) {
|
|
1090
1136
|
// Accumulate mesh data in the bucket when we need later rebatching or
|
|
1091
1137
|
// CPU-side lookups. Huge-file mode intentionally skips this to keep JS
|
|
@@ -1117,46 +1163,20 @@ export class Scene {
|
|
|
1117
1163
|
// NON-STREAMING: Rebuild full batches immediately
|
|
1118
1164
|
this.rebuildPendingBatches(device, pipeline);
|
|
1119
1165
|
}
|
|
1120
|
-
/**
|
|
1121
|
-
* Rebuild all pending batches (call this after streaming completes)
|
|
1122
|
-
*
|
|
1123
|
-
* Each bucket key already maps to data that fits within the GPU buffer
|
|
1124
|
-
* limit (enforced at accumulation time by resolveActiveBucket), so no
|
|
1125
|
-
* splitting is needed here — just create one batch per key.
|
|
1126
|
-
*/
|
|
1166
|
+
/** Rebuild pending buckets after streaming or a geometry mutation. */
|
|
1127
1167
|
rebuildPendingBatches(device, pipeline) {
|
|
1128
1168
|
if (this.pendingBatchKeys.size === 0)
|
|
1129
1169
|
return;
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
// drop this batch's cached sub-batch clones first or they are stranded
|
|
1136
|
-
// with live GPU buffers. Every other batch-destroying path already
|
|
1137
|
-
// clears the cache (eviction per batch; finalize/release/clear wholesale).
|
|
1138
|
-
this.dropPartialCacheForBatch(bucket.batchedMesh);
|
|
1139
|
-
destroyGpuResources(bucket.batchedMesh);
|
|
1140
|
-
bucket.batchedMesh = null;
|
|
1141
|
-
}
|
|
1142
|
-
if (!bucket || bucket.meshData.length === 0) {
|
|
1143
|
-
// Bucket is empty — clean up
|
|
1144
|
-
this.buckets.delete(key);
|
|
1145
|
-
continue;
|
|
1146
|
-
}
|
|
1147
|
-
// Create new batch with all accumulated meshes for this bucket
|
|
1148
|
-
const color = bucket.meshData[0].color;
|
|
1149
|
-
const batchedMesh = this.createBatchedMesh(bucket.meshData, color, device, pipeline, key);
|
|
1150
|
-
bucket.batchedMesh = batchedMesh;
|
|
1151
|
-
}
|
|
1152
|
-
// Rebuild the flat render array from all buckets (148 max batches — not perf critical)
|
|
1153
|
-
this.batchedMeshes = [];
|
|
1154
|
-
for (const bucket of this.buckets.values()) {
|
|
1155
|
-
if (bucket.batchedMesh) {
|
|
1156
|
-
this.batchedMeshes.push(bucket.batchedMesh);
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1170
|
+
const rebuilt = rebuildSceneBatches({ pendingKeys: this.pendingBatchKeys, buckets: this.buckets,
|
|
1171
|
+
create: (meshes, color, target, renderPipeline, key) => this.createBatchedMesh(meshes, color, target, renderPipeline, key),
|
|
1172
|
+
dropPartial: batch => this.dropPartialCacheForBatch(batch),
|
|
1173
|
+
}, device, pipeline);
|
|
1174
|
+
this.batchedMeshes = [...this.buckets.values()].flatMap(bucket => bucket.batchedMesh ? [bucket.batchedMesh] : []);
|
|
1159
1175
|
this.pendingBatchKeys.clear();
|
|
1176
|
+
// Overlays follow their depth writers only when one actually changed under
|
|
1177
|
+
// them (flip / bucket move, #4832); finalize re-applies once itself.
|
|
1178
|
+
if (!this.finalizeInProgress && overlaysInvalidatedBy(rebuilt, this.overlaySources))
|
|
1179
|
+
this.reapplyColorOverrides(device, pipeline);
|
|
1160
1180
|
}
|
|
1161
1181
|
/**
|
|
1162
1182
|
* Check if there are pending batch rebuilds
|
|
@@ -1177,8 +1197,8 @@ export class Scene {
|
|
|
1177
1197
|
* raycast bounds. This is the proper removal path.
|
|
1178
1198
|
*
|
|
1179
1199
|
* Notes:
|
|
1180
|
-
* - For color-merged meshes (
|
|
1181
|
-
* a single MeshData
|
|
1200
|
+
* - For color-merged meshes (`entityIds` naming other entities,
|
|
1201
|
+
* `hostsOtherEntities`) a single MeshData hosts many entities. We do NOT
|
|
1182
1202
|
* drop the whole mesh in that case — that would also remove
|
|
1183
1203
|
* the other entities — but we DO clear the bbox + meshDataMap
|
|
1184
1204
|
* for the requested expressId, so picking and selection stop
|
|
@@ -1207,10 +1227,9 @@ export class Scene {
|
|
|
1207
1227
|
// caller's bulk-count contract.
|
|
1208
1228
|
let removedDedicated = false;
|
|
1209
1229
|
for (const meshData of meshDataList) {
|
|
1210
|
-
// Color-merged path: shared mesh,
|
|
1211
|
-
if (meshData
|
|
1230
|
+
// Color-merged path (entityIds naming OTHER entities): keep the shared mesh, drop our entry.
|
|
1231
|
+
if (hostsOtherEntities(meshData))
|
|
1212
1232
|
continue;
|
|
1213
|
-
}
|
|
1214
1233
|
removedDedicated = true;
|
|
1215
1234
|
// Dedicated mesh — drop from its bucket and decrement the
|
|
1216
1235
|
// bucket's vertexBytes counter so subsequent
|
|
@@ -1558,21 +1577,40 @@ export class Scene {
|
|
|
1558
1577
|
}
|
|
1559
1578
|
this.meshes = kept;
|
|
1560
1579
|
}
|
|
1561
|
-
/**
|
|
1562
|
-
|
|
1563
|
-
|
|
1580
|
+
/** Shared `TranslationScene` view for `setModelTranslation`/`setModelRotation`
|
|
1581
|
+
* (#4890) — the placement helpers in scene-model-translation.ts read scene
|
|
1582
|
+
* state through this narrow interface instead of the full `Scene`. */
|
|
1583
|
+
translationScope() {
|
|
1564
1584
|
const batches = this.finalizeInProgress ? [...new Set([...this.batchedMeshes,
|
|
1565
1585
|
...[...this.buckets.values()].flatMap((bucket) => bucket.batchedMesh ? [bucket.batchedMesh] : [])])] : this.batchedMeshes;
|
|
1566
|
-
|
|
1586
|
+
return { translations: this.modelTranslations, pieces: this.meshDataMap,
|
|
1567
1587
|
bounds: this.boundingBoxes, batches, meshes: this.meshes, overrides: this.overrideBatches, textured: this.texturedMeshes,
|
|
1568
1588
|
templates: this.instancedTemplates, cpu: this.instancedTemplateCpu, occurrences: this.instancedEntityMap,
|
|
1569
1589
|
device: this.instancedDevice, evictHighlight: (id) => this.evictHighlightMeshes(id, true),
|
|
1570
1590
|
clearPartial: () => this.dropAllPartialCaches(),
|
|
1571
|
-
unionBounds: (id, view, offset, min, max) => this.unionInstancedWorldAabb(id, view, offset, ...min, ...max)
|
|
1572
|
-
|
|
1591
|
+
unionBounds: (id, view, offset, min, max) => this.unionInstancedWorldAabb(id, view, offset, ...min, ...max) };
|
|
1592
|
+
}
|
|
1593
|
+
dropOrphanedSuppressedBounds() {
|
|
1573
1594
|
for (const id of this.instanceSuppression.suppressedIds())
|
|
1574
1595
|
if (!this.meshDataMap.has(id))
|
|
1575
1596
|
this.boundingBoxes.delete(id);
|
|
1597
|
+
}
|
|
1598
|
+
/** Absolute workspace translation, renderer Y-up metres (#4226). */
|
|
1599
|
+
getModelTranslation(modelIndex) { return this.modelTranslations.get(modelIndex); }
|
|
1600
|
+
setModelTranslation(modelIndex, translation) {
|
|
1601
|
+
const changed = translateSceneModel(this.translationScope(), modelIndex, translation);
|
|
1602
|
+
this.dropOrphanedSuppressedBounds();
|
|
1603
|
+
return changed;
|
|
1604
|
+
}
|
|
1605
|
+
/** Absolute workspace yaw, renderer Y-up radians about the render-frame
|
|
1606
|
+
* pivot `(pivot[0], *, pivot[2])` — the GPU-instanced half of a whole-model
|
|
1607
|
+
* rotation (#4890); `angle === 0` clears it. Flat/authored/batched geometry
|
|
1608
|
+
* is rotated by the viewer's bake, not here. */
|
|
1609
|
+
getModelRotation(modelIndex) { return this.modelTranslations.getYaw(modelIndex); }
|
|
1610
|
+
setModelRotation(modelIndex, angle, pivot) {
|
|
1611
|
+
const yaw = angle === 0 ? null : { angle, px: pivot[0], pz: pivot[2] };
|
|
1612
|
+
const changed = rotateSceneModelInstances(this.translationScope(), modelIndex, yaw);
|
|
1613
|
+
this.dropOrphanedSuppressedBounds();
|
|
1576
1614
|
return changed;
|
|
1577
1615
|
}
|
|
1578
1616
|
/** Bulk variant of `translateMeshesForEntity`. */
|
|
@@ -1800,7 +1838,7 @@ export class Scene {
|
|
|
1800
1838
|
// applies to cells exactly like it does to buckets.
|
|
1801
1839
|
const colorGroups = new Map();
|
|
1802
1840
|
for (const meshData of meshDataArray) {
|
|
1803
|
-
const key = this.bucketBaseKey(meshData);
|
|
1841
|
+
const key = this.meshDataBucket.get(meshData)?.key ?? this.bucketBaseKey(meshData);
|
|
1804
1842
|
for (const fragment of this.splitMeshForStreaming(meshData)) {
|
|
1805
1843
|
let group = colorGroups.get(key);
|
|
1806
1844
|
if (!group) {
|
|
@@ -1811,11 +1849,11 @@ export class Scene {
|
|
|
1811
1849
|
}
|
|
1812
1850
|
}
|
|
1813
1851
|
// Create one fragment batch per color group (with buffer limit splitting)
|
|
1814
|
-
for (const [, group] of colorGroups) {
|
|
1852
|
+
for (const [key, group] of colorGroups) {
|
|
1815
1853
|
const chunks = this.splitMeshDataForBufferLimit(group, this.cachedMaxBufferSize);
|
|
1816
1854
|
for (const chunk of chunks) {
|
|
1817
1855
|
const color = chunk[0].color;
|
|
1818
|
-
const fragment = this.createBatchedMesh(chunk, color, device, pipeline);
|
|
1856
|
+
const fragment = this.createBatchedMesh(chunk, color, device, pipeline, undefined, undefined, this.buckets.get(key)?.frameOrigin);
|
|
1819
1857
|
this.batchedMeshes.push(fragment);
|
|
1820
1858
|
this.streamingFragments.push(fragment);
|
|
1821
1859
|
}
|
|
@@ -1943,6 +1981,7 @@ export class Scene {
|
|
|
1943
1981
|
if (!fragmentSet.has(batch))
|
|
1944
1982
|
destroyGpuResources(batch);
|
|
1945
1983
|
}
|
|
1984
|
+
this.reapplyColorOverrides(device, pipeline);
|
|
1946
1985
|
}
|
|
1947
1986
|
/**
|
|
1948
1987
|
* Time-sliced version of finalizeStreaming.
|
|
@@ -1985,15 +2024,6 @@ export class Scene {
|
|
|
1985
2024
|
// always clear finalizeInProgress.
|
|
1986
2025
|
return new Promise((resolve, reject) => {
|
|
1987
2026
|
const newBatches = [];
|
|
1988
|
-
// Every batch this attempt creates, paired with the bucket that now owns
|
|
1989
|
-
// it and the value it displaced. `newBatches` alone is not enough to roll
|
|
1990
|
-
// back: processChunk publishes each batch into `bucket.batchedMesh`, so
|
|
1991
|
-
// freeing it without repairing the owner leaves the bucket map pointing
|
|
1992
|
-
// at destroyed GPU resources (use-after-free on the next bucket-driven
|
|
1993
|
-
// access). `previous` is null for the freshly built buckets and, for a
|
|
1994
|
-
// carried COLD bucket a re-grouped meshData landed in, the shell that the
|
|
1995
|
-
// restored `batchedMeshes` still holds — which must be put back, not
|
|
1996
|
-
// nulled.
|
|
1997
2027
|
const createdOwned = [];
|
|
1998
2028
|
let carriedCold = [];
|
|
1999
2029
|
let pendingKeys = [];
|
|
@@ -2004,11 +2034,14 @@ export class Scene {
|
|
|
2004
2034
|
// are what the restored arrays point back at). Iterating the owned
|
|
2005
2035
|
// pairs rather than `newBatches` also skips the carried cold shells
|
|
2006
2036
|
// appended just before the swap, which this attempt did not create.
|
|
2007
|
-
for (const { bucket, previous, batch } of createdOwned) {
|
|
2037
|
+
for (const { bucket, previous, previousFrameOrigin, batch } of createdOwned) {
|
|
2008
2038
|
// Repair the owner BEFORE the free, so no bucket is ever observable
|
|
2009
|
-
// holding a destroyed batch
|
|
2010
|
-
|
|
2039
|
+
// holding a destroyed batch; its frame origin (what a later
|
|
2040
|
+
// createBatchedMesh seeds from) must describe the restored batch.
|
|
2041
|
+
if (bucket.batchedMesh === batch) {
|
|
2011
2042
|
bucket.batchedMesh = previous;
|
|
2043
|
+
bucket.frameOrigin = previousFrameOrigin;
|
|
2044
|
+
}
|
|
2012
2045
|
if (!oldBatchSet.has(batch) && !fragmentSet.has(batch)) {
|
|
2013
2046
|
destroyGpuResources(batch);
|
|
2014
2047
|
}
|
|
@@ -2029,9 +2062,11 @@ export class Scene {
|
|
|
2029
2062
|
}
|
|
2030
2063
|
const color = bucket.meshData[0].color;
|
|
2031
2064
|
const previous = bucket.batchedMesh;
|
|
2065
|
+
const previousFrameOrigin = bucket.frameOrigin;
|
|
2032
2066
|
const batchedMesh = scene.createBatchedMesh(bucket.meshData, color, device, pipeline, key);
|
|
2033
2067
|
bucket.batchedMesh = batchedMesh;
|
|
2034
|
-
|
|
2068
|
+
bucket.frameOrigin = batchedMesh.origin;
|
|
2069
|
+
createdOwned.push({ bucket, previous, previousFrameOrigin, batch: batchedMesh });
|
|
2035
2070
|
newBatches.push(batchedMesh);
|
|
2036
2071
|
// Check time budget — yield if exceeded
|
|
2037
2072
|
if (performance.now() - chunkStart >= budgetMs) {
|
|
@@ -2062,6 +2097,7 @@ export class Scene {
|
|
|
2062
2097
|
destroyGpuResources(batch);
|
|
2063
2098
|
}
|
|
2064
2099
|
scene.finalizeInProgress = false;
|
|
2100
|
+
scene.reapplyColorOverrides(device, pipeline); // never throws: the swap above is committed
|
|
2065
2101
|
resolve();
|
|
2066
2102
|
}
|
|
2067
2103
|
catch (err) {
|
|
@@ -2169,7 +2205,6 @@ export class Scene {
|
|
|
2169
2205
|
* Call this after finalizeStreaming() when all color updates have been applied.
|
|
2170
2206
|
*/
|
|
2171
2207
|
releaseGeometryData() {
|
|
2172
|
-
this.authoredGeneration++;
|
|
2173
2208
|
if (this.geometryReleased)
|
|
2174
2209
|
return;
|
|
2175
2210
|
if (this.instanceSuppression.retained) {
|
|
@@ -2183,6 +2218,8 @@ export class Scene {
|
|
|
2183
2218
|
`Call finalizeStreaming()/rebuildPendingBatches() first.`);
|
|
2184
2219
|
return;
|
|
2185
2220
|
}
|
|
2221
|
+
this.authoredGeneration++;
|
|
2222
|
+
this.authoredPreparations.invalidate();
|
|
2186
2223
|
this.appearanceController?.forget();
|
|
2187
2224
|
// 1. Precompute and cache ALL entity bounding boxes before releasing data.
|
|
2188
2225
|
// Same rule as `finishEphemeralStreaming`: an entity with no usable vertex
|
|
@@ -2323,16 +2360,20 @@ export class Scene {
|
|
|
2323
2360
|
* @param bucketKey - Optional unique key for this batch. When omitted the
|
|
2324
2361
|
* base color key is used (fine for overlay / partial batches that don't
|
|
2325
2362
|
* participate in the main buckets map).
|
|
2363
|
+
* @param quantization - 'auto' for bucket batches; derived batches pass `inheritedQuantization(source)` (#4832).
|
|
2326
2364
|
*/
|
|
2327
|
-
createBatchedMesh(meshes, color, device, pipeline, bucketKey) {
|
|
2365
|
+
createBatchedMesh(meshes, color, device, pipeline, bucketKey, quantization = this.quantizedBatchesEnabled ? 'auto' : 'off', frameOrigin) {
|
|
2328
2366
|
// Keep main's model-local frame and translation registration while staging
|
|
2329
2367
|
// every GPU allocation before publishing Scene state.
|
|
2330
2368
|
const modelIndex = meshes[0]?.modelIndex ?? 0;
|
|
2331
2369
|
const offset = this.modelTranslations.get(modelIndex);
|
|
2370
|
+
const origin = topologySafeBatchOrigin(meshes, frameOrigin ?? this.meshDataBucket.get(meshes[0])?.batchedMesh?.origin, this.modelTranslations.frameOrigin(this.sharedFrameOrigins.get(modelIndex) ?? null, modelIndex), this.meshDataBucket.get(meshes[0])?.frameOrigin);
|
|
2371
|
+
if (!origin)
|
|
2372
|
+
throw new Error('Unable to resolve a topology-safe GPU frame for mesh geometry.');
|
|
2332
2373
|
const result = createSceneBatch(meshes, color, device, pipeline, {
|
|
2333
2374
|
id: this.nextBatchId, colorKey: bucketKey ?? this.colorKey(color),
|
|
2334
|
-
origin
|
|
2335
|
-
quantized:
|
|
2375
|
+
origin,
|
|
2376
|
+
quantized: quantization, lod: this.lodBuildsEnabled,
|
|
2336
2377
|
}, bucketKey);
|
|
2337
2378
|
this.nextBatchId++;
|
|
2338
2379
|
if (!this.sharedFrameOrigins.has(modelIndex) && result.origin) {
|
|
@@ -2361,38 +2402,15 @@ export class Scene {
|
|
|
2361
2402
|
* Returns the bucket key to use (may be the base key or a suffixed key).
|
|
2362
2403
|
*/
|
|
2363
2404
|
resolveActiveBucket(baseColorKey, meshData) {
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
bucketKey = `${baseColorKey}#${this.nextSplitId++}`;
|
|
2374
|
-
this.activeBucketKey.set(baseColorKey, bucketKey);
|
|
2375
|
-
let target = this.buckets.get(bucketKey);
|
|
2376
|
-
if (!target) {
|
|
2377
|
-
target = { key: bucketKey, meshData: [], batchedMesh: null, vertexBytes: 0 };
|
|
2378
|
-
this.buckets.set(bucketKey, target);
|
|
2379
|
-
}
|
|
2380
|
-
target.vertexBytes += meshBytes;
|
|
2381
|
-
return bucketKey;
|
|
2382
|
-
}
|
|
2383
|
-
if (currentBytes > 0 && currentBytes + meshBytes > this.cachedMaxBufferSize) {
|
|
2384
|
-
// Overflow — create a new sub-bucket
|
|
2385
|
-
bucketKey = `${baseColorKey}#${this.nextSplitId++}`;
|
|
2386
|
-
this.activeBucketKey.set(baseColorKey, bucketKey);
|
|
2387
|
-
}
|
|
2388
|
-
// Update size tracking on the bucket (create if needed)
|
|
2389
|
-
let targetBucket = this.buckets.get(bucketKey);
|
|
2390
|
-
if (!targetBucket) {
|
|
2391
|
-
targetBucket = { key: bucketKey, meshData: [], batchedMesh: null, vertexBytes: 0 };
|
|
2392
|
-
this.buckets.set(bucketKey, targetBucket);
|
|
2393
|
-
}
|
|
2394
|
-
targetBucket.vertexBytes += meshBytes;
|
|
2395
|
-
return bucketKey;
|
|
2405
|
+
const modelIndex = meshData.modelIndex ?? 0;
|
|
2406
|
+
return resolvePrecisionBucket({
|
|
2407
|
+
buckets: this.buckets,
|
|
2408
|
+
activeKeys: this.activeBucketKey,
|
|
2409
|
+
coldKeys: this.coldBuckets,
|
|
2410
|
+
maxBufferSize: this.cachedMaxBufferSize,
|
|
2411
|
+
sharedOrigin: this.modelTranslations.frameOrigin(this.sharedFrameOrigins.get(modelIndex) ?? null, modelIndex),
|
|
2412
|
+
nextSplitKey: () => `${baseColorKey}#${this.nextSplitId++}`,
|
|
2413
|
+
}, baseColorKey, meshData);
|
|
2396
2414
|
}
|
|
2397
2415
|
/**
|
|
2398
2416
|
* Extract the base color key from a bucket key (strips "#N" suffix if present).
|
|
@@ -2500,32 +2518,27 @@ export class Scene {
|
|
|
2500
2518
|
this.partialBatchCache.delete(currentCacheKey);
|
|
2501
2519
|
}
|
|
2502
2520
|
}
|
|
2503
|
-
//
|
|
2504
|
-
//
|
|
2505
|
-
//
|
|
2506
|
-
//
|
|
2507
|
-
|
|
2508
|
-
// belongs to this batch when BOTH its cell and colour match).
|
|
2521
|
+
// `colorKey` is the bucket key: match each piece's OWNING bucket exactly.
|
|
2522
|
+
// Matching the base key (suffix "#N" stripped) pulled sibling overflow
|
|
2523
|
+
// buckets' pieces in — drawn twice, wrong quantization inherited (#4832).
|
|
2524
|
+
// Only a batch with no bucket (a streaming fragment) uses the base key.
|
|
2525
|
+
const bucket = this.buckets.get(colorKey);
|
|
2509
2526
|
const baseKey = this.baseColorKey(colorKey);
|
|
2510
2527
|
const visibleMeshData = [];
|
|
2511
2528
|
for (const expressId of visibleIds) {
|
|
2512
|
-
const
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
for (const piece of pieces) {
|
|
2516
|
-
// Only include pieces that match this batch's cell + color
|
|
2517
|
-
if (this.bucketBaseKey(piece) === baseKey) {
|
|
2518
|
-
visibleMeshData.push(piece);
|
|
2519
|
-
}
|
|
2529
|
+
for (const piece of this.meshDataMap.get(expressId) ?? []) {
|
|
2530
|
+
if (bucket ? this.meshDataBucket.get(piece) === bucket : this.bucketBaseKey(piece) === baseKey) {
|
|
2531
|
+
visibleMeshData.push(piece);
|
|
2520
2532
|
}
|
|
2521
2533
|
}
|
|
2522
2534
|
}
|
|
2523
2535
|
if (visibleMeshData.length === 0) {
|
|
2524
2536
|
return undefined;
|
|
2525
2537
|
}
|
|
2526
|
-
//
|
|
2538
|
+
// Drawn INSTEAD of its source batch, so it inherits the source's f32/quantized
|
|
2539
|
+
// decision (#4832): the overlay built from the same source must match its depth.
|
|
2527
2540
|
const color = visibleMeshData[0].color;
|
|
2528
|
-
const partialBatch = this.createBatchedMesh(visibleMeshData, color, device, pipeline);
|
|
2541
|
+
const partialBatch = this.createBatchedMesh(visibleMeshData, color, device, pipeline, undefined, inheritedQuantization(this.quantizedBatchesEnabled, bucket?.batchedMesh), bucket?.batchedMesh?.origin);
|
|
2529
2542
|
// Cache it
|
|
2530
2543
|
this.partialBatchCache.set(cacheKey, partialBatch);
|
|
2531
2544
|
this.partialBatchCacheKeys.set(sourceBatchKey, cacheKey);
|
|
@@ -2540,12 +2553,10 @@ export class Scene {
|
|
|
2540
2553
|
// override colors. They are rendered on top of existing depth via the overlay
|
|
2541
2554
|
// pipeline (depthCompare 'equal'), so hidden entities never leak through.
|
|
2542
2555
|
/**
|
|
2543
|
-
* Set color overrides for lens coloring.
|
|
2544
|
-
*
|
|
2545
|
-
*
|
|
2546
|
-
*
|
|
2547
|
-
* Applies the same buffer-size splitting as regular batches to prevent
|
|
2548
|
-
* GPU buffer overflow on large models.
|
|
2556
|
+
* Set color overrides for lens / chart / IDS / compare / 4D coloring. Overlay
|
|
2557
|
+
* batches are grouped by SOURCE bucket + override color and inherit the
|
|
2558
|
+
* source batch's quantization (#4832, `scene-derived-batches.ts`). Original
|
|
2559
|
+
* batches are NEVER modified — clearing is instant.
|
|
2549
2560
|
*/
|
|
2550
2561
|
setColorOverrides(overrides, device, pipeline) {
|
|
2551
2562
|
// Destroy previous overlay batches
|
|
@@ -2564,44 +2575,37 @@ export class Scene {
|
|
|
2564
2575
|
this.setInstancedColorOverrides(null);
|
|
2565
2576
|
return;
|
|
2566
2577
|
}
|
|
2567
|
-
// Defensive copy
|
|
2568
|
-
// without aliasing the renderer's pipeline-routing state. Tuples are
|
|
2569
|
-
// frozen by the readonly type — we don't deep-clone the inner arrays
|
|
2570
|
-
// because they're treated as immutable by every consumer.
|
|
2578
|
+
// Defensive copy: callers may mutate/reuse `overrides`; the tuples are treated as immutable by every consumer.
|
|
2571
2579
|
this.colorOverrides = new Map(overrides);
|
|
2572
|
-
//
|
|
2573
|
-
// bytes in place (no separate overlay pass — the instanced records carry the
|
|
2574
|
-
// override colour directly). No-op when no instanced data is loaded.
|
|
2580
|
+
// Instanced occurrences carry the override colour in their records (no overlay pass); no-op without instanced data.
|
|
2575
2581
|
this.setInstancedColorOverrides(overrides);
|
|
2576
|
-
//
|
|
2577
|
-
|
|
2578
|
-
const colorGroups = new Map();
|
|
2579
|
-
for (const [expressId, color] of overrides) {
|
|
2580
|
-
for (const piece of this.meshDataMap.get(expressId) ?? []) {
|
|
2581
|
-
const key = `${piece.modelIndex ?? 0}:${this.colorKey(color)}`;
|
|
2582
|
-
let group = colorGroups.get(key);
|
|
2583
|
-
if (!group) {
|
|
2584
|
-
group = { color, meshData: [] };
|
|
2585
|
-
colorGroups.set(key, group);
|
|
2586
|
-
}
|
|
2587
|
-
group.meshData.push(piece);
|
|
2588
|
-
}
|
|
2589
|
-
}
|
|
2590
|
-
// Build overlay batches per override color, splitting if buffers would exceed GPU limit
|
|
2582
|
+
// Bucket keys carry cell + colour + model: an overlay batch is a subset of ONE base batch, in one model.
|
|
2583
|
+
const colorGroups = groupOverridePieces(overrides, (id) => this.meshDataMap.get(id), (piece) => this.meshDataBucket.get(piece), (piece) => this.bucketBaseKey(piece), (c) => this.colorKey(c));
|
|
2591
2584
|
const maxBufferSize = this.getMaxBufferSize(device);
|
|
2592
|
-
for (const
|
|
2593
|
-
if (
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
this.overrideBatches.push(
|
|
2585
|
+
for (const { color, meshData, sourceBatch, sourceKey } of colorGroups.values()) {
|
|
2586
|
+
if (sourceKey !== null)
|
|
2587
|
+
for (const piece of meshData)
|
|
2588
|
+
this.overlaySources.set(piece, sourceKey);
|
|
2589
|
+
const quantization = inheritedQuantization(this.quantizedBatchesEnabled, sourceBatch);
|
|
2590
|
+
for (const chunk of this.splitMeshDataForBufferLimit(meshData, maxBufferSize)) {
|
|
2591
|
+
this.overrideBatches.push(this.createBatchedMesh(chunk, color, device, pipeline, undefined, quantization, sourceBatch?.origin));
|
|
2599
2592
|
}
|
|
2600
2593
|
}
|
|
2601
2594
|
}
|
|
2602
|
-
/**
|
|
2603
|
-
*
|
|
2604
|
-
*/
|
|
2595
|
+
/** Rebuild the installed overlays against the batches that write depth NOW
|
|
2596
|
+
* (#4832). Best-effort — a committed finalize/rebuild must not be undone by
|
|
2597
|
+
* an overlay allocation failure, so that is reported, not thrown. */
|
|
2598
|
+
reapplyColorOverrides(device, pipeline) {
|
|
2599
|
+
if (!this.colorOverrides)
|
|
2600
|
+
return;
|
|
2601
|
+
try {
|
|
2602
|
+
this.setColorOverrides(cloneOverrides(this.colorOverrides), device, pipeline);
|
|
2603
|
+
}
|
|
2604
|
+
catch (err) {
|
|
2605
|
+
console.warn('[Scene] overlay rebuild after finalize failed — colours may be missing until the next repaint', err);
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
/** Clear all color overrides — instant, no batch rebuild needed. */
|
|
2605
2609
|
clearColorOverrides() {
|
|
2606
2610
|
this.destroyOverrideBatches();
|
|
2607
2611
|
this.colorOverrideGeneration++;
|
|
@@ -2644,6 +2648,7 @@ export class Scene {
|
|
|
2644
2648
|
for (const batch of this.overrideBatches)
|
|
2645
2649
|
destroyGpuResources(batch);
|
|
2646
2650
|
this.overrideBatches = [];
|
|
2651
|
+
this.overlaySources.clear();
|
|
2647
2652
|
}
|
|
2648
2653
|
/**
|
|
2649
2654
|
* Clear scene
|
|
@@ -3326,22 +3331,17 @@ export class Scene {
|
|
|
3326
3331
|
this.restoreInstanceColor(device, eid);
|
|
3327
3332
|
}
|
|
3328
3333
|
for (const eid of toFade) {
|
|
3329
|
-
const
|
|
3330
|
-
if (
|
|
3331
|
-
|
|
3332
|
-
|
|
3334
|
+
const override = this.instancedOverrideColors?.get(eid);
|
|
3335
|
+
if (override)
|
|
3336
|
+
this.writeInstanceColor(device, eid, [override[0], override[1], override[2], ghostAlpha]);
|
|
3337
|
+
else
|
|
3338
|
+
this.writeOriginalInstanceColors(device, eid, ghostAlpha);
|
|
3333
3339
|
}
|
|
3334
3340
|
this.instancedGhosted = next;
|
|
3335
3341
|
this.lastGhostAlpha = ghostAlpha;
|
|
3336
3342
|
this.instancedGhostDirty = false;
|
|
3337
3343
|
this.instancedGhostTransparent = next.size > 0 && ghostAlpha < OPAQUE_ALPHA_CUTOFF;
|
|
3338
3344
|
}
|
|
3339
|
-
/** The colour an occurrence was uploaded with, before any override or ghost. */
|
|
3340
|
-
originalInstanceColor(eid) {
|
|
3341
|
-
const locs = this.instancedEntityMap.get(eid);
|
|
3342
|
-
const first = locs?.[0];
|
|
3343
|
-
return first ? first.originalColor : null;
|
|
3344
|
-
}
|
|
3345
3345
|
/** Write the combined flag lane (selected | hidden) for every occurrence of `eid`
|
|
3346
3346
|
* (shares one u32 lane at INSTANCE_FLAGS_OFFSET, so both bits are folded here).
|
|
3347
3347
|
* #4382: for the item-filtered eid, selected narrows per-occurrence to
|
|
@@ -3383,6 +3383,7 @@ export class Scene {
|
|
|
3383
3383
|
device.queue.writeBuffer(buf, loc.byteOffset + INSTANCE_COLOR_OFFSET, new Float32Array(loc.originalColor));
|
|
3384
3384
|
}
|
|
3385
3385
|
}
|
|
3386
|
+
writeOriginalInstanceColors(device, eid, alpha) { writeOriginalInstancedColors(device, this.instancedEntityMap.get(eid) ?? [], this.instancedTemplates, INSTANCE_COLOR_OFFSET, alpha); }
|
|
3386
3387
|
/**
|
|
3387
3388
|
* Build a textured mesh (#961): interleave position+normal+entityId+uv into one
|
|
3388
3389
|
* vertex buffer, upload the decoded RGBA8 texture, create a sampler honouring
|
|
@@ -3567,6 +3568,7 @@ export class Scene {
|
|
|
3567
3568
|
*/
|
|
3568
3569
|
clearFlatGeometry() {
|
|
3569
3570
|
this.authoredGeneration++;
|
|
3571
|
+
this.authoredPreparations.invalidate();
|
|
3570
3572
|
this.instanceSuppression.restore();
|
|
3571
3573
|
this.appearanceController?.forget();
|
|
3572
3574
|
this.clearFlatBuffers();
|
|
@@ -3575,6 +3577,7 @@ export class Scene {
|
|
|
3575
3577
|
* owners keep their original-instance history. Full reset remains separate. */
|
|
3576
3578
|
clearFlatGeometryForRebuild(geometry, models, sourceGeometry = geometry) {
|
|
3577
3579
|
this.authoredGeneration++;
|
|
3580
|
+
this.authoredPreparations.invalidate();
|
|
3578
3581
|
const retained = this.appearanceController?.prepareRebuild(sourceGeometry, models) ?? new Set();
|
|
3579
3582
|
const discarded = this.appearanceController?.discardedForRebuild(retained) ?? [];
|
|
3580
3583
|
// A discarded converted owner must not resurrect its obsolete type instance.
|