@preference-sl/pref-viewer 2.11.0-beta.3 → 2.11.0-beta.4

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-beta.3",
3
+ "version": "2.11.0-beta.4",
4
4
  "description": "Web Component to preview GLTF models with Babylon.js",
5
5
  "author": "Alex Moreno Palacio <amoreno@preference.es>",
6
6
  "scripts": {
@@ -30,7 +30,6 @@
30
30
  "sideEffects": false,
31
31
  "files": [
32
32
  "src",
33
- "src/images",
34
33
  "index.d.ts"
35
34
  ],
36
35
  "dependencies": {
@@ -1,382 +1,21 @@
1
- import { Color3, PointerEventTypes, HighlightLayer } from "@babylonjs/core";
2
- import { AdvancedDynamicTexture, StackPanel, Control, Button, Image } from "@babylonjs/gui";
3
-
4
- // https://doc.babylonjs.com/typedoc/classes/BABYLON.AnimationGroup
5
- class OpeningAnimationController {
6
- static states = {
7
- paused: 0,
8
- closed: 1,
9
- opened: 2,
10
- opening: 3,
11
- closing: 4,
12
- };
13
-
14
- #openAnimation = null;
15
- #closeAnimation = null;
16
-
17
- #nodes = [];
18
- #state = OpeningAnimationController.states.closed;
19
- #currentFrame = 0;
20
- #startFrame = 0;
21
- #endFrame = 0;
22
- #speedRatio = 1.0;
23
-
24
- #advancedDynamicTexture = null;
25
- #menu = null;
26
-
27
- constructor(name, openAnimationGroup, closeAnimationGroup) {
28
- this.name = name;
29
- this.#openAnimation = openAnimationGroup;
30
- this.#closeAnimation = closeAnimationGroup;
31
-
32
- this.#openAnimation.stop();
33
- this.#openAnimation._loopAnimation = false;
34
- this.#closeAnimation.stop();
35
- this.#closeAnimation._loopAnimation = false;
36
-
37
- this.#startFrame = this.#openAnimation.from;
38
- this.#endFrame = this.#openAnimation.to;
39
- this.#speedRatio = this.#openAnimation.speedRatio || 1.0;
40
-
41
- this.#getNodesFromAnimationGroups();
42
- this.#openAnimation.onAnimationGroupEndObservable.add(this.#onOpened.bind(this));
43
- this.#closeAnimation.onAnimationGroupEndObservable.add(this.#onClosed.bind(this));
44
- }
45
-
46
- #getNodesFromAnimationGroups() {
47
- [this.#openAnimation, this.#closeAnimation].forEach((animationGroup) => {
48
- animationGroup._targetedAnimations.forEach((targetedAnimation) => {
49
- if (!this.#nodes.includes(targetedAnimation.target.id)) {
50
- this.#nodes.push(targetedAnimation.target.id);
51
- }
52
- });
53
- });
54
- }
55
-
56
- #onOpened() {
57
- this.goToOpened();
58
- }
59
-
60
- #onClosed() {
61
- this.goToClosed();
62
- }
63
-
64
- /**
65
- * ---------------------------
66
- * Public methods
67
- * ---------------------------
68
- */
69
-
70
- isAnimationForNode(node) {
71
- return this.#nodes.includes(node);
72
- }
73
-
74
- playOpen() {
75
- if (this.#state === OpeningAnimationController.states.opening || this.#state === OpeningAnimationController.states.opened) {
76
- return;
77
- }
78
- if (this.#state === OpeningAnimationController.states.closing) {
79
- this.#currentFrame = this.#endFrame - this.#closeAnimation.getCurrentFrame();
80
- this.#closeAnimation.pause();
81
- }
82
-
83
- if (this.#openAnimation._isStarted && this.#openAnimation._isPaused) {
84
- this.#openAnimation.goToFrame(this.#currentFrame);
85
- this.#openAnimation.restart();
86
- } else {
87
- this.#openAnimation.start(false, this.#speedRatio, this.#currentFrame, this.#endFrame, undefined);
88
- }
89
-
90
- this.#state = OpeningAnimationController.states.opening;
91
- this.updateControls();
92
- }
93
-
94
- playClose() {
95
- if (this.#state === OpeningAnimationController.states.closing || this.#state === OpeningAnimationController.states.closed) {
96
- return;
97
- }
98
- if (this.#state === OpeningAnimationController.states.opening) {
99
- this.#currentFrame = this.#openAnimation.getCurrentFrame();
100
- this.#openAnimation.pause();
101
- }
102
-
103
- if (this.#closeAnimation._isStarted && this.#closeAnimation._isPaused) {
104
- this.#closeAnimation.goToFrame(this.#endFrame - this.#currentFrame);
105
- this.#closeAnimation.restart();
106
- } else {
107
- this.#closeAnimation.start(false, this.#speedRatio, this.#endFrame - this.#currentFrame, this.#endFrame, undefined);
108
- }
109
-
110
- this.#state = OpeningAnimationController.states.closing;
111
- this.updateControls();
112
- }
113
-
114
- pause() {
115
- if (this.#state === OpeningAnimationController.states.opening) {
116
- this.#currentFrame = this.#openAnimation.getCurrentFrame();
117
- this.#openAnimation.pause();
118
- }
119
- if (this.#state === OpeningAnimationController.states.closing) {
120
- this.#currentFrame = this.#endFrame - this.#closeAnimation.getCurrentFrame();
121
- this.#closeAnimation.pause();
122
- }
123
- this.#state = OpeningAnimationController.states.paused;
124
- this.updateControls();
125
- }
126
-
127
- goToOpened() {
128
- this.#currentFrame = this.#endFrame;
129
-
130
- if (this.#openAnimation._isStarted) {
131
- this.#openAnimation.start();
132
- }
133
- this.#openAnimation.pause();
134
- this.#openAnimation.goToFrame(this.#endFrame);
135
-
136
- if (!this.#closeAnimation._isStarted) {
137
- this.#closeAnimation.start();
138
- }
139
- this.#closeAnimation.pause();
140
- this.#closeAnimation.goToFrame(this.#startFrame);
141
-
142
- this.#state = OpeningAnimationController.states.opened;
143
- this.updateControls();
144
- }
145
-
146
- goToClosed() {
147
- this.#currentFrame = this.#startFrame;
148
- if (this.#openAnimation._isStarted) {
149
- this.#openAnimation.start();
150
- }
151
- this.#openAnimation.pause();
152
- this.#openAnimation.goToFrame(this.#startFrame);
153
-
154
- if (this.#closeAnimation._isStarted) {
155
- this.#closeAnimation.start();
156
- }
157
- this.#closeAnimation.pause();
158
- this.#closeAnimation.goToFrame(this.#endFrame);
159
-
160
- this.#state = OpeningAnimationController.states.closed;
161
- this.updateControls();
162
- }
163
-
164
- showControls(advancedDynamicTexture, mesh) {
165
- this.#advancedDynamicTexture = advancedDynamicTexture;
166
- this.#advancedDynamicTexture.metadata = { name: this.name };
167
- const controlCallbacks = {
168
- onGoToOpened: () => {
169
- if (this.#state === OpeningAnimationController.states.opened) {
170
- return;
171
- }
172
- this.goToOpened();
173
- this.hideControls();
174
- },
175
- onOpen: () => {
176
- if (this.#state === OpeningAnimationController.states.opened || this.#state === OpeningAnimationController.states.opening) {
177
- return;
178
- }
179
- this.playOpen();
180
- this.hideControls();
181
- },
182
- onPause: () => {
183
- if (this.#state === OpeningAnimationController.states.paused || this.#state === OpeningAnimationController.states.closed || this.#state === OpeningAnimationController.states.opened) {
184
- return;
185
- }
186
- this.pause();
187
- this.hideControls();
188
- },
189
- onClose: () => {
190
- if (this.#state === OpeningAnimationController.states.closed || this.#state === OpeningAnimationController.states.closing) {
191
- return;
192
- }
193
- this.playClose();
194
- this.hideControls();
195
- },
196
- onGoToClosed: () => {
197
- if (this.#state === OpeningAnimationController.states.closed) {
198
- return;
199
- }
200
- this.goToClosed();
201
- this.hideControls();
202
- },
203
- };
204
- this.#menu = new AnimationMenu(this.#advancedDynamicTexture, mesh, this.#state, controlCallbacks);
205
- }
206
-
207
- hideControls() {
208
- if (!this.isControlsVisible()) {
209
- return;
210
- }
211
- this.#advancedDynamicTexture.dispose();
212
- this.#advancedDynamicTexture = null;
213
- }
214
-
215
- updateControls() {
216
- if (!this.isControlsVisible()) {
217
- return;
218
- }
219
- if (!this.#menu) {
220
- return;
221
- }
222
- this.#menu.animationState = this.#state;
223
- }
224
-
225
- isControlsVisible() {
226
- return this.#advancedDynamicTexture !== null || this.#advancedDynamicTexture?.metadata?.name === this.name;
227
- }
228
-
229
- /**
230
- * ---------------------------
231
- * Public properties
232
- * ---------------------------
233
- */
234
-
235
- get state() {
236
- return this.#state;
237
- }
238
- }
239
-
240
- /**
241
- * AnimationMenu - Models and renders a popup menu for controlling animation playback in a Babylon.js 3D scene.
242
- *
243
- * Responsibilities:
244
- * - Creates a Babylon.js GUI panel with buttons for animation actions (open, close, pause, go to opened/closed).
245
- * - Handles button states (enabled/disabled, active/inactive) based on animation state.
246
- * - Attaches the menu to a mesh and disposes it when an action is taken.
247
- * - Allows external callbacks for each action.
248
- *
249
- * Usage:
250
- * const menu = new AnimationMenu(advancedDynamicTexture, mesh, animationState, {
251
- * onOpen: () => { ... },
252
- * onClose: () => { ... },
253
- * onPause: () => { ... },
254
- * onGoToOpened: () => { ... },
255
- * onGoToClosed: () => { ... }
256
- * });
257
- * menu.show();
258
- * menu.hide();
259
- */
260
-
261
- class AnimationMenu {
262
- #advancedDynamicTexture = null;
263
- #animationState = OpeningAnimationController.states.closed;
264
- #mesh = null;
265
- #callbacks = null;
266
- #panel = null;
267
- #colorActive = "#6BA53A";
268
- #colorEnabled = "#333333";
269
- #colorDisabled = "#777777";
270
-
271
- /**
272
- * @param {AdvancedDynamicTexture} advancedDynamicTexture - Babylon.js GUI texture.
273
- * @param {BABYLON.Mesh} mesh - Mesh to attach the menu to.
274
- * @param {number} animationState - Current animation state (enum).
275
- * @param {object} callbacks - Callback functions for menu actions.
276
- */
277
- constructor(advancedDynamicTexture, mesh, animationState, callbacks) {
278
- this.#advancedDynamicTexture = advancedDynamicTexture;
279
- this.#mesh = mesh;
280
- this.#animationState = animationState;
281
- this.#callbacks = callbacks;
282
-
283
- this.#createMenu(animationState);
284
- }
285
-
286
- /**
287
- * Renders the menu and attaches it to the mesh.
288
- */
289
- #createMenu() {
290
- if (!this.#advancedDynamicTexture || !this.#mesh) {
291
- return;
292
- }
293
- this.#panel = new StackPanel();
294
- this.#panel.isVertical = false;
295
- this.#panel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM;
296
- this.#panel.left = 0;
297
- this.#panel.top = 0;
298
- this.#advancedDynamicTexture.addControl(this.#panel);
299
- this.#panel.linkWithMesh(this.#mesh);
300
-
301
- this.#createButtons();
302
- this.#setButtonsState();
303
- }
304
-
305
- /**
306
- * Internal helper to add a button to the menu.
307
- * @private
308
- */
309
- #addButton(name, imageURL, callback) {
310
- const button = Button.CreateImageOnlyButton(`button_animation_${name}`, imageURL);
311
- button.image.stretch = Image.STRETCH_UNIFORM;
312
- button.color = "white";
313
- button.hoverCursor = "pointer";
314
- button.width = "28px";
315
- button.height = "28px";
316
- button.cornerRadius = 0;
317
- button.background = this.#colorEnabled;
318
- button.onPointerUpObservable.add(() => {
319
- if (callback) {
320
- callback();
321
- }
322
- });
323
- this.#panel.addControl(button);
324
- }
325
-
326
- #createButtons() {
327
- this.#addButton("closed", "../src/images/icon-skip-backward.svg", this.#callbacks.onGoToClosed);
328
- this.#addButton("close", "../src/images/icon-play-backwards.svg", this.#callbacks.onClose);
329
- this.#addButton("pause", "../src/images/icon-pause.svg", this.#callbacks.onPause);
330
- this.#addButton("open", "../src/images/icon-play.svg", this.#callbacks.onOpen);
331
- this.#addButton("opened", "../src/images/icon-skip-forward.svg", this.#callbacks.onGoToOpened);
332
- }
333
-
334
- #setButtonState(name, enabled, active) {
335
- const button = this.#advancedDynamicTexture.getControlByName(`button_animation_${name}`);
336
- if (!button) {
337
- return;
338
- }
339
- button.background = active ? this.#colorActive : enabled ? this.#colorEnabled : this.#colorDisabled;
340
- }
341
-
342
- #setButtonsState() {
343
- const goToOpenedButtonEnabled = this.#animationState !== OpeningAnimationController.states.opened;
344
- const goToOpenedButtonActive = false;
345
- const openButtonEnabled = this.#animationState !== OpeningAnimationController.states.opened && this.#animationState !== OpeningAnimationController.states.opening;
346
- const openButtonActive = this.#animationState === OpeningAnimationController.states.opening;
347
- const pauseButtonEnabled = this.#animationState !== OpeningAnimationController.states.paused && this.#animationState !== OpeningAnimationController.states.closed && this.#animationState !== OpeningAnimationController.states.opened;
348
- const pauseButtonActive = this.#animationState === OpeningAnimationController.states.paused;
349
- const closeButtonEnabled = this.#animationState !== OpeningAnimationController.states.closed && this.#animationState !== OpeningAnimationController.states.closing;
350
- const closeButtonActive = this.#animationState === OpeningAnimationController.states.closing;
351
- const goToClosedButtonEnabled = this.#animationState !== OpeningAnimationController.states.closed;
352
- const goToClosedButtonActive = false;
353
-
354
- this.#setButtonState("opened", goToOpenedButtonEnabled, goToOpenedButtonActive);
355
- this.#setButtonState("open", openButtonEnabled, openButtonActive);
356
- this.#setButtonState("pause", pauseButtonEnabled, pauseButtonActive);
357
- this.#setButtonState("close", closeButtonEnabled, closeButtonActive);
358
- this.#setButtonState("closed", goToClosedButtonEnabled, goToClosedButtonActive);
359
- }
360
-
361
- set animationState(state) {
362
- this.#animationState = state;
363
- this.#setButtonsState();
364
- }
365
- }
1
+ import { Color3, HighlightLayer, Mesh, PickingInfo, PointerEventTypes, Scene } from "@babylonjs/core";
2
+ import { AdvancedDynamicTexture } from "@babylonjs/gui";
3
+ import { OpeningAnimation } from "./babylonjs-animation-opening.js";
366
4
 
