@preference-sl/pref-viewer 2.13.0-beta.8 → 2.13.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 +303 -97
- package/src/file-storage.js +23 -3
- package/src/gltf-resolver.js +13 -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,6 +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.
|
|
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.
|
|
42
45
|
*
|
|
43
46
|
* Public API Highlights
|
|
44
47
|
* - constructor(canvas, containers, options)
|
|
@@ -62,6 +65,14 @@ import { translate } from "./localization/i18n.js";
|
|
|
62
65
|
* while `options.ibl.valid` remains true.
|
|
63
66
|
* - Browser-only features guard `window`, localStorage, and XR APIs before use so the controller is safe to construct
|
|
64
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.
|
|
65
76
|
*/
|
|
66
77
|
export default class BabylonJSController {
|
|
67
78
|
#RENDER_SETTINGS_STORAGE_KEY = "pref-viewer/render-settings";
|
|
@@ -93,9 +104,8 @@ export default class BabylonJSController {
|
|
|
93
104
|
#shadowGen = [];
|
|
94
105
|
#XRExperience = null;
|
|
95
106
|
#canvasResizeObserver = null;
|
|
96
|
-
|
|
107
|
+
|
|
97
108
|
#hdrTexture = null; // reusable in-memory HDR source cloned into scene.environmentTexture across reloads
|
|
98
|
-
#lastPickedMeshId = null;
|
|
99
109
|
|
|
100
110
|
#containers = {};
|
|
101
111
|
#options = {};
|
|
@@ -110,28 +120,61 @@ export default class BabylonJSController {
|
|
|
110
120
|
};
|
|
111
121
|
|
|
112
122
|
#handlers = {
|
|
123
|
+
onAnimationGroupChanged: null,
|
|
113
124
|
onKeyUp: null,
|
|
114
125
|
onPointerObservable: null,
|
|
115
|
-
onAnimationGroupChanged: null,
|
|
116
126
|
onResize: null,
|
|
117
127
|
renderLoop: null,
|
|
118
128
|
};
|
|
119
129
|
|
|
120
130
|
#settings = { ...BabylonJSController.DEFAULT_RENDER_SETTINGS };
|
|
121
131
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
+
},
|
|
127
154
|
};
|
|
128
155
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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,
|
|
135
178
|
};
|
|
136
179
|
|
|
137
180
|
/**
|
|
@@ -298,11 +341,11 @@ export default class BabylonJSController {
|
|
|
298
341
|
* @returns {boolean} True when the loop was started, false when no engine is available or it was already running.
|
|
299
342
|
*/
|
|
300
343
|
#startEngineRenderLoop() {
|
|
301
|
-
if (!this.#engine || this.#
|
|
344
|
+
if (!this.#engine || this.#state.render.isLoopRunning) {
|
|
302
345
|
return false;
|
|
303
346
|
}
|
|
304
347
|
this.#engine.runRenderLoop(this.#handlers.renderLoop);
|
|
305
|
-
this.#
|
|
348
|
+
this.#state.render.isLoopRunning = true;
|
|
306
349
|
return true;
|
|
307
350
|
}
|
|
308
351
|
|
|
@@ -312,13 +355,25 @@ export default class BabylonJSController {
|
|
|
312
355
|
* @returns {boolean} True when the loop was stopped, false when no engine is available or it was already stopped.
|
|
313
356
|
*/
|
|
314
357
|
#stopEngineRenderLoop() {
|
|
315
|
-
if (!this.#engine || !this.#
|
|
358
|
+
if (!this.#engine || !this.#state.render.isLoopRunning) {
|
|
316
359
|
return false;
|
|
317
360
|
}
|
|
318
361
|
this.#engine.stopRenderLoop(this.#handlers.renderLoop);
|
|
319
|
-
this.#
|
|
362
|
+
this.#state.render.isLoopRunning = false;
|
|
320
363
|
return true;
|
|
321
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
|
+
}
|
|
322
377
|
|
|
323
378
|
/**
|
|
324
379
|
* Marks the scene as dirty and optionally extends a short continuous-render window.
|
|
@@ -335,9 +390,9 @@ export default class BabylonJSController {
|
|
|
335
390
|
}
|
|
336
391
|
|
|
337
392
|
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
338
|
-
this.#
|
|
393
|
+
this.#state.render.dirtyFrames = Math.max(this.#state.render.dirtyFrames, Math.max(1, frames));
|
|
339
394
|
if (continuousMs > 0) {
|
|
340
|
-
this.#
|
|
395
|
+
this.#state.render.continuousUntil = Math.max(this.#state.render.continuousUntil, now + continuousMs);
|
|
341
396
|
}
|
|
342
397
|
this.#startEngineRenderLoop();
|
|
343
398
|
return true;
|
|
@@ -422,13 +477,13 @@ export default class BabylonJSController {
|
|
|
422
477
|
const cameraInMotion = this.#isCameraInMotion();
|
|
423
478
|
|
|
424
479
|
if (animationRunning) {
|
|
425
|
-
this.#
|
|
480
|
+
this.#state.render.continuousUntil = Math.max(this.#state.render.continuousUntil, now + this.#config.render.animationMs);
|
|
426
481
|
}
|
|
427
482
|
if (cameraInMotion) {
|
|
428
|
-
this.#
|
|
483
|
+
this.#state.render.continuousUntil = Math.max(this.#state.render.continuousUntil, now + this.#config.render.interactionMs);
|
|
429
484
|
}
|
|
430
485
|
|
|
431
|
-
return animationRunning || cameraInMotion || this.#
|
|
486
|
+
return animationRunning || cameraInMotion || this.#state.render.continuousUntil > now;
|
|
432
487
|
}
|
|
433
488
|
|
|
434
489
|
/**
|
|
@@ -446,22 +501,22 @@ export default class BabylonJSController {
|
|
|
446
501
|
|
|
447
502
|
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
448
503
|
const continuous = this.#shouldRenderContinuously(now);
|
|
449
|
-
const needsRender = continuous || this.#
|
|
504
|
+
const needsRender = continuous || this.#state.render.dirtyFrames > 0;
|
|
450
505
|
|
|
451
506
|
if (!needsRender) {
|
|
452
507
|
this.#stopEngineRenderLoop();
|
|
453
508
|
return;
|
|
454
509
|
}
|
|
455
510
|
|
|
456
|
-
if (!continuous && this.#
|
|
511
|
+
if (!continuous && this.#state.render.lastRenderAt > 0 && now - this.#state.render.lastRenderAt < this.#config.render.idleThrottleMs) {
|
|
457
512
|
return;
|
|
458
513
|
}
|
|
459
|
-
|
|
514
|
+
|
|
460
515
|
this.#scene.render();
|
|
461
|
-
this.#
|
|
516
|
+
this.#state.render.lastRenderAt = now;
|
|
462
517
|
|
|
463
|
-
if (this.#
|
|
464
|
-
this.#
|
|
518
|
+
if (this.#state.render.dirtyFrames > 0) {
|
|
519
|
+
this.#state.render.dirtyFrames -= 1;
|
|
465
520
|
}
|
|
466
521
|
}
|
|
467
522
|
|
|
@@ -1180,6 +1235,55 @@ export default class BabylonJSController {
|
|
|
1180
1235
|
}
|
|
1181
1236
|
}
|
|
1182
1237
|
|
|
1238
|
+
/**
|
|
1239
|
+
* Resets pointer-picking sampling state.
|
|
1240
|
+
* @private
|
|
1241
|
+
* @returns {void}
|
|
1242
|
+
*/
|
|
1243
|
+
#resetPointerPickingState() {
|
|
1244
|
+
this.#state.pointerPicking.lastMovePickAt = 0;
|
|
1245
|
+
this.#state.pointerPicking.lastMovePickX = NaN;
|
|
1246
|
+
this.#state.pointerPicking.lastMovePickY = NaN;
|
|
1247
|
+
this.#state.pointerPicking.lastPickedMeshId = null;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
/**
|
|
1251
|
+
* Decides whether a POINTERMOVE event should trigger a scene raycast.
|
|
1252
|
+
* Uses time + distance sampling to avoid expensive pick calls on every mouse move.
|
|
1253
|
+
* @private
|
|
1254
|
+
* @returns {boolean}
|
|
1255
|
+
*/
|
|
1256
|
+
#shouldPickOnPointerMove() {
|
|
1257
|
+
if (!this.#scene || !this.#babylonJSAnimationController) {
|
|
1258
|
+
return false;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1262
|
+
const x = this.#scene.pointerX;
|
|
1263
|
+
const y = this.#scene.pointerY;
|
|
1264
|
+
const state = this.#state.pointerPicking;
|
|
1265
|
+
const config = this.#config.pointerPicking;
|
|
1266
|
+
|
|
1267
|
+
if (!Number.isFinite(state.lastMovePickX) || !Number.isFinite(state.lastMovePickY)) {
|
|
1268
|
+
state.lastMovePickX = x;
|
|
1269
|
+
state.lastMovePickY = y;
|
|
1270
|
+
state.lastMovePickAt = now;
|
|
1271
|
+
return true;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
const elapsed = now - state.lastMovePickAt >= config.movePickIntervalMs;
|
|
1275
|
+
const moved = Math.abs(x - state.lastMovePickX) >= config.movePickMinDistancePx || Math.abs(y - state.lastMovePickY) >= config.movePickMinDistancePx;
|
|
1276
|
+
|
|
1277
|
+
if (!elapsed || !moved) {
|
|
1278
|
+
return false;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
state.lastMovePickX = x;
|
|
1282
|
+
state.lastMovePickY = y;
|
|
1283
|
+
state.lastMovePickAt = now;
|
|
1284
|
+
return true;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1183
1287
|
/**
|
|
1184
1288
|
* Sets up interaction handlers for the Babylon.js canvas and scene.
|
|
1185
1289
|
* @private
|
|
@@ -1193,10 +1297,7 @@ export default class BabylonJSController {
|
|
|
1193
1297
|
this.#scene.onPointerObservable.add(this.#handlers.onPointerObservable);
|
|
1194
1298
|
}
|
|
1195
1299
|
if (this.#engine) {
|
|
1196
|
-
this.#canvasResizeObserver = new ResizeObserver(
|
|
1197
|
-
this.#engine.resize();
|
|
1198
|
-
this.#requestRender({ frames: this.#renderConfig.burstFramesBase, continuousMs: this.#renderConfig.interactionMs });
|
|
1199
|
-
});
|
|
1300
|
+
this.#canvasResizeObserver = new ResizeObserver(this.#handlers.onResize);
|
|
1200
1301
|
this.#canvasResizeObserver.observe(this.#canvas);
|
|
1201
1302
|
}
|
|
1202
1303
|
}
|
|
@@ -1213,9 +1314,11 @@ export default class BabylonJSController {
|
|
|
1213
1314
|
if (this.#scene !== null) {
|
|
1214
1315
|
this.#scene.onPointerObservable.removeCallback(this.#handlers.onPointerObservable);
|
|
1215
1316
|
}
|
|
1317
|
+
this.#cancelScheduledResize();
|
|
1216
1318
|
this.#canvasResizeObserver?.disconnect();
|
|
1217
1319
|
this.#canvasResizeObserver = null;
|
|
1218
1320
|
this.#detachAnimationChangedListener();
|
|
1321
|
+
this.#resetPointerPickingState();
|
|
1219
1322
|
}
|
|
1220
1323
|
|
|
1221
1324
|
/**
|
|
@@ -1231,31 +1334,56 @@ export default class BabylonJSController {
|
|
|
1231
1334
|
}
|
|
1232
1335
|
|
|
1233
1336
|
/**
|
|
1234
|
-
* Disposes the
|
|
1337
|
+
* Disposes the shared GLTFResolver instance and closes its underlying storage handle.
|
|
1235
1338
|
* @private
|
|
1236
1339
|
* @returns {void}
|
|
1237
1340
|
*/
|
|
1238
|
-
#
|
|
1239
|
-
if (
|
|
1341
|
+
#disposeGLTFResolver() {
|
|
1342
|
+
if (this.#gltfResolver) {
|
|
1343
|
+
this.#gltfResolver.dispose();
|
|
1344
|
+
this.#gltfResolver = null;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/**
|
|
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.
|
|
1353
|
+
* @private
|
|
1354
|
+
* @returns {Promise<void>}
|
|
1355
|
+
*/
|
|
1356
|
+
async #disposeXRExperience() {
|
|
1357
|
+
if (this.#disablingPromises.xr) {
|
|
1358
|
+
return await this.#disablingPromises.xr;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
const xrExperience = this.#XRExperience;
|
|
1362
|
+
if (!xrExperience) {
|
|
1240
1363
|
return;
|
|
1241
1364
|
}
|
|
1242
1365
|
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
.
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
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) {
|
|
1253
1380
|
this.#XRExperience = null;
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1381
|
+
}
|
|
1382
|
+
this.#disablingPromises.xr = null;
|
|
1383
|
+
}
|
|
1384
|
+
})();
|
|
1385
|
+
|
|
1386
|
+
await this.#disablingPromises.xr;
|
|
1259
1387
|
}
|
|
1260
1388
|
|
|
1261
1389
|
/**
|
|
@@ -1303,7 +1431,7 @@ export default class BabylonJSController {
|
|
|
1303
1431
|
* @returns {void}
|
|
1304
1432
|
*/
|
|
1305
1433
|
#onAnimationGroupPlay() {
|
|
1306
|
-
this.#requestRender({ frames: 1, continuousMs: this.#
|
|
1434
|
+
this.#requestRender({ frames: 1, continuousMs: this.#config.render.animationMs });
|
|
1307
1435
|
}
|
|
1308
1436
|
|
|
1309
1437
|
/**
|
|
@@ -1312,7 +1440,11 @@ export default class BabylonJSController {
|
|
|
1312
1440
|
* @returns {void}
|
|
1313
1441
|
*/
|
|
1314
1442
|
#onAnimationGroupStop() {
|
|
1315
|
-
|
|
1443
|
+
if (this.#settings.iblEnabled && this.#renderPipelines.iblShadows) {
|
|
1444
|
+
this.#renderPipelines.iblShadows.updateVoxelization();
|
|
1445
|
+
this.#scene?.postProcessRenderPipelineManager?.update();
|
|
1446
|
+
}
|
|
1447
|
+
const frames = this.#settings.antiAliasingEnabled || this.#settings.ambientOcclusionEnabled || this.#settings.iblEnabled ? this.#config.render.burstFramesEnhanced : this.#config.render.burstFramesBase;
|
|
1316
1448
|
this.#requestRender({ frames: frames });
|
|
1317
1449
|
}
|
|
1318
1450
|
|
|
@@ -1352,10 +1484,10 @@ export default class BabylonJSController {
|
|
|
1352
1484
|
* @returns {void}
|
|
1353
1485
|
*/
|
|
1354
1486
|
#onKeyUp(event) {
|
|
1355
|
-
// CTRL + ALT + letter
|
|
1356
|
-
if (event.ctrlKey && event.altKey && event.
|
|
1357
|
-
switch (event.
|
|
1358
|
-
case "
|
|
1487
|
+
// CTRL + ALT + letter (uses event.code for physical key, layout-independent — fixes Mac Option+D producing "∂" instead of "d")
|
|
1488
|
+
if (event.ctrlKey && event.altKey && event.code !== undefined) {
|
|
1489
|
+
switch (event.code) {
|
|
1490
|
+
case "KeyD":
|
|
1359
1491
|
this.#openDownloadDialog();
|
|
1360
1492
|
break;
|
|
1361
1493
|
default:
|
|
@@ -1390,7 +1522,7 @@ export default class BabylonJSController {
|
|
|
1390
1522
|
const movementVector = direction.scale(zoomSpeed);
|
|
1391
1523
|
camera.position = camera.position.add(movementVector);
|
|
1392
1524
|
}
|
|
1393
|
-
this.#requestRender({ frames: 1, continuousMs: this.#
|
|
1525
|
+
this.#requestRender({ frames: 1, continuousMs: this.#config.render.interactionMs });
|
|
1394
1526
|
}
|
|
1395
1527
|
}
|
|
1396
1528
|
|
|
@@ -1403,7 +1535,9 @@ export default class BabylonJSController {
|
|
|
1403
1535
|
*/
|
|
1404
1536
|
#onPointerUp(event, pickInfo) {
|
|
1405
1537
|
if (this.#babylonJSAnimationController) {
|
|
1406
|
-
|
|
1538
|
+
if (event.button !== 2 || !pickInfo || !pickInfo.pickedMesh) {
|
|
1539
|
+
this.#babylonJSAnimationController.hideMenu();
|
|
1540
|
+
}
|
|
1407
1541
|
// Right click for showing animation menu
|
|
1408
1542
|
if (event.button === 2) {
|
|
1409
1543
|
this.#babylonJSAnimationController.showMenu(pickInfo);
|
|
@@ -1415,20 +1549,20 @@ export default class BabylonJSController {
|
|
|
1415
1549
|
* Handles pointer move events on the Babylon.js scene.
|
|
1416
1550
|
* @private
|
|
1417
1551
|
* @param {PointerEvent} event - The pointer move event.
|
|
1418
|
-
* @param {PickInfo} pickInfo - The result of the scene pick operation.
|
|
1552
|
+
* @param {PickInfo|null} pickInfo - The sampled result of the scene pick operation (may be null when sampling skips a raycast).
|
|
1419
1553
|
* @returns {void}
|
|
1420
1554
|
*/
|
|
1421
1555
|
#onPointerMove(event, pickInfo) {
|
|
1422
1556
|
const camera = this.#scene?.activeCamera;
|
|
1423
1557
|
if (camera && !camera.metadata?.locked) {
|
|
1424
|
-
this.#requestRender({ frames: 1, continuousMs: this.#
|
|
1558
|
+
this.#requestRender({ frames: 1, continuousMs: this.#config.render.interactionMs });
|
|
1425
1559
|
}
|
|
1426
|
-
if (this.#babylonJSAnimationController) {
|
|
1560
|
+
if (this.#babylonJSAnimationController && pickInfo) {
|
|
1427
1561
|
const pickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
1428
|
-
if (this.#lastPickedMeshId !== pickedMeshId) {
|
|
1562
|
+
if (this.#state.pointerPicking.lastPickedMeshId !== pickedMeshId) {
|
|
1429
1563
|
const highlightResult = this.#babylonJSAnimationController.highlightMeshes(pickInfo);
|
|
1430
1564
|
if (highlightResult.changed) {
|
|
1431
|
-
this.#requestRender({ frames: 1, continuousMs: this.#
|
|
1565
|
+
this.#requestRender({ frames: 1, continuousMs: this.#config.render.interactionMs });
|
|
1432
1566
|
}
|
|
1433
1567
|
}
|
|
1434
1568
|
}
|
|
@@ -1436,22 +1570,30 @@ export default class BabylonJSController {
|
|
|
1436
1570
|
|
|
1437
1571
|
/**
|
|
1438
1572
|
* Handles pointer events observed on the Babylon.js scene.
|
|
1573
|
+
* Uses pointer-move sampling to avoid running expensive `scene.pick()` on every event.
|
|
1439
1574
|
* @private
|
|
1440
1575
|
* @param {PointerInfo} info - The pointer event information from Babylon.js.
|
|
1441
1576
|
* @returns {void}
|
|
1442
1577
|
*/
|
|
1443
1578
|
#onPointerObservable(info) {
|
|
1444
|
-
const pickInfo = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
|
|
1445
|
-
const pickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
1446
|
-
|
|
1447
1579
|
if (info.type === PointerEventTypes.POINTERMOVE) {
|
|
1580
|
+
let pickInfo = null;
|
|
1581
|
+
let lastPickedMeshId = null;
|
|
1582
|
+
if (this.#shouldPickOnPointerMove()) {
|
|
1583
|
+
pickInfo = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
|
|
1584
|
+
lastPickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
1585
|
+
}
|
|
1448
1586
|
this.#onPointerMove(info.event, pickInfo);
|
|
1587
|
+
this.#state.pointerPicking.lastPickedMeshId = lastPickedMeshId;
|
|
1449
1588
|
} else if (info.type === PointerEventTypes.POINTERUP) {
|
|
1589
|
+
const pickInfo = info.event?.button === 2 ? this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY) : null;
|
|
1590
|
+
if (pickInfo) {
|
|
1591
|
+
this.#state.pointerPicking.lastPickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
1592
|
+
}
|
|
1450
1593
|
this.#onPointerUp(info.event, pickInfo);
|
|
1451
1594
|
} else if (info.type === PointerEventTypes.POINTERWHEEL) {
|
|
1452
|
-
this.#onMouseWheel(info.event,
|
|
1595
|
+
this.#onMouseWheel(info.event, null);
|
|
1453
1596
|
}
|
|
1454
|
-
this.#lastPickedMeshId = pickedMeshId;
|
|
1455
1597
|
}
|
|
1456
1598
|
|
|
1457
1599
|
/**
|
|
@@ -1465,8 +1607,47 @@ export default class BabylonJSController {
|
|
|
1465
1607
|
if (!this.#engine) {
|
|
1466
1608
|
return;
|
|
1467
1609
|
}
|
|
1468
|
-
|
|
1469
|
-
|
|
1610
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1611
|
+
const elapsed = now - this.#state.resize.lastAppliedAt;
|
|
1612
|
+
const applyResize = () => {
|
|
1613
|
+
this.#state.resize.timeoutId = null;
|
|
1614
|
+
this.#state.resize.isScheduled = false;
|
|
1615
|
+
if (!this.#engine) {
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
this.#state.resize.lastAppliedAt = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1619
|
+
console.log(`PrefViewer: Applying resize after ${Math.round(elapsed)}ms`);
|
|
1620
|
+
this.#engine.resize();
|
|
1621
|
+
this.#requestRender({ frames: this.#config.render.burstFramesBase, continuousMs: this.#config.render.interactionMs });
|
|
1622
|
+
};
|
|
1623
|
+
|
|
1624
|
+
if (elapsed >= this.#config.resize.throttleMs) {
|
|
1625
|
+
applyResize();
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
if (this.#state.resize.isScheduled) {
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
this.#state.resize.isScheduled = true;
|
|
1634
|
+
const waitMs = Math.max(0, this.#config.resize.throttleMs - elapsed);
|
|
1635
|
+
this.#state.resize.timeoutId = setTimeout(() => {
|
|
1636
|
+
applyResize();
|
|
1637
|
+
}, waitMs);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/**
|
|
1641
|
+
* Clears any queued throttled resize callback.
|
|
1642
|
+
* @private
|
|
1643
|
+
* @returns {void}
|
|
1644
|
+
*/
|
|
1645
|
+
#cancelScheduledResize() {
|
|
1646
|
+
if (this.#state.resize.timeoutId !== null) {
|
|
1647
|
+
clearTimeout(this.#state.resize.timeoutId);
|
|
1648
|
+
}
|
|
1649
|
+
this.#state.resize.timeoutId = null;
|
|
1650
|
+
this.#state.resize.isScheduled = false;
|
|
1470
1651
|
}
|
|
1471
1652
|
|
|
1472
1653
|
/**
|
|
@@ -1812,11 +1993,10 @@ export default class BabylonJSController {
|
|
|
1812
1993
|
*/
|
|
1813
1994
|
async #stopRender() {
|
|
1814
1995
|
this.#stopEngineRenderLoop();
|
|
1815
|
-
this.#
|
|
1816
|
-
this.#renderState.continuousUntil = 0;
|
|
1817
|
-
this.#renderState.lastRenderAt = 0;
|
|
1996
|
+
this.#resetRenderState();
|
|
1818
1997
|
await this.#unloadCameraDependentEffects();
|
|
1819
1998
|
}
|
|
1999
|
+
|
|
1820
2000
|
/**
|
|
1821
2001
|
* Starts the Babylon.js render loop for the current scene.
|
|
1822
2002
|
* Waits until the scene is ready before beginning continuous rendering.
|
|
@@ -1826,8 +2006,8 @@ export default class BabylonJSController {
|
|
|
1826
2006
|
async #startRender() {
|
|
1827
2007
|
await this.#loadCameraDependentEffects();
|
|
1828
2008
|
await this.#scene.whenReadyAsync();
|
|
1829
|
-
const frames = this.#settings.antiAliasingEnabled || this.#settings.ambientOcclusionEnabled || this.#settings.iblEnabled ? this.#
|
|
1830
|
-
this.#requestRender({ frames: frames, continuousMs: this.#
|
|
2009
|
+
const frames = this.#settings.antiAliasingEnabled || this.#settings.ambientOcclusionEnabled || this.#settings.iblEnabled ? this.#config.render.burstFramesEnhanced : this.#config.render.burstFramesBase;
|
|
2010
|
+
this.#requestRender({ frames: frames, continuousMs: this.#config.render.interactionMs });
|
|
1831
2011
|
}
|
|
1832
2012
|
|
|
1833
2013
|
/**
|
|
@@ -1906,7 +2086,7 @@ export default class BabylonJSController {
|
|
|
1906
2086
|
* Applies material and camera options, sets wall/floor visibility, and initializes lights and shadows.
|
|
1907
2087
|
* Returns an object with success status and error details.
|
|
1908
2088
|
*/
|
|
1909
|
-
async #loadContainers() {
|
|
2089
|
+
async #loadContainers(force = false) {
|
|
1910
2090
|
this.#detachAnimationChangedListener();
|
|
1911
2091
|
await this.#stopRender();
|
|
1912
2092
|
|
|
@@ -1915,7 +2095,7 @@ export default class BabylonJSController {
|
|
|
1915
2095
|
|
|
1916
2096
|
const promiseArray = [];
|
|
1917
2097
|
Object.values(this.#containers).forEach((container) => {
|
|
1918
|
-
promiseArray.push(this.#loadAssetContainer(container));
|
|
2098
|
+
promiseArray.push(this.#loadAssetContainer(container, force));
|
|
1919
2099
|
});
|
|
1920
2100
|
|
|
1921
2101
|
let detail = {
|
|
@@ -2258,17 +2438,41 @@ export default class BabylonJSController {
|
|
|
2258
2438
|
|
|
2259
2439
|
/**
|
|
2260
2440
|
* Disposes the Babylon.js engine and disconnects the canvas resize observer.
|
|
2261
|
-
* Cleans up all scene, camera, light, and
|
|
2441
|
+
* Cleans up all scene, camera, light, XR, and GLTF resolver resources.
|
|
2442
|
+
* The teardown is asynchronous: it waits for XR/session-dependent shutdown work
|
|
2443
|
+
* before disposing the engine, and coalesces concurrent calls into one in-flight promise.
|
|
2262
2444
|
* @public
|
|
2263
|
-
* @returns {void}
|
|
2445
|
+
* @returns {Promise<void>}
|
|
2264
2446
|
*/
|
|
2265
|
-
disable() {
|
|
2266
|
-
this.#
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
this.#
|
|
2271
|
-
|
|
2447
|
+
async disable() {
|
|
2448
|
+
if (this.#disablingPromises.general) {
|
|
2449
|
+
return await this.#disablingPromises.general;
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
this.#disablingPromises.general = (async () => {
|
|
2453
|
+
this.#disableInteraction();
|
|
2454
|
+
this.#disposeAnimationController();
|
|
2455
|
+
this.#disposeGLTFResolver();
|
|
2456
|
+
try {
|
|
2457
|
+
await this.#disposeXRExperience();
|
|
2458
|
+
} catch (error) {
|
|
2459
|
+
console.warn("PrefViewer: Error while disposing XR experience:", error);
|
|
2460
|
+
}
|
|
2461
|
+
try {
|
|
2462
|
+
await this.#unloadCameraDependentEffects();
|
|
2463
|
+
} catch (error) {
|
|
2464
|
+
console.warn("PrefViewer: Error while unloading camera-dependent effects:", error);
|
|
2465
|
+
} finally {
|
|
2466
|
+
this.#stopEngineRenderLoop();
|
|
2467
|
+
this.#disposeEngine();
|
|
2468
|
+
}
|
|
2469
|
+
})();
|
|
2470
|
+
|
|
2471
|
+
try {
|
|
2472
|
+
await this.#disablingPromises.general;
|
|
2473
|
+
} finally {
|
|
2474
|
+
this.#disablingPromises.general = null;
|
|
2475
|
+
}
|
|
2272
2476
|
}
|
|
2273
2477
|
|
|
2274
2478
|
/**
|
|
@@ -2389,8 +2593,8 @@ export default class BabylonJSController {
|
|
|
2389
2593
|
* @public
|
|
2390
2594
|
* @returns {Promise<boolean>} Resolves to true if loading succeeds, false otherwise.
|
|
2391
2595
|
*/
|
|
2392
|
-
async load() {
|
|
2393
|
-
return await this.#loadContainers();
|
|
2596
|
+
async load(force = false) {
|
|
2597
|
+
return await this.#loadContainers(force);
|
|
2394
2598
|
}
|
|
2395
2599
|
|
|
2396
2600
|
/**
|
|
@@ -2425,11 +2629,13 @@ export default class BabylonJSController {
|
|
|
2425
2629
|
* @public
|
|
2426
2630
|
* @returns {boolean} True if IBL options were set successfully, false otherwise.
|
|
2427
2631
|
*/
|
|
2428
|
-
setIBLOptions() {
|
|
2429
|
-
this.#stopRender();
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2632
|
+
async setIBLOptions() {
|
|
2633
|
+
await this.#stopRender();
|
|
2634
|
+
try {
|
|
2635
|
+
return await this.#setOptions_IBL();
|
|
2636
|
+
} finally {
|
|
2637
|
+
await this.#startRender();
|
|
2638
|
+
}
|
|
2433
2639
|
}
|
|
2434
2640
|
|
|
2435
2641
|
/**
|
package/src/file-storage.js
CHANGED
|
@@ -36,6 +36,7 @@ import { openDB } from "idb";
|
|
|
36
36
|
* - getBlob(uri): Retrieves file blob from cache or server.
|
|
37
37
|
* - get(uri): Gets file from cache with automatic server sync and cache versioning.
|
|
38
38
|
* - put(uri): Stores file from server in IndexedDB cache.
|
|
39
|
+
* - dispose(): Closes the active IndexedDB handle held by the instance.
|
|
39
40
|
*
|
|
40
41
|
* Features:
|
|
41
42
|
* - Automatic Cache Versioning: Compares server and cached timestamps to update cache.
|
|
@@ -105,6 +106,7 @@ import { openDB } from "idb";
|
|
|
105
106
|
* - Supports both HTTPS and HTTP (HTTP not recommended for production)
|
|
106
107
|
* - Object URLs should be revoked after use to free memory
|
|
107
108
|
* - IndexedDB cache is auto-maintained (TTL, max entries, quota cleanup)
|
|
109
|
+
* - Call dispose() when the owner is torn down to release the DB connection proactively.
|
|
108
110
|
*
|
|
109
111
|
* Error Handling:
|
|
110
112
|
* - Network failures: Returns undefined (get) or false (other methods)
|
|
@@ -753,9 +755,12 @@ export default class FileStorage {
|
|
|
753
755
|
}
|
|
754
756
|
await this.#runCleanupIfDue();
|
|
755
757
|
let storedFile = await this.#getFile(uri);
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
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) {
|
|
759
764
|
const fileToStore = await this.#getServerFile(uri);
|
|
760
765
|
if (fileToStore && !!(await this.#putFile(fileToStore, uri))) {
|
|
761
766
|
storedFile = await this.#getFile(uri);
|
|
@@ -782,4 +787,19 @@ export default class FileStorage {
|
|
|
782
787
|
const fileToStore = await this.#getServerFile(uri);
|
|
783
788
|
return fileToStore ? !!(await this.#putFile(fileToStore, uri)) : false;
|
|
784
789
|
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Closes the IndexedDB handle held by this storage instance.
|
|
793
|
+
* Safe to call multiple times; next storage operation will lazily reopen the DB.
|
|
794
|
+
* @public
|
|
795
|
+
* @returns {void}
|
|
796
|
+
*/
|
|
797
|
+
dispose() {
|
|
798
|
+
if (this.#db) {
|
|
799
|
+
try {
|
|
800
|
+
this.#db.close();
|
|
801
|
+
} catch {}
|
|
802
|
+
}
|
|
803
|
+
this.#db = undefined;
|
|
804
|
+
}
|
|
785
805
|
}
|
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.
|
|
@@ -19,6 +19,7 @@ import { initDb, loadModel } from "./gltf-storage.js";
|
|
|
19
19
|
* Public Methods:
|
|
20
20
|
* - getSource(storage, currentSize, currentTimeStamp): Resolves and prepares a glTF/GLB source for loading.
|
|
21
21
|
* - revokeObjectURLs(objectURLs): Releases temporary blob URLs generated during source resolution.
|
|
22
|
+
* - dispose(): Releases resolver resources and closes the internal FileStorage DB handle.
|
|
22
23
|
*
|
|
23
24
|
* Private Methods:
|
|
24
25
|
* - #initializeStorage(db, table): Ensures IndexedDB store is initialized.
|
|
@@ -231,6 +232,17 @@ export default class GLTFResolver {
|
|
|
231
232
|
objectURLs.length = 0;
|
|
232
233
|
}
|
|
233
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Disposes resolver-owned resources.
|
|
237
|
+
* @public
|
|
238
|
+
* @returns {void}
|
|
239
|
+
*/
|
|
240
|
+
dispose() {
|
|
241
|
+
this.#fileStorage?.dispose?.();
|
|
242
|
+
this.#fileStorage = null;
|
|
243
|
+
closeDb();
|
|
244
|
+
}
|
|
245
|
+
|
|
234
246
|
/**
|
|
235
247
|
* Resolves and prepares a glTF/GLB source from various storage backends.
|
|
236
248
|
* Supports IndexedDB, direct URLs, and base64-encoded data.
|
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;
|