@preference-sl/pref-viewer 2.13.0-beta.9 → 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 +290 -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
|
|
|
@@ -1181,6 +1235,55 @@ export default class BabylonJSController {
|
|
|
1181
1235
|
}
|
|
1182
1236
|
}
|
|
1183
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
|
+
|
|
1184
1287
|
/**
|
|
1185
1288
|
* Sets up interaction handlers for the Babylon.js canvas and scene.
|
|
1186
1289
|
* @private
|
|
@@ -1194,10 +1297,7 @@ export default class BabylonJSController {
|
|
|
1194
1297
|
this.#scene.onPointerObservable.add(this.#handlers.onPointerObservable);
|
|
1195
1298
|
}
|
|
1196
1299
|
if (this.#engine) {
|
|
1197
|
-
this.#canvasResizeObserver = new ResizeObserver(
|
|
1198
|
-
this.#engine.resize();
|
|
1199
|
-
this.#requestRender({ frames: this.#renderConfig.burstFramesBase, continuousMs: this.#renderConfig.interactionMs });
|
|
1200
|
-
});
|
|
1300
|
+
this.#canvasResizeObserver = new ResizeObserver(this.#handlers.onResize);
|
|
1201
1301
|
this.#canvasResizeObserver.observe(this.#canvas);
|
|
1202
1302
|
}
|
|
1203
1303
|
}
|
|
@@ -1214,9 +1314,11 @@ export default class BabylonJSController {
|
|
|
1214
1314
|
if (this.#scene !== null) {
|
|
1215
1315
|
this.#scene.onPointerObservable.removeCallback(this.#handlers.onPointerObservable);
|
|
1216
1316
|
}
|
|
1317
|
+
this.#cancelScheduledResize();
|
|
1217
1318
|
this.#canvasResizeObserver?.disconnect();
|
|
1218
1319
|
this.#canvasResizeObserver = null;
|
|
1219
1320
|
this.#detachAnimationChangedListener();
|
|
1321
|
+
this.#resetPointerPickingState();
|
|
1220
1322
|
}
|
|
1221
1323
|
|
|
1222
1324
|
/**
|
|
@@ -1245,30 +1347,43 @@ export default class BabylonJSController {
|
|
|
1245
1347
|
|
|
1246
1348
|
/**
|
|
1247
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.
|
|
1248
1353
|
* @private
|
|
1249
|
-
* @returns {void}
|
|
1354
|
+
* @returns {Promise<void>}
|
|
1250
1355
|
*/
|
|
1251
|
-
#disposeXRExperience() {
|
|
1252
|
-
if (
|
|
1356
|
+
async #disposeXRExperience() {
|
|
1357
|
+
if (this.#disablingPromises.xr) {
|
|
1358
|
+
return await this.#disablingPromises.xr;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
const xrExperience = this.#XRExperience;
|
|
1362
|
+
if (!xrExperience) {
|
|
1253
1363
|
return;
|
|
1254
1364
|
}
|
|
1255
1365
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
.
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
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) {
|
|
1266
1380
|
this.#XRExperience = null;
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1381
|
+
}
|
|
1382
|
+
this.#disablingPromises.xr = null;
|
|
1383
|
+
}
|
|
1384
|
+
})();
|
|
1385
|
+
|
|
1386
|
+
await this.#disablingPromises.xr;
|
|
1272
1387
|
}
|
|
1273
1388
|
|
|
1274
1389
|
/**
|
|
@@ -1316,7 +1431,7 @@ export default class BabylonJSController {
|
|
|
1316
1431
|
* @returns {void}
|
|
1317
1432
|
*/
|
|
1318
1433
|
#onAnimationGroupPlay() {
|
|
1319
|
-
this.#requestRender({ frames: 1, continuousMs: this.#
|
|
1434
|
+
this.#requestRender({ frames: 1, continuousMs: this.#config.render.animationMs });
|
|
1320
1435
|
}
|
|
1321
1436
|
|
|
1322
1437
|
/**
|
|
@@ -1325,7 +1440,11 @@ export default class BabylonJSController {
|
|
|
1325
1440
|
* @returns {void}
|
|
1326
1441
|
*/
|
|
1327
1442
|
#onAnimationGroupStop() {
|
|
1328
|
-
|
|
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;
|
|
1329
1448
|
this.#requestRender({ frames: frames });
|
|
1330
1449
|
}
|
|
1331
1450
|
|
|
@@ -1365,10 +1484,10 @@ export default class BabylonJSController {
|
|
|
1365
1484
|
* @returns {void}
|
|
1366
1485
|
*/
|
|
1367
1486
|
#onKeyUp(event) {
|
|
1368
|
-
// CTRL + ALT + letter
|
|
1369
|
-
if (event.ctrlKey && event.altKey && event.
|
|
1370
|
-
switch (event.
|
|
1371
|
-
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":
|
|
1372
1491
|
this.#openDownloadDialog();
|
|
1373
1492
|
break;
|
|
1374
1493
|
default:
|
|
@@ -1403,7 +1522,7 @@ export default class BabylonJSController {
|
|
|
1403
1522
|
const movementVector = direction.scale(zoomSpeed);
|
|
1404
1523
|
camera.position = camera.position.add(movementVector);
|
|
1405
1524
|
}
|
|
1406
|
-
this.#requestRender({ frames: 1, continuousMs: this.#
|
|
1525
|
+
this.#requestRender({ frames: 1, continuousMs: this.#config.render.interactionMs });
|
|
1407
1526
|
}
|
|
1408
1527
|
}
|
|
1409
1528
|
|
|
@@ -1416,7 +1535,9 @@ export default class BabylonJSController {
|
|
|
1416
1535
|
*/
|
|
1417
1536
|
#onPointerUp(event, pickInfo) {
|
|
1418
1537
|
if (this.#babylonJSAnimationController) {
|
|
1419
|
-
|
|
1538
|
+
if (event.button !== 2 || !pickInfo || !pickInfo.pickedMesh) {
|
|
1539
|
+
this.#babylonJSAnimationController.hideMenu();
|
|
1540
|
+
}
|
|
1420
1541
|
// Right click for showing animation menu
|
|
1421
1542
|
if (event.button === 2) {
|
|
1422
1543
|
this.#babylonJSAnimationController.showMenu(pickInfo);
|
|
@@ -1428,20 +1549,20 @@ export default class BabylonJSController {
|
|
|
1428
1549
|
* Handles pointer move events on the Babylon.js scene.
|
|
1429
1550
|
* @private
|
|
1430
1551
|
* @param {PointerEvent} event - The pointer move event.
|
|
1431
|
-
* @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).
|
|
1432
1553
|
* @returns {void}
|
|
1433
1554
|
*/
|
|
1434
1555
|
#onPointerMove(event, pickInfo) {
|
|
1435
1556
|
const camera = this.#scene?.activeCamera;
|
|
1436
1557
|
if (camera && !camera.metadata?.locked) {
|
|
1437
|
-
this.#requestRender({ frames: 1, continuousMs: this.#
|
|
1558
|
+
this.#requestRender({ frames: 1, continuousMs: this.#config.render.interactionMs });
|
|
1438
1559
|
}
|
|
1439
|
-
if (this.#babylonJSAnimationController) {
|
|
1560
|
+
if (this.#babylonJSAnimationController && pickInfo) {
|
|
1440
1561
|
const pickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
1441
|
-
if (this.#lastPickedMeshId !== pickedMeshId) {
|
|
1562
|
+
if (this.#state.pointerPicking.lastPickedMeshId !== pickedMeshId) {
|
|
1442
1563
|
const highlightResult = this.#babylonJSAnimationController.highlightMeshes(pickInfo);
|
|
1443
1564
|
if (highlightResult.changed) {
|
|
1444
|
-
this.#requestRender({ frames: 1, continuousMs: this.#
|
|
1565
|
+
this.#requestRender({ frames: 1, continuousMs: this.#config.render.interactionMs });
|
|
1445
1566
|
}
|
|
1446
1567
|
}
|
|
1447
1568
|
}
|
|
@@ -1449,22 +1570,30 @@ export default class BabylonJSController {
|
|
|
1449
1570
|
|
|
1450
1571
|
/**
|
|
1451
1572
|
* Handles pointer events observed on the Babylon.js scene.
|
|
1573
|
+
* Uses pointer-move sampling to avoid running expensive `scene.pick()` on every event.
|
|
1452
1574
|
* @private
|
|
1453
1575
|
* @param {PointerInfo} info - The pointer event information from Babylon.js.
|
|
1454
1576
|
* @returns {void}
|
|
1455
1577
|
*/
|
|
1456
1578
|
#onPointerObservable(info) {
|
|
1457
|
-
const pickInfo = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
|
|
1458
|
-
const pickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
1459
|
-
|
|
1460
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
|
+
}
|
|
1461
1586
|
this.#onPointerMove(info.event, pickInfo);
|
|
1587
|
+
this.#state.pointerPicking.lastPickedMeshId = lastPickedMeshId;
|
|
1462
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
|
+
}
|
|
1463
1593
|
this.#onPointerUp(info.event, pickInfo);
|
|
1464
1594
|
} else if (info.type === PointerEventTypes.POINTERWHEEL) {
|
|
1465
|
-
this.#onMouseWheel(info.event,
|
|
1595
|
+
this.#onMouseWheel(info.event, null);
|
|
1466
1596
|
}
|
|
1467
|
-
this.#lastPickedMeshId = pickedMeshId;
|
|
1468
1597
|
}
|
|
1469
1598
|
|
|
1470
1599
|
/**
|
|
@@ -1478,8 +1607,47 @@ export default class BabylonJSController {
|
|
|
1478
1607
|
if (!this.#engine) {
|
|
1479
1608
|
return;
|
|
1480
1609
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
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;
|
|
1483
1651
|
}
|
|
1484
1652
|
|
|
1485
1653
|
/**
|
|
@@ -1825,11 +1993,10 @@ export default class BabylonJSController {
|
|
|
1825
1993
|
*/
|
|
1826
1994
|
async #stopRender() {
|
|
1827
1995
|
this.#stopEngineRenderLoop();
|
|
1828
|
-
this.#
|
|
1829
|
-
this.#renderState.continuousUntil = 0;
|
|
1830
|
-
this.#renderState.lastRenderAt = 0;
|
|
1996
|
+
this.#resetRenderState();
|
|
1831
1997
|
await this.#unloadCameraDependentEffects();
|
|
1832
1998
|
}
|
|
1999
|
+
|
|
1833
2000
|
/**
|
|
1834
2001
|
* Starts the Babylon.js render loop for the current scene.
|
|
1835
2002
|
* Waits until the scene is ready before beginning continuous rendering.
|
|
@@ -1839,8 +2006,8 @@ export default class BabylonJSController {
|
|
|
1839
2006
|
async #startRender() {
|
|
1840
2007
|
await this.#loadCameraDependentEffects();
|
|
1841
2008
|
await this.#scene.whenReadyAsync();
|
|
1842
|
-
const frames = this.#settings.antiAliasingEnabled || this.#settings.ambientOcclusionEnabled || this.#settings.iblEnabled ? this.#
|
|
1843
|
-
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 });
|
|
1844
2011
|
}
|
|
1845
2012
|
|
|
1846
2013
|
/**
|
|
@@ -1919,7 +2086,7 @@ export default class BabylonJSController {
|
|
|
1919
2086
|
* Applies material and camera options, sets wall/floor visibility, and initializes lights and shadows.
|
|
1920
2087
|
* Returns an object with success status and error details.
|
|
1921
2088
|
*/
|
|
1922
|
-
async #loadContainers() {
|
|
2089
|
+
async #loadContainers(force = false) {
|
|
1923
2090
|
this.#detachAnimationChangedListener();
|
|
1924
2091
|
await this.#stopRender();
|
|
1925
2092
|
|
|
@@ -1928,7 +2095,7 @@ export default class BabylonJSController {
|
|
|
1928
2095
|
|
|
1929
2096
|
const promiseArray = [];
|
|
1930
2097
|
Object.values(this.#containers).forEach((container) => {
|
|
1931
|
-
promiseArray.push(this.#loadAssetContainer(container));
|
|
2098
|
+
promiseArray.push(this.#loadAssetContainer(container, force));
|
|
1932
2099
|
});
|
|
1933
2100
|
|
|
1934
2101
|
let detail = {
|
|
@@ -2272,17 +2439,40 @@ export default class BabylonJSController {
|
|
|
2272
2439
|
/**
|
|
2273
2440
|
* Disposes the Babylon.js engine and disconnects the canvas resize observer.
|
|
2274
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.
|
|
2275
2444
|
* @public
|
|
2276
|
-
* @returns {void}
|
|
2445
|
+
* @returns {Promise<void>}
|
|
2277
2446
|
*/
|
|
2278
|
-
disable() {
|
|
2279
|
-
this.#
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
this.#
|
|
2284
|
-
|
|
2285
|
-
|
|
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
|
+
}
|
|
2286
2476
|
}
|
|
2287
2477
|
|
|
2288
2478
|
/**
|
|
@@ -2403,8 +2593,8 @@ export default class BabylonJSController {
|
|
|
2403
2593
|
* @public
|
|
2404
2594
|
* @returns {Promise<boolean>} Resolves to true if loading succeeds, false otherwise.
|
|
2405
2595
|
*/
|
|
2406
|
-
async load() {
|
|
2407
|
-
return await this.#loadContainers();
|
|
2596
|
+
async load(force = false) {
|
|
2597
|
+
return await this.#loadContainers(force);
|
|
2408
2598
|
}
|
|
2409
2599
|
|
|
2410
2600
|
/**
|
|
@@ -2439,11 +2629,13 @@ export default class BabylonJSController {
|
|
|
2439
2629
|
* @public
|
|
2440
2630
|
* @returns {boolean} True if IBL options were set successfully, false otherwise.
|
|
2441
2631
|
*/
|
|
2442
|
-
setIBLOptions() {
|
|
2443
|
-
this.#stopRender();
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2632
|
+
async setIBLOptions() {
|
|
2633
|
+
await this.#stopRender();
|
|
2634
|
+
try {
|
|
2635
|
+
return await this.#setOptions_IBL();
|
|
2636
|
+
} finally {
|
|
2637
|
+
await this.#startRender();
|
|
2638
|
+
}
|
|
2447
2639
|
}
|
|
2448
2640
|
|
|
2449
2641
|
/**
|
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;
|