@preference-sl/pref-viewer 2.13.0-beta.12 → 2.13.0-beta.14
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 +140 -67
- package/src/gltf-storage.js +48 -55
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
|
*
|
|
@@ -66,6 +67,10 @@ import { translate } from "./localization/i18n.js";
|
|
|
66
67
|
* in SSR/Node contexts (though functionality activates only in browsers).
|
|
67
68
|
* - Pointer-pick lifecycle: hover/highlight raycasts are sampled on POINTERMOVE (time + distance thresholds), wheel
|
|
68
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.
|
|
69
74
|
* - Teardown lifecycle: concurrent `disable()` calls are coalesced into a single in-flight promise to avoid races
|
|
70
75
|
* during XR exit and engine disposal.
|
|
71
76
|
*/
|
|
@@ -101,7 +106,6 @@ export default class BabylonJSController {
|
|
|
101
106
|
#canvasResizeObserver = null;
|
|
102
107
|
|
|
103
108
|
#hdrTexture = null; // reusable in-memory HDR source cloned into scene.environmentTexture across reloads
|
|
104
|
-
#lastPickedMeshId = null;
|
|
105
109
|
|
|
106
110
|
#containers = {};
|
|
107
111
|
#options = {};
|
|
@@ -116,39 +120,55 @@ export default class BabylonJSController {
|
|
|
116
120
|
};
|
|
117
121
|
|
|
118
122
|
#handlers = {
|
|
123
|
+
onAnimationGroupChanged: null,
|
|
119
124
|
onKeyUp: null,
|
|
120
125
|
onPointerObservable: null,
|
|
121
|
-
onAnimationGroupChanged: null,
|
|
122
126
|
onResize: null,
|
|
123
127
|
renderLoop: null,
|
|
124
128
|
};
|
|
125
129
|
|
|
126
130
|
#settings = { ...BabylonJSController.DEFAULT_RENDER_SETTINGS };
|
|
127
131
|
|
|
128
|
-
//
|
|
129
|
-
#
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
+
},
|
|
141
154
|
};
|
|
142
155
|
|
|
143
|
-
//
|
|
144
|
-
#
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
+
},
|
|
152
172
|
};
|
|
153
173
|
|
|
154
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.
|
|
@@ -321,11 +341,11 @@ export default class BabylonJSController {
|
|
|
321
341
|
* @returns {boolean} True when the loop was started, false when no engine is available or it was already running.
|
|
322
342
|
*/
|
|
323
343
|
#startEngineRenderLoop() {
|
|
324
|
-
if (!this.#engine || this.#
|
|
344
|
+
if (!this.#engine || this.#state.render.isLoopRunning) {
|
|
325
345
|
return false;
|
|
326
346
|
}
|
|
327
347
|
this.#engine.runRenderLoop(this.#handlers.renderLoop);
|
|
328
|
-
this.#
|
|
348
|
+
this.#state.render.isLoopRunning = true;
|
|
329
349
|
return true;
|
|
330
350
|
}
|
|
331
351
|
|
|
@@ -335,13 +355,25 @@ export default class BabylonJSController {
|
|
|
335
355
|
* @returns {boolean} True when the loop was stopped, false when no engine is available or it was already stopped.
|
|
336
356
|
*/
|
|
337
357
|
#stopEngineRenderLoop() {
|
|
338
|
-
if (!this.#engine || !this.#
|
|
358
|
+
if (!this.#engine || !this.#state.render.isLoopRunning) {
|
|
339
359
|
return false;
|
|
340
360
|
}
|
|
341
361
|
this.#engine.stopRenderLoop(this.#handlers.renderLoop);
|
|
342
|
-
this.#
|
|
362
|
+
this.#state.render.isLoopRunning = false;
|
|
343
363
|
return true;
|
|
344
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
|
+
}
|
|
345
377
|
|
|
346
378
|
/**
|
|
347
379
|
* Marks the scene as dirty and optionally extends a short continuous-render window.
|
|
@@ -358,9 +390,9 @@ export default class BabylonJSController {
|
|
|
358
390
|
}
|
|
359
391
|
|
|
360
392
|
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
361
|
-
this.#
|
|
393
|
+
this.#state.render.dirtyFrames = Math.max(this.#state.render.dirtyFrames, Math.max(1, frames));
|
|
362
394
|
if (continuousMs > 0) {
|
|
363
|
-
this.#
|
|
395
|
+
this.#state.render.continuousUntil = Math.max(this.#state.render.continuousUntil, now + continuousMs);
|
|
364
396
|
}
|
|
365
397
|
this.#startEngineRenderLoop();
|
|
366
398
|
return true;
|
|
@@ -445,13 +477,13 @@ export default class BabylonJSController {
|
|
|
445
477
|
const cameraInMotion = this.#isCameraInMotion();
|
|
446
478
|
|
|
447
479
|
if (animationRunning) {
|
|
448
|
-
this.#
|
|
480
|
+
this.#state.render.continuousUntil = Math.max(this.#state.render.continuousUntil, now + this.#config.render.animationMs);
|
|
449
481
|
}
|
|
450
482
|
if (cameraInMotion) {
|
|
451
|
-
this.#
|
|
483
|
+
this.#state.render.continuousUntil = Math.max(this.#state.render.continuousUntil, now + this.#config.render.interactionMs);
|
|
452
484
|
}
|
|
453
485
|
|
|
454
|
-
return animationRunning || cameraInMotion || this.#
|
|
486
|
+
return animationRunning || cameraInMotion || this.#state.render.continuousUntil > now;
|
|
455
487
|
}
|
|
456
488
|
|
|
457
489
|
/**
|
|
@@ -469,22 +501,22 @@ export default class BabylonJSController {
|
|
|
469
501
|
|
|
470
502
|
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
471
503
|
const continuous = this.#shouldRenderContinuously(now);
|
|
472
|
-
const needsRender = continuous || this.#
|
|
504
|
+
const needsRender = continuous || this.#state.render.dirtyFrames > 0;
|
|
473
505
|
|
|
474
506
|
if (!needsRender) {
|
|
475
507
|
this.#stopEngineRenderLoop();
|
|
476
508
|
return;
|
|
477
509
|
}
|
|
478
510
|
|
|
479
|
-
if (!continuous && this.#
|
|
511
|
+
if (!continuous && this.#state.render.lastRenderAt > 0 && now - this.#state.render.lastRenderAt < this.#config.render.idleThrottleMs) {
|
|
480
512
|
return;
|
|
481
513
|
}
|
|
482
|
-
|
|
514
|
+
|
|
483
515
|
this.#scene.render();
|
|
484
|
-
this.#
|
|
516
|
+
this.#state.render.lastRenderAt = now;
|
|
485
517
|
|
|
486
|
-
if (this.#
|
|
487
|
-
this.#
|
|
518
|
+
if (this.#state.render.dirtyFrames > 0) {
|
|
519
|
+
this.#state.render.dirtyFrames -= 1;
|
|
488
520
|
}
|
|
489
521
|
}
|
|
490
522
|
|
|
@@ -1209,10 +1241,10 @@ export default class BabylonJSController {
|
|
|
1209
1241
|
* @returns {void}
|
|
1210
1242
|
*/
|
|
1211
1243
|
#resetPointerPickingState() {
|
|
1212
|
-
this.#
|
|
1213
|
-
this.#
|
|
1214
|
-
this.#
|
|
1215
|
-
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;
|
|
1216
1248
|
}
|
|
1217
1249
|
|
|
1218
1250
|
/**
|
|
@@ -1229,8 +1261,8 @@ export default class BabylonJSController {
|
|
|
1229
1261
|
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1230
1262
|
const x = this.#scene.pointerX;
|
|
1231
1263
|
const y = this.#scene.pointerY;
|
|
1232
|
-
const state = this.#
|
|
1233
|
-
const config = this.#
|
|
1264
|
+
const state = this.#state.pointerPicking;
|
|
1265
|
+
const config = this.#config.pointerPicking;
|
|
1234
1266
|
|
|
1235
1267
|
if (!Number.isFinite(state.lastMovePickX) || !Number.isFinite(state.lastMovePickY)) {
|
|
1236
1268
|
state.lastMovePickX = x;
|
|
@@ -1265,10 +1297,7 @@ export default class BabylonJSController {
|
|
|
1265
1297
|
this.#scene.onPointerObservable.add(this.#handlers.onPointerObservable);
|
|
1266
1298
|
}
|
|
1267
1299
|
if (this.#engine) {
|
|
1268
|
-
this.#canvasResizeObserver = new ResizeObserver(
|
|
1269
|
-
this.#engine.resize();
|
|
1270
|
-
this.#requestRender({ frames: this.#renderConfig.burstFramesBase, continuousMs: this.#renderConfig.interactionMs });
|
|
1271
|
-
});
|
|
1300
|
+
this.#canvasResizeObserver = new ResizeObserver(this.#handlers.onResize);
|
|
1272
1301
|
this.#canvasResizeObserver.observe(this.#canvas);
|
|
1273
1302
|
}
|
|
1274
1303
|
}
|
|
@@ -1285,6 +1314,7 @@ export default class BabylonJSController {
|
|
|
1285
1314
|
if (this.#scene !== null) {
|
|
1286
1315
|
this.#scene.onPointerObservable.removeCallback(this.#handlers.onPointerObservable);
|
|
1287
1316
|
}
|
|
1317
|
+
this.#cancelScheduledResize();
|
|
1288
1318
|
this.#canvasResizeObserver?.disconnect();
|
|
1289
1319
|
this.#canvasResizeObserver = null;
|
|
1290
1320
|
this.#detachAnimationChangedListener();
|
|
@@ -1401,7 +1431,7 @@ export default class BabylonJSController {
|
|
|
1401
1431
|
* @returns {void}
|
|
1402
1432
|
*/
|
|
1403
1433
|
#onAnimationGroupPlay() {
|
|
1404
|
-
this.#requestRender({ frames: 1, continuousMs: this.#
|
|
1434
|
+
this.#requestRender({ frames: 1, continuousMs: this.#config.render.animationMs });
|
|
1405
1435
|
}
|
|
1406
1436
|
|
|
1407
1437
|
/**
|
|
@@ -1410,7 +1440,12 @@ export default class BabylonJSController {
|
|
|
1410
1440
|
* @returns {void}
|
|
1411
1441
|
*/
|
|
1412
1442
|
#onAnimationGroupStop() {
|
|
1413
|
-
|
|
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;
|
|
1414
1449
|
this.#requestRender({ frames: frames });
|
|
1415
1450
|
}
|
|
1416
1451
|
|
|
@@ -1488,7 +1523,7 @@ export default class BabylonJSController {
|
|
|
1488
1523
|
const movementVector = direction.scale(zoomSpeed);
|
|
1489
1524
|
camera.position = camera.position.add(movementVector);
|
|
1490
1525
|
}
|
|
1491
|
-
this.#requestRender({ frames: 1, continuousMs: this.#
|
|
1526
|
+
this.#requestRender({ frames: 1, continuousMs: this.#config.render.interactionMs });
|
|
1492
1527
|
}
|
|
1493
1528
|
}
|
|
1494
1529
|
|
|
@@ -1521,14 +1556,14 @@ export default class BabylonJSController {
|
|
|
1521
1556
|
#onPointerMove(event, pickInfo) {
|
|
1522
1557
|
const camera = this.#scene?.activeCamera;
|
|
1523
1558
|
if (camera && !camera.metadata?.locked) {
|
|
1524
|
-
this.#requestRender({ frames: 1, continuousMs: this.#
|
|
1559
|
+
this.#requestRender({ frames: 1, continuousMs: this.#config.render.interactionMs });
|
|
1525
1560
|
}
|
|
1526
1561
|
if (this.#babylonJSAnimationController && pickInfo) {
|
|
1527
1562
|
const pickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
1528
|
-
if (this.#lastPickedMeshId !== pickedMeshId) {
|
|
1563
|
+
if (this.#state.pointerPicking.lastPickedMeshId !== pickedMeshId) {
|
|
1529
1564
|
const highlightResult = this.#babylonJSAnimationController.highlightMeshes(pickInfo);
|
|
1530
1565
|
if (highlightResult.changed) {
|
|
1531
|
-
this.#requestRender({ frames: 1, continuousMs: this.#
|
|
1566
|
+
this.#requestRender({ frames: 1, continuousMs: this.#config.render.interactionMs });
|
|
1532
1567
|
}
|
|
1533
1568
|
}
|
|
1534
1569
|
}
|
|
@@ -1550,11 +1585,11 @@ export default class BabylonJSController {
|
|
|
1550
1585
|
lastPickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
1551
1586
|
}
|
|
1552
1587
|
this.#onPointerMove(info.event, pickInfo);
|
|
1553
|
-
this.#lastPickedMeshId = lastPickedMeshId;
|
|
1588
|
+
this.#state.pointerPicking.lastPickedMeshId = lastPickedMeshId;
|
|
1554
1589
|
} else if (info.type === PointerEventTypes.POINTERUP) {
|
|
1555
1590
|
const pickInfo = info.event?.button === 2 ? this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY) : null;
|
|
1556
1591
|
if (pickInfo) {
|
|
1557
|
-
this.#lastPickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
1592
|
+
this.#state.pointerPicking.lastPickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
1558
1593
|
}
|
|
1559
1594
|
this.#onPointerUp(info.event, pickInfo);
|
|
1560
1595
|
} else if (info.type === PointerEventTypes.POINTERWHEEL) {
|
|
@@ -1573,8 +1608,47 @@ export default class BabylonJSController {
|
|
|
1573
1608
|
if (!this.#engine) {
|
|
1574
1609
|
return;
|
|
1575
1610
|
}
|
|
1576
|
-
|
|
1577
|
-
|
|
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;
|
|
1578
1652
|
}
|
|
1579
1653
|
|
|
1580
1654
|
/**
|
|
@@ -1920,11 +1994,10 @@ export default class BabylonJSController {
|
|
|
1920
1994
|
*/
|
|
1921
1995
|
async #stopRender() {
|
|
1922
1996
|
this.#stopEngineRenderLoop();
|
|
1923
|
-
this.#
|
|
1924
|
-
this.#renderState.continuousUntil = 0;
|
|
1925
|
-
this.#renderState.lastRenderAt = 0;
|
|
1997
|
+
this.#resetRenderState();
|
|
1926
1998
|
await this.#unloadCameraDependentEffects();
|
|
1927
1999
|
}
|
|
2000
|
+
|
|
1928
2001
|
/**
|
|
1929
2002
|
* Starts the Babylon.js render loop for the current scene.
|
|
1930
2003
|
* Waits until the scene is ready before beginning continuous rendering.
|
|
@@ -1934,8 +2007,8 @@ export default class BabylonJSController {
|
|
|
1934
2007
|
async #startRender() {
|
|
1935
2008
|
await this.#loadCameraDependentEffects();
|
|
1936
2009
|
await this.#scene.whenReadyAsync();
|
|
1937
|
-
const frames = this.#settings.antiAliasingEnabled || this.#settings.ambientOcclusionEnabled || this.#settings.iblEnabled ? this.#
|
|
1938
|
-
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 });
|
|
1939
2012
|
}
|
|
1940
2013
|
|
|
1941
2014
|
/**
|
package/src/gltf-storage.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
const PC = (globalThis.PrefConfigurator ??= {});
|
|
5
5
|
PC.version = PC.version ?? "1.0.1"; // bump if your schema changes!!
|
|
6
6
|
PC.db = PC.db ?? null;
|
|
7
|
+
PC.dbName = PC.dbName ?? null;
|
|
8
|
+
PC.storeName = PC.storeName ?? null;
|
|
7
9
|
|
|
8
10
|
function _getDbOrThrow() {
|
|
9
11
|
const db = PC.db;
|
|
@@ -23,22 +25,50 @@ function _openEnsuringStore(dbName, storeName) {
|
|
|
23
25
|
}
|
|
24
26
|
open.onupgradeneeded = (ev) => {
|
|
25
27
|
const upgradeDb = ev.target.result;
|
|
28
|
+
const tx = ev.target.transaction;
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
if (upgradeDb.objectStoreNames.contains(storeName)) {
|
|
29
|
-
upgradeDb.
|
|
30
|
+
let store;
|
|
31
|
+
if (!upgradeDb.objectStoreNames.contains(storeName)) {
|
|
32
|
+
store = upgradeDb.createObjectStore(storeName, { keyPath: 'id' });
|
|
33
|
+
} else if (tx) {
|
|
34
|
+
store = tx.objectStore(storeName);
|
|
30
35
|
}
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
if (store && !store.indexNames.contains('expirationTimeStamp')) {
|
|
38
|
+
store.createIndex('expirationTimeStamp', 'expirationTimeStamp', { unique: false });
|
|
39
|
+
}
|
|
34
40
|
};
|
|
35
41
|
});
|
|
36
42
|
}
|
|
37
43
|
|
|
44
|
+
function _isRecoverableDbError(err) {
|
|
45
|
+
const msg = String(err?.message ?? err ?? "");
|
|
46
|
+
if (!msg) return false;
|
|
47
|
+
return msg.includes("Database not initialized") ||
|
|
48
|
+
msg.includes("The database connection is closing") ||
|
|
49
|
+
msg.includes("InvalidStateError") ||
|
|
50
|
+
msg.includes("NotFoundError");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function _withDbRetry(work) {
|
|
54
|
+
try {
|
|
55
|
+
return await work(_getDbOrThrow());
|
|
56
|
+
} catch (e) {
|
|
57
|
+
if (!_isRecoverableDbError(e) || !PC.dbName || !PC.storeName) {
|
|
58
|
+
throw e;
|
|
59
|
+
}
|
|
60
|
+
await initDb(PC.dbName, PC.storeName);
|
|
61
|
+
return await work(_getDbOrThrow());
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
38
65
|
// --- public API -------------------------------------------------------------
|
|
39
66
|
|
|
40
67
|
// Inicializar IndexedDB y dejar el handle en PrefConfigurator.db (público)
|
|
41
68
|
export async function initDb(dbName, storeName) {
|
|
69
|
+
PC.dbName = dbName;
|
|
70
|
+
PC.storeName = storeName;
|
|
71
|
+
|
|
42
72
|
const db = await _openEnsuringStore(dbName, storeName);
|
|
43
73
|
// Close any previous handle to avoid versionchange blocking
|
|
44
74
|
try { PC.db?.close?.(); } catch { }
|
|
@@ -54,10 +84,7 @@ export async function initDb(dbName, storeName) {
|
|
|
54
84
|
|
|
55
85
|
// Guardar modelo
|
|
56
86
|
export async function saveModel(modelDataStr, storeName) {
|
|
57
|
-
return new Promise((resolve, reject) => {
|
|
58
|
-
let db;
|
|
59
|
-
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
60
|
-
|
|
87
|
+
return _withDbRetry((db) => new Promise((resolve, reject) => {
|
|
61
88
|
let modelData;
|
|
62
89
|
try { modelData = JSON.parse(modelDataStr); }
|
|
63
90
|
catch (e) { reject(new Error("saveModel: modelDataStr is not valid JSON")); return; }
|
|
@@ -74,59 +101,34 @@ export async function saveModel(modelDataStr, storeName) {
|
|
|
74
101
|
|
|
75
102
|
req.onerror = () => reject(req.error);
|
|
76
103
|
req.onsuccess = () => resolve();
|
|
77
|
-
});
|
|
104
|
+
}));
|
|
78
105
|
}
|
|
79
106
|
|
|
80
107
|
// Cargar modelo
|
|
81
108
|
export function loadModel(modelId, storeName) {
|
|
82
|
-
return new Promise((resolve, reject) => {
|
|
83
|
-
let db;
|
|
84
|
-
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
85
|
-
|
|
109
|
+
return _withDbRetry((db) => new Promise((resolve, reject) => {
|
|
86
110
|
const tx = db.transaction([storeName], "readonly");
|
|
87
111
|
const store = tx.objectStore(storeName);
|
|
88
112
|
const req = store.get(modelId);
|
|
89
113
|
|
|
90
114
|
req.onerror = () => reject(req.error);
|
|
91
115
|
req.onsuccess = () => resolve(req.result ?? null);
|
|
92
|
-
});
|
|
116
|
+
}));
|
|
93
117
|
}
|
|
94
118
|
|
|
95
119
|
// Descargar archivo desde base64
|
|
96
120
|
export function downloadFileFromBytes(fileName, bytesBase64, mimeType) {
|
|
97
|
-
let objectURL = null;
|
|
98
121
|
const link = document.createElement("a");
|
|
99
122
|
link.download = fileName;
|
|
100
|
-
|
|
101
|
-
const decoded = atob(bytesBase64);
|
|
102
|
-
const bytes = new Uint8Array(decoded.length);
|
|
103
|
-
for (let i = 0; i < decoded.length; i++) {
|
|
104
|
-
bytes[i] = decoded.charCodeAt(i);
|
|
105
|
-
}
|
|
106
|
-
const blob = new Blob([bytes], { type: mimeType || "application/octet-stream" });
|
|
107
|
-
objectURL = URL.createObjectURL(blob);
|
|
108
|
-
link.href = objectURL;
|
|
109
|
-
} catch {
|
|
110
|
-
// Fallback if base64 decoding fails for any reason.
|
|
111
|
-
link.href = `data:${mimeType};base64,${bytesBase64}`;
|
|
112
|
-
}
|
|
123
|
+
link.href = `data:${mimeType};base64,${bytesBase64}`;
|
|
113
124
|
document.body.appendChild(link);
|
|
114
125
|
link.click();
|
|
115
|
-
link.href = "";
|
|
116
126
|
link.remove();
|
|
117
|
-
if (objectURL) {
|
|
118
|
-
setTimeout(() => {
|
|
119
|
-
URL.revokeObjectURL(objectURL);
|
|
120
|
-
}, 0);
|
|
121
|
-
}
|
|
122
127
|
}
|
|
123
128
|
|
|
124
129
|
// Obtener todos los modelos (solo metadata)
|
|
125
130
|
export async function getAllModels(storeName) {
|
|
126
|
-
return new Promise((resolve, reject) => {
|
|
127
|
-
let db;
|
|
128
|
-
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
129
|
-
|
|
131
|
+
return _withDbRetry((db) => new Promise((resolve, reject) => {
|
|
130
132
|
const tx = db.transaction([storeName], "readonly");
|
|
131
133
|
const store = tx.objectStore(storeName);
|
|
132
134
|
const req = store.getAll();
|
|
@@ -143,45 +145,36 @@ export async function getAllModels(storeName) {
|
|
|
143
145
|
// keep old behavior: return JSON string
|
|
144
146
|
resolve(JSON.stringify(results));
|
|
145
147
|
};
|
|
146
|
-
});
|
|
148
|
+
}));
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
// Eliminar modelo por id
|
|
150
152
|
export async function deleteModel(modelId, storeName) {
|
|
151
|
-
return new Promise((resolve, reject) => {
|
|
152
|
-
let db;
|
|
153
|
-
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
154
|
-
|
|
153
|
+
return _withDbRetry((db) => new Promise((resolve, reject) => {
|
|
155
154
|
const tx = db.transaction([storeName], "readwrite");
|
|
156
155
|
const store = tx.objectStore(storeName);
|
|
157
156
|
const req = store.delete(modelId);
|
|
158
157
|
|
|
159
158
|
req.onerror = () => reject(req.error);
|
|
160
159
|
req.onsuccess = () => resolve();
|
|
161
|
-
});
|
|
160
|
+
}));
|
|
162
161
|
}
|
|
163
162
|
|
|
164
163
|
// Limpiar toda la store
|
|
165
164
|
export async function clearAll(storeName) {
|
|
166
|
-
return new Promise((resolve, reject) => {
|
|
167
|
-
let db;
|
|
168
|
-
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
169
|
-
|
|
165
|
+
return _withDbRetry((db) => new Promise((resolve, reject) => {
|
|
170
166
|
const tx = db.transaction([storeName], "readwrite");
|
|
171
167
|
const store = tx.objectStore(storeName);
|
|
172
168
|
const req = store.clear();
|
|
173
169
|
|
|
174
170
|
req.onerror = () => reject(req.error);
|
|
175
171
|
req.onsuccess = () => resolve();
|
|
176
|
-
});
|
|
172
|
+
}));
|
|
177
173
|
}
|
|
178
174
|
|
|
179
175
|
// Borrar modelos expirados usando el índice "expirationTimeStamp"
|
|
180
176
|
export async function cleanExpiredModels(storeName) {
|
|
181
|
-
return new Promise((resolve, reject) => {
|
|
182
|
-
let db;
|
|
183
|
-
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
184
|
-
|
|
177
|
+
return _withDbRetry((db) => new Promise((resolve, reject) => {
|
|
185
178
|
const tx = db.transaction([storeName], "readwrite");
|
|
186
179
|
const store = tx.objectStore(storeName);
|
|
187
180
|
|
|
@@ -201,7 +194,7 @@ export async function cleanExpiredModels(storeName) {
|
|
|
201
194
|
|
|
202
195
|
tx.oncomplete = () => resolve();
|
|
203
196
|
tx.onerror = () => reject(tx.error);
|
|
204
|
-
});
|
|
197
|
+
}));
|
|
205
198
|
}
|
|
206
199
|
|
|
207
200
|
// Utilidades opcionales y visibles (por si las quieres usar en consola)
|