367
5
  /**
368
6
  * BabylonJSAnimationController - Manages animation playback and interactive highlighting for model containers in Babylon.js scenes.
369
7
  *
370
8
  * Responsibilities:
371
- * - Detects if the loaded model container contains animations.
372
- * - Controls playback state: playing forward, playing backward, paused.
373
- * - Tracks and manages animated transformation nodes.
374
- * - Highlights animated nodes when hovered by the cursor.
375
- * - Provides API for play, pause, reverse, and highlight operations.
9
+ * - Detects and groups opening/closing animations in the scene.
10
+ * - Tracks animated transformation nodes and their relationships to meshes.
11
+ * - Highlights animated nodes and their child meshes when hovered.
12
+ * - Displays and disposes the animation control menu (GUI) for animated nodes.
13
+ * - Provides API for interaction and animation control.
14
+ *
15
+ * @class
376
16
  */
377
17
  export default class BabylonJSAnimationController {
378
18
  #scene = null;
379
- #assetContainer = null;
380
19
  #animatedNodes = [];
381
20
  #highlightLayer = null;
382
21
  #highlightColor = new Color3(0, 1, 0); // Color para resaltar los elementos animados (Verde)
@@ -384,31 +23,33 @@ export default class BabylonJSAnimationController {
384
23
  #openingAnimations = [];
385
24
 
386
25
  /**
387
- * @param {BABYLON.Scene} scene - The Babylon.js scene instance.
388
- * @param {BABYLON.AssetContainer} assetContainer - The loaded asset container.
26
+ * Creates a new BabylonJSAnimationController for a Babylon.js scene.
27
+ * @param {Scene} scene - The Babylon.js scene instance.
389
28
  */
390
- constructor(scene, assetContainer) {
29
+ constructor(scene) {
391
30
  this.#scene = scene;
392
- this.#assetContainer = assetContainer;
393
31
  this.#initializeAnimations();
394
32
  this.#setupPointerObservers();
395
33
  }
396
34
 
397
35
  /**
398
- * Detects and stores animatable objects and animated nodes in the model container.
36
+ * Detects and stores animatable objects and animated nodes in the scene.
399
37
  * @private
400
38
  */
401
39
  #initializeAnimations() {
402
- if (!this.#assetContainer.animationGroups.length) {
40
+ if (!this.#scene.animationGroups.length) {
403
41
  return;
404
42
  }
405
-
406
43
  this.#getAnimatedNodes();
407
44
  this.#getOpeneingAnimations();
408
45
  }
409
46
 
47
+ /**
48
+ * Collects the IDs of all nodes targeted by any animation group in the scene.
49
+ * @private
50
+ */
410
51
  #getAnimatedNodes() {
411
- this.#assetContainer.animationGroups.forEach((animationGroup) => {
52
+ this.#scene.animationGroups.forEach((animationGroup) => {
412
53
  if (!animationGroup._targetedAnimations.length) {
413
54
  return;
414
55
  }
@@ -420,9 +61,15 @@ export default class BabylonJSAnimationController {
420
61
  });
421
62
  }
422
63
 
64
+ /**
65
+ * Groups opening and closing animations by node name and creates OpeningAnimation instances.
66
+ * @private
67
+ * @description
68
+ * Uses animation group names with the pattern "animation_open_<name>" and "animation_close_<name>".
69
+ */
423
70
  #getOpeneingAnimations() {
424
71
  const openings = {};
425
- this.#assetContainer.animationGroups.forEach((animationGroup) => {
72
+ this.#scene.animationGroups.forEach((animationGroup) => {
426
73
  const match = animationGroup.name.match(/^animation_(open|close)_(.+)$/);
427
74
  if (!match) {
428
75
  return;
@@ -439,21 +86,25 @@ export default class BabylonJSAnimationController {
439
86
  });
440
87
 
441
88
  Object.values(openings).forEach((opening) => {
442
- this.#openingAnimations.push(new OpeningAnimationController(opening.name, opening.animationOpen, opening.animationClose));
89
+ this.#openingAnimations.push(new OpeningAnimation(opening.name, opening.animationOpen, opening.animationClose));
443
90
  });
444
91
  }
