@preference-sl/pref-viewer 2.13.0-beta.9 → 2.13.1-beta.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 +1 -1
- package/package.json +1 -1
- package/src/babylonjs-animation-controller.js +28 -7
- package/src/babylonjs-controller.js +305 -98
- package/src/file-storage.js +6 -3
- package/src/gltf-resolver.js +2 -1
- package/src/gltf-storage.js +47 -35
- package/src/localization/i18n.js +1 -1
- package/src/pref-viewer-3d.js +31 -7
- package/src/pref-viewer.js +113 -13
package/Readme.md
CHANGED
package/package.json
CHANGED
|
@@ -45,7 +45,7 @@ export default class BabylonJSAnimationController {
|
|
|
45
45
|
#highlightColor = Color3.FromHexString(PrefViewerStyleVariables.colorPrimary);
|
|
46
46
|
#highlightLayer = null;
|
|
47
47
|
#overlayLayer = null;
|
|
48
|
-
#useHighlightLayer =
|
|
48
|
+
#useHighlightLayer = true; // Set to true to use HighlightLayer (better performance) and false to use overlay meshes (UtilityLayerRenderer - always on top)
|
|
49
49
|
#lastHighlightedMeshId = null; // Cache to avoid redundant highlight updates
|
|
50
50
|
#lastHighlightedNodeIds = []; // Cache to avoid redundant highlight updates
|
|
51
51
|
|
|
@@ -317,6 +317,9 @@ export default class BabylonJSAnimationController {
|
|
|
317
317
|
baseToOverlayBase.forEach((overlayBase) => {
|
|
318
318
|
overlayBase.dispose();
|
|
319
319
|
});
|
|
320
|
+
|
|
321
|
+
// Dispose utility layer scene/resources; without this each rebuild leaks GPU/JS resources.
|
|
322
|
+
utilityLayer.dispose();
|
|
320
323
|
},
|
|
321
324
|
};
|
|
322
325
|
}
|
|
@@ -513,20 +516,26 @@ export default class BabylonJSAnimationController {
|
|
|
513
516
|
|
|
514
517
|
/**
|
|
515
518
|
* Displays the animation control menu for the animated node under the pointer.
|
|
519
|
+
* Hides the current menu when picking info is invalid or when no animated node is found.
|
|
520
|
+
* Prefers a currently started animation; otherwise falls back to the first available one.
|
|
516
521
|
* @public
|
|
517
|
-
* @param {PickingInfo} pickingInfo - Raycast info from pointer position.
|
|
522
|
+
* @param {PickingInfo|null|undefined} pickingInfo - Raycast info from pointer position.
|
|
523
|
+
* Can be null/undefined when caller intentionally skips picking.
|
|
524
|
+
* @returns {void}
|
|
518
525
|
*/
|
|
519
526
|
showMenu(pickingInfo) {
|
|
520
|
-
|
|
527
|
+
const pickedMesh = pickingInfo?.pickedMesh;
|
|
528
|
+
if (!pickingInfo?.hit || !pickedMesh) {
|
|
529
|
+
this.hideMenu();
|
|
521
530
|
return;
|
|
522
531
|
}
|
|
523
532
|
|
|
524
|
-
this
|
|
525
|
-
|
|
526
|
-
const nodeIds = this.#getNodesAnimatedByMesh(pickingInfo.pickedMesh);
|
|
533
|
+
const nodeIds = this.#getNodesAnimatedByMesh(pickedMesh);
|
|
527
534
|
if (!nodeIds.length) {
|
|
535
|
+
this.hideMenu();
|
|
528
536
|
return;
|
|
529
537
|
}
|
|
538
|
+
|
|
530
539
|
const openingAnimations = [];
|
|
531
540
|
nodeIds.forEach((nodeId) => {
|
|
532
541
|
const openingAnimation = this.#getOpeningAnimationByNode(nodeId);
|
|
@@ -536,7 +545,19 @@ export default class BabylonJSAnimationController {
|
|
|
536
545
|
openingAnimations.push(openingAnimation);
|
|
537
546
|
});
|
|
538
547
|
|
|
548
|
+
if (!openingAnimations.length) {
|
|
549
|
+
this.hideMenu();
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
539
553
|
const startedAnimation = openingAnimations.find((animation) => animation.state !== OpeningAnimation.states.closed);
|
|
540
|
-
startedAnimation ? startedAnimation
|
|
554
|
+
const animationToShow = startedAnimation ? startedAnimation : openingAnimations[0];
|
|
555
|
+
|
|
556
|
+
if (animationToShow.isControlsVisible()) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
this.hideMenu();
|
|
561
|
+
animationToShow.showControls(this.#canvas, openingAnimations);
|
|
541
562
|
}
|
|
542
563
|
}
|
|
@@ -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
|
|
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)
|
|
@@ -63,6 +65,14 @@ import { translate } from "./localization/i18n.js";
|
|
|
63
65
|
* while `options.ibl.valid` remains true.
|
|
64
66
|
* - Browser-only features guard `window`, localStorage, and XR APIs before use so the controller is safe to construct
|
|
65
67
|
* in SSR/Node contexts (though functionality activates only in browsers).
|
|
68
|
+
* - Pointer-pick lifecycle: hover/highlight raycasts are sampled on POINTERMOVE (time + distance thresholds), wheel
|
|
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.
|
|
66
76
|
*/
|
|
67
77
|
export default class BabylonJSController {
|
|
68
78
|
#RENDER_SETTINGS_STORAGE_KEY = "pref-viewer/render-settings";
|
|
@@ -94,9 +104,8 @@ export default class BabylonJSController {
|
|
|
94
104
|
#shadowGen = [];
|
|
95
105
|
#XRExperience = null;
|
|
96
106
|
#canvasResizeObserver = null;
|
|
97
|
-
|
|
107
|
+
|
|
98
108
|
#hdrTexture = null; // reusable in-memory HDR source cloned into scene.environmentTexture across reloads
|
|
99
|
-
#lastPickedMeshId = null;
|
|
100
109
|
|
|
101
110
|
#containers = {};
|
|
102
111
|
#options = {};
|
|
@@ -111,28 +120,61 @@ export default class BabylonJSController {
|
|
|
111
120
|
};
|
|
112
121
|
|
|
113
122
|
#handlers = {
|
|
123
|
+
onAnimationGroupChanged: null,
|
|
114
124
|
onKeyUp: null,
|
|
115
125
|
onPointerObservable: null,
|
|
116
|
-
onAnimationGroupChanged: null,
|
|
117
126
|
onResize: null,
|
|
118
127
|
renderLoop: null,
|
|
119
128
|
};
|
|
120
129
|
|
|
121
130
|
#settings = { ...BabylonJSController.DEFAULT_RENDER_SETTINGS };
|
|
122
131
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
+
},
|
|
128
154
|
};
|
|
129
155
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
+
},
|
|
172
|
+
};
|
|
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,
|
|
136
178
|
};
|
|
137
179
|
|
|
138
180
|
/**
|
|
@@ -299,11 +341,11 @@ export default class BabylonJSController {
|
|
|
299
341
|
* @returns {boolean} True when the loop was started, false when no engine is available or it was already running.
|
|
300
342
|
*/
|
|
301
343
|
#startEngineRenderLoop() {
|
|
302
|
-
if (!this.#engine || this.#
|
|
344
|
+
if (!this.#engine || this.#state.render.isLoopRunning) {
|
|
303
345
|
return false;
|
|
304
346
|
}
|
|
305
347
|
this.#engine.runRenderLoop(this.#handlers.renderLoop);
|
|
306
|
-
this.#
|
|
348
|
+
this.#state.render.isLoopRunning = true;
|
|
307
349
|
return true;
|
|
308
350
|
}
|
|
309
351
|
|
|
@@ -313,13 +355,25 @@ export default class BabylonJSController {
|
|
|
313
355
|
* @returns {boolean} True when the loop was stopped, false when no engine is available or it was already stopped.
|
|
314
356
|
*/
|
|
315
357
|
#stopEngineRenderLoop() {
|
|
316
|
-
if (!this.#engine || !this.#
|
|
358
|
+
if (!this.#engine || !this.#state.render.isLoopRunning) {
|
|
317
359
|
return false;
|
|
318
360
|
}
|
|
319
361
|
this.#engine.stopRenderLoop(this.#handlers.renderLoop);
|
|
320
|
-
this.#
|
|
362
|
+
this.#state.render.isLoopRunning = false;
|
|
321
363
|
return true;
|
|
322
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
|
+
}
|
|
323
377
|
|
|
324
378
|
/**
|
|
325
379
|
* Marks the scene as dirty and optionally extends a short continuous-render window.
|
|
@@ -336,9 +390,9 @@ export default class BabylonJSController {
|
|
|
336
390
|
}
|
|
337
391
|
|
|
338
392
|
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
339
|
-
this.#
|
|
393
|
+
this.#state.render.dirtyFrames = Math.max(this.#state.render.dirtyFrames, Math.max(1, frames));
|
|
340
394
|
if (continuousMs > 0) {
|
|
341
|
-
this.#
|
|
395
|
+
this.#state.render.continuousUntil = Math.max(this.#state.render.continuousUntil, now + continuousMs);
|
|
342
396
|
}
|
|
343
397
|
this.#startEngineRenderLoop();
|
|
344
398
|
return true;
|
|
@@ -423,13 +477,13 @@ export default class BabylonJSController {
|
|
|
423
477
|
const cameraInMotion = this.#isCameraInMotion();
|
|
424
478
|
|
|
425
479
|
if (animationRunning) {
|
|
426
|
-
this.#
|
|
480
|
+
this.#state.render.continuousUntil = Math.max(this.#state.render.continuousUntil, now + this.#config.render.animationMs);
|
|
427
481
|
}
|
|
428
482
|
if (cameraInMotion) {
|
|
429
|
-
this.#
|
|
483
|
+
this.#state.render.continuousUntil = Math.max(this.#state.render.continuousUntil, now + this.#config.render.interactionMs);
|
|
430
484
|
}
|
|
431
485
|
|
|
432
|
-
return animationRunning || cameraInMotion || this.#
|
|
486
|
+
return animationRunning || cameraInMotion || this.#state.render.continuousUntil > now;
|
|
433
487
|
}
|
|
434
488
|
|
|
435
489
|
/**
|
|
@@ -447,22 +501,22 @@ export default class BabylonJSController {
|
|
|
447
501
|
|
|
448
502
|
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
449
503
|
const continuous = this.#shouldRenderContinuously(now);
|
|
450
|
-
const needsRender = continuous || this.#
|
|
504
|
+
const needsRender = continuous || this.#state.render.dirtyFrames > 0;
|
|
451
505
|
|
|
452
506
|
if (!needsRender) {
|
|
453
507
|
this.#stopEngineRenderLoop();
|
|
454
508
|
return;
|
|
455
509
|
}
|
|
456
510
|
|
|
457
|
-
if (!continuous && this.#
|
|
511
|
+
if (!continuous && this.#state.render.lastRenderAt > 0 && now - this.#state.render.lastRenderAt < this.#config.render.idleThrottleMs) {
|
|
458
512
|
return;
|
|
459
513
|
}
|
|
460
|
-
|
|
514
|
+
|
|
461
515
|
this.#scene.render();
|
|
462
|
-
this.#
|
|
516
|
+
this.#state.render.lastRenderAt = now;
|
|
463
517
|
|
|
464
|
-
if (this.#
|
|
465
|
-
this.#
|
|
518
|
+
if (this.#state.render.dirtyFrames > 0) {
|
|
519
|
+
this.#state.render.dirtyFrames -= 1;
|
|
466
520
|
}
|
|
467
521
|
}
|
|
468
522
|
|
|
@@ -1170,6 +1224,7 @@ export default class BabylonJSController {
|
|
|
1170
1224
|
* @returns {void}
|
|
1171
1225
|
*/
|
|
1172
1226
|
#setMaxSimultaneousLights() {
|
|
1227
|
+
if (!this.#scene) return;
|
|
1173
1228
|
let lightsNumber = 1; // At least one light coming from the environment texture contribution
|
|
1174
1229
|
this.#scene.lights.forEach((light) => {
|
|
1175
1230
|
if (light.isEnabled()) {
|
|
@@ -1181,6 +1236,55 @@ export default class BabylonJSController {
|
|
|
1181
1236
|
}
|
|
1182
1237
|
}
|
|
1183
1238
|
|
|
1239
|
+
/**
|
|
1240
|
+
* Resets pointer-picking sampling state.
|
|
1241
|
+
* @private
|
|
1242
|
+
* @returns {void}
|
|
1243
|
+
*/
|
|
1244
|
+
#resetPointerPickingState() {
|
|
1245
|
+
this.#state.pointerPicking.lastMovePickAt = 0;
|
|
1246
|
+
this.#state.pointerPicking.lastMovePickX = NaN;
|
|
1247
|
+
this.#state.pointerPicking.lastMovePickY = NaN;
|
|
1248
|
+
this.#state.pointerPicking.lastPickedMeshId = null;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* Decides whether a POINTERMOVE event should trigger a scene raycast.
|
|
1253
|
+
* Uses time + distance sampling to avoid expensive pick calls on every mouse move.
|
|
1254
|
+
* @private
|
|
1255
|
+
* @returns {boolean}
|
|
1256
|
+
*/
|
|
1257
|
+
#shouldPickOnPointerMove() {
|
|
1258
|
+
if (!this.#scene || !this.#babylonJSAnimationController) {
|
|
1259
|
+
return false;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1263
|
+
const x = this.#scene.pointerX;
|
|
1264
|
+
const y = this.#scene.pointerY;
|
|
1265
|
+
const state = this.#state.pointerPicking;
|
|
1266
|
+
const config = this.#config.pointerPicking;
|
|
1267
|
+
|
|
1268
|
+
if (!Number.isFinite(state.lastMovePickX) || !Number.isFinite(state.lastMovePickY)) {
|
|
1269
|
+
state.lastMovePickX = x;
|
|
1270
|
+
state.lastMovePickY = y;
|
|
1271
|
+
state.lastMovePickAt = now;
|
|
1272
|
+
return true;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
const elapsed = now - state.lastMovePickAt >= config.movePickIntervalMs;
|
|
1276
|
+
const moved = Math.abs(x - state.lastMovePickX) >= config.movePickMinDistancePx || Math.abs(y - state.lastMovePickY) >= config.movePickMinDistancePx;
|
|
1277
|
+
|
|
1278
|
+
if (!elapsed || !moved) {
|
|
1279
|
+
return false;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
state.lastMovePickX = x;
|
|
1283
|
+
state.lastMovePickY = y;
|
|
1284
|
+
state.lastMovePickAt = now;
|
|
1285
|
+
return true;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1184
1288
|
/**
|
|
1185
1289
|
* Sets up interaction handlers for the Babylon.js canvas and scene.
|
|
1186
1290
|
* @private
|
|
@@ -1194,10 +1298,7 @@ export default class BabylonJSController {
|
|
|
1194
1298
|
this.#scene.onPointerObservable.add(this.#handlers.onPointerObservable);
|
|
1195
1299
|
}
|
|
1196
1300
|
if (this.#engine) {
|
|
1197
|
-
this.#canvasResizeObserver = new ResizeObserver(
|
|
1198
|
-
this.#engine.resize();
|
|
1199
|
-
this.#requestRender({ frames: this.#renderConfig.burstFramesBase, continuousMs: this.#renderConfig.interactionMs });
|
|
1200
|
-
});
|
|
1301
|
+
this.#canvasResizeObserver = new ResizeObserver(this.#handlers.onResize);
|
|
1201
1302
|
this.#canvasResizeObserver.observe(this.#canvas);
|
|
1202
1303
|
}
|
|
1203
1304
|
}
|
|
@@ -1214,9 +1315,11 @@ export default class BabylonJSController {
|
|
|
1214
1315
|
if (this.#scene !== null) {
|
|
1215
1316
|
this.#scene.onPointerObservable.removeCallback(this.#handlers.onPointerObservable);
|
|
1216
1317
|
}
|
|
1318
|
+
this.#cancelScheduledResize();
|
|
1217
1319
|
this.#canvasResizeObserver?.disconnect();
|
|
1218
1320
|
this.#canvasResizeObserver = null;
|
|
1219
1321
|
this.#detachAnimationChangedListener();
|
|
1322
|
+
this.#resetPointerPickingState();
|
|
1220
1323
|
}
|
|
1221
1324
|
|
|
1222
1325
|
/**
|
|
@@ -1245,30 +1348,43 @@ export default class BabylonJSController {
|
|
|
1245
1348
|
|
|
1246
1349
|
/**
|
|
1247
1350
|
* Disposes the Babylon.js WebXR experience if it exists.
|
|
1351
|
+
* If XR is currently active, waits for `exitXRAsync()` before disposing to avoid
|
|
1352
|
+
* tearing down the engine while the XR session is still shutting down.
|
|
1353
|
+
* Concurrent calls share the same in-flight promise so disposal runs only once.
|
|
1248
1354
|
* @private
|
|
1249
|
-
* @returns {void}
|
|
1355
|
+
* @returns {Promise<void>}
|
|
1250
1356
|
*/
|
|
1251
|
-
#disposeXRExperience() {
|
|
1252
|
-
if (
|
|
1357
|
+
async #disposeXRExperience() {
|
|
1358
|
+
if (this.#disablingPromises.xr) {
|
|
1359
|
+
return await this.#disablingPromises.xr;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
const xrExperience = this.#XRExperience;
|
|
1363
|
+
if (!xrExperience) {
|
|
1253
1364
|
return;
|
|
1254
1365
|
}
|
|
1255
1366
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
.
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1367
|
+
this.#disablingPromises.xr = (async () => {
|
|
1368
|
+
try {
|
|
1369
|
+
if (xrExperience.baseExperience?.state === WebXRState.IN_XR) {
|
|
1370
|
+
await xrExperience.baseExperience.exitXRAsync();
|
|
1371
|
+
}
|
|
1372
|
+
} catch (error) {
|
|
1373
|
+
console.warn("PrefViewer: Error exiting XR experience:", error);
|
|
1374
|
+
} finally {
|
|
1375
|
+
try {
|
|
1376
|
+
xrExperience.dispose();
|
|
1377
|
+
} catch (error) {
|
|
1378
|
+
console.warn("PrefViewer: Error disposing XR experience:", error);
|
|
1379
|
+
}
|
|
1380
|
+
if (this.#XRExperience === xrExperience) {
|
|
1266
1381
|
this.#XRExperience = null;
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1382
|
+
}
|
|
1383
|
+
this.#disablingPromises.xr = null;
|
|
1384
|
+
}
|
|
1385
|
+
})();
|
|
1386
|
+
|
|
1387
|
+
await this.#disablingPromises.xr;
|
|
1272
1388
|
}
|
|
1273
1389
|
|
|
1274
1390
|
/**
|
|
@@ -1316,7 +1432,7 @@ export default class BabylonJSController {
|
|
|
1316
1432
|
* @returns {void}
|
|
1317
1433
|
*/
|
|
1318
1434
|
#onAnimationGroupPlay() {
|
|
1319
|
-
this.#requestRender({ frames: 1, continuousMs: this.#
|
|
1435
|
+
this.#requestRender({ frames: 1, continuousMs: this.#config.render.animationMs });
|
|
1320
1436
|
}
|
|
1321
1437
|
|
|
1322
1438
|
/**
|
|
@@ -1325,7 +1441,11 @@ export default class BabylonJSController {
|
|
|
1325
1441
|
* @returns {void}
|
|
1326
1442
|
*/
|
|
1327
1443
|
#onAnimationGroupStop() {
|
|
1328
|
-
|
|
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;
|
|
1329
1449
|
this.#requestRender({ frames: frames });
|
|
1330
1450
|
}
|
|
1331
1451
|
|
|
@@ -1365,10 +1485,10 @@ export default class BabylonJSController {
|
|
|
1365
1485
|
* @returns {void}
|
|
1366
1486
|
*/
|
|
1367
1487
|
#onKeyUp(event) {
|
|
1368
|
-
// CTRL + ALT + letter
|
|
1369
|
-
if (event.ctrlKey && event.altKey && event.
|
|
1370
|
-
switch (event.
|
|
1371
|
-
case "
|
|
1488
|
+
// CTRL + ALT + letter (uses event.code for physical key, layout-independent — fixes Mac Option+D producing "∂" instead of "d")
|
|
1489
|
+
if (event.ctrlKey && event.altKey && event.code !== undefined) {
|
|
1490
|
+
switch (event.code) {
|
|
1491
|
+
case "KeyD":
|
|
1372
1492
|
this.#openDownloadDialog();
|
|
1373
1493
|
break;
|
|
1374
1494
|
default:
|
|
@@ -1403,7 +1523,7 @@ export default class BabylonJSController {
|
|
|
1403
1523
|
const movementVector = direction.scale(zoomSpeed);
|
|
1404
1524
|
camera.position = camera.position.add(movementVector);
|
|
1405
1525
|
}
|
|
1406
|
-
this.#requestRender({ frames: 1, continuousMs: this.#
|
|
1526
|
+
this.#requestRender({ frames: 1, continuousMs: this.#config.render.interactionMs });
|
|
1407
1527
|
}
|
|
1408
1528
|
}
|
|
1409
1529
|
|
|
@@ -1416,7 +1536,9 @@ export default class BabylonJSController {
|
|
|
1416
1536
|
*/
|
|
1417
1537
|
#onPointerUp(event, pickInfo) {
|
|
1418
1538
|
if (this.#babylonJSAnimationController) {
|
|
1419
|
-
|
|
1539
|
+
if (event.button !== 2 || !pickInfo || !pickInfo.pickedMesh) {
|
|
1540
|
+
this.#babylonJSAnimationController.hideMenu();
|
|
1541
|
+
}
|
|
1420
1542
|
// Right click for showing animation menu
|
|
1421
1543
|
if (event.button === 2) {
|
|
1422
1544
|
this.#babylonJSAnimationController.showMenu(pickInfo);
|
|
@@ -1428,20 +1550,20 @@ export default class BabylonJSController {
|
|
|
1428
1550
|
* Handles pointer move events on the Babylon.js scene.
|
|
1429
1551
|
* @private
|
|
1430
1552
|
* @param {PointerEvent} event - The pointer move event.
|
|
1431
|
-
* @param {PickInfo} pickInfo - The result of the scene pick operation.
|
|
1553
|
+
* @param {PickInfo|null} pickInfo - The sampled result of the scene pick operation (may be null when sampling skips a raycast).
|
|
1432
1554
|
* @returns {void}
|
|
1433
1555
|
*/
|
|
1434
1556
|
#onPointerMove(event, pickInfo) {
|
|
1435
1557
|
const camera = this.#scene?.activeCamera;
|
|
1436
1558
|
if (camera && !camera.metadata?.locked) {
|
|
1437
|
-
this.#requestRender({ frames: 1, continuousMs: this.#
|
|
1559
|
+
this.#requestRender({ frames: 1, continuousMs: this.#config.render.interactionMs });
|
|
1438
1560
|
}
|
|
1439
|
-
if (this.#babylonJSAnimationController) {
|
|
1561
|
+
if (this.#babylonJSAnimationController && pickInfo) {
|
|
1440
1562
|
const pickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
1441
|
-
if (this.#lastPickedMeshId !== pickedMeshId) {
|
|
1563
|
+
if (this.#state.pointerPicking.lastPickedMeshId !== pickedMeshId) {
|
|
1442
1564
|
const highlightResult = this.#babylonJSAnimationController.highlightMeshes(pickInfo);
|
|
1443
1565
|
if (highlightResult.changed) {
|
|
1444
|
-
this.#requestRender({ frames: 1, continuousMs: this.#
|
|
1566
|
+
this.#requestRender({ frames: 1, continuousMs: this.#config.render.interactionMs });
|
|
1445
1567
|
}
|
|
1446
1568
|
}
|
|
1447
1569
|
}
|
|
@@ -1449,22 +1571,30 @@ export default class BabylonJSController {
|
|
|
1449
1571
|
|
|
1450
1572
|
/**
|
|
1451
1573
|
* Handles pointer events observed on the Babylon.js scene.
|
|
1574
|
+
* Uses pointer-move sampling to avoid running expensive `scene.pick()` on every event.
|
|
1452
1575
|
* @private
|
|
1453
1576
|
* @param {PointerInfo} info - The pointer event information from Babylon.js.
|
|
1454
1577
|
* @returns {void}
|
|
1455
1578
|
*/
|
|
1456
1579
|
#onPointerObservable(info) {
|
|
1457
|
-
const pickInfo = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
|
|
1458
|
-
const pickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
1459
|
-
|
|
1460
1580
|
if (info.type === PointerEventTypes.POINTERMOVE) {
|
|
1581
|
+
let pickInfo = null;
|
|
1582
|
+
let lastPickedMeshId = null;
|
|
1583
|
+
if (this.#shouldPickOnPointerMove()) {
|
|
1584
|
+
pickInfo = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
|
|
1585
|
+
lastPickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
1586
|
+
}
|
|
1461
1587
|
this.#onPointerMove(info.event, pickInfo);
|
|
1588
|
+
this.#state.pointerPicking.lastPickedMeshId = lastPickedMeshId;
|
|
1462
1589
|
} else if (info.type === PointerEventTypes.POINTERUP) {
|
|
1590
|
+
const pickInfo = info.event?.button === 2 ? this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY) : null;
|
|
1591
|
+
if (pickInfo) {
|
|
1592
|
+
this.#state.pointerPicking.lastPickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
1593
|
+
}
|
|
1463
1594
|
this.#onPointerUp(info.event, pickInfo);
|
|
1464
1595
|
} else if (info.type === PointerEventTypes.POINTERWHEEL) {
|
|
1465
|
-
this.#onMouseWheel(info.event,
|
|
1596
|
+
this.#onMouseWheel(info.event, null);
|
|
1466
1597
|
}
|
|
1467
|
-
this.#lastPickedMeshId = pickedMeshId;
|
|
1468
1598
|
}
|
|
1469
1599
|
|
|
1470
1600
|
/**
|
|
@@ -1478,8 +1608,47 @@ export default class BabylonJSController {
|
|
|
1478
1608
|
if (!this.#engine) {
|
|
1479
1609
|
return;
|
|
1480
1610
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
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;
|
|
1483
1652
|
}
|
|
1484
1653
|
|
|
1485
1654
|
/**
|
|
@@ -1749,6 +1918,12 @@ export default class BabylonJSController {
|
|
|
1749
1918
|
container.assetContainer.dispose();
|
|
1750
1919
|
container.assetContainer = null;
|
|
1751
1920
|
}
|
|
1921
|
+
if (!this.#scene) {
|
|
1922
|
+
// Scene was disposed (disconnect) while async loading was in progress; discard the
|
|
1923
|
+
// newly loaded container so we don't leak GPU resources and avoid the null-deref below.
|
|
1924
|
+
newAssetContainer?.dispose();
|
|
1925
|
+
return false;
|
|
1926
|
+
}
|
|
1752
1927
|
this.#scene.getEngine().releaseEffects();
|
|
1753
1928
|
this.#scene.getEngine().releaseComputeEffects();
|
|
1754
1929
|
|
|
@@ -1825,11 +2000,10 @@ export default class BabylonJSController {
|
|
|
1825
2000
|
*/
|
|
1826
2001
|
async #stopRender() {
|
|
1827
2002
|
this.#stopEngineRenderLoop();
|
|
1828
|
-
this.#
|
|
1829
|
-
this.#renderState.continuousUntil = 0;
|
|
1830
|
-
this.#renderState.lastRenderAt = 0;
|
|
2003
|
+
this.#resetRenderState();
|
|
1831
2004
|
await this.#unloadCameraDependentEffects();
|
|
1832
2005
|
}
|
|
2006
|
+
|
|
1833
2007
|
/**
|
|
1834
2008
|
* Starts the Babylon.js render loop for the current scene.
|
|
1835
2009
|
* Waits until the scene is ready before beginning continuous rendering.
|
|
@@ -1837,10 +2011,11 @@ export default class BabylonJSController {
|
|
|
1837
2011
|
* @returns {Promise<void>}
|
|
1838
2012
|
*/
|
|
1839
2013
|
async #startRender() {
|
|
2014
|
+
if (!this.#scene) return;
|
|
1840
2015
|
await this.#loadCameraDependentEffects();
|
|
1841
2016
|
await this.#scene.whenReadyAsync();
|
|
1842
|
-
const frames = this.#settings.antiAliasingEnabled || this.#settings.ambientOcclusionEnabled || this.#settings.iblEnabled ? this.#
|
|
1843
|
-
this.#requestRender({ frames: frames, continuousMs: this.#
|
|
2017
|
+
const frames = this.#settings.antiAliasingEnabled || this.#settings.ambientOcclusionEnabled || this.#settings.iblEnabled ? this.#config.render.burstFramesEnhanced : this.#config.render.burstFramesBase;
|
|
2018
|
+
this.#requestRender({ frames: frames, continuousMs: this.#config.render.interactionMs });
|
|
1844
2019
|
}
|
|
1845
2020
|
|
|
1846
2021
|
/**
|
|
@@ -1919,7 +2094,7 @@ export default class BabylonJSController {
|
|
|
1919
2094
|
* Applies material and camera options, sets wall/floor visibility, and initializes lights and shadows.
|
|
1920
2095
|
* Returns an object with success status and error details.
|
|
1921
2096
|
*/
|
|
1922
|
-
async #loadContainers() {
|
|
2097
|
+
async #loadContainers(force = false) {
|
|
1923
2098
|
this.#detachAnimationChangedListener();
|
|
1924
2099
|
await this.#stopRender();
|
|
1925
2100
|
|
|
@@ -1928,7 +2103,7 @@ export default class BabylonJSController {
|
|
|
1928
2103
|
|
|
1929
2104
|
const promiseArray = [];
|
|
1930
2105
|
Object.values(this.#containers).forEach((container) => {
|
|
1931
|
-
promiseArray.push(this.#loadAssetContainer(container));
|
|
2106
|
+
promiseArray.push(this.#loadAssetContainer(container, force));
|
|
1932
2107
|
});
|
|
1933
2108
|
|
|
1934
2109
|
let detail = {
|
|
@@ -1938,6 +2113,13 @@ export default class BabylonJSController {
|
|
|
1938
2113
|
|
|
1939
2114
|
await Promise.allSettled(promiseArray)
|
|
1940
2115
|
.then(async (values) => {
|
|
2116
|
+
// Scene may have been disposed (disconnectedCallback) while async loading was in
|
|
2117
|
+
// progress. Abort cleanly: #replaceContainer already guards the GPU calls, but
|
|
2118
|
+
// we skip the post-load option/visibility calls too to avoid further null-derefs.
|
|
2119
|
+
if (!this.#scene) {
|
|
2120
|
+
values.forEach((result) => { result.value?.[1]?.dispose(); });
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
1941
2123
|
this.#disposeAnimationController();
|
|
1942
2124
|
values.forEach((result) => {
|
|
1943
2125
|
const container = result.value ? result.value[0] : null;
|
|
@@ -2272,17 +2454,40 @@ export default class BabylonJSController {
|
|
|
2272
2454
|
/**
|
|
2273
2455
|
* Disposes the Babylon.js engine and disconnects the canvas resize observer.
|
|
2274
2456
|
* Cleans up all scene, camera, light, XR, and GLTF resolver resources.
|
|
2457
|
+
* The teardown is asynchronous: it waits for XR/session-dependent shutdown work
|
|
2458
|
+
* before disposing the engine, and coalesces concurrent calls into one in-flight promise.
|
|
2275
2459
|
* @public
|
|
2276
|
-
* @returns {void}
|
|
2460
|
+
* @returns {Promise<void>}
|
|
2277
2461
|
*/
|
|
2278
|
-
disable() {
|
|
2279
|
-
this.#
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
this.#
|
|
2284
|
-
|
|
2285
|
-
|
|
2462
|
+
async disable() {
|
|
2463
|
+
if (this.#disablingPromises.general) {
|
|
2464
|
+
return await this.#disablingPromises.general;
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
this.#disablingPromises.general = (async () => {
|
|
2468
|
+
this.#disableInteraction();
|
|
2469
|
+
this.#disposeAnimationController();
|
|
2470
|
+
this.#disposeGLTFResolver();
|
|
2471
|
+
try {
|
|
2472
|
+
await this.#disposeXRExperience();
|
|
2473
|
+
} catch (error) {
|
|
2474
|
+
console.warn("PrefViewer: Error while disposing XR experience:", error);
|
|
2475
|
+
}
|
|
2476
|
+
try {
|
|
2477
|
+
await this.#unloadCameraDependentEffects();
|
|
2478
|
+
} catch (error) {
|
|
2479
|
+
console.warn("PrefViewer: Error while unloading camera-dependent effects:", error);
|
|
2480
|
+
} finally {
|
|
2481
|
+
this.#stopEngineRenderLoop();
|
|
2482
|
+
this.#disposeEngine();
|
|
2483
|
+
}
|
|
2484
|
+
})();
|
|
2485
|
+
|
|
2486
|
+
try {
|
|
2487
|
+
await this.#disablingPromises.general;
|
|
2488
|
+
} finally {
|
|
2489
|
+
this.#disablingPromises.general = null;
|
|
2490
|
+
}
|
|
2286
2491
|
}
|
|
2287
2492
|
|
|
2288
2493
|
/**
|
|
@@ -2403,8 +2608,8 @@ export default class BabylonJSController {
|
|
|
2403
2608
|
* @public
|
|
2404
2609
|
* @returns {Promise<boolean>} Resolves to true if loading succeeds, false otherwise.
|
|
2405
2610
|
*/
|
|
2406
|
-
async load() {
|
|
2407
|
-
return await this.#loadContainers();
|
|
2611
|
+
async load(force = false) {
|
|
2612
|
+
return await this.#loadContainers(force);
|
|
2408
2613
|
}
|
|
2409
2614
|
|
|
2410
2615
|
/**
|
|
@@ -2439,11 +2644,13 @@ export default class BabylonJSController {
|
|
|
2439
2644
|
* @public
|
|
2440
2645
|
* @returns {boolean} True if IBL options were set successfully, false otherwise.
|
|
2441
2646
|
*/
|
|
2442
|
-
setIBLOptions() {
|
|
2443
|
-
this.#stopRender();
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2647
|
+
async setIBLOptions() {
|
|
2648
|
+
await this.#stopRender();
|
|
2649
|
+
try {
|
|
2650
|
+
return await this.#setOptions_IBL();
|
|
2651
|
+
} finally {
|
|
2652
|
+
await this.#startRender();
|
|
2653
|
+
}
|
|
2447
2654
|
}
|
|
2448
2655
|
|
|
2449
2656
|
/**
|
package/src/file-storage.js
CHANGED
|
@@ -755,9 +755,12 @@ export default class FileStorage {
|
|
|
755
755
|
}
|
|
756
756
|
await this.#runCleanupIfDue();
|
|
757
757
|
let storedFile = await this.#getFile(uri);
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
if (
|
|
758
|
+
// If there is already a cached file, return it without HEAD revalidation.
|
|
759
|
+
// In ecommerce flow, new IDs/URLs from WASM drive real resource updates.
|
|
760
|
+
if (storedFile) {
|
|
761
|
+
return storedFile;
|
|
762
|
+
}
|
|
763
|
+
if (!storedFile) {
|
|
761
764
|
const fileToStore = await this.#getServerFile(uri);
|
|
762
765
|
if (fileToStore && !!(await this.#putFile(fileToStore, uri))) {
|
|
763
766
|
storedFile = await this.#getFile(uri);
|
package/src/gltf-resolver.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import FileStorage from "./file-storage.js";
|
|
2
|
-
import { initDb, loadModel } from "./gltf-storage.js";
|
|
2
|
+
import { closeDb, initDb, loadModel } from "./gltf-storage.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* GLTFResolver - Utility class for resolving, decoding, and preparing glTF/GLB assets from various sources.
|
|
@@ -240,6 +240,7 @@ export default class GLTFResolver {
|
|
|
240
240
|
dispose() {
|
|
241
241
|
this.#fileStorage?.dispose?.();
|
|
242
242
|
this.#fileStorage = null;
|
|
243
|
+
closeDb();
|
|
243
244
|
}
|
|
244
245
|
|
|
245
246
|
/**
|
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,22 +101,19 @@ 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
|
|
@@ -104,10 +128,7 @@ export function downloadFileFromBytes(fileName, bytesBase64, mimeType) {
|
|
|
104
128
|
|
|
105
129
|
// Obtener todos los modelos (solo metadata)
|
|
106
130
|
export async function getAllModels(storeName) {
|
|
107
|
-
return new Promise((resolve, reject) => {
|
|
108
|
-
let db;
|
|
109
|
-
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
110
|
-
|
|
131
|
+
return _withDbRetry((db) => new Promise((resolve, reject) => {
|
|
111
132
|
const tx = db.transaction([storeName], "readonly");
|
|
112
133
|
const store = tx.objectStore(storeName);
|
|
113
134
|
const req = store.getAll();
|
|
@@ -124,45 +145,36 @@ export async function getAllModels(storeName) {
|
|
|
124
145
|
// keep old behavior: return JSON string
|
|
125
146
|
resolve(JSON.stringify(results));
|
|
126
147
|
};
|
|
127
|
-
});
|
|
148
|
+
}));
|
|
128
149
|
}
|
|
129
150
|
|
|
130
151
|
// Eliminar modelo por id
|
|
131
152
|
export async function deleteModel(modelId, storeName) {
|
|
132
|
-
return new Promise((resolve, reject) => {
|
|
133
|
-
let db;
|
|
134
|
-
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
135
|
-
|
|
153
|
+
return _withDbRetry((db) => new Promise((resolve, reject) => {
|
|
136
154
|
const tx = db.transaction([storeName], "readwrite");
|
|
137
155
|
const store = tx.objectStore(storeName);
|
|
138
156
|
const req = store.delete(modelId);
|
|
139
157
|
|
|
140
158
|
req.onerror = () => reject(req.error);
|
|
141
159
|
req.onsuccess = () => resolve();
|
|
142
|
-
});
|
|
160
|
+
}));
|
|
143
161
|
}
|
|
144
162
|
|
|
145
163
|
// Limpiar toda la store
|
|
146
164
|
export async function clearAll(storeName) {
|
|
147
|
-
return new Promise((resolve, reject) => {
|
|
148
|
-
let db;
|
|
149
|
-
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
150
|
-
|
|
165
|
+
return _withDbRetry((db) => new Promise((resolve, reject) => {
|
|
151
166
|
const tx = db.transaction([storeName], "readwrite");
|
|
152
167
|
const store = tx.objectStore(storeName);
|
|
153
168
|
const req = store.clear();
|
|
154
169
|
|
|
155
170
|
req.onerror = () => reject(req.error);
|
|
156
171
|
req.onsuccess = () => resolve();
|
|
157
|
-
});
|
|
172
|
+
}));
|
|
158
173
|
}
|
|
159
174
|
|
|
160
175
|
// Borrar modelos expirados usando el índice "expirationTimeStamp"
|
|
161
176
|
export async function cleanExpiredModels(storeName) {
|
|
162
|
-
return new Promise((resolve, reject) => {
|
|
163
|
-
let db;
|
|
164
|
-
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
165
|
-
|
|
177
|
+
return _withDbRetry((db) => new Promise((resolve, reject) => {
|
|
166
178
|
const tx = db.transaction([storeName], "readwrite");
|
|
167
179
|
const store = tx.objectStore(storeName);
|
|
168
180
|
|
|
@@ -182,7 +194,7 @@ export async function cleanExpiredModels(storeName) {
|
|
|
182
194
|
|
|
183
195
|
tx.oncomplete = () => resolve();
|
|
184
196
|
tx.onerror = () => reject(tx.error);
|
|
185
|
-
});
|
|
197
|
+
}));
|
|
186
198
|
}
|
|
187
199
|
|
|
188
200
|
// Utilidades opcionales y visibles (por si las quieres usar en consola)
|
package/src/localization/i18n.js
CHANGED
package/src/pref-viewer-3d.js
CHANGED
|
@@ -75,6 +75,7 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
75
75
|
#isInitialized = false;
|
|
76
76
|
#isLoaded = false;
|
|
77
77
|
#isVisible = false;
|
|
78
|
+
#lastLoadTimestamp = undefined;
|
|
78
79
|
|
|
79
80
|
#wrapper = null;
|
|
80
81
|
#canvas = null;
|
|
@@ -162,7 +163,7 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
162
163
|
*/
|
|
163
164
|
disconnectedCallback() {
|
|
164
165
|
if (this.#babylonJSController) {
|
|
165
|
-
this.#babylonJSController.disable();
|
|
166
|
+
void this.#babylonJSController.disable();
|
|
166
167
|
}
|
|
167
168
|
}
|
|
168
169
|
|
|
@@ -230,6 +231,27 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
230
231
|
this.#babylonJSController.enable();
|
|
231
232
|
}
|
|
232
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Determines whether the next 3D load should bypass cache checks.
|
|
236
|
+
* @private
|
|
237
|
+
* @param {object} config - Incoming config payload.
|
|
238
|
+
* @returns {boolean} True when force reload should be enabled.
|
|
239
|
+
*/
|
|
240
|
+
#shouldForceReload(config) {
|
|
241
|
+
if (!config || typeof config !== "object") {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
if (config.forceReload === true) {
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
if (config.timestamp === undefined || config.timestamp === null) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
const changed = config.timestamp !== this.#lastLoadTimestamp;
|
|
251
|
+
this.#lastLoadTimestamp = config.timestamp;
|
|
252
|
+
return changed;
|
|
253
|
+
}
|
|
254
|
+
|
|
233
255
|
/**
|
|
234
256
|
* Resets update flags for all containers and material/camera options after loading or setting options.
|
|
235
257
|
* @private
|
|
@@ -558,6 +580,7 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
558
580
|
}
|
|
559
581
|
|
|
560
582
|
this.#onLoading();
|
|
583
|
+
const forceReload = this.#shouldForceReload(config);
|
|
561
584
|
|
|
562
585
|
// Containers
|
|
563
586
|
this.#checkNeedToUpdateContainers(config);
|
|
@@ -566,10 +589,10 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
566
589
|
if (config.options) {
|
|
567
590
|
this.#checkNeedToUpdateCamera(config.options);
|
|
568
591
|
this.#checkNeedToUpdateMaterials(config.options);
|
|
569
|
-
this.#checkNeedToUpdateIBL(config.options);
|
|
592
|
+
await this.#checkNeedToUpdateIBL(config.options);
|
|
570
593
|
}
|
|
571
594
|
|
|
572
|
-
const loadDetail = await this.#babylonJSController.load();
|
|
595
|
+
const loadDetail = await this.#babylonJSController.load(forceReload);
|
|
573
596
|
|
|
574
597
|
return { ...loadDetail, load: this.#onLoaded() };
|
|
575
598
|
}
|
|
@@ -579,9 +602,9 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
579
602
|
* Updates internal states, triggers option setting events, and returns the result.
|
|
580
603
|
* @public
|
|
581
604
|
* @param {object} options - Options object containing camera and material settings.
|
|
582
|
-
* @returns {object} Object containing success status and details of set options.
|
|
605
|
+
* @returns {Promise<object>} Object containing success status and details of set options.
|
|
583
606
|
*/
|
|
584
|
-
setOptions(options) {
|
|
607
|
+
async setOptions(options) {
|
|
585
608
|
if (!this.#babylonJSController) {
|
|
586
609
|
return;
|
|
587
610
|
}
|
|
@@ -595,8 +618,9 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
595
618
|
if (this.#checkNeedToUpdateMaterials(options)) {
|
|
596
619
|
someSetted = someSetted || this.#babylonJSController.setMaterialOptions();
|
|
597
620
|
}
|
|
598
|
-
|
|
599
|
-
|
|
621
|
+
const needUpdateIBL = await this.#checkNeedToUpdateIBL(options);
|
|
622
|
+
if (needUpdateIBL) {
|
|
623
|
+
someSetted = someSetted || (await this.#babylonJSController.setIBLOptions());
|
|
600
624
|
}
|
|
601
625
|
const detail = this.#onSetOptions();
|
|
602
626
|
return { success: someSetted, detail: detail };
|
package/src/pref-viewer.js
CHANGED
|
@@ -81,6 +81,7 @@ export default class PrefViewer extends HTMLElement {
|
|
|
81
81
|
onViewerHoverStart: null,
|
|
82
82
|
onViewerHoverEnd: null,
|
|
83
83
|
on3DSceneLoaded: null,
|
|
84
|
+
on3DSceneError: null,
|
|
84
85
|
};
|
|
85
86
|
|
|
86
87
|
/**
|
|
@@ -136,6 +137,9 @@ export default class PrefViewer extends HTMLElement {
|
|
|
136
137
|
this.loadMaterials(value);
|
|
137
138
|
break;
|
|
138
139
|
case "mode":
|
|
140
|
+
if (typeof value !== "string") {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
139
143
|
if (_old === value || value.toLowerCase() === this.#mode) {
|
|
140
144
|
return;
|
|
141
145
|
}
|
|
@@ -151,6 +155,9 @@ export default class PrefViewer extends HTMLElement {
|
|
|
151
155
|
this.setOptions(value);
|
|
152
156
|
break;
|
|
153
157
|
case "show-model":
|
|
158
|
+
if (typeof value !== "string") {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
154
161
|
if (_old === value) {
|
|
155
162
|
return;
|
|
156
163
|
}
|
|
@@ -158,6 +165,9 @@ export default class PrefViewer extends HTMLElement {
|
|
|
158
165
|
showModel ? this.showModel() : this.hideModel();
|
|
159
166
|
break;
|
|
160
167
|
case "show-scene":
|
|
168
|
+
if (typeof value !== "string") {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
161
171
|
if (_old === value) {
|
|
162
172
|
return;
|
|
163
173
|
}
|
|
@@ -213,6 +223,7 @@ export default class PrefViewer extends HTMLElement {
|
|
|
213
223
|
}
|
|
214
224
|
if (this.#component3D) {
|
|
215
225
|
this.#component3D.removeEventListener("scene-loaded", this.#handlers.on3DSceneLoaded);
|
|
226
|
+
this.#component3D.removeEventListener("scene-error", this.#handlers.on3DSceneError);
|
|
216
227
|
this.#component3D.remove();
|
|
217
228
|
}
|
|
218
229
|
if (this.#menu3D) {
|
|
@@ -249,7 +260,11 @@ export default class PrefViewer extends HTMLElement {
|
|
|
249
260
|
this.#menu3DSyncSettings();
|
|
250
261
|
this.#menu3D?.setApplying(false);
|
|
251
262
|
};
|
|
263
|
+
this.#handlers.on3DSceneError = () => {
|
|
264
|
+
this.#menu3D?.setApplying(false, true);
|
|
265
|
+
};
|
|
252
266
|
this.#component3D.addEventListener("scene-loaded", this.#handlers.on3DSceneLoaded);
|
|
267
|
+
this.#component3D.addEventListener("scene-error", this.#handlers.on3DSceneError);
|
|
253
268
|
this.#wrapper.appendChild(this.#component3D);
|
|
254
269
|
}
|
|
255
270
|
|
|
@@ -454,7 +469,14 @@ export default class PrefViewer extends HTMLElement {
|
|
|
454
469
|
* @returns {void}
|
|
455
470
|
*/
|
|
456
471
|
#addTaskToQueue(value, type) {
|
|
457
|
-
|
|
472
|
+
const task = new PrefViewerTask(value, type);
|
|
473
|
+
if (
|
|
474
|
+
task.type === PrefViewerTask.Types.Options ||
|
|
475
|
+
task.type === PrefViewerTask.Types.Drawing
|
|
476
|
+
) {
|
|
477
|
+
this.#taskQueue = this.#taskQueue.filter((queuedTask) => queuedTask.type !== task.type);
|
|
478
|
+
}
|
|
479
|
+
this.#taskQueue.push(task);
|
|
458
480
|
if (this.#isInitialized && !this.#isLoading) {
|
|
459
481
|
this.#processNextTask();
|
|
460
482
|
}
|
|
@@ -552,6 +574,32 @@ export default class PrefViewer extends HTMLElement {
|
|
|
552
574
|
this.dispatchEvent(new CustomEvent("scene-loaded", customEventOptions));
|
|
553
575
|
}
|
|
554
576
|
|
|
577
|
+
/**
|
|
578
|
+
* Handles 3D load errors.
|
|
579
|
+
* Clears loading state and dispatches a "scene-error" event.
|
|
580
|
+
* @private
|
|
581
|
+
* @param {object} [detail] - Optional details about the failure.
|
|
582
|
+
* @returns {void}
|
|
583
|
+
*/
|
|
584
|
+
#on3DError(detail) {
|
|
585
|
+
this.#isLoaded = false;
|
|
586
|
+
this.#isLoading = false;
|
|
587
|
+
this.#menu3D?.setApplying(false, true);
|
|
588
|
+
|
|
589
|
+
this.removeAttribute("loading-3d");
|
|
590
|
+
this.removeAttribute("loaded-3d");
|
|
591
|
+
|
|
592
|
+
const customEventOptions = {
|
|
593
|
+
bubbles: true,
|
|
594
|
+
cancelable: true,
|
|
595
|
+
composed: true,
|
|
596
|
+
};
|
|
597
|
+
if (detail) {
|
|
598
|
+
customEventOptions.detail = detail;
|
|
599
|
+
}
|
|
600
|
+
this.dispatchEvent(new CustomEvent("scene-error", customEventOptions));
|
|
601
|
+
}
|
|
602
|
+
|
|
555
603
|
/**
|
|
556
604
|
* Handles the start of a 2D loading operation.
|
|
557
605
|
* Updates loading state, sets attributes, and dispatches a "drawing-loading" event.
|
|
@@ -598,6 +646,31 @@ export default class PrefViewer extends HTMLElement {
|
|
|
598
646
|
this.dispatchEvent(new CustomEvent("drawing-loaded", customEventOptions));
|
|
599
647
|
}
|
|
600
648
|
|
|
649
|
+
/**
|
|
650
|
+
* Handles 2D load errors.
|
|
651
|
+
* Clears loading state and dispatches a "drawing-error" event.
|
|
652
|
+
* @private
|
|
653
|
+
* @param {object} [detail] - Optional details about the failure.
|
|
654
|
+
* @returns {void}
|
|
655
|
+
*/
|
|
656
|
+
#on2DError(detail) {
|
|
657
|
+
this.#isLoaded = false;
|
|
658
|
+
this.#isLoading = false;
|
|
659
|
+
|
|
660
|
+
this.removeAttribute("loading-2d");
|
|
661
|
+
this.removeAttribute("loaded-2d");
|
|
662
|
+
|
|
663
|
+
const customEventOptions = {
|
|
664
|
+
bubbles: true,
|
|
665
|
+
cancelable: true,
|
|
666
|
+
composed: true,
|
|
667
|
+
};
|
|
668
|
+
if (detail) {
|
|
669
|
+
customEventOptions.detail = detail;
|
|
670
|
+
}
|
|
671
|
+
this.dispatchEvent(new CustomEvent("drawing-error", customEventOptions));
|
|
672
|
+
}
|
|
673
|
+
|
|
601
674
|
/**
|
|
602
675
|
* Handles the "drawing-zoom-changed" event from the 2D viewer component.
|
|
603
676
|
* Dispatches a custom "drawing-zoom-changed" event from the PrefViewer element, forwarding the event detail to external listeners.
|
|
@@ -640,16 +713,27 @@ export default class PrefViewer extends HTMLElement {
|
|
|
640
713
|
* @param {object} config - The configuration object to process.
|
|
641
714
|
* @returns {void}
|
|
642
715
|
*/
|
|
643
|
-
#processConfig(config) {
|
|
716
|
+
async #processConfig(config) {
|
|
644
717
|
if (!this.#component3D) {
|
|
718
|
+
this.#processNextTask();
|
|
645
719
|
return;
|
|
646
720
|
}
|
|
647
721
|
|
|
648
722
|
this.#on3DLoading();
|
|
649
|
-
|
|
650
|
-
this.#
|
|
723
|
+
try {
|
|
724
|
+
const detail = await this.#component3D.load(config);
|
|
725
|
+
if (detail?.success === false) {
|
|
726
|
+
this.#on3DError(detail);
|
|
727
|
+
} else {
|
|
728
|
+
this.#on3DLoaded(detail);
|
|
729
|
+
}
|
|
730
|
+
} catch (error) {
|
|
731
|
+
this.#on3DError({
|
|
732
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
733
|
+
});
|
|
734
|
+
} finally {
|
|
651
735
|
this.#processNextTask();
|
|
652
|
-
}
|
|
736
|
+
}
|
|
653
737
|
}
|
|
654
738
|
|
|
655
739
|
/**
|
|
@@ -670,16 +754,23 @@ export default class PrefViewer extends HTMLElement {
|
|
|
670
754
|
* @param {object} drawing - The drawing object to process.
|
|
671
755
|
* @returns {void}
|
|
672
756
|
*/
|
|
673
|
-
#processDrawing(drawing) {
|
|
757
|
+
async #processDrawing(drawing) {
|
|
674
758
|
if (!this.#component2D) {
|
|
759
|
+
this.#processNextTask();
|
|
675
760
|
return;
|
|
676
761
|
}
|
|
677
762
|
|
|
678
763
|
this.#on2DLoading();
|
|
679
|
-
|
|
764
|
+
try {
|
|
765
|
+
const detail = await this.#component2D.load(drawing);
|
|
680
766
|
this.#on2DLoaded(detail);
|
|
767
|
+
} catch (error) {
|
|
768
|
+
this.#on2DError({
|
|
769
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
770
|
+
});
|
|
771
|
+
} finally {
|
|
681
772
|
this.#processNextTask();
|
|
682
|
-
}
|
|
773
|
+
}
|
|
683
774
|
}
|
|
684
775
|
|
|
685
776
|
/**
|
|
@@ -725,15 +816,23 @@ export default class PrefViewer extends HTMLElement {
|
|
|
725
816
|
* @param {object} options - The options object to process.
|
|
726
817
|
* @returns {void}
|
|
727
818
|
*/
|
|
728
|
-
#processOptions(options) {
|
|
819
|
+
async #processOptions(options) {
|
|
729
820
|
if (!this.#component3D) {
|
|
821
|
+
this.#processNextTask();
|
|
730
822
|
return;
|
|
731
823
|
}
|
|
732
824
|
|
|
733
825
|
this.#on3DLoading();
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
826
|
+
try {
|
|
827
|
+
const detail = await this.#component3D.setOptions(options);
|
|
828
|
+
this.#on3DLoaded(detail);
|
|
829
|
+
} catch (error) {
|
|
830
|
+
this.#on3DError({
|
|
831
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
832
|
+
});
|
|
833
|
+
} finally {
|
|
834
|
+
this.#processNextTask();
|
|
835
|
+
}
|
|
737
836
|
}
|
|
738
837
|
|
|
739
838
|
/**
|
|
@@ -745,6 +844,7 @@ export default class PrefViewer extends HTMLElement {
|
|
|
745
844
|
*/
|
|
746
845
|
#processVisibility(config) {
|
|
747
846
|
if (!this.#component3D) {
|
|
847
|
+
this.#processNextTask();
|
|
748
848
|
return;
|
|
749
849
|
}
|
|
750
850
|
const showModel = config.model?.visible;
|
|
@@ -974,7 +1074,7 @@ export default class PrefViewer extends HTMLElement {
|
|
|
974
1074
|
* @returns {void}
|
|
975
1075
|
*/
|
|
976
1076
|
setMode(mode = this.#mode) {
|
|
977
|
-
mode = mode.toLowerCase();
|
|
1077
|
+
mode = typeof mode === "string" ? mode.toLowerCase() : this.#mode;
|
|
978
1078
|
if (mode !== "2d" && mode !== "3d") {
|
|
979
1079
|
console.warn(`PrefViewer: invalid mode "${mode}". Allowed modes are "2d" and "3d".`);
|
|
980
1080
|
mode = this.#mode;
|