@preference-sl/pref-viewer 2.13.0-beta.11 → 2.13.0-beta.13
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/package.json +1 -1
- package/src/babylonjs-controller.js +213 -95
- package/src/localization/i18n.js +1 -1
- package/src/pref-viewer-3d.js +1 -1
package/package.json
CHANGED
|
@@ -18,14 +18,15 @@ import { translate } from "./localization/i18n.js";
|
|
|
18
18
|
* turns it into a deterministic scene lifecycle with exports.
|
|
19
19
|
*
|
|
20
20
|
* Overview
|
|
21
|
-
* - Creates the Babylon.js engine/scene/camera stack, configures Draco decoders, wires
|
|
22
|
-
* exposes download/
|
|
21
|
+
* - Creates the Babylon.js engine/scene/camera stack, configures Draco decoders, wires render loops plus a throttled
|
|
22
|
+
* ResizeObserver, and exposes download/XR helpers.
|
|
23
23
|
* - Resolves GLTF/GLB sources via GLTFResolver, loads them into `AssetContainer`s, and toggles visibility by
|
|
24
24
|
* mutating container state plus `show-model/show-scene` attributes.
|
|
25
25
|
* - Applies/persists AA, SSAO, IBL, and shadow flags; when they change it stops the render loop, tears down pipelines,
|
|
26
26
|
* reloads containers, and reinstalls effects after environment textures finish loading.
|
|
27
27
|
* - Manages keyboard/pointer/wheel handlers, animation menus, and WebXR so PrefViewer menus and DOM attributes stay
|
|
28
|
-
* synchronized with Babylon state, using sampled pointer picking to reduce raycast
|
|
28
|
+
* synchronized with Babylon state, using sampled pointer picking and last-picked tracking to reduce raycast/highlight
|
|
29
|
+
* cost on dense scenes.
|
|
29
30
|
* - Generates GLB, glTF+ZIP, or USDZ exports with timestamped names and localized dialog copy.
|
|
30
31
|
* - Translates metadata (inner floor offsets, cast/receive shadows, camera locks) into scene adjustments after reloads.
|
|
31
32
|
*
|
|
@@ -39,7 +40,8 @@ import { translate } from "./localization/i18n.js";
|
|
|
39
40
|
* 5. Use `setContainerVisibility`, `setMaterialOptions`, `setCameraOptions`, or `setIBLOptions` for targeted updates; these
|
|
40
41
|
* helpers stop/restart the render loop while they rebuild camera-dependent resources.
|
|
41
42
|
* 6. Invoke `disable()` when the element disconnects to tear down scenes, XR sessions, observers, and handlers.
|
|
42
|
-
*
|
|
43
|
+
* `disable()` is asynchronous; it waits for XR/session shutdown before engine disposal and also disposes the
|
|
44
|
+
* shared GLTF resolver, which closes its internal IndexedDB handle.
|
|
43
45
|
*
|
|
44
46
|
* Public API Highlights
|
|
45
47
|
* - constructor(canvas, containers, options)
|
|
@@ -65,6 +67,12 @@ import { translate } from "./localization/i18n.js";
|
|
|
65
67
|
* in SSR/Node contexts (though functionality activates only in browsers).
|
|
66
68
|
* - Pointer-pick lifecycle: hover/highlight raycasts are sampled on POINTERMOVE (time + distance thresholds), wheel
|
|
67
69
|
* input avoids picks entirely, and right-click POINTERUP performs an on-demand pick for context-menu targeting.
|
|
70
|
+
* - Runtime bookkeeping is split into mutable `#state` and tuning constants under `#config` to keep behavior changes
|
|
71
|
+
* explicit and reduce cross-field drift.
|
|
72
|
+
* - Resize lifecycle: canvas resize notifications are throttled (with trailing execution) before calling `engine.resize()`
|
|
73
|
+
* and any queued resize callback is canceled during teardown.
|
|
74
|
+
* - Teardown lifecycle: concurrent `disable()` calls are coalesced into a single in-flight promise to avoid races
|
|
75
|
+
* during XR exit and engine disposal.
|
|
68
76
|
*/
|
|
69
77
|
export default class BabylonJSController {
|
|
70
78
|
#RENDER_SETTINGS_STORAGE_KEY = "pref-viewer/render-settings";
|
|
@@ -96,9 +104,8 @@ export default class BabylonJSController {
|
|
|
96
104
|
#shadowGen = [];
|
|
97
105
|
#XRExperience = null;
|
|
98
106
|
#canvasResizeObserver = null;
|
|
99
|
-
|
|
107
|
+
|
|
100
108
|
#hdrTexture = null; // reusable in-memory HDR source cloned into scene.environmentTexture across reloads
|
|
101
|
-
#lastPickedMeshId = null;
|
|
102
109
|
|
|
103
110
|
#containers = {};
|
|
104
111
|
#options = {};
|
|
@@ -113,39 +120,61 @@ export default class BabylonJSController {
|
|
|
113
120
|
};
|
|
114
121
|
|
|
115
122
|
#handlers = {
|
|
123
|
+
onAnimationGroupChanged: null,
|
|
116
124
|
onKeyUp: null,
|
|
117
125
|
onPointerObservable: null,
|
|
118
|
-
onAnimationGroupChanged: null,
|
|
119
126
|
onResize: null,
|
|
120
127
|
renderLoop: null,
|
|
121
128
|
};
|
|
122
129
|
|
|
123
130
|
#settings = { ...BabylonJSController.DEFAULT_RENDER_SETTINGS };
|
|
124
131
|
|
|
125
|
-
//
|
|
126
|
-
#
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
132
|
+
// Runtime mutable state (changes while the app is running).
|
|
133
|
+
#state = {
|
|
134
|
+
// Pointer-picking sampling state avoids expensive scene.pick on every move.
|
|
135
|
+
pointerPicking: {
|
|
136
|
+
lastMovePickAt: 0,
|
|
137
|
+
lastMovePickX: NaN,
|
|
138
|
+
lastMovePickY: NaN,
|
|
139
|
+
lastPickedMeshId: null,
|
|
140
|
+
},
|
|
141
|
+
// Render loop state balances performance with responsiveness.
|
|
142
|
+
render: {
|
|
143
|
+
isLoopRunning: false,
|
|
144
|
+
dirtyFrames: 0,
|
|
145
|
+
continuousUntil: 0,
|
|
146
|
+
lastRenderAt: 0,
|
|
147
|
+
},
|
|
148
|
+
// Resize state batches frequent ResizeObserver notifications.
|
|
149
|
+
resize: {
|
|
150
|
+
isScheduled: false,
|
|
151
|
+
timeoutId: null,
|
|
152
|
+
lastAppliedAt: 0,
|
|
153
|
+
},
|
|
138
154
|
};
|
|
139
155
|
|
|
140
|
-
//
|
|
141
|
-
#
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
156
|
+
// Runtime configuration constants (tuning knobs, not per-frame state).
|
|
157
|
+
#config = {
|
|
158
|
+
pointerPicking: {
|
|
159
|
+
movePickIntervalMs: 50, // cap expensive scene.pick calls to ~20 Hz while moving the pointer
|
|
160
|
+
movePickMinDistancePx: 2, // skip picks for sub-pixel jitter
|
|
161
|
+
},
|
|
162
|
+
render: {
|
|
163
|
+
burstFramesBase: 2,
|
|
164
|
+
burstFramesEnhanced: 32, // when AA/SSAO/IBL is enabled, more frames are needed to reach stable output
|
|
165
|
+
interactionMs: 250,
|
|
166
|
+
animationMs: 200,
|
|
167
|
+
idleThrottleMs: 1000 / 15,
|
|
168
|
+
},
|
|
169
|
+
resize: {
|
|
170
|
+
throttleMs: 50, // cap resize work to ~20 Hz while dragging/resizing containers
|
|
171
|
+
},
|
|
145
172
|
};
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
173
|
+
|
|
174
|
+
// Promises to track async disable() lifecycle when XR and general teardown may run concurrently; ensures idempotent disable calls are safe and callers can await full teardown completion.
|
|
175
|
+
#disablingPromises = {
|
|
176
|
+
xr: null,
|
|
177
|
+
general: null,
|
|
149
178
|
};
|
|
150
179
|
|
|
151
180
|
/**
|
|
@@ -312,11 +341,11 @@ export default class BabylonJSController {
|
|
|
312
341
|
* @returns {boolean} True when the loop was started, false when no engine is available or it was already running.
|
|
313
342
|
*/
|
|
314
343
|
#startEngineRenderLoop() {
|
|
315
|
-
if (!this.#engine || this.#
|
|
344
|
+
if (!this.#engine || this.#state.render.isLoopRunning) {
|
|
316
345
|
return false;
|
|
317
346
|
}
|
|
318
347
|
this.#engine.runRenderLoop(this.#handlers.renderLoop);
|
|
319
|
-
this.#
|
|
348
|
+
this.#state.render.isLoopRunning = true;
|
|
320
349
|
return true;
|
|
321
350
|
}
|
|
322
351
|
|
|
@@ -326,13 +355,25 @@ export default class BabylonJSController {
|
|
|
326
355
|
* @returns {boolean} True when the loop was stopped, false when no engine is available or it was already stopped.
|
|
327
356
|
*/
|
|
328
357
|
#stopEngineRenderLoop() {
|
|
329
|
-
if (!this.#engine || !this.#
|
|
358
|
+
if (!this.#engine || !this.#state.render.isLoopRunning) {
|
|
330
359
|
return false;
|
|
331
360
|
}
|
|
332
361
|
this.#engine.stopRenderLoop(this.#handlers.renderLoop);
|
|
333
|
-
this.#
|
|
362
|
+
this.#state.render.isLoopRunning = false;
|
|
334
363
|
return true;
|
|
335
364
|
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Resets transient render-loop bookkeeping so the next render request starts from a clean baseline.
|
|
368
|
+
* Clears queued burst frames, ends any active continuous-render window, and drops the idle-throttle timestamp.
|
|
369
|
+
* @private
|
|
370
|
+
* @returns {void}
|
|
371
|
+
*/
|
|
372
|
+
#resetRenderState() {
|
|
373
|
+
this.#state.render.dirtyFrames = 0;
|
|
374
|
+
this.#state.render.continuousUntil = 0;
|
|
375
|
+
this.#state.render.lastRenderAt = 0;
|
|
376
|
+
}
|
|
336
377
|
|
|
337
378
|
/**
|
|
338
379
|
* Marks the scene as dirty and optionally extends a short continuous-render window.
|
|
@@ -349,9 +390,9 @@ export default class BabylonJSController {
|
|
|
349
390
|
}
|
|
350
391
|
|
|
351
392
|
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
352
|
-
this.#
|
|
393
|
+
this.#state.render.dirtyFrames = Math.max(this.#state.render.dirtyFrames, Math.max(1, frames));
|
|
353
394
|
if (continuousMs > 0) {
|
|
354
|
-
this.#
|
|
395
|
+
this.#state.render.continuousUntil = Math.max(this.#state.render.continuousUntil, now + continuousMs);
|
|
355
396
|
}
|
|
356
397
|
this.#startEngineRenderLoop();
|
|
357
398
|
return true;
|
|
@@ -436,13 +477,13 @@ export default class BabylonJSController {
|
|
|
436
477
|
const cameraInMotion = this.#isCameraInMotion();
|
|
437
478
|
|
|
438
479
|
if (animationRunning) {
|
|
439
|
-
this.#
|
|
480
|
+
this.#state.render.continuousUntil = Math.max(this.#state.render.continuousUntil, now + this.#config.render.animationMs);
|
|
440
481
|
}
|
|
441
482
|
if (cameraInMotion) {
|
|
442
|
-
this.#
|
|
483
|
+
this.#state.render.continuousUntil = Math.max(this.#state.render.continuousUntil, now + this.#config.render.interactionMs);
|
|
443
484
|
}
|
|
444
485
|
|
|
445
|
-
return animationRunning || cameraInMotion || this.#
|
|
486
|
+
return animationRunning || cameraInMotion || this.#state.render.continuousUntil > now;
|
|
446
487
|
}
|
|
447
488
|
|
|
448
489
|
/**
|
|
@@ -460,22 +501,22 @@ export default class BabylonJSController {
|
|
|
460
501
|
|
|
461
502
|
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
462
503
|
const continuous = this.#shouldRenderContinuously(now);
|
|
463
|
-
const needsRender = continuous || this.#
|
|
504
|
+
const needsRender = continuous || this.#state.render.dirtyFrames > 0;
|
|
464
505
|
|
|
465
506
|
if (!needsRender) {
|
|
466
507
|
this.#stopEngineRenderLoop();
|
|
467
508
|
return;
|
|
468
509
|
}
|
|
469
510
|
|
|
470
|
-
if (!continuous && this.#
|
|
511
|
+
if (!continuous && this.#state.render.lastRenderAt > 0 && now - this.#state.render.lastRenderAt < this.#config.render.idleThrottleMs) {
|
|
471
512
|
return;
|
|
472
513
|
}
|
|
473
|
-
|
|
514
|
+
|
|
474
515
|
this.#scene.render();
|
|
475
|
-
this.#
|
|
516
|
+
this.#state.render.lastRenderAt = now;
|
|
476
517
|
|
|
477
|
-
if (this.#
|
|
478
|
-
this.#
|
|
518
|
+
if (this.#state.render.dirtyFrames > 0) {
|
|
519
|
+
this.#state.render.dirtyFrames -= 1;
|
|
479
520
|
}
|
|
480
521
|
}
|
|
481
522
|
|
|
@@ -1200,10 +1241,10 @@ export default class BabylonJSController {
|
|
|
1200
1241
|
* @returns {void}
|
|
1201
1242
|
*/
|
|
1202
1243
|
#resetPointerPickingState() {
|
|
1203
|
-
this.#
|
|
1204
|
-
this.#
|
|
1205
|
-
this.#
|
|
1206
|
-
this.#lastPickedMeshId = null;
|
|
1244
|
+
this.#state.pointerPicking.lastMovePickAt = 0;
|
|
1245
|
+
this.#state.pointerPicking.lastMovePickX = NaN;
|
|
1246
|
+
this.#state.pointerPicking.lastMovePickY = NaN;
|
|
1247
|
+
this.#state.pointerPicking.lastPickedMeshId = null;
|
|
1207
1248
|
}
|
|
1208
1249
|
|
|
1209
1250
|
/**
|
|
@@ -1220,8 +1261,8 @@ export default class BabylonJSController {
|
|
|
1220
1261
|
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1221
1262
|
const x = this.#scene.pointerX;
|
|
1222
1263
|
const y = this.#scene.pointerY;
|
|
1223
|
-
const state = this.#
|
|
1224
|
-
const config = this.#
|
|
1264
|
+
const state = this.#state.pointerPicking;
|
|
1265
|
+
const config = this.#config.pointerPicking;
|
|
1225
1266
|
|
|
1226
1267
|
if (!Number.isFinite(state.lastMovePickX) || !Number.isFinite(state.lastMovePickY)) {
|
|
1227
1268
|
state.lastMovePickX = x;
|
|
@@ -1256,10 +1297,7 @@ export default class BabylonJSController {
|
|
|
1256
1297
|
this.#scene.onPointerObservable.add(this.#handlers.onPointerObservable);
|
|
1257
1298
|
}
|
|
1258
1299
|
if (this.#engine) {
|
|
1259
|
-
this.#canvasResizeObserver = new ResizeObserver(
|
|
1260
|
-
this.#engine.resize();
|
|
1261
|
-
this.#requestRender({ frames: this.#renderConfig.burstFramesBase, continuousMs: this.#renderConfig.interactionMs });
|
|
1262
|
-
});
|
|
1300
|
+
this.#canvasResizeObserver = new ResizeObserver(this.#handlers.onResize);
|
|
1263
1301
|
this.#canvasResizeObserver.observe(this.#canvas);
|
|
1264
1302
|
}
|
|
1265
1303
|
}
|
|
@@ -1276,6 +1314,7 @@ export default class BabylonJSController {
|
|
|
1276
1314
|
if (this.#scene !== null) {
|
|
1277
1315
|
this.#scene.onPointerObservable.removeCallback(this.#handlers.onPointerObservable);
|
|
1278
1316
|
}
|
|
1317
|
+
this.#cancelScheduledResize();
|
|
1279
1318
|
this.#canvasResizeObserver?.disconnect();
|
|
1280
1319
|
this.#canvasResizeObserver = null;
|
|
1281
1320
|
this.#detachAnimationChangedListener();
|
|
@@ -1308,30 +1347,43 @@ export default class BabylonJSController {
|
|
|
1308
1347
|
|
|
1309
1348
|
/**
|
|
1310
1349
|
* Disposes the Babylon.js WebXR experience if it exists.
|
|
1350
|
+
* If XR is currently active, waits for `exitXRAsync()` before disposing to avoid
|
|
1351
|
+
* tearing down the engine while the XR session is still shutting down.
|
|
1352
|
+
* Concurrent calls share the same in-flight promise so disposal runs only once.
|
|
1311
1353
|
* @private
|
|
1312
|
-
* @returns {void}
|
|
1354
|
+
* @returns {Promise<void>}
|
|
1313
1355
|
*/
|
|
1314
|
-
#disposeXRExperience() {
|
|
1315
|
-
if (
|
|
1356
|
+
async #disposeXRExperience() {
|
|
1357
|
+
if (this.#disablingPromises.xr) {
|
|
1358
|
+
return await this.#disablingPromises.xr;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
const xrExperience = this.#XRExperience;
|
|
1362
|
+
if (!xrExperience) {
|
|
1316
1363
|
return;
|
|
1317
1364
|
}
|
|
1318
1365
|
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
.
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1366
|
+
this.#disablingPromises.xr = (async () => {
|
|
1367
|
+
try {
|
|
1368
|
+
if (xrExperience.baseExperience?.state === WebXRState.IN_XR) {
|
|
1369
|
+
await xrExperience.baseExperience.exitXRAsync();
|
|
1370
|
+
}
|
|
1371
|
+
} catch (error) {
|
|
1372
|
+
console.warn("PrefViewer: Error exiting XR experience:", error);
|
|
1373
|
+
} finally {
|
|
1374
|
+
try {
|
|
1375
|
+
xrExperience.dispose();
|
|
1376
|
+
} catch (error) {
|
|
1377
|
+
console.warn("PrefViewer: Error disposing XR experience:", error);
|
|
1378
|
+
}
|
|
1379
|
+
if (this.#XRExperience === xrExperience) {
|
|
1329
1380
|
this.#XRExperience = null;
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1381
|
+
}
|
|
1382
|
+
this.#disablingPromises.xr = null;
|
|
1383
|
+
}
|
|
1384
|
+
})();
|
|
1385
|
+
|
|
1386
|
+
await this.#disablingPromises.xr;
|
|
1335
1387
|
}
|
|
1336
1388
|
|
|
1337
1389
|
/**
|
|
@@ -1379,7 +1431,7 @@ export default class BabylonJSController {
|
|
|
1379
1431
|
* @returns {void}
|
|
1380
1432
|
*/
|
|
1381
1433
|
#onAnimationGroupPlay() {
|
|
1382
|
-
this.#requestRender({ frames: 1, continuousMs: this.#
|
|
1434
|
+
this.#requestRender({ frames: 1, continuousMs: this.#config.render.animationMs });
|
|
1383
1435
|
}
|
|
1384
1436
|
|
|
1385
1437
|
/**
|
|
@@ -1388,7 +1440,12 @@ export default class BabylonJSController {
|
|
|
1388
1440
|
* @returns {void}
|
|
1389
1441
|
*/
|
|
1390
1442
|
#onAnimationGroupStop() {
|
|
1391
|
-
|
|
1443
|
+
debugger;
|
|
1444
|
+
if (this.#settings.iblEnabled && this.#renderPipelines.iblShadows) {
|
|
1445
|
+
this.#renderPipelines.iblShadows.updateVoxelization();
|
|
1446
|
+
this.#scene?.postProcessRenderPipelineManager?.update();
|
|
1447
|
+
}
|
|
1448
|
+
const frames = this.#settings.antiAliasingEnabled || this.#settings.ambientOcclusionEnabled || this.#settings.iblEnabled ? this.#config.render.burstFramesEnhanced : this.#config.render.burstFramesBase;
|
|
1392
1449
|
this.#requestRender({ frames: frames });
|
|
1393
1450
|
}
|
|
1394
1451
|
|
|
@@ -1466,7 +1523,7 @@ export default class BabylonJSController {
|
|
|
1466
1523
|
const movementVector = direction.scale(zoomSpeed);
|
|
1467
1524
|
camera.position = camera.position.add(movementVector);
|
|
1468
1525
|
}
|
|
1469
|
-
this.#requestRender({ frames: 1, continuousMs: this.#
|
|
1526
|
+
this.#requestRender({ frames: 1, continuousMs: this.#config.render.interactionMs });
|
|
1470
1527
|
}
|
|
1471
1528
|
}
|
|
1472
1529
|
|
|
@@ -1499,14 +1556,14 @@ export default class BabylonJSController {
|
|
|
1499
1556
|
#onPointerMove(event, pickInfo) {
|
|
1500
1557
|
const camera = this.#scene?.activeCamera;
|
|
1501
1558
|
if (camera && !camera.metadata?.locked) {
|
|
1502
|
-
this.#requestRender({ frames: 1, continuousMs: this.#
|
|
1559
|
+
this.#requestRender({ frames: 1, continuousMs: this.#config.render.interactionMs });
|
|
1503
1560
|
}
|
|
1504
1561
|
if (this.#babylonJSAnimationController && pickInfo) {
|
|
1505
1562
|
const pickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
1506
|
-
if (this.#lastPickedMeshId !== pickedMeshId) {
|
|
1563
|
+
if (this.#state.pointerPicking.lastPickedMeshId !== pickedMeshId) {
|
|
1507
1564
|
const highlightResult = this.#babylonJSAnimationController.highlightMeshes(pickInfo);
|
|
1508
1565
|
if (highlightResult.changed) {
|
|
1509
|
-
this.#requestRender({ frames: 1, continuousMs: this.#
|
|
1566
|
+
this.#requestRender({ frames: 1, continuousMs: this.#config.render.interactionMs });
|
|
1510
1567
|
}
|
|
1511
1568
|
}
|
|
1512
1569
|
}
|
|
@@ -1528,11 +1585,11 @@ export default class BabylonJSController {
|
|
|
1528
1585
|
lastPickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
1529
1586
|
}
|
|
1530
1587
|
this.#onPointerMove(info.event, pickInfo);
|
|
1531
|
-
this.#lastPickedMeshId = lastPickedMeshId;
|
|
1588
|
+
this.#state.pointerPicking.lastPickedMeshId = lastPickedMeshId;
|
|
1532
1589
|
} else if (info.type === PointerEventTypes.POINTERUP) {
|
|
1533
1590
|
const pickInfo = info.event?.button === 2 ? this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY) : null;
|
|
1534
1591
|
if (pickInfo) {
|
|
1535
|
-
this.#lastPickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
1592
|
+
this.#state.pointerPicking.lastPickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
1536
1593
|
}
|
|
1537
1594
|
this.#onPointerUp(info.event, pickInfo);
|
|
1538
1595
|
} else if (info.type === PointerEventTypes.POINTERWHEEL) {
|
|
@@ -1551,8 +1608,47 @@ export default class BabylonJSController {
|
|
|
1551
1608
|
if (!this.#engine) {
|
|
1552
1609
|
return;
|
|
1553
1610
|
}
|
|
1554
|
-
|
|
1555
|
-
|
|
1611
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1612
|
+
const elapsed = now - this.#state.resize.lastAppliedAt;
|
|
1613
|
+
const applyResize = () => {
|
|
1614
|
+
this.#state.resize.timeoutId = null;
|
|
1615
|
+
this.#state.resize.isScheduled = false;
|
|
1616
|
+
if (!this.#engine) {
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
this.#state.resize.lastAppliedAt = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1620
|
+
console.log(`PrefViewer: Applying resize after ${Math.round(elapsed)}ms`);
|
|
1621
|
+
this.#engine.resize();
|
|
1622
|
+
this.#requestRender({ frames: this.#config.render.burstFramesBase, continuousMs: this.#config.render.interactionMs });
|
|
1623
|
+
};
|
|
1624
|
+
|
|
1625
|
+
if (elapsed >= this.#config.resize.throttleMs) {
|
|
1626
|
+
applyResize();
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
if (this.#state.resize.isScheduled) {
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
this.#state.resize.isScheduled = true;
|
|
1635
|
+
const waitMs = Math.max(0, this.#config.resize.throttleMs - elapsed);
|
|
1636
|
+
this.#state.resize.timeoutId = setTimeout(() => {
|
|
1637
|
+
applyResize();
|
|
1638
|
+
}, waitMs);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
/**
|
|
1642
|
+
* Clears any queued throttled resize callback.
|
|
1643
|
+
* @private
|
|
1644
|
+
* @returns {void}
|
|
1645
|
+
*/
|
|
1646
|
+
#cancelScheduledResize() {
|
|
1647
|
+
if (this.#state.resize.timeoutId !== null) {
|
|
1648
|
+
clearTimeout(this.#state.resize.timeoutId);
|
|
1649
|
+
}
|
|
1650
|
+
this.#state.resize.timeoutId = null;
|
|
1651
|
+
this.#state.resize.isScheduled = false;
|
|
1556
1652
|
}
|
|
1557
1653
|
|
|
1558
1654
|
/**
|
|
@@ -1898,11 +1994,10 @@ export default class BabylonJSController {
|
|
|
1898
1994
|
*/
|
|
1899
1995
|
async #stopRender() {
|
|
1900
1996
|
this.#stopEngineRenderLoop();
|
|
1901
|
-
this.#
|
|
1902
|
-
this.#renderState.continuousUntil = 0;
|
|
1903
|
-
this.#renderState.lastRenderAt = 0;
|
|
1997
|
+
this.#resetRenderState();
|
|
1904
1998
|
await this.#unloadCameraDependentEffects();
|
|
1905
1999
|
}
|
|
2000
|
+
|
|
1906
2001
|
/**
|
|
1907
2002
|
* Starts the Babylon.js render loop for the current scene.
|
|
1908
2003
|
* Waits until the scene is ready before beginning continuous rendering.
|
|
@@ -1912,8 +2007,8 @@ export default class BabylonJSController {
|
|
|
1912
2007
|
async #startRender() {
|
|
1913
2008
|
await this.#loadCameraDependentEffects();
|
|
1914
2009
|
await this.#scene.whenReadyAsync();
|
|
1915
|
-
const frames = this.#settings.antiAliasingEnabled || this.#settings.ambientOcclusionEnabled || this.#settings.iblEnabled ? this.#
|
|
1916
|
-
this.#requestRender({ frames: frames, continuousMs: this.#
|
|
2010
|
+
const frames = this.#settings.antiAliasingEnabled || this.#settings.ambientOcclusionEnabled || this.#settings.iblEnabled ? this.#config.render.burstFramesEnhanced : this.#config.render.burstFramesBase;
|
|
2011
|
+
this.#requestRender({ frames: frames, continuousMs: this.#config.render.interactionMs });
|
|
1917
2012
|
}
|
|
1918
2013
|
|
|
1919
2014
|
/**
|
|
@@ -2345,17 +2440,40 @@ export default class BabylonJSController {
|
|
|
2345
2440
|
/**
|
|
2346
2441
|
* Disposes the Babylon.js engine and disconnects the canvas resize observer.
|
|
2347
2442
|
* Cleans up all scene, camera, light, XR, and GLTF resolver resources.
|
|
2443
|
+
* The teardown is asynchronous: it waits for XR/session-dependent shutdown work
|
|
2444
|
+
* before disposing the engine, and coalesces concurrent calls into one in-flight promise.
|
|
2348
2445
|
* @public
|
|
2349
|
-
* @returns {void}
|
|
2446
|
+
* @returns {Promise<void>}
|
|
2350
2447
|
*/
|
|
2351
|
-
disable() {
|
|
2352
|
-
this.#
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
this.#
|
|
2357
|
-
|
|
2358
|
-
|
|
2448
|
+
async disable() {
|
|
2449
|
+
if (this.#disablingPromises.general) {
|
|
2450
|
+
return await this.#disablingPromises.general;
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
this.#disablingPromises.general = (async () => {
|
|
2454
|
+
this.#disableInteraction();
|
|
2455
|
+
this.#disposeAnimationController();
|
|
2456
|
+
this.#disposeGLTFResolver();
|
|
2457
|
+
try {
|
|
2458
|
+
await this.#disposeXRExperience();
|
|
2459
|
+
} catch (error) {
|
|
2460
|
+
console.warn("PrefViewer: Error while disposing XR experience:", error);
|
|
2461
|
+
}
|
|
2462
|
+
try {
|
|
2463
|
+
await this.#unloadCameraDependentEffects();
|
|
2464
|
+
} catch (error) {
|
|
2465
|
+
console.warn("PrefViewer: Error while unloading camera-dependent effects:", error);
|
|
2466
|
+
} finally {
|
|
2467
|
+
this.#stopEngineRenderLoop();
|
|
2468
|
+
this.#disposeEngine();
|
|
2469
|
+
}
|
|
2470
|
+
})();
|
|
2471
|
+
|
|
2472
|
+
try {
|
|
2473
|
+
await this.#disablingPromises.general;
|
|
2474
|
+
} finally {
|
|
2475
|
+
this.#disablingPromises.general = null;
|
|
2476
|
+
}
|
|
2359
2477
|
}
|
|
2360
2478
|
|
|
2361
2479
|
/**
|
package/src/localization/i18n.js
CHANGED
package/src/pref-viewer-3d.js
CHANGED