@preference-sl/pref-viewer 2.11.0 → 2.12.0-beta.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preference-sl/pref-viewer",
3
- "version": "2.11.0",
3
+ "version": "2.12.0-beta.1",
4
4
  "description": "Web Component to preview GLTF models with Babylon.js",
5
5
  "author": "Alex Moreno Palacio <amoreno@preference.es>",
6
6
  "scripts": {
@@ -113,18 +113,18 @@ export default class BabylonJSAnimationController {
113
113
  }
114
114
 
115
115
  /**
116
- * Determines if a mesh belongs to a node targeted by an animation.
116
+ * Finds all animated node IDs associated with a given mesh by traversing its parent hierarchy.
117
117
  * @private
118
118
  * @param {Mesh} mesh - The mesh to check.
119
- * @returns {string|false} The animated node ID if found, otherwise false.
119
+ * @returns {Array<string>} Array of animated node IDs associated with the mesh.
120
120
  */
121
- #getNodeAnimatedByMesh = function (mesh) {
122
- let nodeId = false;
121
+ #getNodesAnimatedByMesh = function (mesh) {
122
+ let nodeId = [];
123
123
  let node = mesh;
124
- while (node.parent !== null && !nodeId) {
124
+ while (node.parent !== null) {
125
125
  node = node.parent;
126
- if (this.#animatedNodes.includes(node.id)) {
127
- nodeId = node.id;
126
+ if (this.#animatedNodes.includes(node.id) && !nodeId.includes(node.id)) {
127
+ nodeId.push(node.id);
128
128
  }
129
129
  }
130
130
  return nodeId;
@@ -169,16 +169,29 @@ export default class BabylonJSAnimationController {
169
169
  return;
170
170
  }
171
171
 
172
- const nodeId = this.#getNodeAnimatedByMesh(pickingInfo.pickedMesh);
173
- if (!nodeId) {
172
+ const nodeIds = this.#getNodesAnimatedByMesh(pickingInfo.pickedMesh);
173
+ if (!nodeIds.length) {
174
174
  return;
175
175
  }
176
176
 
177
- const transformNode = this.#scene.getTransformNodeByID(nodeId);
178
- const nodeMeshes = transformNode.getChildMeshes();
179
- if (nodeMeshes.length) {
180
- nodeMeshes.forEach((mesh) => this.#highlightLayer.addMesh(mesh, this.#highlightColor));
181
- }
177
+ const transformNodes = [];
178
+ nodeIds.forEach((nodeId) => {
179
+ const transformNode = this.#scene.getTransformNodeByID(nodeId);
180
+ if (transformNode) {
181
+ transformNodes.push(transformNode);
182
+ }
183
+ });
184
+
185
+ transformNodes.forEach((transformNode) => {
186
+ const nodeMeshes = transformNode.getChildMeshes();
187
+ if (nodeMeshes.length) {
188
+ nodeMeshes.forEach((mesh) => {
189
+ if (!this.#highlightLayer.hasMesh(mesh)) {
190
+ this.#highlightLayer.addMesh(mesh, this.#highlightColor);
191
+ }
192
+ });
193
+ }
194
+ });
182
195
  }
183
196
 
184
197
  /**
@@ -203,15 +216,20 @@ export default class BabylonJSAnimationController {
203
216
 
204
217
  this.hideMenu();
205
218
 
206
- const nodeId = this.#getNodeAnimatedByMesh(pickingInfo.pickedMesh);
207
- if (!nodeId) {
208
- return;
209
- }
210
- const openingAnimation = this.#getOpeningAnimationByNode(nodeId);
211
- if (!openingAnimation) {
219
+ const nodeIds = this.#getNodesAnimatedByMesh(pickingInfo.pickedMesh);
220
+ if (!nodeIds.length) {
212
221
  return;
213
222
  }
223
+ const openingAnimations = [];
224
+ nodeIds.forEach((nodeId) => {
225
+ const openingAnimation = this.#getOpeningAnimationByNode(nodeId);
226
+ if (!openingAnimation) {
227
+ return;
228
+ }
229
+ openingAnimations.push(openingAnimation);
230
+ });
214
231
 
215
- openingAnimation.showControls(this.#canvas);
232
+ const startedAnimation = openingAnimations.find((animation) => animation.state !== OpeningAnimation.states.closed);
233
+ startedAnimation ? startedAnimation.showControls(this.#canvas, openingAnimations) : openingAnimations[0].showControls(this.#canvas, openingAnimations);
216
234
  }
217
235
  }
@@ -5,17 +5,19 @@ import { PrefViewer3DAnimationMenuStyles } from "./styles.js";
5
5
  * OpeningAnimationMenu - Manages and renders the interactive animation control menu for opening/closing animations in a Babylon.js scene.
6
6
  *
7
7
  * Summary:
8
- * Provides a floating HTML-based GUI menu with playback and loop controls for animated nodes, including buttons and a progress slider.
8
+ * Provides a floating HTML-based GUI menu with playback and loop controls for animated nodes, including buttons, a progress slider, and a selector for switching between multiple animations.
9
9
  * Handles user interaction, updates UI state based on animation state, and invokes external callbacks for animation actions.
10
10
  * Designed for integration with Babylon.js product configurators and interactive 3D applications.
11
11
  *
12
12
  * Key Features:
13
13
  * - Renders a menu with buttons for controlling animation playback (open, close, pause, go to opened/closed, loop).
14
+ * - Displays a selector UI for switching between multiple opening animations, showing only the animation name without prefix.
14
15
  * - Updates button states and slider position based on animation state, progress, and loop mode.
15
16
  * - Handles user interactions and invokes provided callbacks for animation actions.
16
17
  * - Synchronizes the slider value with animation progress, avoiding callback loops.
17
18
  * - Provides setters to update animation state, progress, and loop mode from external controllers.
18
19
  * - Disposes and cleans up all DOM resources when no longer needed.
20
+ * - Resets other animation menus when switching between animations.
19
21
  *
20
22
  * Public Setters:
21
23
  * - set animationState(state): Updates the animation state and button states.
@@ -29,9 +31,10 @@ import { PrefViewer3DAnimationMenuStyles } from "./styles.js";
29
31
  * - isVisible: Returns whether the animation menu is currently visible in the DOM.
30
32
  *
31
33
  * Private Methods:
32
- * - #createMenu(): Initializes and renders the menu UI.
34
+ * - #createMenu(): Initializes and renders the menu UI, including the selector, buttons, and slider.
33
35
  * - #addButton(name, imageSVG, enabled, active, visible, callback): Adds a button to the menu with specified properties.
34
36
  * - #createButtons(): Creates all control buttons and sets their initial states.
37
+ * - #createSelector(): Creates a selector UI element for switching between available opening animations, omitting the prefix in the display name.
35
38
  * - #getButtonByName(name): Retrieves a button control by its name.
36
39
  * - #setButtonState(name, enabled, active, visible): Updates the visual state of a button.
37
40
  * - #getPlayerButtonsState(): Returns the state (enabled, active, visible) for playback control buttons.
@@ -41,16 +44,18 @@ import { PrefViewer3DAnimationMenuStyles } from "./styles.js";
41
44
  * - #setLoopButtonsState(): Updates all loop control buttons.
42
45
  * - #createSlider(): Creates and configures the animation progress slider.
43
46
  * - #onSliderInput(event): Handles the slider input event for animation progress.
47
+ * - #resetOtherAnimations(): Resets all other opening animations except the current one.
44
48
  *
45
49
  * Usage Example:
46
- * const menu = new OpeningAnimationMenu(name, canvas, state, progress, loop, {
50
+ * const menu = new OpeningAnimationMenu(name, canvas, state, progress, loop, openingAnimations, {
47
51
  * onOpen: () => { ... },
48
52
  * onClose: () => { ... },
49
53
  * onPause: () => { ... },
50
54
  * onGoToOpened: () => { ... },
51
55
  * onGoToClosed: () => { ... },
52
56
  * onToggleLoop: () => { ... },
53
- * onSetAnimationProgress: (progress) => { ... }
57
+ * onSetAnimationProgress: (progress) => { ... },
58
+ * onChangeAnimation: () => { ... }
54
59
  * });
55
60
  * menu.animationState = OpeningAnimation.states.opening;
56
61
  * menu.animationProgress = 0.5;
@@ -60,6 +65,7 @@ export default class OpeningAnimationMenu {
60
65
  #animationState = OpeningAnimation.states.closed;
61
66
  #animationProgress = 0;
62
67
  #animationLoop = false;
68
+ #openingAnimations = [];
63
69
  #callbacks = null;
64
70
 
65
71
  #name = "";
@@ -89,17 +95,20 @@ export default class OpeningAnimationMenu {
89
95
  * @param {number} animationState - Current animation state (enum).
90
96
  * @param {number} animationProgress - Current animation progress (0 to 1).
91
97
  * @param {boolean} animationLoop - Whether the animation is set to loop.
98
+ * @param {Array<OpeningAnimation>} openingAnimations - Array of OpeningAnimation instances to manage.
92
99
  * @param {object} callbacks - Callback functions for menu actions (play, pause, open, close, etc.).
93
100
  * @public
94
101
  */
95
- constructor(name, canvas, animationState, animationProgress, animationLoop, callbacks) {
102
+ constructor(name, canvas, animationState, animationProgress, animationLoop, openingAnimations, callbacks) {
96
103
  this.#name = name;
97
104
  this.#canvas = canvas;
98
105
  this.#animationState = animationState;
99
106
  this.#animationProgress = animationProgress;
100
107
  this.#animationLoop = animationLoop;
108
+ this.#openingAnimations = openingAnimations;
101
109
  this.#callbacks = callbacks;
102
110
 
111
+ this.#resetOtherAnimations();
103
112
  this.#createMenu(animationState);
104
113
  }
105
114
 
@@ -131,6 +140,46 @@ export default class OpeningAnimationMenu {
131
140
  this.#containerButtons.appendChild(button);
132
141
  }
133
142
 
143
+ /**
144
+ * Creates a selector UI element for switching between available opening animations.
145
+ * @private
146
+ * @returns {void}
147
+ */
148
+ #createSelector() {
149
+ if (!Array.isArray(this.#openingAnimations) || this.#openingAnimations.length < 2) {
150
+ return; // No selector needed if only one animation
151
+ }
152
+
153
+ const selector = document.createElement("div");
154
+ selector.classList.add("animation-menu-selector");
155
+
156
+ this.#openingAnimations.forEach((animation) => {
157
+ const button = document.createElement("button");
158
+ button.classList.add("button-selector");
159
+ // Remove prefix before "_" for display
160
+ const nameParts = animation.name.split("_");
161
+ button.textContent = nameParts.length > 1 ? nameParts.slice(1).join("_") : animation.name;
162
+
163
+ // Highlight the current animation
164
+ if (animation.name === this.#name) {
165
+ button.classList.add("active");
166
+ }
167
+
168
+ button.addEventListener("click", (event) => {
169
+ if (animation.name !== this.#name) {
170
+ if (this.#callbacks.onChangeAnimation && typeof this.#callbacks.onChangeAnimation === "function") {
171
+ this.#callbacks.onChangeAnimation();
172
+ }
173
+ animation.showControls(this.#canvas, this.#openingAnimations);
174
+ }
175
+ });
176
+
177
+ selector.appendChild(button);
178
+ });
179
+
180
+ this.#containerMain.prepend(selector);
181
+ }
182
+
134
183
  /**
135
184
  * Creates all control buttons and sets their initial states.
136
185
  * @private
@@ -183,6 +232,7 @@ export default class OpeningAnimationMenu {
183
232
  this.#containerButtons.classList.add("animation-menu-buttons");
184
233
  this.#containerMain.appendChild(this.#containerButtons);
185
234
 
235
+ this.#createSelector();
186
236
  this.#createButtons();
187
237
  this.#createSlider();
188
238
  }
@@ -263,6 +313,20 @@ export default class OpeningAnimationMenu {
263
313
  return buttonsState;
264
314
  }
265
315
 
316
+ /**
317
+ * Resets all other opening animations except the current one.
318
+ * @private
319
+ * @returns {void}
320
+ */
321
+ #resetOtherAnimations() {
322
+ this.#openingAnimations.forEach((animation) => {
323
+ if (animation.name !== this.#name) {
324
+ animation.hideControls();
325
+ animation.goToClosed();
326
+ }
327
+ });
328
+ }
329
+
266
330
  /**
267
331
  * Updates the visual state of a button (enabled, active, visible).
268
332
  * @private
@@ -19,7 +19,7 @@ import OpeningAnimationMenu from "./babylonjs-animation-opening-menu.js";
19
19
  * - pause(): Pauses the current animation.
20
20
  * - goToOpened(): Moves animation to the fully opened state.
21
21
  * - goToClosed(): Moves animation to the fully closed state.
22
- * - showControls(canvas): Displays the animation control menu.
22
+ * - showControls(canvas, openingAnimations): Displays the animation control menu and sets up callbacks.
23
23
  * - hideControls(): Hides the animation control menu.
24
24
  * - isControlsVisible(): Returns true if the control menu is visible for this animation.
25
25
  *
@@ -433,8 +433,9 @@ export default class OpeningAnimation {
433
433
  * Synchronizes slider and button states with animation.
434
434
  * @public
435
435
  * @param {HTMLCanvasElement} canvas - The canvas element for rendering.
436
+ * @param {Array<OpeningAnimation>} openingAnimations - Array of OpeningAnimation instances to manage.
436
437
  */
437
- showControls(canvas) {
438
+ showControls(canvas, openingAnimations = []) {
438
439
  const controlCallbacks = {
439
440
  onGoToOpened: () => {
440
441
  if (this.#state === OpeningAnimation.states.opened) {
@@ -481,8 +482,14 @@ export default class OpeningAnimation {
481
482
  this.#loop = !this.#loop;
482
483
  this.#menu.animationLoop = this.#loop;
483
484
  },
485
+ onChangeAnimation: () => {
486
+ if (this.#state !== OpeningAnimation.states.closed) {
487
+ this.goToClosed();
488
+ }
489
+ this.hideControls();
490
+ },
484
491
  };
485
- this.#menu = new OpeningAnimationMenu(this.name, canvas, this.#state, this.#getProgress(), this.#loop, controlCallbacks);
492
+ this.#menu = new OpeningAnimationMenu(this.name, canvas, this.#state, this.#getProgress(), this.#loop, openingAnimations, controlCallbacks);
486
493
 
487
494
  // Attach to Babylon.js scene render loop for real-time updates
488
495
  this.#openAnimation._scene.onBeforeRenderObservable.add(this.#handlers.updateControlsSlider);
package/src/styles.js CHANGED
@@ -111,15 +111,62 @@ export const PrefViewer3DAnimationMenuStyles = `
111
111
  --slider-thumb-width: 20px;
112
112
  --slider-bar-height: 8px;
113
113
  --slider-bar-offset: 10px;
114
+ --selector-button-radius: 6px;
115
+ --selector-button-padding-horizontal: 8px;
116
+ --selector-button-padding-vertical: 4px;
117
+ --selector-button-font-size: 14px;
118
+ --selector-button-font-weight: 500;
119
+ --width: calc(var(--button-size) * 6 + var(--button-spacing) * 5 + var(--button-loop-margin-left));
114
120
 
115
121
  display: block;
116
122
  position: absolute;
117
123
  bottom: 10px;
118
124
 
119
- right: calc(50% - ((6 * var(--button-size) + 5 * var(--button-spacing) + var(--button-loop-margin-left)) / 2));
125
+ right: calc(50% - (var(--width) / 2));
120
126
  z-index: 1000;
121
127
  }
122
128
 
129
+ div.pref-viewer-3d.animation-menu>div.animation-menu-selector {
130
+ fore
131
+ padding: 0;
132
+ margin: 0;
133
+ display: flex;
134
+ flex-wrap: wrap;
135
+ gap: var(--button-spacing);
136
+ max-width: var(--width);
137
+ margin-bottom: var(--button-spacing);
138
+ }
139
+
140
+ div.pref-viewer-3d.animation-menu>div.animation-menu-selector>button {
141
+ display: block;
142
+ padding: var(--selector-button-padding-vertical) var(--selector-button-padding-horizontal);
143
+ margin: 0;
144
+ border: 1px solid var(--color-border);
145
+ border-radius: var(--selector-button-radius);
146
+ background: var(--color-enabled);
147
+ color: var(--color-icon);
148
+ font-size: var(--selector-button-font-size);
149
+ font-weight: var(--selector-button-font-weight);
150
+ line-height: 1;
151
+ flex: 1;
152
+ }
153
+
154
+ div.pref-viewer-3d.animation-menu>div.animation-menu-selector>button.active {
155
+ background: var(--color-active);
156
+ }
157
+
158
+ div.pref-viewer-3d.animation-menu>div.animation-menu-selector>button.active:hover {
159
+ background: var(--color-active-hover);
160
+ }
161
+
162
+ div.pref-viewer-3d.animation-menu>div.animation-menu-selector>button:not(.active) {
163
+ cursor: pointer;
164
+ }
165
+
166
+ div.pref-viewer-3d.animation-menu>div.animation-menu-buttons>button:not(.active):hover {
167
+ background: var(--color-enabled-hover);
168
+ }
169
+
123
170
  div.pref-viewer-3d.animation-menu>div.animation-menu-buttons {
124
171
  padding: 0;
125
172
  margin: 0;