@preference-sl/pref-viewer 2.13.0-beta.6 → 2.13.0-beta.8
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 +468 -114
- package/src/file-storage.js +382 -21
- package/src/gltf-resolver.js +52 -8
- package/src/pref-viewer-3d-data.js +60 -2
- package/src/pref-viewer-3d.js +11 -4
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
|
}
|