445
92
 
93
+ /**
94
+ * Finds the OpeningAnimation instance associated with a given node ID.
95
+ * @private
96
+ * @param {string} nodeId - The node identifier.
97
+ * @returns {OpeningAnimation|null} The matching OpeningAnimation instance or null.
98
+ */
446
99
  #getOpeningAnimationByNode(nodeId) {
447
100
  return this.#openingAnimations.find((openingAnimation) => openingAnimation.isAnimationForNode(nodeId));
448
101
  }
449
102
 
450
103
  /**
451
- * Busca si una malla pertenece a un nodo que es objetivo de alguna animación
452
- * @param {BABYLON.Mesh} mesh Malla
453
- * @param {Array} targetNodeIds Array con los identificadores de los nodos que son objetivo de alguna animación
454
- * @returns {Array} Array de 2 posiciones: [0] Identificador del nodo que es objetivo de alguna animación (o false si no lo es), [1] Array con las mallas hijas del nodo que es objetivo de alguna animación (o vacío si no lo es)
455
- * @description
456
- * El segundo elemento del array devuelto es se necesita para poder resaltar esas mallas hijas cuando el puntero del ratón está sobre alguna de ellas
104
+ * Determines if a mesh belongs to a node targeted by an animation.
105
+ * @private
106
+ * @param {Mesh} mesh - The mesh to check.
107
+ * @returns {string|false} The animated node ID if found, otherwise false.
457
108
  */
