@preference-sl/pref-viewer 2.13.0-beta.2 → 2.13.0-beta.21
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 +5 -5
- package/src/babylonjs-animation-controller.js +187 -76
- package/src/babylonjs-animation-opening.js +58 -2
- package/src/babylonjs-controller.js +1008 -359
- package/src/file-storage.js +405 -24
- package/src/gltf-resolver.js +65 -9
- package/src/gltf-storage.js +47 -35
- package/src/localization/i18n.js +1 -1
- package/src/localization/translations.js +3 -3
- package/src/pref-viewer-3d-data.js +102 -52
- package/src/pref-viewer-3d.js +71 -15
- package/src/pref-viewer-menu-3d.js +44 -3
- package/src/pref-viewer.js +134 -17
- package/src/styles.js +21 -5
package/Readme.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@preference-sl/pref-viewer",
|
|
3
|
-
"version": "2.13.0-beta.
|
|
3
|
+
"version": "2.13.0-beta.21",
|
|
4
4
|
"description": "Web Component to preview GLTF models with Babylon.js",
|
|
5
5
|
"author": "Alex Moreno Palacio <amoreno@preference.es>",
|
|
6
6
|
"scripts": {
|
|
@@ -35,11 +35,11 @@
|
|
|
35
35
|
"index.d.ts"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@babylonjs/core": "^8.
|
|
39
|
-
"@babylonjs/loaders": "^8.
|
|
40
|
-
"@babylonjs/serializers": "^8.
|
|
38
|
+
"@babylonjs/core": "^8.50.5",
|
|
39
|
+
"@babylonjs/loaders": "^8.50.5",
|
|
40
|
+
"@babylonjs/serializers": "^8.50.5",
|
|
41
41
|
"@panzoom/panzoom": "^4.6.0",
|
|
42
|
-
"babylonjs-gltf2interface": "^8.
|
|
42
|
+
"babylonjs-gltf2interface": "^8.50.5",
|
|
43
43
|
"buffer": "^6.0.3",
|
|
44
44
|
"idb": "^8.0.3",
|
|
45
45
|
"is-svg": "^6.1.0",
|
|
@@ -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 {
|
|
@@ -35,19 +45,18 @@ export default class BabylonJSAnimationController {
|
|
|
35
45
|
#highlightColor = Color3.FromHexString(PrefViewerStyleVariables.colorPrimary);
|
|
36
46
|
#highlightLayer = null;
|
|
37
47
|
#overlayLayer = null;
|
|
38
|
-
#useHighlightLayer =
|
|
48
|
+
#useHighlightLayer = true; // 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.
|
|
43
54
|
* @param {AssetContainer|Scene} assetContainer - The Babylon.js asset container or scene instance.
|
|
44
55
|
*/
|
|
45
56
|
constructor(assetContainer) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
this.#canvas = this.#scene._engine._renderingCanvas;
|
|
50
|
-
}
|
|
57
|
+
this.#scene = assetContainer.scene ? assetContainer.scene : assetContainer;
|
|
58
|
+
this.#assetContainer = assetContainer;
|
|
59
|
+
this.#canvas = this.#scene._engine._renderingCanvas;
|
|
51
60
|
this.#initializeAnimations();
|
|
52
61
|
}
|
|
53
62
|
|
|
@@ -128,15 +137,18 @@ export default class BabylonJSAnimationController {
|
|
|
128
137
|
* @returns {Array<string>} Array of animated node IDs associated with the mesh.
|
|
129
138
|
*/
|
|
130
139
|
#getNodesAnimatedByMesh(mesh) {
|
|
131
|
-
let
|
|
140
|
+
let nodeIds = [];
|
|
141
|
+
if (!mesh || !mesh.id) {
|
|
142
|
+
return nodeIds;
|
|
143
|
+
}
|
|
132
144
|
let node = mesh;
|
|
133
145
|
while (node.parent !== null) {
|
|
134
146
|
node = node.parent;
|
|
135
|
-
if (this.#animatedNodes.includes(node.id) && !
|
|
136
|
-
|
|
147
|
+
if (this.#animatedNodes.includes(node.id) && !nodeIds.includes(node.id)) {
|
|
148
|
+
nodeIds.push(node.id);
|
|
137
149
|
}
|
|
138
150
|
}
|
|
139
|
-
return
|
|
151
|
+
return nodeIds;
|
|
140
152
|
}
|
|
141
153
|
|
|
142
154
|
/**
|
|
@@ -163,7 +175,8 @@ export default class BabylonJSAnimationController {
|
|
|
163
175
|
const RENDERING_GROUP_ID = 0; // MUST be > 0 to render on top of main scene
|
|
164
176
|
|
|
165
177
|
// Use UtilityLayerRenderer for rendering
|
|
166
|
-
const
|
|
178
|
+
const utilityLayer = new UtilityLayerRenderer(this.#scene, undefined, undefined);
|
|
179
|
+
const uScene = utilityLayer.utilityLayerScene;
|
|
167
180
|
|
|
168
181
|
// Cache map: baseMesh -> overlayBaseMesh to reuse for InstancedMeshes
|
|
169
182
|
const baseToOverlayBase = new Map();
|
|
@@ -304,67 +317,68 @@ export default class BabylonJSAnimationController {
|
|
|
304
317
|
baseToOverlayBase.forEach((overlayBase) => {
|
|
305
318
|
overlayBase.dispose();
|
|
306
319
|
});
|
|
320
|
+
|
|
321
|
+
// Dispose utility layer scene/resources; without this each rebuild leaks GPU/JS resources.
|
|
322
|
+
utilityLayer.dispose();
|
|
307
323
|
},
|
|
308
324
|
};
|
|
309
325
|
}
|
|
310
326
|
|
|
311
327
|
/**
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
328
|
+
* Compares two node ID arrays and checks whether they contain the same values,
|
|
329
|
+
* regardless of element order.
|
|
330
|
+
* @private
|
|
331
|
+
* @param {string[]} arr1 - First node ID array.
|
|
332
|
+
* @param {string[]} arr2 - Second node ID array.
|
|
333
|
+
* @returns {boolean} True when both arrays contain the same node IDs; otherwise false.
|
|
315
334
|
*/
|
|
335
|
+
#haveSameNodeIds(arr1, arr2) {
|
|
336
|
+
if (arr1.length !== arr2.length) {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
const sortedArr1 = [...arr1].sort();
|
|
340
|
+
const sortedArr2 = [...arr2].sort();
|
|
341
|
+
return sortedArr1.every((value, index) => value === sortedArr2[index]);
|
|
342
|
+
}
|
|
316
343
|
|
|
317
344
|
/**
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
* @
|
|
345
|
+
* Clears the current highlight state from either HighlightLayer or overlay meshes.
|
|
346
|
+
* Also resets cached mesh/node tracking used to avoid redundant highlight updates.
|
|
347
|
+
* @private
|
|
348
|
+
* @returns {void}
|
|
322
349
|
*/
|
|
323
|
-
|
|
324
|
-
if (this.#
|
|
325
|
-
this.#highlightLayer
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
350
|
+
#removeHighlight() {
|
|
351
|
+
if (this.#useHighlightLayer) {
|
|
352
|
+
if (this.#highlightLayer) {
|
|
353
|
+
this.#highlightLayer.removeAllMeshes();
|
|
354
|
+
}
|
|
355
|
+
} else {
|
|
356
|
+
if (this.#overlayLayer) {
|
|
357
|
+
this.#overlayLayer.dispose();
|
|
358
|
+
this.#overlayLayer = null;
|
|
359
|
+
}
|
|
332
360
|
}
|
|
333
|
-
this
|
|
334
|
-
this.#
|
|
335
|
-
this.#openingAnimations.forEach((openingAnimation) => openingAnimation.dispose());
|
|
336
|
-
this.#openingAnimations = [];
|
|
361
|
+
this.#lastHighlightedMeshId = null;
|
|
362
|
+
this.#lastHighlightedNodeIds = [];
|
|
337
363
|
}
|
|
338
364
|
|
|
339
365
|
/**
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
366
|
+
* Applies highlighting for all meshes belonging to the provided animated node IDs.
|
|
367
|
+
* Depending on configuration, it uses Babylon's HighlightLayer or overlay meshes rendered
|
|
368
|
+
* in a utility layer for always-on-top outlines/fills.
|
|
369
|
+
* @private
|
|
370
|
+
* @param {string[]} nodeIds - Animated node IDs whose child meshes should be highlighted.
|
|
371
|
+
* @returns {void}
|
|
343
372
|
*/
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
return; // No need to update if hovering the same mesh
|
|
373
|
+
#addHighlight(nodeIds) {
|
|
374
|
+
this.#removeHighlight();
|
|
375
|
+
if (!nodeIds.length) {
|
|
376
|
+
return;
|
|
349
377
|
}
|
|
350
|
-
this.#lastHighlightedMeshId = pickedMeshId;
|
|
351
|
-
|
|
352
378
|
if (this.#useHighlightLayer) {
|
|
353
379
|
if (!this.#highlightLayer) {
|
|
354
380
|
this.#highlightLayer = new HighlightLayer("hl_animations", this.#scene);
|
|
355
381
|
}
|
|
356
|
-
|
|
357
|
-
this.#highlightLayer.removeAllMeshes();
|
|
358
|
-
|
|
359
|
-
if (!pickingInfo?.hit || !pickingInfo?.pickedMesh) {
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const nodeIds = this.#getNodesAnimatedByMesh(pickingInfo.pickedMesh);
|
|
364
|
-
if (!nodeIds.length) {
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
382
|
const transformNodes = [];
|
|
369
383
|
nodeIds.forEach((nodeId) => {
|
|
370
384
|
const transformNode = this.#scene.getTransformNodeByID(nodeId);
|
|
@@ -384,20 +398,6 @@ export default class BabylonJSAnimationController {
|
|
|
384
398
|
}
|
|
385
399
|
});
|
|
386
400
|
} else {
|
|
387
|
-
if (this.#overlayLayer) {
|
|
388
|
-
this.#overlayLayer.dispose();
|
|
389
|
-
this.#overlayLayer = null;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
if (!pickingInfo?.hit || !pickingInfo?.pickedMesh) {
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
const nodeIds = this.#getNodesAnimatedByMesh(pickingInfo.pickedMesh);
|
|
397
|
-
if (!nodeIds.length) {
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
401
|
const meshes = [];
|
|
402
402
|
nodeIds.forEach((nodeId) => {
|
|
403
403
|
const transformNode = this.#scene.getTransformNodeByID(nodeId);
|
|
@@ -409,6 +409,99 @@ export default class BabylonJSAnimationController {
|
|
|
409
409
|
|
|
410
410
|
this.#overlayLayer = this.#addGroupOutlineOverlayForInstances(meshes);
|
|
411
411
|
}
|
|
412
|
+
this.#lastHighlightedNodeIds = nodeIds;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* ---------------------------
|
|
417
|
+
* Public methods
|
|
418
|
+
* ---------------------------
|
|
419
|
+
*/
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Disposes all resources managed by the animation controller.
|
|
423
|
+
* Cleans up the highlight layer, animation menu, and internal animation/node lists.
|
|
424
|
+
* Should be called when the controller is no longer needed to prevent memory leaks.
|
|
425
|
+
* @public
|
|
426
|
+
*/
|
|
427
|
+
dispose() {
|
|
428
|
+
if (this.#highlightLayer) {
|
|
429
|
+
this.#highlightLayer.removeAllMeshes();
|
|
430
|
+
this.#highlightLayer.dispose();
|
|
431
|
+
this.#highlightLayer = null;
|
|
432
|
+
}
|
|
433
|
+
if (this.#overlayLayer) {
|
|
434
|
+
this.#overlayLayer.dispose();
|
|
435
|
+
this.#overlayLayer = null;
|
|
436
|
+
}
|
|
437
|
+
this.hideMenu();
|
|
438
|
+
this.#animatedNodes = [];
|
|
439
|
+
this.#openingAnimations.forEach((openingAnimation) => openingAnimation.dispose());
|
|
440
|
+
this.#openingAnimations = [];
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Indicates whether the current asset container exposes at least one opening/closing animation pair.
|
|
445
|
+
* @public
|
|
446
|
+
* @returns {boolean} True when interactive opening animations are available; otherwise false.
|
|
447
|
+
*/
|
|
448
|
+
hasAnimations() {
|
|
449
|
+
return this.#openingAnimations.length > 0;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Updates hover highlighting for meshes under animated nodes.
|
|
454
|
+
* Uses cached mesh/node state to avoid rebuilding highlight layers when the effective
|
|
455
|
+
* highlighted node set has not changed.
|
|
456
|
+
* @public
|
|
457
|
+
* @param {PickingInfo} pickingInfo - Raycast info from pointer position.
|
|
458
|
+
* @returns {{changed:boolean, highlighted:boolean}}
|
|
459
|
+
* Returns whether highlight visuals changed in this call and whether any mesh remains highlighted.
|
|
460
|
+
*/
|
|
461
|
+
highlightMeshes(pickingInfo) {
|
|
462
|
+
let changed = false;
|
|
463
|
+
let highlighted = false;
|
|
464
|
+
|
|
465
|
+
// Check if we're hovering the same mesh to avoid recreating overlays/highlights
|
|
466
|
+
const pickedMeshId = !pickingInfo?.hit || !pickingInfo?.pickedMesh?.id ? null : pickingInfo.pickedMesh.id;
|
|
467
|
+
const pickedMesh = pickingInfo?.pickedMesh ? pickingInfo.pickedMesh : null;
|
|
468
|
+
|
|
469
|
+
if (this.#lastHighlightedMeshId === pickedMeshId) {
|
|
470
|
+
// No need to update if hovering the same mesh
|
|
471
|
+
if (this.#lastHighlightedMeshId !== null) {
|
|
472
|
+
changed = false;
|
|
473
|
+
highlighted = true;
|
|
474
|
+
} else {
|
|
475
|
+
changed = false;
|
|
476
|
+
highlighted = false;
|
|
477
|
+
}
|
|
478
|
+
} else {
|
|
479
|
+
const nodeIds = this.#getNodesAnimatedByMesh(pickedMesh);
|
|
480
|
+
if (!nodeIds.length) {
|
|
481
|
+
if (this.#lastHighlightedMeshId !== null) {
|
|
482
|
+
this.#removeHighlight();
|
|
483
|
+
changed = true;
|
|
484
|
+
highlighted = false;
|
|
485
|
+
} else {
|
|
486
|
+
changed = false;
|
|
487
|
+
highlighted = false;
|
|
488
|
+
}
|
|
489
|
+
} else {
|
|
490
|
+
if (this.#haveSameNodeIds(nodeIds, this.#lastHighlightedNodeIds)) {
|
|
491
|
+
// No need to update if hovering a mesh under the same animated nodes
|
|
492
|
+
changed = false;
|
|
493
|
+
highlighted = true;
|
|
494
|
+
} else {
|
|
495
|
+
// Hovering a different mesh or same mesh under different animated nodes - update highlight
|
|
496
|
+
this.#addHighlight(nodeIds);
|
|
497
|
+
changed = true;
|
|
498
|
+
highlighted = true;
|
|
499
|
+
}
|
|
500
|
+
this.#lastHighlightedMeshId = pickedMeshId;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return { changed: changed, highlighted: highlighted };
|
|
412
505
|
}
|
|
413
506
|
|
|
414
507
|
/**
|
|
@@ -423,20 +516,26 @@ export default class BabylonJSAnimationController {
|
|
|
423
516
|
|
|
424
517
|
/**
|
|
425
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.
|
|
426
521
|
* @public
|
|
427
|
-
* @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}
|
|
428
525
|
*/
|
|
429
526
|
showMenu(pickingInfo) {
|
|
430
|
-
|
|
527
|
+
const pickedMesh = pickingInfo?.pickedMesh;
|
|
528
|
+
if (!pickingInfo?.hit || !pickedMesh) {
|
|
529
|
+
this.hideMenu();
|
|
431
530
|
return;
|
|
432
531
|
}
|
|
433
532
|
|
|
434
|
-
this
|
|
435
|
-
|
|
436
|
-
const nodeIds = this.#getNodesAnimatedByMesh(pickingInfo.pickedMesh);
|
|
533
|
+
const nodeIds = this.#getNodesAnimatedByMesh(pickedMesh);
|
|
437
534
|
if (!nodeIds.length) {
|
|
535
|
+
this.hideMenu();
|
|
438
536
|
return;
|
|
439
537
|
}
|
|
538
|
+
|
|
440
539
|
const openingAnimations = [];
|
|
441
540
|
nodeIds.forEach((nodeId) => {
|
|
442
541
|
const openingAnimation = this.#getOpeningAnimationByNode(nodeId);
|
|
@@ -446,7 +545,19 @@ export default class BabylonJSAnimationController {
|
|
|
446
545
|
openingAnimations.push(openingAnimation);
|
|
447
546
|
});
|
|
448
547
|
|
|
548
|
+
if (!openingAnimations.length) {
|
|
549
|
+
this.hideMenu();
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
449
553
|
const startedAnimation = openingAnimations.find((animation) => animation.state !== OpeningAnimation.states.closed);
|
|
450
|
-
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);
|
|
451
562
|
}
|
|
452
563
|
}
|
|
@@ -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
|
}
|