@preference-sl/pref-viewer 2.13.0-beta.7 → 2.13.0-beta.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/babylonjs-animation-controller.js +154 -63
- package/src/babylonjs-animation-opening.js +58 -2
- package/src/babylonjs-controller.js +407 -86
- package/src/file-storage.js +17 -0
- package/src/gltf-resolver.js +11 -0
package/package.json
CHANGED
|
@@ -6,22 +6,32 @@ import OpeningAnimation from "./babylonjs-animation-opening.js";
|
|
|
6
6
|
* BabylonJSAnimationController - Manages animation playback, highlighting, and interactive controls for animated nodes in Babylon.js scenes.
|
|
7
7
|
*
|
|
8
8
|
* Summary:
|
|
9
|
-
* This class detects
|
|
9
|
+
* This class detects and groups opening/closing animations, manages hover highlighting for animated nodes,
|
|
10
|
+
* exposes whether interactive animations are available, and coordinates the contextual animation menu.
|
|
10
11
|
*
|
|
11
12
|
* Key features:
|
|
12
13
|
* - Detects and groups opening/closing animations in the scene.
|
|
13
14
|
* - Tracks animated transformation nodes and their relationships to meshes.
|
|
15
|
+
* - Exposes animation availability through `hasAnimations()`.
|
|
14
16
|
* - Highlights animated nodes and their child meshes on pointer hover.
|
|
17
|
+
* - Avoids redundant highlight rebuilds using cached mesh/node IDs and order-insensitive node-id comparison.
|
|
18
|
+
* - Reports highlight-state deltas from `highlightMeshes()` so callers can react efficiently.
|
|
15
19
|
* - Displays and disposes the animation control menu for animated nodes.
|
|
16
|
-
* - Provides public API for highlighting, showing the animation menu, and disposing resources.
|
|
20
|
+
* - Provides public API for availability checks, highlighting, showing the animation menu, and disposing resources.
|
|
17
21
|
* - Cleans up all resources and observers to prevent memory leaks.
|
|
18
22
|
*
|
|
19
23
|
* Public Methods:
|
|
20
24
|
* - dispose(): Disposes all resources managed by the animation controller.
|
|
21
|
-
* -
|
|
25
|
+
* - hasAnimations(): Returns whether opening/closing animations are available.
|
|
26
|
+
* - highlightMeshes(pickingInfo): Updates hover highlighting and returns `{ changed, highlighted }`.
|
|
22
27
|
* - hideMenu(): Hides and disposes the animation control menu if it exists.
|
|
23
28
|
* - showMenu(pickingInfo): Displays the animation control menu for the animated node under the pointer.
|
|
24
29
|
*
|
|
30
|
+
* Private Methods (high level):
|
|
31
|
+
* - #haveSameNodeIds(arr1, arr2): Compares hovered animated node sets ignoring order.
|
|
32
|
+
* - #addHighlight(nodeIds): Applies highlighting with HighlightLayer or overlay meshes.
|
|
33
|
+
* - #removeHighlight(): Clears current highlights and cached hover state.
|
|
34
|
+
*
|
|
25
35
|
* @class
|
|
26
36
|
*/
|
|
27
37
|
export default class BabylonJSAnimationController {
|
|
@@ -37,6 +47,7 @@ export default class BabylonJSAnimationController {
|
|
|
37
47
|
#overlayLayer = null;
|
|
38
48
|
#useHighlightLayer = false; // Set to true to use HighlightLayer (better performance) and false to use overlay meshes (UtilityLayerRenderer - always on top)
|
|
39
49
|
#lastHighlightedMeshId = null; // Cache to avoid redundant highlight updates
|
|
50
|
+
#lastHighlightedNodeIds = []; // Cache to avoid redundant highlight updates
|
|
40
51
|
|
|
41
52
|
/**
|
|
42
53
|
* Creates a new BabylonJSAnimationController for a Babylon.js scene.
|
|
@@ -126,15 +137,18 @@ export default class BabylonJSAnimationController {
|
|
|
126
137
|
* @returns {Array<string>} Array of animated node IDs associated with the mesh.
|
|
127
138
|
*/
|
|
128
139
|
#getNodesAnimatedByMesh(mesh) {
|
|
129
|
-
let
|
|
140
|
+
let nodeIds = [];
|
|
141
|
+
if (!mesh || !mesh.id) {
|
|
142
|
+
return nodeIds;
|
|
143
|
+
}
|
|
130
144
|
let node = mesh;
|
|
131
145
|
while (node.parent !== null) {
|
|
132
146
|
node = node.parent;
|
|
133
|
-
if (this.#animatedNodes.includes(node.id) && !
|
|
134
|
-
|
|
147
|
+
if (this.#animatedNodes.includes(node.id) && !nodeIds.includes(node.id)) {
|
|
148
|
+
nodeIds.push(node.id);
|
|
135
149
|
}
|
|
136
150
|
}
|
|
137
|
-
return
|
|
151
|
+
return nodeIds;
|
|
138
152
|
}
|
|
139
153
|
|
|
140
154
|
/**
|
|
@@ -308,62 +322,60 @@ export default class BabylonJSAnimationController {
|
|
|
308
322
|
}
|
|
309
323
|
|
|
310
324
|
/**
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
325
|
+
* Compares two node ID arrays and checks whether they contain the same values,
|
|
326
|
+
* regardless of element order.
|
|
327
|
+
* @private
|
|
328
|
+
* @param {string[]} arr1 - First node ID array.
|
|
329
|
+
* @param {string[]} arr2 - Second node ID array.
|
|
330
|
+
* @returns {boolean} True when both arrays contain the same node IDs; otherwise false.
|
|
314
331
|
*/
|
|
332
|
+
#haveSameNodeIds(arr1, arr2) {
|
|
333
|
+
if (arr1.length !== arr2.length) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
const sortedArr1 = [...arr1].sort();
|
|
337
|
+
const sortedArr2 = [...arr2].sort();
|
|
338
|
+
return sortedArr1.every((value, index) => value === sortedArr2[index]);
|
|
339
|
+
}
|
|
315
340
|
|
|
316
341
|
/**
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
* @
|
|
342
|
+
* Clears the current highlight state from either HighlightLayer or overlay meshes.
|
|
343
|
+
* Also resets cached mesh/node tracking used to avoid redundant highlight updates.
|
|
344
|
+
* @private
|
|
345
|
+
* @returns {void}
|
|
321
346
|
*/
|
|
322
|
-
|
|
323
|
-
if (this.#
|
|
324
|
-
this.#highlightLayer
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
347
|
+
#removeHighlight() {
|
|
348
|
+
if (this.#useHighlightLayer) {
|
|
349
|
+
if (this.#highlightLayer) {
|
|
350
|
+
this.#highlightLayer.removeAllMeshes();
|
|
351
|
+
}
|
|
352
|
+
} else {
|
|
353
|
+
if (this.#overlayLayer) {
|
|
354
|
+
this.#overlayLayer.dispose();
|
|
355
|
+
this.#overlayLayer = null;
|
|
356
|
+
}
|
|
331
357
|
}
|
|
332
|
-
this
|
|
333
|
-
this.#
|
|
334
|
-
this.#openingAnimations.forEach((openingAnimation) => openingAnimation.dispose());
|
|
335
|
-
this.#openingAnimations = [];
|
|
358
|
+
this.#lastHighlightedMeshId = null;
|
|
359
|
+
this.#lastHighlightedNodeIds = [];
|
|
336
360
|
}
|
|
337
361
|
|
|
338
362
|
/**
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
363
|
+
* Applies highlighting for all meshes belonging to the provided animated node IDs.
|
|
364
|
+
* Depending on configuration, it uses Babylon's HighlightLayer or overlay meshes rendered
|
|
365
|
+
* in a utility layer for always-on-top outlines/fills.
|
|
366
|
+
* @private
|
|
367
|
+
* @param {string[]} nodeIds - Animated node IDs whose child meshes should be highlighted.
|
|
368
|
+
* @returns {void}
|
|
342
369
|
*/
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
return; // No need to update if hovering the same mesh
|
|
370
|
+
#addHighlight(nodeIds) {
|
|
371
|
+
this.#removeHighlight();
|
|
372
|
+
if (!nodeIds.length) {
|
|
373
|
+
return;
|
|
348
374
|
}
|
|
349
|
-
this.#lastHighlightedMeshId = pickedMeshId;
|
|
350
|
-
|
|
351
375
|
if (this.#useHighlightLayer) {
|
|
352
376
|
if (!this.#highlightLayer) {
|
|
353
377
|
this.#highlightLayer = new HighlightLayer("hl_animations", this.#scene);
|
|
354
378
|
}
|
|
355
|
-
|
|
356
|
-
this.#highlightLayer.removeAllMeshes();
|
|
357
|
-
|
|
358
|
-
if (!pickingInfo?.hit || !pickingInfo?.pickedMesh) {
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
const nodeIds = this.#getNodesAnimatedByMesh(pickingInfo.pickedMesh);
|
|
363
|
-
if (!nodeIds.length) {
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
379
|
const transformNodes = [];
|
|
368
380
|
nodeIds.forEach((nodeId) => {
|
|
369
381
|
const transformNode = this.#scene.getTransformNodeByID(nodeId);
|
|
@@ -383,20 +395,6 @@ export default class BabylonJSAnimationController {
|
|
|
383
395
|
}
|
|
384
396
|
});
|
|
385
397
|
} else {
|
|
386
|
-
if (this.#overlayLayer) {
|
|
387
|
-
this.#overlayLayer.dispose();
|
|
388
|
-
this.#overlayLayer = null;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
if (!pickingInfo?.hit || !pickingInfo?.pickedMesh) {
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const nodeIds = this.#getNodesAnimatedByMesh(pickingInfo.pickedMesh);
|
|
396
|
-
if (!nodeIds.length) {
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
398
|
const meshes = [];
|
|
401
399
|
nodeIds.forEach((nodeId) => {
|
|
402
400
|
const transformNode = this.#scene.getTransformNodeByID(nodeId);
|
|
@@ -408,6 +406,99 @@ export default class BabylonJSAnimationController {
|
|
|
408
406
|
|
|
409
407
|
this.#overlayLayer = this.#addGroupOutlineOverlayForInstances(meshes);
|
|
410
408
|
}
|
|
409
|
+
this.#lastHighlightedNodeIds = nodeIds;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* ---------------------------
|
|
414
|
+
* Public methods
|
|
415
|
+
* ---------------------------
|
|
416
|
+
*/
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Disposes all resources managed by the animation controller.
|
|
420
|
+
* Cleans up the highlight layer, animation menu, and internal animation/node lists.
|
|
421
|
+
* Should be called when the controller is no longer needed to prevent memory leaks.
|
|
422
|
+
* @public
|
|
423
|
+
*/
|
|
424
|
+
dispose() {
|
|
425
|
+
if (this.#highlightLayer) {
|
|
426
|
+
this.#highlightLayer.removeAllMeshes();
|
|
427
|
+
this.#highlightLayer.dispose();
|
|
428
|
+
this.#highlightLayer = null;
|
|
429
|
+
}
|
|
430
|
+
if (this.#overlayLayer) {
|
|
431
|
+
this.#overlayLayer.dispose();
|
|
432
|
+
this.#overlayLayer = null;
|
|
433
|
+
}
|
|
434
|
+
this.hideMenu();
|
|
435
|
+
this.#animatedNodes = [];
|
|
436
|
+
this.#openingAnimations.forEach((openingAnimation) => openingAnimation.dispose());
|
|
437
|
+
this.#openingAnimations = [];
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Indicates whether the current asset container exposes at least one opening/closing animation pair.
|
|
442
|
+
* @public
|
|
443
|
+
* @returns {boolean} True when interactive opening animations are available; otherwise false.
|
|
444
|
+
*/
|
|
445
|
+
hasAnimations() {
|
|
446
|
+
return this.#openingAnimations.length > 0;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Updates hover highlighting for meshes under animated nodes.
|
|
451
|
+
* Uses cached mesh/node state to avoid rebuilding highlight layers when the effective
|
|
452
|
+
* highlighted node set has not changed.
|
|
453
|
+
* @public
|
|
454
|
+
* @param {PickingInfo} pickingInfo - Raycast info from pointer position.
|
|
455
|
+
* @returns {{changed:boolean, highlighted:boolean}}
|
|
456
|
+
* Returns whether highlight visuals changed in this call and whether any mesh remains highlighted.
|
|
457
|
+
*/
|
|
458
|
+
highlightMeshes(pickingInfo) {
|
|
459
|
+
let changed = false;
|
|
460
|
+
let highlighted = false;
|
|
461
|
+
|
|
462
|
+
// Check if we're hovering the same mesh to avoid recreating overlays/highlights
|
|
463
|
+
const pickedMeshId = !pickingInfo?.hit || !pickingInfo?.pickedMesh?.id ? null : pickingInfo.pickedMesh.id;
|
|
464
|
+
const pickedMesh = pickingInfo?.pickedMesh ? pickingInfo.pickedMesh : null;
|
|
465
|
+
|
|
466
|
+
if (this.#lastHighlightedMeshId === pickedMeshId) {
|
|
467
|
+
// No need to update if hovering the same mesh
|
|
468
|
+
if (this.#lastHighlightedMeshId !== null) {
|
|
469
|
+
changed = false;
|
|
470
|
+
highlighted = true;
|
|
471
|
+
} else {
|
|
472
|
+
changed = false;
|
|
473
|
+
highlighted = false;
|
|
474
|
+
}
|
|
475
|
+
} else {
|
|
476
|
+
const nodeIds = this.#getNodesAnimatedByMesh(pickedMesh);
|
|
477
|
+
if (!nodeIds.length) {
|
|
478
|
+
if (this.#lastHighlightedMeshId !== null) {
|
|
479
|
+
this.#removeHighlight();
|
|
480
|
+
changed = true;
|
|
481
|
+
highlighted = false;
|
|
482
|
+
} else {
|
|
483
|
+
changed = false;
|
|
484
|
+
highlighted = false;
|
|
485
|
+
}
|
|
486
|
+
} else {
|
|
487
|
+
if (this.#haveSameNodeIds(nodeIds, this.#lastHighlightedNodeIds)) {
|
|
488
|
+
// No need to update if hovering a mesh under the same animated nodes
|
|
489
|
+
changed = false;
|
|
490
|
+
highlighted = true;
|
|
491
|
+
} else {
|
|
492
|
+
// Hovering a different mesh or same mesh under different animated nodes - update highlight
|
|
493
|
+
this.#addHighlight(nodeIds);
|
|
494
|
+
changed = true;
|
|
495
|
+
highlighted = true;
|
|
496
|
+
}
|
|
497
|
+
this.#lastHighlightedMeshId = pickedMeshId;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return { changed: changed, highlighted: highlighted };
|
|
411
502
|
}
|
|
412
503
|
|
|
413
504
|
/**
|
|
@@ -8,6 +8,7 @@ import OpeningAnimationMenu from "./babylonjs-animation-opening-menu.js";
|
|
|
8
8
|
* - Tracks animation state (paused, closed, opened, opening, closing).
|
|
9
9
|
* - Synchronizes animation progress and UI controls.
|
|
10
10
|
* - Handles loop mode and progress threshold logic.
|
|
11
|
+
* - Dispatches `prefviewer-animation-update` events with animation state snapshots.
|
|
11
12
|
* - Provides methods for play, pause, go to opened/closed, and progress control.
|
|
12
13
|
* - Manages the animation control menu (OpeningAnimationMenu) and its callbacks.
|
|
13
14
|
*
|
|
@@ -36,9 +37,11 @@ import OpeningAnimationMenu from "./babylonjs-animation-opening-menu.js";
|
|
|
36
37
|
* - #getCurrentFrame(): Gets the current frame based on state.
|
|
37
38
|
* - #getFrameFromProgress(progress): Calculates frame from progress value.
|
|
38
39
|
* - #getProgress(): Calculates progress (0-1) from current frame.
|
|
40
|
+
* - #getPrefViewer3DComponent(): Resolves and caches the nearest `pref-viewer-3d` host element.
|
|
39
41
|
* - #checkProgress(progress): Applies threshold logic to progress.
|
|
42
|
+
* - #notifyStateChange(): Emits `prefviewer-animation-update` with state/progress/loop metadata.
|
|
40
43
|
* - #updateControlsSlider(): Updates the slider in the control menu.
|
|
41
|
-
* - #updateControls():
|
|
44
|
+
* - #updateControls(): Emits state updates and syncs controls when the menu is visible.
|
|
42
45
|
*/
|
|
43
46
|
export default class OpeningAnimation {
|
|
44
47
|
static states = {
|
|
@@ -54,6 +57,7 @@ export default class OpeningAnimation {
|
|
|
54
57
|
#closeAnimation = null;
|
|
55
58
|
#nodes = [];
|
|
56
59
|
#menu = null;
|
|
60
|
+
#prefViewer3D = undefined; // Reference to parent custom elements for event dispatching
|
|
57
61
|
|
|
58
62
|
#state = OpeningAnimation.states.closed;
|
|
59
63
|
#lastPausedFrame = 0;
|
|
@@ -271,6 +275,29 @@ export default class OpeningAnimation {
|
|
|
271
275
|
return progress;
|
|
272
276
|
}
|
|
273
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Resolves and caches the closest `pref-viewer-3d` host associated with the animation canvas.
|
|
280
|
+
* @private
|
|
281
|
+
* @returns {void}
|
|
282
|
+
*/
|
|
283
|
+
#getPrefViewer3DComponent() {
|
|
284
|
+
if (this.#prefViewer3D !== undefined) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const scene = this.#openAnimation?.getScene?.();
|
|
289
|
+
const engine = scene?.getEngine?.();
|
|
290
|
+
const canvas = engine?.getRenderingCanvas?.();
|
|
291
|
+
|
|
292
|
+
let prefViewer3D = canvas?.closest?.("pref-viewer-3d") || null;
|
|
293
|
+
if (!prefViewer3D) {
|
|
294
|
+
const host = canvas?.getRootNode?.()?.host;
|
|
295
|
+
prefViewer3D = host?.nodeName === "PREF-VIEWER-3D" ? host : null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
this.#prefViewer3D = prefViewer3D;
|
|
299
|
+
}
|
|
300
|
+
|
|
274
301
|
/**
|
|
275
302
|
* Applies threshold logic to the progress value to snap to 0 or 1 if near the ends.
|
|
276
303
|
* Prevents floating point errors from leaving the animation in an in-between state.
|
|
@@ -287,6 +314,33 @@ export default class OpeningAnimation {
|
|
|
287
314
|
return progress;
|
|
288
315
|
}
|
|
289
316
|
|
|
317
|
+
/**
|
|
318
|
+
* Dispatches a `prefviewer-animation-update` CustomEvent from the nearest `pref-viewer-3d` host.
|
|
319
|
+
* The event bubbles and crosses shadow DOM boundaries, exposing the current animation snapshot
|
|
320
|
+
* (`name`, `state`, `progress`, and `loop`) in `event.detail`.
|
|
321
|
+
* @private
|
|
322
|
+
* @returns {boolean} True when the event was dispatched, false when no `pref-viewer-3d` host is available.
|
|
323
|
+
*/
|
|
324
|
+
#notifyStateChange() {
|
|
325
|
+
this.#getPrefViewer3DComponent();
|
|
326
|
+
if (!this.#prefViewer3D) {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const customEventOptions = {
|
|
331
|
+
bubbles: true,
|
|
332
|
+
composed: true,
|
|
333
|
+
detail: {
|
|
334
|
+
name: this.name,
|
|
335
|
+
state: this.#state,
|
|
336
|
+
progress: this.#getProgress(),
|
|
337
|
+
loop: this.#loop,
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
this.#prefViewer3D.dispatchEvent(new CustomEvent("prefviewer-animation-changed", customEventOptions));
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
|
|
290
344
|
/**
|
|
291
345
|
* Updates the slider value in the animation control menu to match the current progress.
|
|
292
346
|
* @private
|
|
@@ -299,10 +353,12 @@ export default class OpeningAnimation {
|
|
|
299
353
|
}
|
|
300
354
|
|
|
301
355
|
/**
|
|
302
|
-
*
|
|
356
|
+
* Broadcasts the latest animation state and, when the menu is visible, syncs its controls
|
|
357
|
+
* (buttons and progress slider) with the current state/progress values.
|
|
303
358
|
* @private
|
|
304
359
|
*/
|
|
305
360
|
#updateControls() {
|
|
361
|
+
this.#notifyStateChange();
|
|
306
362
|
if (!this.isControlsVisible()) {
|
|
307
363
|
return;
|
|
308
364
|
}
|
|
@@ -8,6 +8,7 @@ import JSZip from "jszip";
|
|
|
8
8
|
import GLTFResolver from "./gltf-resolver.js";
|
|
9
9
|
import { MaterialData } from "./pref-viewer-3d-data.js";
|
|
10
10
|
import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
|
|
11
|
+
import OpeningAnimation from "./babylonjs-animation-opening.js";
|
|
11
12
|
import { translate } from "./localization/i18n.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -38,6 +39,7 @@ import { translate } from "./localization/i18n.js";
|
|
|
38
39
|
* 5. Use `setContainerVisibility`, `setMaterialOptions`, `setCameraOptions`, or `setIBLOptions` for targeted updates; these
|
|
39
40
|
* helpers stop/restart the render loop while they rebuild camera-dependent resources.
|
|
40
41
|
* 6. Invoke `disable()` when the element disconnects to tear down scenes, XR sessions, observers, and handlers.
|
|
42
|
+
* The controller also disposes the shared GLTF resolver, which closes its internal IndexedDB handle.
|
|
41
43
|
*
|
|
42
44
|
* Public API Highlights
|
|
43
45
|
* - constructor(canvas, containers, options)
|
|
@@ -63,7 +65,6 @@ import { translate } from "./localization/i18n.js";
|
|
|
63
65
|
* in SSR/Node contexts (though functionality activates only in browsers).
|
|
64
66
|
*/
|
|
65
67
|
export default class BabylonJSController {
|
|
66
|
-
|
|
67
68
|
#RENDER_SETTINGS_STORAGE_KEY = "pref-viewer/render-settings";
|
|
68
69
|
|
|
69
70
|
// Default render settings
|
|
@@ -75,7 +76,7 @@ export default class BabylonJSController {
|
|
|
75
76
|
iblEnabled: true,
|
|
76
77
|
shadowsEnabled: false,
|
|
77
78
|
};
|
|
78
|
-
|
|
79
|
+
|
|
79
80
|
// Canvas HTML element
|
|
80
81
|
#canvas = null;
|
|
81
82
|
|
|
@@ -93,11 +94,13 @@ export default class BabylonJSController {
|
|
|
93
94
|
#shadowGen = [];
|
|
94
95
|
#XRExperience = null;
|
|
95
96
|
#canvasResizeObserver = null;
|
|
97
|
+
|
|
96
98
|
#hdrTexture = null; // reusable in-memory HDR source cloned into scene.environmentTexture across reloads
|
|
97
|
-
|
|
99
|
+
#lastPickedMeshId = null;
|
|
100
|
+
|
|
98
101
|
#containers = {};
|
|
99
102
|
#options = {};
|
|
100
|
-
|
|
103
|
+
|
|
101
104
|
#gltfResolver = null; // GLTFResolver instance
|
|
102
105
|
#babylonJSAnimationController = null; // AnimationController instance
|
|
103
106
|
|
|
@@ -110,11 +113,28 @@ export default class BabylonJSController {
|
|
|
110
113
|
#handlers = {
|
|
111
114
|
onKeyUp: null,
|
|
112
115
|
onPointerObservable: null,
|
|
116
|
+
onAnimationGroupChanged: null,
|
|
117
|
+
onResize: null,
|
|
113
118
|
renderLoop: null,
|
|
114
119
|
};
|
|
115
|
-
|
|
120
|
+
|
|
116
121
|
#settings = { ...BabylonJSController.DEFAULT_RENDER_SETTINGS };
|
|
117
122
|
|
|
123
|
+
#renderState = {
|
|
124
|
+
isLoopRunning: false,
|
|
125
|
+
dirtyFrames: 0,
|
|
126
|
+
continuousUntil: 0,
|
|
127
|
+
lastRenderAt: 0,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
#renderConfig = {
|
|
131
|
+
burstFramesBase: 2,
|
|
132
|
+
burstFramesEnhanced: 32, // when AA/SSAO/IBL is enabled, more frames are needed to reach stable output
|
|
133
|
+
interactionMs: 250,
|
|
134
|
+
animationMs: 200,
|
|
135
|
+
idleThrottleMs: 1000 / 15,
|
|
136
|
+
};
|
|
137
|
+
|
|
118
138
|
/**
|
|
119
139
|
* Constructs a new BabylonJSController instance.
|
|
120
140
|
* Initializes the canvas, asset containers, and options for the Babylon.js scene.
|
|
@@ -147,8 +167,10 @@ export default class BabylonJSController {
|
|
|
147
167
|
* @returns {void}
|
|
148
168
|
*/
|
|
149
169
|
#bindHandlers() {
|
|
170
|
+
this.#handlers.onAnimationGroupChanged = this.#onAnimationGroupChanged.bind(this);
|
|
150
171
|
this.#handlers.onKeyUp = this.#onKeyUp.bind(this);
|
|
151
172
|
this.#handlers.onPointerObservable = this.#onPointerObservable.bind(this);
|
|
173
|
+
this.#handlers.onResize = this.#onResize.bind(this);
|
|
152
174
|
this.#handlers.renderLoop = this.#renderLoop.bind(this);
|
|
153
175
|
}
|
|
154
176
|
|
|
@@ -271,16 +293,177 @@ export default class BabylonJSController {
|
|
|
271
293
|
}
|
|
272
294
|
}
|
|
273
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Starts Babylon's engine render loop if it is not already running.
|
|
298
|
+
* @private
|
|
299
|
+
* @returns {boolean} True when the loop was started, false when no engine is available or it was already running.
|
|
300
|
+
*/
|
|
301
|
+
#startEngineRenderLoop() {
|
|
302
|
+
if (!this.#engine || this.#renderState.isLoopRunning) {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
this.#engine.runRenderLoop(this.#handlers.renderLoop);
|
|
306
|
+
this.#renderState.isLoopRunning = true;
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Stops Babylon's engine render loop when it is currently active.
|
|
312
|
+
* @private
|
|
313
|
+
* @returns {boolean} True when the loop was stopped, false when no engine is available or it was already stopped.
|
|
314
|
+
*/
|
|
315
|
+
#stopEngineRenderLoop() {
|
|
316
|
+
if (!this.#engine || !this.#renderState.isLoopRunning) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
this.#engine.stopRenderLoop(this.#handlers.renderLoop);
|
|
320
|
+
this.#renderState.isLoopRunning = false;
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Marks the scene as dirty and optionally extends a short continuous-render window.
|
|
326
|
+
* Ensures the engine loop is running so the requested frames can be produced.
|
|
327
|
+
* @private
|
|
328
|
+
* @param {{frames?:number, continuousMs?:number}} [options={}] - Render request options.
|
|
329
|
+
* @param {number} [options.frames=1] - Minimum number of frames to render.
|
|
330
|
+
* @param {number} [options.continuousMs=0] - Milliseconds to keep continuous rendering active.
|
|
331
|
+
* @returns {boolean} True when the request was accepted, false when scene/engine are unavailable.
|
|
332
|
+
*/
|
|
333
|
+
#requestRender({ frames = 1, continuousMs = 0 } = {}) {
|
|
334
|
+
if (!this.#scene || !this.#engine) {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
339
|
+
this.#renderState.dirtyFrames = Math.max(this.#renderState.dirtyFrames, Math.max(1, frames));
|
|
340
|
+
if (continuousMs > 0) {
|
|
341
|
+
this.#renderState.continuousUntil = Math.max(this.#renderState.continuousUntil, now + continuousMs);
|
|
342
|
+
}
|
|
343
|
+
this.#startEngineRenderLoop();
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Checks whether an ArcRotateCamera still has non-zero inertial movement.
|
|
349
|
+
* @private
|
|
350
|
+
* @param {ArcRotateCamera} camera - Camera to evaluate.
|
|
351
|
+
* @returns {boolean} True when any inertial offset is still active.
|
|
352
|
+
*/
|
|
353
|
+
#isArcRotateCameraInMotion(camera) {
|
|
354
|
+
const EPSILON = 0.00001;
|
|
355
|
+
return Math.abs(camera?.inertialAlphaOffset || 0) > EPSILON || Math.abs(camera?.inertialBetaOffset || 0) > EPSILON || Math.abs(camera?.inertialRadiusOffset || 0) > EPSILON || Math.abs(camera?.inertialPanningX || 0) > EPSILON || Math.abs(camera?.inertialPanningY || 0) > EPSILON;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Checks whether a FreeCamera/UniversalCamera is currently moving or rotating.
|
|
360
|
+
* @private
|
|
361
|
+
* @param {FreeCamera|UniversalCamera} camera - Camera to evaluate.
|
|
362
|
+
* @returns {boolean} True when translation or rotation deltas are active.
|
|
363
|
+
*/
|
|
364
|
+
#isUniversalOrFreeCameraInMotion(camera) {
|
|
365
|
+
const EPSILON = 0.00001;
|
|
366
|
+
const direction = camera?.cameraDirection;
|
|
367
|
+
const rotation = camera?.cameraRotation;
|
|
368
|
+
const directionMoving = !!direction && (Math.abs(direction.x) > EPSILON || Math.abs(direction.y) > EPSILON || Math.abs(direction.z) > EPSILON);
|
|
369
|
+
const rotationMoving = !!rotation && (Math.abs(rotation.x) > EPSILON || Math.abs(rotation.y) > EPSILON || Math.abs(rotation.z) > EPSILON);
|
|
370
|
+
return directionMoving || rotationMoving;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Detects motion for the current active camera based on its concrete camera type.
|
|
375
|
+
* @private
|
|
376
|
+
* @returns {boolean} True when the active camera is moving, otherwise false.
|
|
377
|
+
*/
|
|
378
|
+
#isCameraInMotion() {
|
|
379
|
+
const camera = this.#scene?.activeCamera;
|
|
380
|
+
if (!camera) {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
if (camera instanceof ArcRotateCamera) {
|
|
384
|
+
return this.#isArcRotateCameraInMotion(camera);
|
|
385
|
+
}
|
|
386
|
+
if (camera instanceof UniversalCamera || camera instanceof FreeCamera) {
|
|
387
|
+
return this.#isUniversalOrFreeCameraInMotion(camera);
|
|
388
|
+
}
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Determines whether scene animations are currently running.
|
|
394
|
+
* @private
|
|
395
|
+
* @returns {boolean} True when at least one animation group is playing.
|
|
396
|
+
*/
|
|
397
|
+
#isAnimationRunning() {
|
|
398
|
+
if (!this.#scene) {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
const hasAnimatables = (this.#scene.animatables?.length || 0) > 0;
|
|
402
|
+
if (!hasAnimatables) {
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
return this.#scene.animationGroups?.some((group) => group?.isPlaying) || false;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Evaluates whether the renderer should stay in continuous mode.
|
|
410
|
+
* XR always forces continuous rendering; animation/camera motion also extends the
|
|
411
|
+
* continuous deadline window to avoid abrupt stop-start behavior.
|
|
412
|
+
* @private
|
|
413
|
+
* @param {number} now - Current high-resolution timestamp.
|
|
414
|
+
* @returns {boolean} True when continuous rendering should remain active.
|
|
415
|
+
*/
|
|
416
|
+
#shouldRenderContinuously(now) {
|
|
417
|
+
const inXR = this.#XRExperience?.baseExperience?.state === WebXRState.IN_XR;
|
|
418
|
+
if (inXR) {
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const animationRunning = this.#isAnimationRunning();
|
|
423
|
+
const cameraInMotion = this.#isCameraInMotion();
|
|
424
|
+
|
|
425
|
+
if (animationRunning) {
|
|
426
|
+
this.#renderState.continuousUntil = Math.max(this.#renderState.continuousUntil, now + this.#renderConfig.animationMs);
|
|
427
|
+
}
|
|
428
|
+
if (cameraInMotion) {
|
|
429
|
+
this.#renderState.continuousUntil = Math.max(this.#renderState.continuousUntil, now + this.#renderConfig.interactionMs);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return animationRunning || cameraInMotion || this.#renderState.continuousUntil > now;
|
|
433
|
+
}
|
|
434
|
+
|
|
274
435
|
/**
|
|
275
436
|
* Render loop callback for Babylon.js.
|
|
437
|
+
* Runs only while scene state is dirty, interactive motion is active, animations are running, or XR is active.
|
|
438
|
+
* It self-stops when the scene becomes idle.
|
|
276
439
|
* @private
|
|
277
440
|
* @returns {void}
|
|
278
|
-
* @description
|
|
279
|
-
* Continuously renders the current scene if it exists.
|
|
280
|
-
* Used by the engine's runRenderLoop method to update the view.
|
|
281
441
|
*/
|
|
282
442
|
#renderLoop() {
|
|
283
|
-
|
|
443
|
+
if (!this.#scene) {
|
|
444
|
+
this.#stopEngineRenderLoop();
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
449
|
+
const continuous = this.#shouldRenderContinuously(now);
|
|
450
|
+
const needsRender = continuous || this.#renderState.dirtyFrames > 0;
|
|
451
|
+
|
|
452
|
+
if (!needsRender) {
|
|
453
|
+
this.#stopEngineRenderLoop();
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (!continuous && this.#renderState.lastRenderAt > 0 && now - this.#renderState.lastRenderAt < this.#renderConfig.idleThrottleMs) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
this.#scene.render();
|
|
462
|
+
this.#renderState.lastRenderAt = now;
|
|
463
|
+
|
|
464
|
+
if (this.#renderState.dirtyFrames > 0) {
|
|
465
|
+
this.#renderState.dirtyFrames -= 1;
|
|
466
|
+
}
|
|
284
467
|
}
|
|
285
468
|
|
|
286
469
|
/**
|
|
@@ -417,24 +600,20 @@ export default class BabylonJSController {
|
|
|
417
600
|
this.#scene.environmentTexture = null;
|
|
418
601
|
lightsChanged = true;
|
|
419
602
|
}
|
|
420
|
-
if (this.#hdrTexture) {
|
|
421
|
-
this.#hdrTexture.dispose();
|
|
422
|
-
this.#hdrTexture = null;
|
|
423
|
-
}
|
|
424
603
|
|
|
425
604
|
// Add a hemispheric light for basic ambient illumination
|
|
426
605
|
if (!this.#hemiLight) {
|
|
427
606
|
this.#hemiLight = new HemisphericLight(hemiLightName, new Vector3(-10, 10, -10), this.#scene);
|
|
428
607
|
this.#hemiLight.intensity = 0.6;
|
|
429
608
|
}
|
|
430
|
-
|
|
609
|
+
|
|
431
610
|
// Add a directional light to cast shadows and provide stronger directional illumination
|
|
432
611
|
if (!this.#dirLight) {
|
|
433
612
|
this.#dirLight = new DirectionalLight(dirLightName, new Vector3(-10, 10, -10), this.#scene);
|
|
434
613
|
this.#dirLight.position = new Vector3(5, 4, 5); // light is IN FRONT + ABOVE + to the RIGHT
|
|
435
614
|
this.#dirLight.intensity = 0.6;
|
|
436
615
|
}
|
|
437
|
-
|
|
616
|
+
|
|
438
617
|
// Add a point light that follows the camera to ensure the model is always well-lit from the viewer's perspective
|
|
439
618
|
if (!this.#cameraLight) {
|
|
440
619
|
this.#cameraLight = new PointLight(cameraLightName, this.#camera.position, this.#scene);
|
|
@@ -459,7 +638,7 @@ export default class BabylonJSController {
|
|
|
459
638
|
}
|
|
460
639
|
|
|
461
640
|
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
462
|
-
|
|
641
|
+
|
|
463
642
|
if (supportedPipelines === undefined) {
|
|
464
643
|
return false;
|
|
465
644
|
}
|
|
@@ -499,23 +678,23 @@ export default class BabylonJSController {
|
|
|
499
678
|
return false;
|
|
500
679
|
}
|
|
501
680
|
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
502
|
-
|
|
681
|
+
|
|
503
682
|
if (supportedPipelines === undefined) {
|
|
504
683
|
return false;
|
|
505
684
|
}
|
|
506
|
-
|
|
685
|
+
|
|
507
686
|
const pipelineName = "PrefViewerSSAORenderingPipeline";
|
|
508
687
|
|
|
509
688
|
const ssaoRatio = {
|
|
510
689
|
ssaoRatio: 0.5,
|
|
511
|
-
combineRatio: 1.0
|
|
690
|
+
combineRatio: 1.0,
|
|
512
691
|
};
|
|
513
692
|
|
|
514
693
|
let ssaoPipeline = new SSAORenderingPipeline(pipelineName, this.#scene, ssaoRatio, [this.#scene.activeCamera]);
|
|
515
694
|
|
|
516
|
-
if (!ssaoPipeline){
|
|
695
|
+
if (!ssaoPipeline) {
|
|
517
696
|
return false;
|
|
518
|
-
}
|
|
697
|
+
}
|
|
519
698
|
|
|
520
699
|
if (ssaoPipeline.isSupported) {
|
|
521
700
|
ssaoPipeline.fallOff = 0.000001;
|
|
@@ -523,7 +702,7 @@ export default class BabylonJSController {
|
|
|
523
702
|
ssaoPipeline.radius = 0.0001;
|
|
524
703
|
ssaoPipeline.totalStrength = 1;
|
|
525
704
|
ssaoPipeline.base = 0.6;
|
|
526
|
-
|
|
705
|
+
|
|
527
706
|
// Configure SSAO to calculate only once instead of every frame for better performance
|
|
528
707
|
if (ssaoPipeline._ssaoPostProcess) {
|
|
529
708
|
ssaoPipeline._ssaoPostProcess.autoClear = false;
|
|
@@ -533,7 +712,7 @@ export default class BabylonJSController {
|
|
|
533
712
|
ssaoPipeline._combinePostProcess.autoClear = false;
|
|
534
713
|
ssaoPipeline._combinePostProcess.samples = 1;
|
|
535
714
|
}
|
|
536
|
-
|
|
715
|
+
|
|
537
716
|
this.#renderPipelines.ssao = ssaoPipeline;
|
|
538
717
|
pipelineManager.update();
|
|
539
718
|
return true;
|
|
@@ -559,7 +738,7 @@ export default class BabylonJSController {
|
|
|
559
738
|
}
|
|
560
739
|
|
|
561
740
|
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
562
|
-
|
|
741
|
+
|
|
563
742
|
if (supportedPipelines === undefined) {
|
|
564
743
|
return false;
|
|
565
744
|
}
|
|
@@ -600,18 +779,18 @@ export default class BabylonJSController {
|
|
|
600
779
|
return false;
|
|
601
780
|
}
|
|
602
781
|
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
603
|
-
|
|
782
|
+
|
|
604
783
|
if (supportedPipelines === undefined) {
|
|
605
784
|
return false;
|
|
606
785
|
}
|
|
607
|
-
|
|
786
|
+
|
|
608
787
|
const pipelineName = "PrefViewerDefaultRenderingPipeline";
|
|
609
788
|
|
|
610
789
|
let defaultPipeline = new DefaultRenderingPipeline(pipelineName, true, this.#scene, [this.#scene.activeCamera], true);
|
|
611
790
|
|
|
612
|
-
if (!defaultPipeline){
|
|
791
|
+
if (!defaultPipeline) {
|
|
613
792
|
return false;
|
|
614
|
-
}
|
|
793
|
+
}
|
|
615
794
|
|
|
616
795
|
if (defaultPipeline.isSupported) {
|
|
617
796
|
// MSAA - Multisample Anti-Aliasing
|
|
@@ -716,7 +895,7 @@ export default class BabylonJSController {
|
|
|
716
895
|
}
|
|
717
896
|
|
|
718
897
|
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
719
|
-
|
|
898
|
+
|
|
720
899
|
if (supportedPipelines === undefined) {
|
|
721
900
|
return false;
|
|
722
901
|
}
|
|
@@ -751,13 +930,13 @@ export default class BabylonJSController {
|
|
|
751
930
|
* @private
|
|
752
931
|
* @returns {Promise<void|boolean>} Returns false if no environment texture is set; otherwise void.
|
|
753
932
|
*/
|
|
754
|
-
async #initializeIBLShadows() {
|
|
933
|
+
async #initializeIBLShadows() {
|
|
755
934
|
const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
|
|
756
|
-
|
|
935
|
+
|
|
757
936
|
if (!this.#scene || !this.#scene?.activeCamera || !this.#scene?.environmentTexture || !pipelineManager) {
|
|
758
937
|
return false;
|
|
759
938
|
}
|
|
760
|
-
|
|
939
|
+
|
|
761
940
|
if (!this.#scene.environmentTexture.isReady()) {
|
|
762
941
|
const self = this;
|
|
763
942
|
this.#scene.environmentTexture.onLoadObservable.addOnce(() => {
|
|
@@ -771,11 +950,11 @@ export default class BabylonJSController {
|
|
|
771
950
|
if (isRootMesh) {
|
|
772
951
|
return false;
|
|
773
952
|
}
|
|
774
|
-
|
|
953
|
+
|
|
775
954
|
const isHDRIMesh = mesh.name?.toLowerCase() === "hdri";
|
|
776
955
|
const extrasCastShadows = mesh.metadata?.gltf?.extras?.castShadows;
|
|
777
956
|
const meshGenerateShadows = typeof extrasCastShadows === "boolean" ? extrasCastShadows : isHDRIMesh ? false : true;
|
|
778
|
-
|
|
957
|
+
|
|
779
958
|
if (meshGenerateShadows) {
|
|
780
959
|
return true;
|
|
781
960
|
}
|
|
@@ -793,7 +972,7 @@ export default class BabylonJSController {
|
|
|
793
972
|
}
|
|
794
973
|
|
|
795
974
|
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
796
|
-
|
|
975
|
+
|
|
797
976
|
if (!supportedPipelines) {
|
|
798
977
|
return false;
|
|
799
978
|
}
|
|
@@ -816,7 +995,7 @@ export default class BabylonJSController {
|
|
|
816
995
|
if (!iblShadowsPipeline) {
|
|
817
996
|
return false;
|
|
818
997
|
}
|
|
819
|
-
|
|
998
|
+
|
|
820
999
|
if (iblShadowsPipeline.isSupported) {
|
|
821
1000
|
// Disable all debug passes for performance
|
|
822
1001
|
const pipelineProps = {
|
|
@@ -835,7 +1014,7 @@ export default class BabylonJSController {
|
|
|
835
1014
|
|
|
836
1015
|
meshesForCastingShadows.forEach((mesh) => iblShadowsPipeline.addShadowCastingMesh(mesh));
|
|
837
1016
|
materialsForReceivingShadows.forEach((material) => iblShadowsPipeline.addShadowReceivingMaterial(material));
|
|
838
|
-
|
|
1017
|
+
|
|
839
1018
|
iblShadowsPipeline.updateSceneBounds();
|
|
840
1019
|
iblShadowsPipeline.toggleShadow(true);
|
|
841
1020
|
iblShadowsPipeline.updateVoxelization();
|
|
@@ -1002,23 +1181,6 @@ export default class BabylonJSController {
|
|
|
1002
1181
|
}
|
|
1003
1182
|
}
|
|
1004
1183
|
|
|
1005
|
-
/**
|
|
1006
|
-
* Handles pointer events observed on the Babylon.js scene.
|
|
1007
|
-
* @private
|
|
1008
|
-
* @param {PointerInfo} info - The pointer event information from Babylon.js.
|
|
1009
|
-
* @returns {void}
|
|
1010
|
-
*/
|
|
1011
|
-
#onPointerObservable(info) {
|
|
1012
|
-
const pickInfo = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
|
|
1013
|
-
if (info.type === PointerEventTypes.POINTERUP) {
|
|
1014
|
-
this.#onPointerUp(info.event, pickInfo);
|
|
1015
|
-
} else if (info.type === PointerEventTypes.POINTERMOVE) {
|
|
1016
|
-
this.#onPointerMove(info.event, pickInfo);
|
|
1017
|
-
} else if (info.type === PointerEventTypes.POINTERWHEEL) {
|
|
1018
|
-
this.#onMouseWheel(info.event, pickInfo);
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
1184
|
/**
|
|
1023
1185
|
* Sets up interaction handlers for the Babylon.js canvas and scene.
|
|
1024
1186
|
* @private
|
|
@@ -1031,6 +1193,13 @@ export default class BabylonJSController {
|
|
|
1031
1193
|
if (this.#scene) {
|
|
1032
1194
|
this.#scene.onPointerObservable.add(this.#handlers.onPointerObservable);
|
|
1033
1195
|
}
|
|
1196
|
+
if (this.#engine) {
|
|
1197
|
+
this.#canvasResizeObserver = new ResizeObserver(() => {
|
|
1198
|
+
this.#engine.resize();
|
|
1199
|
+
this.#requestRender({ frames: this.#renderConfig.burstFramesBase, continuousMs: this.#renderConfig.interactionMs });
|
|
1200
|
+
});
|
|
1201
|
+
this.#canvasResizeObserver.observe(this.#canvas);
|
|
1202
|
+
}
|
|
1034
1203
|
}
|
|
1035
1204
|
|
|
1036
1205
|
/**
|
|
@@ -1045,6 +1214,9 @@ export default class BabylonJSController {
|
|
|
1045
1214
|
if (this.#scene !== null) {
|
|
1046
1215
|
this.#scene.onPointerObservable.removeCallback(this.#handlers.onPointerObservable);
|
|
1047
1216
|
}
|
|
1217
|
+
this.#canvasResizeObserver?.disconnect();
|
|
1218
|
+
this.#canvasResizeObserver = null;
|
|
1219
|
+
this.#detachAnimationChangedListener();
|
|
1048
1220
|
}
|
|
1049
1221
|
|
|
1050
1222
|
/**
|
|
@@ -1059,6 +1231,18 @@ export default class BabylonJSController {
|
|
|
1059
1231
|
}
|
|
1060
1232
|
}
|
|
1061
1233
|
|
|
1234
|
+
/**
|
|
1235
|
+
* Disposes the shared GLTFResolver instance and closes its underlying storage handle.
|
|
1236
|
+
* @private
|
|
1237
|
+
* @returns {void}
|
|
1238
|
+
*/
|
|
1239
|
+
#disposeGLTFResolver() {
|
|
1240
|
+
if (this.#gltfResolver) {
|
|
1241
|
+
this.#gltfResolver.dispose();
|
|
1242
|
+
this.#gltfResolver = null;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1062
1246
|
/**
|
|
1063
1247
|
* Disposes the Babylon.js WebXR experience if it exists.
|
|
1064
1248
|
* @private
|
|
@@ -1105,6 +1289,75 @@ export default class BabylonJSController {
|
|
|
1105
1289
|
this.#hemiLight = this.#dirLight = this.#cameraLight = null;
|
|
1106
1290
|
}
|
|
1107
1291
|
|
|
1292
|
+
/**
|
|
1293
|
+
* Handles animation state events emitted by `OpeningAnimation` instances.
|
|
1294
|
+
* Routes opening/closing states to continuous rendering and all other states
|
|
1295
|
+
* (paused/opened/closed) to a short final render burst.
|
|
1296
|
+
* @private
|
|
1297
|
+
* @param {CustomEvent} event - Event carrying animation state in `event.detail.state`.
|
|
1298
|
+
* @returns {void}
|
|
1299
|
+
*/
|
|
1300
|
+
#onAnimationGroupChanged(event) {
|
|
1301
|
+
const state = event?.detail?.state;
|
|
1302
|
+
if (state === undefined) {
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
if (state === OpeningAnimation.states.opening || state === OpeningAnimation.states.closing) {
|
|
1307
|
+
this.#onAnimationGroupPlay();
|
|
1308
|
+
} else {
|
|
1309
|
+
this.#onAnimationGroupStop();
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
/**
|
|
1314
|
+
* Marks animation playback as active and requests short continuous rendering so animated transforms remain smooth while state is changing.
|
|
1315
|
+
* @private
|
|
1316
|
+
* @returns {void}
|
|
1317
|
+
*/
|
|
1318
|
+
#onAnimationGroupPlay() {
|
|
1319
|
+
this.#requestRender({ frames: 1, continuousMs: this.#renderConfig.animationMs });
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/**
|
|
1323
|
+
* Handles animation stop/pause/end transitions by requesting a final render burst.
|
|
1324
|
+
* @private
|
|
1325
|
+
* @returns {void}
|
|
1326
|
+
*/
|
|
1327
|
+
#onAnimationGroupStop() {
|
|
1328
|
+
const frames = this.#settings.antiAliasingEnabled || this.#settings.ambientOcclusionEnabled || this.#settings.iblEnabled ? this.#renderConfig.burstFramesEnhanced : this.#renderConfig.burstFramesBase;
|
|
1329
|
+
this.#requestRender({ frames: frames });
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
/**
|
|
1333
|
+
* Attaches the `prefviewer-animation-changed` listener to the nearest `pref-viewer-3d` host.
|
|
1334
|
+
* Removes any previous registration first to avoid duplicate callbacks across reload cycles.
|
|
1335
|
+
* @private
|
|
1336
|
+
* @returns {boolean} True when the listener is attached, false when no host is available.
|
|
1337
|
+
*/
|
|
1338
|
+
#attachAnimationChangedListener() {
|
|
1339
|
+
this.#getPrefViewer3DComponent();
|
|
1340
|
+
if (!this.#prefViewer3D) {
|
|
1341
|
+
return false;
|
|
1342
|
+
}
|
|
1343
|
+
this.#detachAnimationChangedListener();
|
|
1344
|
+
this.#prefViewer3D.addEventListener("prefviewer-animation-changed", this.#handlers.onAnimationGroupChanged);
|
|
1345
|
+
return true;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/**
|
|
1349
|
+
* Detaches the `prefviewer-animation-changed` listener from the cached `pref-viewer-3d` host.
|
|
1350
|
+
* @private
|
|
1351
|
+
* @returns {boolean} True when a host exists and the listener removal was attempted, false otherwise.
|
|
1352
|
+
*/
|
|
1353
|
+
#detachAnimationChangedListener() {
|
|
1354
|
+
if (!this.#prefViewer3D) {
|
|
1355
|
+
return false;
|
|
1356
|
+
}
|
|
1357
|
+
this.#prefViewer3D.removeEventListener("prefviewer-animation-changed", this.#handlers.onAnimationGroupChanged);
|
|
1358
|
+
return true;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1108
1361
|
/**
|
|
1109
1362
|
* Handles keyup events on the Babylon.js canvas for triggering model and scene downloads.
|
|
1110
1363
|
* @private
|
|
@@ -1132,11 +1385,12 @@ export default class BabylonJSController {
|
|
|
1132
1385
|
* @returns {void|false} Returns false if there is no active camera; otherwise, void.
|
|
1133
1386
|
*/
|
|
1134
1387
|
#onMouseWheel(event, pickInfo) {
|
|
1388
|
+
event.preventDefault();
|
|
1135
1389
|
const camera = this.#scene?.activeCamera;
|
|
1136
1390
|
if (!camera) {
|
|
1137
1391
|
return false;
|
|
1138
1392
|
}
|
|
1139
|
-
if (!camera.metadata?.locked) {
|
|
1393
|
+
if (!camera.metadata?.locked) {
|
|
1140
1394
|
if (camera instanceof ArcRotateCamera) {
|
|
1141
1395
|
camera.wheelPrecision = camera.wheelPrecision || 3.0;
|
|
1142
1396
|
camera.inertialRadiusOffset -= event.deltaY * camera.wheelPrecision * 0.001;
|
|
@@ -1149,8 +1403,8 @@ export default class BabylonJSController {
|
|
|
1149
1403
|
const movementVector = direction.scale(zoomSpeed);
|
|
1150
1404
|
camera.position = camera.position.add(movementVector);
|
|
1151
1405
|
}
|
|
1406
|
+
this.#requestRender({ frames: 1, continuousMs: this.#renderConfig.interactionMs });
|
|
1152
1407
|
}
|
|
1153
|
-
event.preventDefault();
|
|
1154
1408
|
}
|
|
1155
1409
|
|
|
1156
1410
|
/**
|
|
@@ -1178,9 +1432,54 @@ export default class BabylonJSController {
|
|
|
1178
1432
|
* @returns {void}
|
|
1179
1433
|
*/
|
|
1180
1434
|
#onPointerMove(event, pickInfo) {
|
|
1435
|
+
const camera = this.#scene?.activeCamera;
|
|
1436
|
+
if (camera && !camera.metadata?.locked) {
|
|
1437
|
+
this.#requestRender({ frames: 1, continuousMs: this.#renderConfig.interactionMs });
|
|
1438
|
+
}
|
|
1181
1439
|
if (this.#babylonJSAnimationController) {
|
|
1182
|
-
|
|
1440
|
+
const pickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
1441
|
+
if (this.#lastPickedMeshId !== pickedMeshId) {
|
|
1442
|
+
const highlightResult = this.#babylonJSAnimationController.highlightMeshes(pickInfo);
|
|
1443
|
+
if (highlightResult.changed) {
|
|
1444
|
+
this.#requestRender({ frames: 1, continuousMs: this.#renderConfig.interactionMs });
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
/**
|
|
1451
|
+
* Handles pointer events observed on the Babylon.js scene.
|
|
1452
|
+
* @private
|
|
1453
|
+
* @param {PointerInfo} info - The pointer event information from Babylon.js.
|
|
1454
|
+
* @returns {void}
|
|
1455
|
+
*/
|
|
1456
|
+
#onPointerObservable(info) {
|
|
1457
|
+
const pickInfo = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
|
|
1458
|
+
const pickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
1459
|
+
|
|
1460
|
+
if (info.type === PointerEventTypes.POINTERMOVE) {
|
|
1461
|
+
this.#onPointerMove(info.event, pickInfo);
|
|
1462
|
+
} else if (info.type === PointerEventTypes.POINTERUP) {
|
|
1463
|
+
this.#onPointerUp(info.event, pickInfo);
|
|
1464
|
+
} else if (info.type === PointerEventTypes.POINTERWHEEL) {
|
|
1465
|
+
this.#onMouseWheel(info.event, pickInfo);
|
|
1183
1466
|
}
|
|
1467
|
+
this.#lastPickedMeshId = pickedMeshId;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* Handles canvas resize notifications.
|
|
1472
|
+
* Resizes the Babylon engine and requests a short on-demand render burst so camera-dependent
|
|
1473
|
+
* buffers and post-process pipelines are redrawn at the new viewport size.
|
|
1474
|
+
* @private
|
|
1475
|
+
* @returns {void}
|
|
1476
|
+
*/
|
|
1477
|
+
#onResize() {
|
|
1478
|
+
if (!this.#engine) {
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
this.#engine.resize();
|
|
1482
|
+
this.#requestRender({ frames: this.#renderConfig.burstFramesBase, continuousMs: this.#renderConfig.interactionMs });
|
|
1184
1483
|
}
|
|
1185
1484
|
|
|
1186
1485
|
/**
|
|
@@ -1222,7 +1521,7 @@ export default class BabylonJSController {
|
|
|
1222
1521
|
.forEach((mesh) => {
|
|
1223
1522
|
mesh.material = material;
|
|
1224
1523
|
someSetted = true;
|
|
1225
|
-
})
|
|
1524
|
+
}),
|
|
1226
1525
|
);
|
|
1227
1526
|
|
|
1228
1527
|
if (someSetted) {
|
|
@@ -1336,34 +1635,47 @@ export default class BabylonJSController {
|
|
|
1336
1635
|
}
|
|
1337
1636
|
|
|
1338
1637
|
/**
|
|
1339
|
-
*
|
|
1638
|
+
* Resolves and caches the closest `pref-viewer-3d` host associated with the rendering canvas.
|
|
1340
1639
|
* @private
|
|
1341
1640
|
* @returns {void}
|
|
1342
1641
|
*/
|
|
1343
1642
|
#getPrefViewer3DComponent() {
|
|
1344
|
-
if (this.#prefViewer3D
|
|
1345
|
-
|
|
1346
|
-
|
|
1643
|
+
if (this.#prefViewer3D !== undefined) {
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
let prefViewer3D = this.#canvas?.closest?.("pref-viewer-3d") || undefined;
|
|
1648
|
+
if (!prefViewer3D) {
|
|
1649
|
+
const host = this.#canvas?.getRootNode?.()?.host;
|
|
1650
|
+
prefViewer3D = host?.nodeName === "PREF-VIEWER-3D" ? host : undefined;
|
|
1347
1651
|
}
|
|
1652
|
+
|
|
1653
|
+
this.#prefViewer3D = prefViewer3D;
|
|
1348
1654
|
}
|
|
1349
1655
|
|
|
1350
1656
|
/**
|
|
1351
|
-
*
|
|
1657
|
+
* Resolves and caches the closest `pref-viewer` host associated with `#prefViewer3D`.
|
|
1352
1658
|
* @private
|
|
1353
1659
|
* @returns {void}
|
|
1354
1660
|
*/
|
|
1355
1661
|
#getPrefViewerComponent() {
|
|
1356
|
-
if (this.#prefViewer
|
|
1357
|
-
|
|
1358
|
-
this.#getPrefViewer3DComponent();
|
|
1359
|
-
}
|
|
1360
|
-
if (!this.#prefViewer3D) {
|
|
1361
|
-
this.#prefViewer = null;
|
|
1362
|
-
return;
|
|
1363
|
-
}
|
|
1364
|
-
const rootNode = this.#prefViewer3D ? this.#prefViewer3D.getRootNode().host : null;
|
|
1365
|
-
this.#prefViewer = rootNode && rootNode.nodeName === "PREF-VIEWER" ? rootNode : null;
|
|
1662
|
+
if (this.#prefViewer !== undefined) {
|
|
1663
|
+
return;
|
|
1366
1664
|
}
|
|
1665
|
+
|
|
1666
|
+
this.#getPrefViewer3DComponent();
|
|
1667
|
+
if (this.#prefViewer3D === undefined) {
|
|
1668
|
+
this.#prefViewer = undefined;
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
let prefViewer = this.#prefViewer3D.closest?.("pref-viewer") || undefined;
|
|
1673
|
+
if (!prefViewer) {
|
|
1674
|
+
const host = this.#prefViewer3D.getRootNode?.()?.host;
|
|
1675
|
+
prefViewer = host?.nodeName === "PREF-VIEWER" ? host : undefined;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
this.#prefViewer = prefViewer;
|
|
1367
1679
|
}
|
|
1368
1680
|
|
|
1369
1681
|
/**
|
|
@@ -1512,7 +1824,10 @@ export default class BabylonJSController {
|
|
|
1512
1824
|
* @returns {void}
|
|
1513
1825
|
*/
|
|
1514
1826
|
async #stopRender() {
|
|
1515
|
-
this.#
|
|
1827
|
+
this.#stopEngineRenderLoop();
|
|
1828
|
+
this.#renderState.dirtyFrames = 0;
|
|
1829
|
+
this.#renderState.continuousUntil = 0;
|
|
1830
|
+
this.#renderState.lastRenderAt = 0;
|
|
1516
1831
|
await this.#unloadCameraDependentEffects();
|
|
1517
1832
|
}
|
|
1518
1833
|
/**
|
|
@@ -1523,9 +1838,9 @@ export default class BabylonJSController {
|
|
|
1523
1838
|
*/
|
|
1524
1839
|
async #startRender() {
|
|
1525
1840
|
await this.#loadCameraDependentEffects();
|
|
1526
|
-
this.#scene.
|
|
1527
|
-
|
|
1528
|
-
});
|
|
1841
|
+
await this.#scene.whenReadyAsync();
|
|
1842
|
+
const frames = this.#settings.antiAliasingEnabled || this.#settings.ambientOcclusionEnabled || this.#settings.iblEnabled ? this.#renderConfig.burstFramesEnhanced : this.#renderConfig.burstFramesBase;
|
|
1843
|
+
this.#requestRender({ frames: frames, continuousMs: this.#renderConfig.interactionMs });
|
|
1529
1844
|
}
|
|
1530
1845
|
|
|
1531
1846
|
/**
|
|
@@ -1545,7 +1860,6 @@ export default class BabylonJSController {
|
|
|
1545
1860
|
* `LoadAssetContainerAsync`, returning the tuple so the caller can decide how to attach it to the scene.
|
|
1546
1861
|
*/
|
|
1547
1862
|
async #loadAssetContainer(container, force = false) {
|
|
1548
|
-
|
|
1549
1863
|
if (container?.state?.update?.storage === undefined || container?.state?.size === undefined || container?.state?.timeStamp === undefined) {
|
|
1550
1864
|
return [container, false];
|
|
1551
1865
|
}
|
|
@@ -1606,6 +1920,7 @@ export default class BabylonJSController {
|
|
|
1606
1920
|
* Returns an object with success status and error details.
|
|
1607
1921
|
*/
|
|
1608
1922
|
async #loadContainers() {
|
|
1923
|
+
this.#detachAnimationChangedListener();
|
|
1609
1924
|
await this.#stopRender();
|
|
1610
1925
|
|
|
1611
1926
|
let oldModelMetadata = { ...(this.#containers.model?.state?.metadata ?? {}) };
|
|
@@ -1662,6 +1977,9 @@ export default class BabylonJSController {
|
|
|
1662
1977
|
this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
|
|
1663
1978
|
this.#setMaxSimultaneousLights();
|
|
1664
1979
|
this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
|
|
1980
|
+
if (this.#babylonJSAnimationController?.hasAnimations?.()) {
|
|
1981
|
+
this.#attachAnimationChangedListener();
|
|
1982
|
+
}
|
|
1665
1983
|
await this.#startRender();
|
|
1666
1984
|
});
|
|
1667
1985
|
return detail;
|
|
@@ -1921,10 +2239,10 @@ export default class BabylonJSController {
|
|
|
1921
2239
|
this.#engine = new Engine(this.#canvas, true, { alpha: true, stencil: true, preserveDrawingBuffer: false });
|
|
1922
2240
|
this.#engine.disableUniformBuffers = true;
|
|
1923
2241
|
this.#scene = new Scene(this.#engine);
|
|
1924
|
-
|
|
2242
|
+
|
|
1925
2243
|
// Activate the rendering of geometry data into a G-buffer, essential for advanced effects like deferred shading,
|
|
1926
|
-
// SSAO, and Velocity-Texture-Animation (VAT), allowing for complex post-processing by separating rendering into
|
|
1927
|
-
// different buffers (depth, normals, velocity) for later use in shaders.
|
|
2244
|
+
// SSAO, and Velocity-Texture-Animation (VAT), allowing for complex post-processing by separating rendering into
|
|
2245
|
+
// different buffers (depth, normals, velocity) for later use in shaders.
|
|
1928
2246
|
const geometryBufferRenderer = this.#scene.enableGeometryBufferRenderer();
|
|
1929
2247
|
if (geometryBufferRenderer) {
|
|
1930
2248
|
geometryBufferRenderer.enableScreenspaceDepth = true;
|
|
@@ -1941,26 +2259,29 @@ export default class BabylonJSController {
|
|
|
1941
2259
|
this.#scene.imageProcessingConfiguration.vignetteEnabled = false;
|
|
1942
2260
|
this.#scene.imageProcessingConfiguration.colorCurvesEnabled = false;
|
|
1943
2261
|
|
|
2262
|
+
// Skip the built-in pointer picking logic since the controller implements its own optimized raycasting for interaction.
|
|
2263
|
+
this.#scene.skipPointerMovePicking = true;
|
|
2264
|
+
this.#scene.skipPointerDownPicking = true;
|
|
2265
|
+
this.#scene.skipPointerUpPicking = true;
|
|
2266
|
+
|
|
1944
2267
|
this.#createCamera();
|
|
1945
2268
|
this.#enableInteraction();
|
|
1946
2269
|
await this.#createXRExperience();
|
|
1947
|
-
this.#startRender();
|
|
1948
|
-
this.#canvasResizeObserver = new ResizeObserver(() => this.#engine && this.#engine.resize());
|
|
1949
|
-
this.#canvasResizeObserver.observe(this.#canvas);
|
|
1950
2270
|
}
|
|
1951
2271
|
|
|
1952
2272
|
/**
|
|
1953
2273
|
* Disposes the Babylon.js engine and disconnects the canvas resize observer.
|
|
1954
|
-
* Cleans up all scene, camera, light, and
|
|
2274
|
+
* Cleans up all scene, camera, light, XR, and GLTF resolver resources.
|
|
1955
2275
|
* @public
|
|
1956
2276
|
* @returns {void}
|
|
1957
2277
|
*/
|
|
1958
2278
|
disable() {
|
|
1959
|
-
this.#canvasResizeObserver.disconnect();
|
|
1960
2279
|
this.#disableInteraction();
|
|
1961
2280
|
this.#disposeAnimationController();
|
|
2281
|
+
this.#disposeGLTFResolver();
|
|
1962
2282
|
this.#disposeXRExperience();
|
|
1963
2283
|
this.#unloadCameraDependentEffects();
|
|
2284
|
+
this.#stopEngineRenderLoop();
|
|
1964
2285
|
this.#disposeEngine();
|
|
1965
2286
|
}
|
|
1966
2287
|
|
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)
|
|
@@ -782,4 +784,19 @@ export default class FileStorage {
|
|
|
782
784
|
const fileToStore = await this.#getServerFile(uri);
|
|
783
785
|
return fileToStore ? !!(await this.#putFile(fileToStore, uri)) : false;
|
|
784
786
|
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Closes the IndexedDB handle held by this storage instance.
|
|
790
|
+
* Safe to call multiple times; next storage operation will lazily reopen the DB.
|
|
791
|
+
* @public
|
|
792
|
+
* @returns {void}
|
|
793
|
+
*/
|
|
794
|
+
dispose() {
|
|
795
|
+
if (this.#db) {
|
|
796
|
+
try {
|
|
797
|
+
this.#db.close();
|
|
798
|
+
} catch {}
|
|
799
|
+
}
|
|
800
|
+
this.#db = undefined;
|
|
801
|
+
}
|
|
785
802
|
}
|
package/src/gltf-resolver.js
CHANGED
|
@@ -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,16 @@ 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
|
+
}
|
|
244
|
+
|
|
234
245
|
/**
|
|
235
246
|
* Resolves and prepares a glTF/GLB source from various storage backends.
|
|
236
247
|
* Supports IndexedDB, direct URLs, and base64-encoded data.
|