458
109
  #getNodeAnimatedByMesh = function (mesh) {
459
110
  let nodeId = false;
@@ -468,8 +119,9 @@ export default class BabylonJSAnimationController {
468
119
  };
469
120
 
470
121
  /**
471
- * Ilumina las mallas que son objetivo de alguna animación si el puntero del ratón está sobre una de ellas
472
- * @param {BABYLON.PickingInfo} pickingInfo Información del trazado del rayo desde la cámara activa hasta el puntero del ratón
122
+ * Highlights meshes that are children of an animated node when hovered.
123
+ * @private
124
+ * @param {PickingInfo} pickingInfo - Raycast info from pointer position.
473
125
  */
474
126
  #hightlightMeshesForAnimation(pickingInfo) {
475
127
  if (!this.#highlightLayer) {
@@ -494,7 +146,7 @@ export default class BabylonJSAnimationController {
494
146
  }
495
147
 
496
148
  /**
497
- * Sets up pointer observers to highlight animated nodes on hover.
149
+ * Sets up pointer observers to highlight animated nodes on hover and show the animation menu on click.
498
150
  * @private
499
151
  */
500
152
  #setupPointerObservers() {
@@ -504,7 +156,7 @@ export default class BabylonJSAnimationController {
504
156
  this.#hightlightMeshesForAnimation(pickingInfo);
505
157
  }
506
158
  if (pointerInfo.type === PointerEventTypes.POINTERUP) {
507
- // Eliminar cualquier Babylon GUI que se haya creado anteriormente
159
+ // Remove any previously created Babylon GUI
508
160
  if (this.#advancedDynamicTexture) {
509
161
  this.#advancedDynamicTexture.dispose();
510
162
  this.#advancedDynamicTexture = null;
@@ -515,6 +167,13 @@ export default class BabylonJSAnimationController {
515
167
  });
516
168
  }
517
169
 
170
+ /**
171
+ * Displays the animation control menu for the animated node under the pointer.
172
+ * @private
173
+ * @param {PickingInfo} pickingInfo - Raycast info from pointer position.
174
+ * @description
175
+ * Creates the GUI if needed and invokes OpeningAnimation.showControls.
176
+ */
518
177
  #showMenu(pickingInfo) {
519
178
  if (!pickingInfo?.hit && !pickingInfo?.pickedMesh) {
520
179
  return;
@@ -531,6 +190,6 @@ export default class BabylonJSAnimationController {
531
190
  if (!this.#advancedDynamicTexture) {
532
191
  this.#advancedDynamicTexture = AdvancedDynamicTexture.CreateFullscreenUI("UI_Animation");
533
192
  }
534
- openingAnimation.showControls(this.#advancedDynamicTexture, pickingInfo.pickedMesh);
193
+ openingAnimation.showControls(this.#advancedDynamicTexture);
535
194
  }
536
195
  }