@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/index.js
CHANGED
|
@@ -39,6 +39,7 @@ export { LINE_OVERLAY_CHANNELS } from './section-2d-overlay.js';
|
|
|
39
39
|
export { Raycaster } from './raycaster.js';
|
|
40
40
|
export { SnapDetector, SnapType } from './snap-detector.js';
|
|
41
41
|
export { BVH } from './bvh.js';
|
|
42
|
+
export { hostsOtherEntities } from './mesh-entity-hosting.js';
|
|
42
43
|
export { FederationRegistry, federationRegistry } from './federation-registry.js';
|
|
43
44
|
export * from './types.js';
|
|
44
45
|
export { resolveEnvironment, deriveSkyGradient, packEnvironmentUniforms, ENVIRONMENT_UNIFORM_SIZE, } from './environment.js';
|
|
@@ -105,44 +106,10 @@ import { PartialBatchRequests } from './partial-batch-requests.js';
|
|
|
105
106
|
import { colorSaltByte, packEntityLane } from './scene-geometry.js';
|
|
106
107
|
import { PointCloudRenderer } from './pointcloud/point-cloud-renderer.js';
|
|
107
108
|
import { DeviationComputer } from './deviation/deviation-computer.js';
|
|
109
|
+
import { runGuardedGpuUpload, isDeviceLossThrow } from './gpu-upload-guard.js';
|
|
110
|
+
import { recoverRendererDevice, rendererDeviceLostError } from './device-recovery.js';
|
|
108
111
|
const MAX_ENCODED_ENTITY_ID = 0xFFFFFF;
|
|
109
112
|
let warnedEntityIdRange = false;
|
|
110
|
-
/**
|
|
111
|
-
* Is this throw the GPU device telling us it is gone?
|
|
112
|
-
*
|
|
113
|
-
* The discriminator is the exception TYPE, not its message, because WebGPU
|
|
114
|
-
* draws exactly that line:
|
|
115
|
-
* - a call on a dead / invalid-state device throws a `DOMException`
|
|
116
|
-
* (`InvalidStateError` in Safari 26.5 — the whole of issue #2229);
|
|
117
|
-
* - a buffer allocation the host cannot back throws a plain `RangeError`
|
|
118
|
-
* ("createBuffer failed, size (…) is too large … when mappedAtCreation ==
|
|
119
|
-
* true"), which `gpu-upload-guard` documents happening on a HEALTHY device
|
|
120
|
-
* under memory pressure.
|
|
121
|
-
*
|
|
122
|
-
* Treating the second as a device loss is a false positive that costs the whole
|
|
123
|
-
* session, so only the first latches; everything else degrades one frame.
|
|
124
|
-
*
|
|
125
|
-
* There is deliberately NO consecutive-failure threshold as a middle ground.
|
|
126
|
-
* Not because failures necessarily arrive back-to-back — between two BCF / IDS
|
|
127
|
-
* capture frames the awaited `camera.frameBounds` normally does let an ordinary
|
|
128
|
-
* rAF frame through, which would reset a counter — but because those ordinary
|
|
129
|
-
* frames are not guaranteed to SUCCEED: they allocate too (`ensureMeshResources`
|
|
130
|
-
* creates a buffer per unresourced mesh, and the queued-mesh flush allocates),
|
|
131
|
-
* so under sustained host memory pressure any finite budget is still reachable.
|
|
132
|
-
* A latch whose safety depends on incidental animation timing is the wrong
|
|
133
|
-
* shape of guarantee for "never kill the viewport by mistake".
|
|
134
|
-
*
|
|
135
|
-
* Real losses on browsers that do not throw are still caught by the async
|
|
136
|
-
* `device.lost` promise — which the WebGPU spec makes the sole loss channel
|
|
137
|
-
* anyway (on a conformant engine, calls against a lost device are no-ops, not
|
|
138
|
-
* throws; Safari 26.5's synchronous throw is the deviation being handled here).
|
|
139
|
-
*
|
|
140
|
-
* `typeof` guarded because non-DOM hosts (Node before 17, some workers) have no
|
|
141
|
-
* `DOMException` global; there, no throw can be a WebGPU device signal anyway.
|
|
142
|
-
*/
|
|
143
|
-
function isDeviceLossThrow(error) {
|
|
144
|
-
return typeof DOMException !== 'undefined' && error instanceof DOMException;
|
|
145
|
-
}
|
|
146
113
|
/**
|
|
147
114
|
* The reason `whenReady()` rejects when the renderer is destroyed.
|
|
148
115
|
*
|
|
@@ -156,20 +123,6 @@ function rendererDestroyedError() {
|
|
|
156
123
|
error.name = 'RendererDestroyedError';
|
|
157
124
|
return error;
|
|
158
125
|
}
|
|
159
|
-
/**
|
|
160
|
-
* The reason `whenReady()` rejects once the GPU device has been lost.
|
|
161
|
-
*
|
|
162
|
-
* Deliberately NOT `RendererDestroyedError`: a destroyed renderer is finished,
|
|
163
|
-
* while a lost one is dead only until the host re-initialises it (`init()`
|
|
164
|
-
* clears the latch and readiness is published again). A caller that wants to
|
|
165
|
-
* retry needs to tell those apart, so the loss carries its own `name` — same
|
|
166
|
-
* plain-`Error` shape, for the same reasons.
|
|
167
|
-
*/
|
|
168
|
-
function rendererDeviceLostError() {
|
|
169
|
-
const error = new Error('GPU device was lost before the renderer became ready');
|
|
170
|
-
error.name = 'RendererDeviceLostError';
|
|
171
|
-
return error;
|
|
172
|
-
}
|
|
173
126
|
/**
|
|
174
127
|
* Main renderer class
|
|
175
128
|
*/
|
|
@@ -218,8 +171,9 @@ export class Renderer {
|
|
|
218
171
|
highQuality: true,
|
|
219
172
|
};
|
|
220
173
|
pointCloudRenderer = null;
|
|
221
|
-
|
|
222
|
-
|
|
174
|
+
pointCloudStreamEpoch = 0;
|
|
175
|
+
pointCloudStreamEpochs = new Map(); // handle id → epoch; cleanup rebuilds `{ id }`, cleared per epoch
|
|
176
|
+
/** Set true at the end of the LATEST `init()`; gates `whenReady()` and
|
|
223
177
|
* `isReady()`. Revoked synchronously by `init()` and by `destroy()`, and
|
|
224
178
|
* overridden (not cleared) by a device loss — see `deviceLost`, which the
|
|
225
179
|
* two readiness methods consult alongside this flag because the loss can
|
|
@@ -241,19 +195,18 @@ export class Renderer {
|
|
|
241
195
|
*/
|
|
242
196
|
destroyed = false;
|
|
243
197
|
/**
|
|
244
|
-
* The tail of the `init()`
|
|
245
|
-
* running immediately, so
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
* caller) so one failure does not deadlock every later call.
|
|
198
|
+
* The tail of the GPU lifecycle queue. `init()` and `recoverDevice()` chain
|
|
199
|
+
* onto this rather than running immediately, so overlapping calls cannot
|
|
200
|
+
* both mutate the shared device/pipeline fields while one is awaiting its
|
|
201
|
+
* adapter. Always settled fulfilled — a rejected operation is swallowed
|
|
202
|
+
* HERE (never for its caller) so one failure cannot deadlock later work.
|
|
250
203
|
*/
|
|
251
204
|
initChain = Promise.resolve();
|
|
252
205
|
/**
|
|
253
|
-
* Incremented synchronously by every `init()`
|
|
254
|
-
* `destroy()`. It stamps
|
|
255
|
-
*
|
|
256
|
-
*
|
|
206
|
+
* Incremented synchronously by every `init()` / `recoverDevice()` call and
|
|
207
|
+
* by every public `destroy()`. It stamps the lifecycle event an in-flight
|
|
208
|
+
* operation belongs to: stale work must neither allocate nor publish
|
|
209
|
+
* readiness.
|
|
257
210
|
*
|
|
258
211
|
* Both bumps are load-bearing, for the same reason. Because the queue above
|
|
259
212
|
* defers the body, an init can finish while a later one is still waiting its
|
|
@@ -311,6 +264,10 @@ export class Renderer {
|
|
|
311
264
|
/** Retained so a listener registered AFTER the loss still learns of it. */
|
|
312
265
|
deviceLostInfo = null;
|
|
313
266
|
deviceLostListeners = new Set();
|
|
267
|
+
deviceLossSequence = 0;
|
|
268
|
+
recovery = {
|
|
269
|
+
inFlight: null, lostReferenceImages: false, quantizedBatchesRequested: false, omissions: new Set(), pointCloudOptions: null,
|
|
270
|
+
};
|
|
314
271
|
/** BIM ↔ scan deviation: owns the compute pipeline + its BVH cache. */
|
|
315
272
|
deviationComputer = new DeviationComputer();
|
|
316
273
|
visualEnhancementResolver = new VisualEnhancementResolver();
|
|
@@ -485,7 +442,7 @@ export class Renderer {
|
|
|
485
442
|
this.initChain = run.then(() => undefined, () => undefined);
|
|
486
443
|
return run;
|
|
487
444
|
}
|
|
488
|
-
async initOnce(generation) {
|
|
445
|
+
async initOnce(generation, options = {}) {
|
|
489
446
|
// `pipeline` is the marker for "a previous init() completed": it is
|
|
490
447
|
// assigned unconditionally there and nulled by destroy().
|
|
491
448
|
if (this.pipeline !== null) {
|
|
@@ -503,12 +460,17 @@ export class Renderer {
|
|
|
503
460
|
// the generation and this body running is stamped with the generation
|
|
504
461
|
// that is clearing it. `deviceLostGeneration` deliberately keeps its
|
|
505
462
|
// stale value — it is only ever read alongside this flag.
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
//
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
463
|
+
if (options.clearDeviceLost !== false)
|
|
464
|
+
this.deviceLost = false;
|
|
465
|
+
// Subscribe before init so a loss during the first frames is not missed.
|
|
466
|
+
const initializingDevice = this.device;
|
|
467
|
+
initializingDevice.onDeviceLost((info) => {
|
|
468
|
+
// Ignore a delayed loss from a wrapper recovery already replaced.
|
|
469
|
+
if (this.device !== initializingDevice)
|
|
470
|
+
return;
|
|
471
|
+
this.handleDeviceLost(info, { fromDevicePromise: true });
|
|
472
|
+
});
|
|
473
|
+
await initializingDevice.init(this.canvas);
|
|
512
474
|
// A `destroy()` (or a newer `init()`) landed while we were parked on the
|
|
513
475
|
// device. Everything below allocates a full GPU stack — two pipelines,
|
|
514
476
|
// the picker, the post-processor, the point-cloud and deviation
|
|
@@ -586,7 +548,8 @@ export class Renderer {
|
|
|
586
548
|
// from the GPU click-pick provider above, since raycastScene* is
|
|
587
549
|
// synchronous CPU code while pick() is an async GPU readback.
|
|
588
550
|
this.raycastEngine.setPointCloudProvider(() => this.pointCloudRenderer?.getRayQuerySources() ?? []);
|
|
589
|
-
|
|
551
|
+
if (options.publishReady !== false)
|
|
552
|
+
this.markReady(generation);
|
|
590
553
|
}
|
|
591
554
|
/**
|
|
592
555
|
* Resolves once `init()` has finished and the GPU device + point-cloud
|
|
@@ -718,10 +681,33 @@ export class Renderer {
|
|
|
718
681
|
isDeviceLost() {
|
|
719
682
|
return this.deviceLost;
|
|
720
683
|
}
|
|
721
|
-
|
|
722
|
-
|
|
684
|
+
recoverDevice() {
|
|
685
|
+
return recoverRendererDevice(this);
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* `onLossDetected` for every `runGuardedGpuUpload` call below (#4885):
|
|
689
|
+
* a synchronous Safari-style `DOMException` (issue #2229) caught OUTSIDE
|
|
690
|
+
* `render()` never otherwise reaches `handleDeviceLost` — only the async
|
|
691
|
+
* `device.lost` promise and `render()`'s own catch do — so without this,
|
|
692
|
+
* `isDeviceLost()` would keep answering false and every later upload on
|
|
693
|
+
* this path would keep failing against the same dead device.
|
|
694
|
+
*/
|
|
695
|
+
reportUploadLoss(error) {
|
|
696
|
+
this.handleDeviceLost({
|
|
697
|
+
message: error instanceof Error ? error.message : String(error),
|
|
698
|
+
reason: 'gpu-upload-exception',
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
handleDeviceLost(info, options = {}) {
|
|
702
|
+
// Latched: only the current device's `device.lost` signal is a replacement loss; anything else is a duplicate report.
|
|
703
|
+
if (this.deviceLost) {
|
|
704
|
+
if (options.fromDevicePromise)
|
|
705
|
+
this.deviceLossSequence++;
|
|
723
706
|
return;
|
|
707
|
+
}
|
|
708
|
+
this.deviceLossSequence++;
|
|
724
709
|
this.deviceLost = true;
|
|
710
|
+
this.recovery.lostReferenceImages = this.referenceImages.hasImages();
|
|
725
711
|
this.referenceImages.destroy();
|
|
726
712
|
this.deviceLostGeneration = this.initGeneration;
|
|
727
713
|
this.deviceLostInfo = info;
|
|
@@ -893,24 +879,34 @@ export class Renderer {
|
|
|
893
879
|
* chunks will arrive (currently a no-op but kept for symmetry).
|
|
894
880
|
*/
|
|
895
881
|
beginPointCloudStream(meta) {
|
|
882
|
+
if (this.deviceLost)
|
|
883
|
+
throw rendererDeviceLostError();
|
|
896
884
|
if (!this.pointCloudRenderer) {
|
|
897
885
|
throw new Error('Renderer not initialized. Call init() first.');
|
|
898
886
|
}
|
|
899
|
-
|
|
887
|
+
const handle = this.pointCloudRenderer.beginAsset(meta);
|
|
888
|
+
this.pointCloudStreamEpochs.set(handle.id, this.pointCloudStreamEpoch);
|
|
889
|
+
return handle;
|
|
890
|
+
}
|
|
891
|
+
currentPointCloudStreamRenderer(handle) {
|
|
892
|
+
if (this.deviceLost || !this.pointCloudRenderer
|
|
893
|
+
|| this.pointCloudStreamEpochs.get(handle.id) !== this.pointCloudStreamEpoch)
|
|
894
|
+
throw rendererDeviceLostError();
|
|
895
|
+
return this.pointCloudRenderer;
|
|
900
896
|
}
|
|
901
897
|
appendPointCloudChunk(handle, chunk) {
|
|
902
|
-
|
|
903
|
-
return;
|
|
904
|
-
this.pointCloudRenderer.appendChunk(handle, chunk);
|
|
898
|
+
this.currentPointCloudStreamRenderer(handle).appendChunk(handle, chunk);
|
|
905
899
|
this.modelBoundsTracker.expandForPointClouds();
|
|
906
900
|
this.camera.setSceneBounds(this.modelBounds);
|
|
907
901
|
this.requestRender();
|
|
908
902
|
}
|
|
909
903
|
endPointCloudStream(handle) {
|
|
910
|
-
this.
|
|
904
|
+
this.currentPointCloudStreamRenderer(handle).endAsset(handle);
|
|
911
905
|
this.requestRender();
|
|
912
906
|
}
|
|
913
907
|
removePointCloudAsset(handle) {
|
|
908
|
+
if (!this.pointCloudStreamEpochs.delete(handle.id))
|
|
909
|
+
return;
|
|
914
910
|
this.pointCloudRenderer?.removeAsset(handle);
|
|
915
911
|
// Bounds may have shrunk — recompute from scratch so fit-to-view
|
|
916
912
|
// and section-plane sliders see fresh extents.
|
|
@@ -947,6 +943,15 @@ export class Renderer {
|
|
|
947
943
|
this.clearCaches();
|
|
948
944
|
this.refreshPlacementBounds();
|
|
949
945
|
}
|
|
946
|
+
/** Absolute workspace yaw about the render-frame pivot; GPU-instanced only (#4890). Skips invalidation on a no-op (#4890 review). */
|
|
947
|
+
setModelRotation(modelIndex, angle, pivot) {
|
|
948
|
+
const changed = this.scene.setModelRotation(modelIndex, angle, pivot);
|
|
949
|
+
if (!changed)
|
|
950
|
+
return false;
|
|
951
|
+
this.clearCaches();
|
|
952
|
+
this.refreshPlacementBounds();
|
|
953
|
+
return true;
|
|
954
|
+
}
|
|
950
955
|
/** Streamed clouds are addressed by durable asset handle. */
|
|
951
956
|
setPointCloudTranslation(handle, translation) {
|
|
952
957
|
this.pointCloudRenderer?.setAssetTranslation(handle, translation);
|
|
@@ -1011,22 +1016,26 @@ export class Renderer {
|
|
|
1011
1016
|
* @param geometry - Either a GeometryResult from geometry.process() or an array of MeshData
|
|
1012
1017
|
*/
|
|
1013
1018
|
loadGeometry(geometry) {
|
|
1014
|
-
if (
|
|
1019
|
+
if (this.isDeviceLost())
|
|
1020
|
+
return { ok: false, reason: 'device-lost' }; // #4885: zombie device stays "initialized"
|
|
1021
|
+
if (!this.device.isInitialized() || !this.pipeline)
|
|
1015
1022
|
throw new Error('Renderer not initialized. Call init() first.');
|
|
1016
|
-
}
|
|
1017
1023
|
const meshes = Array.isArray(geometry) ? geometry : geometry.meshes;
|
|
1018
1024
|
if (meshes.length === 0) {
|
|
1019
1025
|
console.warn('[Renderer] loadGeometry called with empty mesh array');
|
|
1020
|
-
return;
|
|
1026
|
+
return { ok: true, value: undefined };
|
|
1021
1027
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1028
|
+
const pipeline = this.pipeline;
|
|
1029
|
+
return runGuardedGpuUpload(() => this.isDeviceLost(), () => {
|
|
1030
|
+
// Use batched rendering for optimal performance
|
|
1031
|
+
const device = this.device.getDevice();
|
|
1032
|
+
this.scene.appendToBatches(meshes, device, pipeline, false);
|
|
1033
|
+
// Calculate and store model bounds for fitToView
|
|
1034
|
+
this.modelBoundsTracker.updateFromMeshes(meshes, (index) => this.scene.getModelTranslation(index));
|
|
1035
|
+
console.log(`[Renderer] Loaded ${meshes.length} meshes`);
|
|
1036
|
+
// Update camera scene bounds for tight orthographic near/far planes
|
|
1037
|
+
this.camera.setSceneBounds(this.modelBounds);
|
|
1038
|
+
}, (error) => this.reportUploadLoss(error));
|
|
1030
1039
|
}
|
|
1031
1040
|
/**
|
|
1032
1041
|
* Add multiple meshes to the scene (convenience method for streaming)
|
|
@@ -1035,17 +1044,20 @@ export class Renderer {
|
|
|
1035
1044
|
* @param isStreaming - If true, throttles batch rebuilding for better streaming performance
|
|
1036
1045
|
*/
|
|
1037
1046
|
addMeshes(meshes, isStreaming = false) {
|
|
1047
|
+
if (this.isDeviceLost())
|
|
1048
|
+
return { ok: false, reason: 'device-lost' };
|
|
1038
1049
|
if (!this.device.isInitialized() || !this.pipeline) {
|
|
1039
1050
|
throw new Error('Renderer not initialized. Call init() first.');
|
|
1040
1051
|
}
|
|
1041
1052
|
if (meshes.length === 0)
|
|
1042
|
-
return;
|
|
1043
|
-
const
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1053
|
+
return { ok: true, value: undefined };
|
|
1054
|
+
const pipeline = this.pipeline;
|
|
1055
|
+
return runGuardedGpuUpload(() => this.isDeviceLost(), () => {
|
|
1056
|
+
const device = this.device.getDevice();
|
|
1057
|
+
this.scene.appendToBatches(meshes, device, pipeline, isStreaming);
|
|
1058
|
+
this.modelBoundsTracker.updateFromMeshes(meshes, (index) => this.scene.getModelTranslation(index));
|
|
1059
|
+
this.camera.setSceneBounds(this.modelBounds);
|
|
1060
|
+
}, (error) => this.reportUploadLoss(error));
|
|
1049
1061
|
}
|
|
1050
1062
|
/**
|
|
1051
1063
|
* Fit camera to view all loaded geometry
|
|
@@ -1073,50 +1085,46 @@ export class Renderer {
|
|
|
1073
1085
|
*/
|
|
1074
1086
|
addMesh(mesh) {
|
|
1075
1087
|
if (!this.pipeline)
|
|
1076
|
-
return;
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
resource: { buffer: mesh.uniformBuffer },
|
|
1092
|
-
},
|
|
1093
|
-
],
|
|
1094
|
-
});
|
|
1095
|
-
}
|
|
1088
|
+
return { ok: true, value: undefined };
|
|
1089
|
+
const pipeline = this.pipeline;
|
|
1090
|
+
const outcome = (!mesh.uniformBuffer && !this.isDeviceLost() && this.device.isInitialized())
|
|
1091
|
+
? runGuardedGpuUpload(() => this.isDeviceLost(), () => {
|
|
1092
|
+
const device = this.device.getDevice();
|
|
1093
|
+
mesh.uniformBuffer = device.createBuffer({
|
|
1094
|
+
size: pipeline.getUniformBufferSize(),
|
|
1095
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1096
|
+
});
|
|
1097
|
+
mesh.bindGroup = device.createBindGroup({
|
|
1098
|
+
layout: pipeline.getBindGroupLayout(),
|
|
1099
|
+
entries: [{ binding: 0, resource: { buffer: mesh.uniformBuffer } }],
|
|
1100
|
+
});
|
|
1101
|
+
}, (error) => this.reportUploadLoss(error))
|
|
1102
|
+
: { ok: true, value: undefined };
|
|
1096
1103
|
this.scene.addMesh(mesh);
|
|
1104
|
+
return outcome;
|
|
1097
1105
|
}
|
|
1098
1106
|
/**
|
|
1099
1107
|
* Ensure all meshes have GPU resources (call after adding meshes if pipeline wasn't ready)
|
|
1100
1108
|
*/
|
|
1101
1109
|
ensureMeshResources() {
|
|
1102
1110
|
if (!this.pipeline || !this.device.isInitialized())
|
|
1103
|
-
return;
|
|
1104
|
-
const
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
}
|
|
1111
|
+
return { ok: true, value: undefined };
|
|
1112
|
+
const pipeline = this.pipeline;
|
|
1113
|
+
return runGuardedGpuUpload(() => this.isDeviceLost(), () => {
|
|
1114
|
+
const device = this.device.getDevice();
|
|
1115
|
+
for (const mesh of this.scene.getMeshes()) {
|
|
1116
|
+
if (!mesh.uniformBuffer) {
|
|
1117
|
+
mesh.uniformBuffer = device.createBuffer({
|
|
1118
|
+
size: pipeline.getUniformBufferSize(),
|
|
1119
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1120
|
+
});
|
|
1121
|
+
mesh.bindGroup = device.createBindGroup({
|
|
1122
|
+
layout: pipeline.getBindGroupLayout(),
|
|
1123
|
+
entries: [{ binding: 0, resource: { buffer: mesh.uniformBuffer } }],
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1118
1126
|
}
|
|
1119
|
-
}
|
|
1127
|
+
}, (error) => this.reportUploadLoss(error));
|
|
1120
1128
|
}
|
|
1121
1129
|
/**
|
|
1122
1130
|
* Get model bounds (used for section planes, fitToView, etc.)
|
|
@@ -1209,11 +1217,15 @@ export class Renderer {
|
|
|
1209
1217
|
this.scene.recordBatchDrawn(batch);
|
|
1210
1218
|
}
|
|
1211
1219
|
}
|
|
1220
|
+
/** Guarded entry point (#4885) for the unguarded body below. */
|
|
1221
|
+
createMeshFromData(meshData) {
|
|
1222
|
+
return runGuardedGpuUpload(() => this.isDeviceLost(), () => this.createMeshFromDataUnguarded(meshData), (error) => this.reportUploadLoss(error));
|
|
1223
|
+
}
|
|
1212
1224
|
/**
|
|
1213
1225
|
* Create a GPU Mesh from MeshData (lazy creation for selection highlighting)
|
|
1214
1226
|
* This is called on-demand when a mesh is selected, avoiding 2x buffer creation during streaming
|
|
1215
1227
|
*/
|
|
1216
|
-
|
|
1228
|
+
createMeshFromDataUnguarded(meshData) {
|
|
1217
1229
|
if (!this.device.isInitialized())
|
|
1218
1230
|
return;
|
|
1219
1231
|
const device = this.device.getDevice();
|
|
@@ -1230,7 +1242,7 @@ export class Renderer {
|
|
|
1230
1242
|
// We compute the same `world` here. When there's no shared origin yet
|
|
1231
1243
|
// (legacy / pre-batch), fall back to a plain f64 fold (local + origin).
|
|
1232
1244
|
const o = meshData.origin;
|
|
1233
|
-
const so = this.scene.getSharedFrameOrigin(meshData.modelIndex);
|
|
1245
|
+
const so = this.scene.getSharedFrameOrigin(meshData.modelIndex, meshData);
|
|
1234
1246
|
const ox = o ? o[0] : 0, oy = o ? o[1] : 0, oz = o ? o[2] : 0;
|
|
1235
1247
|
const fr = Math.fround;
|
|
1236
1248
|
const sox = so ? fr(so[0]) : null, soy = so ? fr(so[1]) : 0, soz = so ? fr(so[2]) : 0;
|
|
@@ -1415,6 +1427,7 @@ export class Renderer {
|
|
|
1415
1427
|
* rejecting pipeline creation) batches stay on the f32 path.
|
|
1416
1428
|
*/
|
|
1417
1429
|
async enableQuantizedBatches() {
|
|
1430
|
+
this.recovery.quantizedBatchesRequested = true;
|
|
1418
1431
|
if (!this.pipeline)
|
|
1419
1432
|
return false;
|
|
1420
1433
|
const ok = await this.pipeline.ensureQuantizedPipelines();
|
|
@@ -3043,8 +3056,8 @@ export class Renderer {
|
|
|
3043
3056
|
}
|
|
3044
3057
|
/** Stage one new owner; borrowed mesh buffers must remain immutable until disposal. */
|
|
3045
3058
|
prepareAuthoredOwner(parts) {
|
|
3046
|
-
if (!this.device.isInitialized() || !this.pipeline)
|
|
3047
|
-
throw new Error('Renderer is not initialized.');
|
|
3059
|
+
if (this.deviceLost || !this.device.isInitialized() || !this.pipeline)
|
|
3060
|
+
throw this.deviceLost ? rendererDeviceLostError() : new Error('Renderer is not initialized.');
|
|
3048
3061
|
const prepared = this.scene.prepareAuthoredOwner(parts, this.device.getDevice(), this.pipeline);
|
|
3049
3062
|
return { commit: () => { prepared.commit(); this.refreshPlacementBounds(); this.invalidateBVHCache(); this.requestRender(); }, dispose: prepared.dispose };
|
|
3050
3063
|
}
|
|
@@ -3305,15 +3318,17 @@ export class Renderer {
|
|
|
3305
3318
|
* in-flight init. Callers: the public `destroy()` (which invalidates first)
|
|
3306
3319
|
* and `initOnce()`, tearing down the previous init before building its own.
|
|
3307
3320
|
*/
|
|
3308
|
-
teardown() {
|
|
3321
|
+
teardown(clearScene = true) {
|
|
3309
3322
|
// Nothing below survives this call, so `whenReady()` / `isReady()` must
|
|
3310
3323
|
// go back to waiting. Set first: every release below is synchronous, but
|
|
3311
3324
|
// the flag is what a caller holding a live reference actually reads.
|
|
3312
3325
|
this.ready = false;
|
|
3313
3326
|
// Scene mesh GPU buffers
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3327
|
+
if (clearScene) {
|
|
3328
|
+
this.scene.clear();
|
|
3329
|
+
// Re-arm the section-bounds diagnostic log for the next model.
|
|
3330
|
+
this._loggedSectionBounds = false;
|
|
3331
|
+
}
|
|
3317
3332
|
// Render pipelines (textures + uniform buffers)
|
|
3318
3333
|
this.pipeline?.destroy();
|
|
3319
3334
|
this.pipeline = null;
|
|
@@ -3338,6 +3353,8 @@ export class Renderer {
|
|
|
3338
3353
|
this.overlays.destroy();
|
|
3339
3354
|
this.referenceImages.destroy();
|
|
3340
3355
|
// Point cloud GPU resources
|
|
3356
|
+
this.pointCloudStreamEpoch++;
|
|
3357
|
+
this.pointCloudStreamEpochs.clear();
|
|
3341
3358
|
this.pointCloudRenderer?.clear();
|
|
3342
3359
|
this.pointCloudRenderer = null;
|
|
3343
3360
|
// BIM ↔ scan deviation pipeline + cached BVH GPU buffers.
|