@ifc-lite/renderer 3.1.0 → 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 +43 -21
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +155 -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/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 +4 -1
- package/dist/scene-batch-upload.d.ts.map +1 -1
- package/dist/scene-batch-upload.js +17 -0
- 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-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 +25 -20
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +149 -137
- package/dist/scene.js.map +1 -1
- package/package.json +2 -2
package/dist/scene.js
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
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';
|
|
@@ -14,9 +14,10 @@ import { hostsOtherEntities } from './mesh-entity-hosting.js';
|
|
|
14
14
|
import { BATCH_CONSTANTS } from './constants.js';
|
|
15
15
|
import { prepareRayDirInv, raycastBoundingBoxes, raycastTriangles, rayIntersectsBox, } from './scene-raycaster.js';
|
|
16
16
|
import { selectBoundingBoxesInRect } from './scene-rect-select.js';
|
|
17
|
-
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';
|
|
18
19
|
import { sumResidentGpuBytes } from './render-stats.js';
|
|
19
|
-
import { composeInstancedOverrideColor } from './instanced-override-color.js';
|
|
20
|
+
import { composeInstancedOverrideColor, writeOriginalInstancedColors } from './instanced-override-color.js';
|
|
20
21
|
import { bucketBaseKeyFor } from './chunk-grid.js';
|
|
21
22
|
import { inheritedQuantization, groupOverridePieces, cloneOverrides, overlaysInvalidatedBy } from './scene-derived-batches.js';
|
|
22
23
|
import { VisibilityEpochTracker } from './visibility-epoch.js';
|
|
@@ -24,11 +25,13 @@ import { isEntityVisible } from './entity-visibility.js';
|
|
|
24
25
|
import { planInstancedGhosting } from './instanced-ghost-plan.js';
|
|
25
26
|
import { selectEvictions } from './residency.js';
|
|
26
27
|
import { OPAQUE_ALPHA_CUTOFF } from './overlay-routing.js';
|
|
27
|
-
import { translateSceneModel, releaseInstanceVertices, refreshTexturedBounds } from './scene-model-translation.js';
|
|
28
|
+
import { translateSceneModel, rotateSceneModelInstances, releaseInstanceVertices, refreshTexturedBounds } from './scene-model-translation.js';
|
|
28
29
|
import { ModelTranslations } from './model-translation.js';
|
|
29
|
-
import {
|
|
30
|
+
import { DerivedMeshProvenance } from './scene-derived-mesh-provenance.js';
|
|
31
|
+
import { rebuildSceneBatches } from './scene-batch-rebuild.js';
|
|
30
32
|
import { dropAllPartialCaches as dropAllPartialCachesIn, dropPartialCacheForBatch as dropPartialCacheForBatchIn, retireUnusedAlphaSlots as retireUnusedAlphaSlotsIn, } from './partial-batch-cache.js';
|
|
31
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';
|
|
32
35
|
const EMPTY_INSTANCED_TEMPLATES = [];
|
|
33
36
|
/**
|
|
34
37
|
* Pure helper: compute the exclusive end index of the next flushPending()
|
|
@@ -84,6 +87,7 @@ export class Scene {
|
|
|
84
87
|
batchedMeshes = []; // flat render array (rebuilt from buckets)
|
|
85
88
|
buckets = new Map(); // bucketKey -> consolidated bucket state
|
|
86
89
|
meshDataBucket = new Map(); // reverse lookup: MeshData -> owning bucket
|
|
90
|
+
derivedMeshProvenance = new DerivedMeshProvenance();
|
|
87
91
|
modelTranslations = new ModelTranslations();
|
|
88
92
|
meshDataMap = new Map(); // Map expressId -> MeshData[] (for lazy buffer creation, accumulates multiple pieces)
|
|
89
93
|
boundingBoxes = new Map(); // Map expressId -> bounding box (computed lazily)
|
|
@@ -95,7 +99,9 @@ export class Scene {
|
|
|
95
99
|
rgbaTexturePool = new RgbaTexturePool();
|
|
96
100
|
appearanceController;
|
|
97
101
|
appearanceBuckets;
|
|
102
|
+
appearanceAccessState;
|
|
98
103
|
authoredGeneration = 0;
|
|
104
|
+
authoredPreparations = new AuthoredPreparationRegistry();
|
|
99
105
|
appearanceAccess(device, pipeline) {
|
|
100
106
|
return {
|
|
101
107
|
meshes: () => this.texturedMeshes, data: this.meshDataMap,
|
|
@@ -135,24 +141,23 @@ export class Scene {
|
|
|
135
141
|
},
|
|
136
142
|
};
|
|
137
143
|
}
|
|
144
|
+
bindAppearanceAccess(device, pipeline) { return this.appearanceAccessState = rebindSceneAppearanceAccess(this.appearanceAccessState, this.appearanceAccess(device, pipeline)); }
|
|
138
145
|
sharedAppearanceBuckets(access) {
|
|
139
146
|
return this.appearanceBuckets ??= new AppearanceBuckets(access.buckets, id => this.meshDataMap.get(id));
|
|
140
147
|
}
|
|
141
148
|
appearancePreview(device, pipeline) {
|
|
142
|
-
const access = this.
|
|
149
|
+
const access = this.bindAppearanceAccess(device, pipeline);
|
|
143
150
|
return this.appearanceController ??= createSceneAppearancePreview(access, this.sharedAppearanceBuckets(access));
|
|
144
151
|
}
|
|
145
|
-
/** Place canonical native appearance source geometry in this model's live frame. */
|
|
146
152
|
placeAppearanceSource(mesh) { return this.modelTranslations.placeMesh(mesh); }
|
|
147
|
-
/** Retain model-local source coordinates when publishing a placed appearance mesh. */
|
|
148
153
|
appearanceSourceMesh(mesh) { return this.modelTranslations.sourceFromPlaced(mesh); }
|
|
149
154
|
/** Stage one new IFC owner with all its coloured or textured geometry parts. */
|
|
150
155
|
prepareAuthoredOwner(parts, device, pipeline) {
|
|
151
|
-
const access = this.
|
|
152
|
-
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, () => {
|
|
153
158
|
if (generation !== this.authoredGeneration)
|
|
154
159
|
throw new Error('The scene changed while preparing the object.');
|
|
155
|
-
});
|
|
160
|
+
}));
|
|
156
161
|
}
|
|
157
162
|
/** Compatibility entry point for an image-backed single-part owner. */
|
|
158
163
|
prepareTexturedOwner(mesh, device, pipeline) {
|
|
@@ -301,12 +306,11 @@ export class Scene {
|
|
|
301
306
|
// the queue via flushPending() with a per-frame time budget.
|
|
302
307
|
meshQueue = [];
|
|
303
308
|
meshQueueReadIndex = 0;
|
|
304
|
-
// ─── GPU-resident mode ──────────────────────────────────────────────
|
|
305
|
-
// After releaseGeometryData(), JS-side typed arrays are freed.
|
|
306
|
-
// Only lightweight metadata is retained for operations that don't need
|
|
307
|
-
// raw vertex data (bounding boxes, color key lookups, expressId sets).
|
|
308
309
|
geometryReleased = false;
|
|
309
310
|
ephemeralStreamingMode = false;
|
|
311
|
+
prepareDeviceRecovery() { return prepareSceneDeviceRecovery(this); }
|
|
312
|
+
discardGpuResourcesForRecovery() { discardSceneGpuResourcesForRecovery(this); }
|
|
313
|
+
restoreGpuResourcesAfterRecovery(device, pipeline) { restoreSceneGpuResourcesAfterRecovery(this, device, pipeline); }
|
|
310
314
|
/**
|
|
311
315
|
* Add mesh to scene
|
|
312
316
|
*/
|
|
@@ -329,8 +333,10 @@ export class Scene {
|
|
|
329
333
|
/** The shared local-frame origin all batches relativize against (null until
|
|
330
334
|
* the first batch is built). Per-mesh highlight/picker VBOs replicate the
|
|
331
335
|
* batch's exact f32 path against this so they render bit-coincident. */
|
|
332
|
-
getSharedFrameOrigin(modelIndex = 0) {
|
|
333
|
-
|
|
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;
|
|
334
340
|
}
|
|
335
341
|
/**
|
|
336
342
|
* Enable/disable spatial chunk bucketing (issue #1682 phase 2). When set,
|
|
@@ -428,6 +434,7 @@ export class Scene {
|
|
|
428
434
|
}
|
|
429
435
|
const rebuilt = this.createBatchedMesh(bucket.meshData, bucket.meshData[0].color, device, pipeline, key);
|
|
430
436
|
bucket.batchedMesh = rebuilt;
|
|
437
|
+
bucket.frameOrigin = rebuilt.origin;
|
|
431
438
|
const idx = this.batchedMeshes.indexOf(old);
|
|
432
439
|
if (idx >= 0)
|
|
433
440
|
this.batchedMeshes[idx] = rebuilt;
|
|
@@ -474,7 +481,8 @@ export class Scene {
|
|
|
474
481
|
* lattice snap in createMeshFromData (an f32 batch must NOT snap). Same rule
|
|
475
482
|
* as overlay/partial batches; global flag while unbucketed. */
|
|
476
483
|
isMeshQuantized(meshData) {
|
|
477
|
-
|
|
484
|
+
const source = this.derivedMeshProvenance.placedSourceFor(meshData, this.modelTranslations);
|
|
485
|
+
return inheritedQuantization(this.quantizedBatchesEnabled, this.meshDataBucket.get(source)?.batchedMesh) !== 'off';
|
|
478
486
|
}
|
|
479
487
|
/** Set (or clear) the HOST budget in bytes for bucket CPU geometry. */
|
|
480
488
|
setHostResidencyBudget(bytes) {
|
|
@@ -629,10 +637,10 @@ export class Scene {
|
|
|
629
637
|
this.meshDataBucket.set(m, bucket);
|
|
630
638
|
this.addMeshData(m);
|
|
631
639
|
}
|
|
632
|
-
this.coldBuckets.delete(key);
|
|
633
640
|
if (members.length > 0) {
|
|
634
|
-
|
|
635
|
-
this
|
|
641
|
+
this.coldBuckets.delete(key);
|
|
642
|
+
for (const restoredKey of repartitionHydratedRecoveryBucket(this, key, bucket, shell))
|
|
643
|
+
this.residencyRestoreQueue.add(restoredKey);
|
|
636
644
|
}
|
|
637
645
|
else {
|
|
638
646
|
console.warn(`[Scene] cold restore for ${key} found no members — bucket stays a shell`);
|
|
@@ -664,6 +672,7 @@ export class Scene {
|
|
|
664
672
|
continue;
|
|
665
673
|
const rebuilt = this.createBatchedMesh(bucket.meshData, bucket.meshData[0].color, device, pipeline, bucket.key);
|
|
666
674
|
bucket.batchedMesh = rebuilt;
|
|
675
|
+
bucket.frameOrigin = rebuilt.origin;
|
|
667
676
|
const idx = this.batchedMeshes.indexOf(old);
|
|
668
677
|
if (idx >= 0)
|
|
669
678
|
this.batchedMeshes[idx] = rebuilt;
|
|
@@ -863,7 +872,7 @@ export class Scene {
|
|
|
863
872
|
// this expressId so selection highlighting is per-entity, not the
|
|
864
873
|
// entire merged batch.
|
|
865
874
|
if (single.entityIds) {
|
|
866
|
-
return
|
|
875
|
+
return this.derivedMeshProvenance.extract(single, expressId);
|
|
867
876
|
}
|
|
868
877
|
return single;
|
|
869
878
|
}
|
|
@@ -873,7 +882,7 @@ export class Scene {
|
|
|
873
882
|
const extracted = [];
|
|
874
883
|
for (const piece of pieces) {
|
|
875
884
|
if (piece.entityIds) {
|
|
876
|
-
const ex =
|
|
885
|
+
const ex = this.derivedMeshProvenance.extract(piece, expressId);
|
|
877
886
|
if (ex)
|
|
878
887
|
extracted.push(ex);
|
|
879
888
|
}
|
|
@@ -888,6 +897,18 @@ export class Scene {
|
|
|
888
897
|
pieces = extracted;
|
|
889
898
|
// Fall through to the normal multi-piece merge below
|
|
890
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;
|
|
891
912
|
// Check if all pieces have the same color (within tolerance)
|
|
892
913
|
// This handles multi-material elements like windows (frame vs glass)
|
|
893
914
|
const firstColor = pieces[0].color;
|
|
@@ -921,8 +942,17 @@ export class Scene {
|
|
|
921
942
|
let idxOffset = 0;
|
|
922
943
|
let vertexOffset = 0;
|
|
923
944
|
for (const piece of pieces) {
|
|
924
|
-
|
|
925
|
-
|
|
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
|
+
}
|
|
926
956
|
mergedNormals.set(piece.normals, posOffset);
|
|
927
957
|
// Copy indices with offset
|
|
928
958
|
for (let i = 0; i < piece.indices.length; i++) {
|
|
@@ -933,7 +963,7 @@ export class Scene {
|
|
|
933
963
|
vertexOffset += piece.positions.length / 3;
|
|
934
964
|
}
|
|
935
965
|
// Return merged MeshData (all pieces have same color)
|
|
936
|
-
|
|
966
|
+
const merged = {
|
|
937
967
|
expressId,
|
|
938
968
|
modelIndex: pieces[0].modelIndex, // Preserve modelIndex for multi-model support
|
|
939
969
|
positions: mergedPositions,
|
|
@@ -941,7 +971,15 @@ export class Scene {
|
|
|
941
971
|
indices: mergedIndices,
|
|
942
972
|
color: firstColor,
|
|
943
973
|
ifcType: pieces[0].ifcType,
|
|
974
|
+
...(mergedOrigin ? { origin: mergedOrigin } : {}),
|
|
944
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;
|
|
945
983
|
}
|
|
946
984
|
/**
|
|
947
985
|
* Check if MeshData exists for an expressId
|
|
@@ -1013,7 +1051,7 @@ export class Scene {
|
|
|
1013
1051
|
const extracted = [];
|
|
1014
1052
|
for (const piece of pieces) {
|
|
1015
1053
|
if (piece.entityIds) {
|
|
1016
|
-
const ex =
|
|
1054
|
+
const ex = this.derivedMeshProvenance.extract(piece, expressId);
|
|
1017
1055
|
if (ex)
|
|
1018
1056
|
extracted.push(ex);
|
|
1019
1057
|
}
|
|
@@ -1049,10 +1087,20 @@ export class Scene {
|
|
|
1049
1087
|
* when streaming completes to do one O(N) full merge.
|
|
1050
1088
|
*/
|
|
1051
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
|
+
}
|
|
1052
1101
|
if (this.appearanceController)
|
|
1053
1102
|
for (const part of meshDataArray)
|
|
1054
1103
|
this.appearanceController.cancelFor(part.expressId);
|
|
1055
|
-
meshDataArray = meshDataArray.map((mesh) => this.modelTranslations.placeMesh(mesh));
|
|
1056
1104
|
// Cache max buffer size on first call
|
|
1057
1105
|
if (this.cachedMaxBufferSize === 0) {
|
|
1058
1106
|
this.cachedMaxBufferSize = this.getMaxBufferSize(device);
|
|
@@ -1081,6 +1129,9 @@ export class Scene {
|
|
|
1081
1129
|
for (const meshData of renderable) {
|
|
1082
1130
|
const baseKey = this.bucketBaseKey(meshData);
|
|
1083
1131
|
const bucketKey = this.resolveActiveBucket(baseKey, meshData);
|
|
1132
|
+
const resolvedBucket = this.buckets.get(bucketKey);
|
|
1133
|
+
if (resolvedBucket)
|
|
1134
|
+
this.meshDataBucket.set(meshData, resolvedBucket);
|
|
1084
1135
|
if (retainStreamingGeometry || !isStreaming) {
|
|
1085
1136
|
// Accumulate mesh data in the bucket when we need later rebatching or
|
|
1086
1137
|
// CPU-side lookups. Huge-file mode intentionally skips this to keep JS
|
|
@@ -1112,48 +1163,15 @@ export class Scene {
|
|
|
1112
1163
|
// NON-STREAMING: Rebuild full batches immediately
|
|
1113
1164
|
this.rebuildPendingBatches(device, pipeline);
|
|
1114
1165
|
}
|
|
1115
|
-
/**
|
|
1116
|
-
* Rebuild all pending batches (call this after streaming completes)
|
|
1117
|
-
*
|
|
1118
|
-
* Each bucket key already maps to data that fits within the GPU buffer
|
|
1119
|
-
* limit (enforced at accumulation time by resolveActiveBucket), so no
|
|
1120
|
-
* splitting is needed here — just create one batch per key.
|
|
1121
|
-
*/
|
|
1166
|
+
/** Rebuild pending buckets after streaming or a geometry mutation. */
|
|
1122
1167
|
rebuildPendingBatches(device, pipeline) {
|
|
1123
1168
|
if (this.pendingBatchKeys.size === 0)
|
|
1124
1169
|
return;
|
|
1125
|
-
const rebuilt =
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
if (bucket?.batchedMesh) {
|
|
1131
|
-
// Slot keys embed the batch id and the replacement gets a fresh one, so
|
|
1132
|
-
// drop this batch's cached sub-batch clones first or they are stranded
|
|
1133
|
-
// with live GPU buffers. Every other batch-destroying path already
|
|
1134
|
-
// clears the cache (eviction per batch; finalize/release/clear wholesale).
|
|
1135
|
-
this.dropPartialCacheForBatch(bucket.batchedMesh);
|
|
1136
|
-
destroyGpuResources(bucket.batchedMesh);
|
|
1137
|
-
bucket.batchedMesh = null;
|
|
1138
|
-
}
|
|
1139
|
-
if (!bucket || bucket.meshData.length === 0) {
|
|
1140
|
-
// Bucket is empty — clean up
|
|
1141
|
-
this.buckets.delete(key);
|
|
1142
|
-
continue;
|
|
1143
|
-
}
|
|
1144
|
-
// Create new batch with all accumulated meshes for this bucket
|
|
1145
|
-
const color = bucket.meshData[0].color;
|
|
1146
|
-
const batchedMesh = this.createBatchedMesh(bucket.meshData, color, device, pipeline, key);
|
|
1147
|
-
bucket.batchedMesh = batchedMesh;
|
|
1148
|
-
rebuilt.push({ bucket, previousQuantized });
|
|
1149
|
-
}
|
|
1150
|
-
// Rebuild the flat render array from all buckets (148 max batches — not perf critical)
|
|
1151
|
-
this.batchedMeshes = [];
|
|
1152
|
-
for (const bucket of this.buckets.values()) {
|
|
1153
|
-
if (bucket.batchedMesh) {
|
|
1154
|
-
this.batchedMeshes.push(bucket.batchedMesh);
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
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] : []);
|
|
1157
1175
|
this.pendingBatchKeys.clear();
|
|
1158
1176
|
// Overlays follow their depth writers only when one actually changed under
|
|
1159
1177
|
// them (flip / bucket move, #4832); finalize re-applies once itself.
|
|
@@ -1559,21 +1577,40 @@ export class Scene {
|
|
|
1559
1577
|
}
|
|
1560
1578
|
this.meshes = kept;
|
|
1561
1579
|
}
|
|
1562
|
-
/**
|
|
1563
|
-
|
|
1564
|
-
|
|
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() {
|
|
1565
1584
|
const batches = this.finalizeInProgress ? [...new Set([...this.batchedMeshes,
|
|
1566
1585
|
...[...this.buckets.values()].flatMap((bucket) => bucket.batchedMesh ? [bucket.batchedMesh] : [])])] : this.batchedMeshes;
|
|
1567
|
-
|
|
1586
|
+
return { translations: this.modelTranslations, pieces: this.meshDataMap,
|
|
1568
1587
|
bounds: this.boundingBoxes, batches, meshes: this.meshes, overrides: this.overrideBatches, textured: this.texturedMeshes,
|
|
1569
1588
|
templates: this.instancedTemplates, cpu: this.instancedTemplateCpu, occurrences: this.instancedEntityMap,
|
|
1570
1589
|
device: this.instancedDevice, evictHighlight: (id) => this.evictHighlightMeshes(id, true),
|
|
1571
1590
|
clearPartial: () => this.dropAllPartialCaches(),
|
|
1572
|
-
unionBounds: (id, view, offset, min, max) => this.unionInstancedWorldAabb(id, view, offset, ...min, ...max)
|
|
1573
|
-
|
|
1591
|
+
unionBounds: (id, view, offset, min, max) => this.unionInstancedWorldAabb(id, view, offset, ...min, ...max) };
|
|
1592
|
+
}
|
|
1593
|
+
dropOrphanedSuppressedBounds() {
|
|
1574
1594
|
for (const id of this.instanceSuppression.suppressedIds())
|
|
1575
1595
|
if (!this.meshDataMap.has(id))
|
|
1576
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();
|
|
1577
1614
|
return changed;
|
|
1578
1615
|
}
|
|
1579
1616
|
/** Bulk variant of `translateMeshesForEntity`. */
|
|
@@ -1801,7 +1838,7 @@ export class Scene {
|
|
|
1801
1838
|
// applies to cells exactly like it does to buckets.
|
|
1802
1839
|
const colorGroups = new Map();
|
|
1803
1840
|
for (const meshData of meshDataArray) {
|
|
1804
|
-
const key = this.bucketBaseKey(meshData);
|
|
1841
|
+
const key = this.meshDataBucket.get(meshData)?.key ?? this.bucketBaseKey(meshData);
|
|
1805
1842
|
for (const fragment of this.splitMeshForStreaming(meshData)) {
|
|
1806
1843
|
let group = colorGroups.get(key);
|
|
1807
1844
|
if (!group) {
|
|
@@ -1812,11 +1849,11 @@ export class Scene {
|
|
|
1812
1849
|
}
|
|
1813
1850
|
}
|
|
1814
1851
|
// Create one fragment batch per color group (with buffer limit splitting)
|
|
1815
|
-
for (const [, group] of colorGroups) {
|
|
1852
|
+
for (const [key, group] of colorGroups) {
|
|
1816
1853
|
const chunks = this.splitMeshDataForBufferLimit(group, this.cachedMaxBufferSize);
|
|
1817
1854
|
for (const chunk of chunks) {
|
|
1818
1855
|
const color = chunk[0].color;
|
|
1819
|
-
const fragment = this.createBatchedMesh(chunk, color, device, pipeline);
|
|
1856
|
+
const fragment = this.createBatchedMesh(chunk, color, device, pipeline, undefined, undefined, this.buckets.get(key)?.frameOrigin);
|
|
1820
1857
|
this.batchedMeshes.push(fragment);
|
|
1821
1858
|
this.streamingFragments.push(fragment);
|
|
1822
1859
|
}
|
|
@@ -1987,15 +2024,6 @@ export class Scene {
|
|
|
1987
2024
|
// always clear finalizeInProgress.
|
|
1988
2025
|
return new Promise((resolve, reject) => {
|
|
1989
2026
|
const newBatches = [];
|
|
1990
|
-
// Every batch this attempt creates, paired with the bucket that now owns
|
|
1991
|
-
// it and the value it displaced. `newBatches` alone is not enough to roll
|
|
1992
|
-
// back: processChunk publishes each batch into `bucket.batchedMesh`, so
|
|
1993
|
-
// freeing it without repairing the owner leaves the bucket map pointing
|
|
1994
|
-
// at destroyed GPU resources (use-after-free on the next bucket-driven
|
|
1995
|
-
// access). `previous` is null for the freshly built buckets and, for a
|
|
1996
|
-
// carried COLD bucket a re-grouped meshData landed in, the shell that the
|
|
1997
|
-
// restored `batchedMeshes` still holds — which must be put back, not
|
|
1998
|
-
// nulled.
|
|
1999
2027
|
const createdOwned = [];
|
|
2000
2028
|
let carriedCold = [];
|
|
2001
2029
|
let pendingKeys = [];
|
|
@@ -2006,11 +2034,14 @@ export class Scene {
|
|
|
2006
2034
|
// are what the restored arrays point back at). Iterating the owned
|
|
2007
2035
|
// pairs rather than `newBatches` also skips the carried cold shells
|
|
2008
2036
|
// appended just before the swap, which this attempt did not create.
|
|
2009
|
-
for (const { bucket, previous, batch } of createdOwned) {
|
|
2037
|
+
for (const { bucket, previous, previousFrameOrigin, batch } of createdOwned) {
|
|
2010
2038
|
// Repair the owner BEFORE the free, so no bucket is ever observable
|
|
2011
|
-
// holding a destroyed batch
|
|
2012
|
-
|
|
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) {
|
|
2013
2042
|
bucket.batchedMesh = previous;
|
|
2043
|
+
bucket.frameOrigin = previousFrameOrigin;
|
|
2044
|
+
}
|
|
2014
2045
|
if (!oldBatchSet.has(batch) && !fragmentSet.has(batch)) {
|
|
2015
2046
|
destroyGpuResources(batch);
|
|
2016
2047
|
}
|
|
@@ -2031,9 +2062,11 @@ export class Scene {
|
|
|
2031
2062
|
}
|
|
2032
2063
|
const color = bucket.meshData[0].color;
|
|
2033
2064
|
const previous = bucket.batchedMesh;
|
|
2065
|
+
const previousFrameOrigin = bucket.frameOrigin;
|
|
2034
2066
|
const batchedMesh = scene.createBatchedMesh(bucket.meshData, color, device, pipeline, key);
|
|
2035
2067
|
bucket.batchedMesh = batchedMesh;
|
|
2036
|
-
|
|
2068
|
+
bucket.frameOrigin = batchedMesh.origin;
|
|
2069
|
+
createdOwned.push({ bucket, previous, previousFrameOrigin, batch: batchedMesh });
|
|
2037
2070
|
newBatches.push(batchedMesh);
|
|
2038
2071
|
// Check time budget — yield if exceeded
|
|
2039
2072
|
if (performance.now() - chunkStart >= budgetMs) {
|
|
@@ -2172,7 +2205,6 @@ export class Scene {
|
|
|
2172
2205
|
* Call this after finalizeStreaming() when all color updates have been applied.
|
|
2173
2206
|
*/
|
|
2174
2207
|
releaseGeometryData() {
|
|
2175
|
-
this.authoredGeneration++;
|
|
2176
2208
|
if (this.geometryReleased)
|
|
2177
2209
|
return;
|
|
2178
2210
|
if (this.instanceSuppression.retained) {
|
|
@@ -2186,6 +2218,8 @@ export class Scene {
|
|
|
2186
2218
|
`Call finalizeStreaming()/rebuildPendingBatches() first.`);
|
|
2187
2219
|
return;
|
|
2188
2220
|
}
|
|
2221
|
+
this.authoredGeneration++;
|
|
2222
|
+
this.authoredPreparations.invalidate();
|
|
2189
2223
|
this.appearanceController?.forget();
|
|
2190
2224
|
// 1. Precompute and cache ALL entity bounding boxes before releasing data.
|
|
2191
2225
|
// Same rule as `finishEphemeralStreaming`: an entity with no usable vertex
|
|
@@ -2328,14 +2362,17 @@ export class Scene {
|
|
|
2328
2362
|
* participate in the main buckets map).
|
|
2329
2363
|
* @param quantization - 'auto' for bucket batches; derived batches pass `inheritedQuantization(source)` (#4832).
|
|
2330
2364
|
*/
|
|
2331
|
-
createBatchedMesh(meshes, color, device, pipeline, bucketKey, quantization = this.quantizedBatchesEnabled ? 'auto' : 'off') {
|
|
2365
|
+
createBatchedMesh(meshes, color, device, pipeline, bucketKey, quantization = this.quantizedBatchesEnabled ? 'auto' : 'off', frameOrigin) {
|
|
2332
2366
|
// Keep main's model-local frame and translation registration while staging
|
|
2333
2367
|
// every GPU allocation before publishing Scene state.
|
|
2334
2368
|
const modelIndex = meshes[0]?.modelIndex ?? 0;
|
|
2335
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.');
|
|
2336
2373
|
const result = createSceneBatch(meshes, color, device, pipeline, {
|
|
2337
2374
|
id: this.nextBatchId, colorKey: bucketKey ?? this.colorKey(color),
|
|
2338
|
-
origin
|
|
2375
|
+
origin,
|
|
2339
2376
|
quantized: quantization, lod: this.lodBuildsEnabled,
|
|
2340
2377
|
}, bucketKey);
|
|
2341
2378
|
this.nextBatchId++;
|
|
@@ -2365,38 +2402,15 @@ export class Scene {
|
|
|
2365
2402
|
* Returns the bucket key to use (may be the base key or a suffixed key).
|
|
2366
2403
|
*/
|
|
2367
2404
|
resolveActiveBucket(baseColorKey, meshData) {
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
bucketKey = `${baseColorKey}#${this.nextSplitId++}`;
|
|
2378
|
-
this.activeBucketKey.set(baseColorKey, bucketKey);
|
|
2379
|
-
let target = this.buckets.get(bucketKey);
|
|
2380
|
-
if (!target) {
|
|
2381
|
-
target = { key: bucketKey, meshData: [], batchedMesh: null, vertexBytes: 0 };
|
|
2382
|
-
this.buckets.set(bucketKey, target);
|
|
2383
|
-
}
|
|
2384
|
-
target.vertexBytes += meshBytes;
|
|
2385
|
-
return bucketKey;
|
|
2386
|
-
}
|
|
2387
|
-
if (currentBytes > 0 && currentBytes + meshBytes > this.cachedMaxBufferSize) {
|
|
2388
|
-
// Overflow — create a new sub-bucket
|
|
2389
|
-
bucketKey = `${baseColorKey}#${this.nextSplitId++}`;
|
|
2390
|
-
this.activeBucketKey.set(baseColorKey, bucketKey);
|
|
2391
|
-
}
|
|
2392
|
-
// Update size tracking on the bucket (create if needed)
|
|
2393
|
-
let targetBucket = this.buckets.get(bucketKey);
|
|
2394
|
-
if (!targetBucket) {
|
|
2395
|
-
targetBucket = { key: bucketKey, meshData: [], batchedMesh: null, vertexBytes: 0 };
|
|
2396
|
-
this.buckets.set(bucketKey, targetBucket);
|
|
2397
|
-
}
|
|
2398
|
-
targetBucket.vertexBytes += meshBytes;
|
|
2399
|
-
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);
|
|
2400
2414
|
}
|
|
2401
2415
|
/**
|
|
2402
2416
|
* Extract the base color key from a bucket key (strips "#N" suffix if present).
|
|
@@ -2524,7 +2538,7 @@ export class Scene {
|
|
|
2524
2538
|
// Drawn INSTEAD of its source batch, so it inherits the source's f32/quantized
|
|
2525
2539
|
// decision (#4832): the overlay built from the same source must match its depth.
|
|
2526
2540
|
const color = visibleMeshData[0].color;
|
|
2527
|
-
const partialBatch = this.createBatchedMesh(visibleMeshData, color, device, pipeline, undefined, inheritedQuantization(this.quantizedBatchesEnabled, bucket?.batchedMesh));
|
|
2541
|
+
const partialBatch = this.createBatchedMesh(visibleMeshData, color, device, pipeline, undefined, inheritedQuantization(this.quantizedBatchesEnabled, bucket?.batchedMesh), bucket?.batchedMesh?.origin);
|
|
2528
2542
|
// Cache it
|
|
2529
2543
|
this.partialBatchCache.set(cacheKey, partialBatch);
|
|
2530
2544
|
this.partialBatchCacheKeys.set(sourceBatchKey, cacheKey);
|
|
@@ -2574,7 +2588,7 @@ export class Scene {
|
|
|
2574
2588
|
this.overlaySources.set(piece, sourceKey);
|
|
2575
2589
|
const quantization = inheritedQuantization(this.quantizedBatchesEnabled, sourceBatch);
|
|
2576
2590
|
for (const chunk of this.splitMeshDataForBufferLimit(meshData, maxBufferSize)) {
|
|
2577
|
-
this.overrideBatches.push(this.createBatchedMesh(chunk, color, device, pipeline, undefined, quantization));
|
|
2591
|
+
this.overrideBatches.push(this.createBatchedMesh(chunk, color, device, pipeline, undefined, quantization, sourceBatch?.origin));
|
|
2578
2592
|
}
|
|
2579
2593
|
}
|
|
2580
2594
|
}
|
|
@@ -3317,22 +3331,17 @@ export class Scene {
|
|
|
3317
3331
|
this.restoreInstanceColor(device, eid);
|
|
3318
3332
|
}
|
|
3319
3333
|
for (const eid of toFade) {
|
|
3320
|
-
const
|
|
3321
|
-
if (
|
|
3322
|
-
|
|
3323
|
-
|
|
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);
|
|
3324
3339
|
}
|
|
3325
3340
|
this.instancedGhosted = next;
|
|
3326
3341
|
this.lastGhostAlpha = ghostAlpha;
|
|
3327
3342
|
this.instancedGhostDirty = false;
|
|
3328
3343
|
this.instancedGhostTransparent = next.size > 0 && ghostAlpha < OPAQUE_ALPHA_CUTOFF;
|
|
3329
3344
|
}
|
|
3330
|
-
/** The colour an occurrence was uploaded with, before any override or ghost. */
|
|
3331
|
-
originalInstanceColor(eid) {
|
|
3332
|
-
const locs = this.instancedEntityMap.get(eid);
|
|
3333
|
-
const first = locs?.[0];
|
|
3334
|
-
return first ? first.originalColor : null;
|
|
3335
|
-
}
|
|
3336
3345
|
/** Write the combined flag lane (selected | hidden) for every occurrence of `eid`
|
|
3337
3346
|
* (shares one u32 lane at INSTANCE_FLAGS_OFFSET, so both bits are folded here).
|
|
3338
3347
|
* #4382: for the item-filtered eid, selected narrows per-occurrence to
|
|
@@ -3374,6 +3383,7 @@ export class Scene {
|
|
|
3374
3383
|
device.queue.writeBuffer(buf, loc.byteOffset + INSTANCE_COLOR_OFFSET, new Float32Array(loc.originalColor));
|
|
3375
3384
|
}
|
|
3376
3385
|
}
|
|
3386
|
+
writeOriginalInstanceColors(device, eid, alpha) { writeOriginalInstancedColors(device, this.instancedEntityMap.get(eid) ?? [], this.instancedTemplates, INSTANCE_COLOR_OFFSET, alpha); }
|
|
3377
3387
|
/**
|
|
3378
3388
|
* Build a textured mesh (#961): interleave position+normal+entityId+uv into one
|
|
3379
3389
|
* vertex buffer, upload the decoded RGBA8 texture, create a sampler honouring
|
|
@@ -3558,6 +3568,7 @@ export class Scene {
|
|
|
3558
3568
|
*/
|
|
3559
3569
|
clearFlatGeometry() {
|
|
3560
3570
|
this.authoredGeneration++;
|
|
3571
|
+
this.authoredPreparations.invalidate();
|
|
3561
3572
|
this.instanceSuppression.restore();
|
|
3562
3573
|
this.appearanceController?.forget();
|
|
3563
3574
|
this.clearFlatBuffers();
|
|
@@ -3566,6 +3577,7 @@ export class Scene {
|
|
|
3566
3577
|
* owners keep their original-instance history. Full reset remains separate. */
|
|
3567
3578
|
clearFlatGeometryForRebuild(geometry, models, sourceGeometry = geometry) {
|
|
3568
3579
|
this.authoredGeneration++;
|
|
3580
|
+
this.authoredPreparations.invalidate();
|
|
3569
3581
|
const retained = this.appearanceController?.prepareRebuild(sourceGeometry, models) ?? new Set();
|
|
3570
3582
|
const discarded = this.appearanceController?.discardedForRebuild(retained) ?? [];
|
|
3571
3583
|
// A discarded converted owner must not resurrect its obsolete type instance.
|