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

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.2",
3
+ "version": "2.11.0-beta.3",
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,15 +30,16 @@
30
30
  "sideEffects": false,
31
31
  "files": [
32
32
  "src",
33
- "src/models",
33
+ "src/images",
34
34
  "index.d.ts"
35
35
  ],
36
36
  "dependencies": {
37
- "@babylonjs/core": "^8.31.3",
38
- "@babylonjs/loaders": "^8.31.3",
39
- "@babylonjs/serializers": "^8.31.3",
37
+ "@babylonjs/core": "^8.36.1",
38
+ "@babylonjs/gui": "^8.36.1",
39
+ "@babylonjs/loaders": "^8.36.1",
40
+ "@babylonjs/serializers": "^8.36.1",
40
41
  "@panzoom/panzoom": "^4.6.0",
41
- "babylonjs-gltf2interface": "^8.31.3",
42
+ "babylonjs-gltf2interface": "^8.36.1",
42
43
  "idb": "^8.0.3",
43
44
  "is-svg": "^6.1.0"
44
45
  },
@@ -0,0 +1,536 @@
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
+ }
366
+
367
+ /**
368
+ * BabylonJSAnimationController - Manages animation playback and interactive highlighting for model containers in Babylon.js scenes.
369
+ *
370
+ * 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.
376
+ */
377
+ export default class BabylonJSAnimationController {
378
+ #scene = null;
379
+ #assetContainer = null;
380
+ #animatedNodes = [];
381
+ #highlightLayer = null;
382
+ #highlightColor = new Color3(0, 1, 0); // Color para resaltar los elementos animados (Verde)
383
+ #advancedDynamicTexture = null;
384
+ #openingAnimations = [];
385
+
386
+ /**
387
+ * @param {BABYLON.Scene} scene - The Babylon.js scene instance.
388
+ * @param {BABYLON.AssetContainer} assetContainer - The loaded asset container.
389
+ */
390
+ constructor(scene, assetContainer) {
391
+ this.#scene = scene;
392
+ this.#assetContainer = assetContainer;
393
+ this.#initializeAnimations();
394
+ this.#setupPointerObservers();
395
+ }
396
+
397
+ /**
398
+ * Detects and stores animatable objects and animated nodes in the model container.
399
+ * @private
400
+ */
401
+ #initializeAnimations() {
402
+ if (!this.#assetContainer.animationGroups.length) {
403
+ return;
404
+ }
405
+
406
+ this.#getAnimatedNodes();
407
+ this.#getOpeneingAnimations();
408
+ }
409
+
410
+ #getAnimatedNodes() {
411
+ this.#assetContainer.animationGroups.forEach((animationGroup) => {
412
+ if (!animationGroup._targetedAnimations.length) {
413
+ return;
414
+ }
415
+ animationGroup._targetedAnimations.forEach((targetedAnimation) => {
416
+ if (!this.#animatedNodes.includes(targetedAnimation.target.id)) {
417
+ this.#animatedNodes.push(targetedAnimation.target.id);
418
+ }
419
+ });
420
+ });
421
+ }
422
+
423
+ #getOpeneingAnimations() {
424
+ const openings = {};
425
+ this.#assetContainer.animationGroups.forEach((animationGroup) => {
426
+ const match = animationGroup.name.match(/^animation_(open|close)_(.+)$/);
427
+ if (!match) {
428
+ return;
429
+ }
430
+ const [, type, openingName] = match;
431
+ if (!openings[openingName]) {
432
+ openings[openingName] = { name: openingName, animationOpen: null, animationClose: null };
433
+ }
434
+ if (type === "open") {
435
+ openings[openingName].animationOpen = animationGroup;
436
+ } else if (type === "close") {
437
+ openings[openingName].animationClose = animationGroup;
438
+ }
439
+ });
440
+
441
+ Object.values(openings).forEach((opening) => {
442
+ this.#openingAnimations.push(new OpeningAnimationController(opening.name, opening.animationOpen, opening.animationClose));
443
+ });
444
+ }
445
+
446
+ #getOpeningAnimationByNode(nodeId) {
447
+ return this.#openingAnimations.find((openingAnimation) => openingAnimation.isAnimationForNode(nodeId));
448
+ }
449
+
450
+ /**
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
457
+ */
458
+ #getNodeAnimatedByMesh = function (mesh) {
459
+ let nodeId = false;
460
+ let node = mesh;
461
+ while (node.parent !== null && !nodeId) {
462
+ node = node.parent;
463
+ if (this.#animatedNodes.includes(node.id)) {
464
+ nodeId = node.id;
465
+ }
466
+ }
467
+ return nodeId;
468
+ };
469
+
470
+ /**
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
473
+ */
474
+ #hightlightMeshesForAnimation(pickingInfo) {
475
+ if (!this.#highlightLayer) {
476
+ this.#highlightLayer = new HighlightLayer("hl_animations", this.#scene);
477
+ }
478
+
479
+ this.#highlightLayer.removeAllMeshes();
480
+ if (!pickingInfo?.hit && !pickingInfo?.pickedMesh) {
481
+ return;
482
+ }
483
+
484
+ const nodeId = this.#getNodeAnimatedByMesh(pickingInfo.pickedMesh);
485
+ if (!nodeId) {
486
+ return;
487
+ }
488
+
489
+ const transformNode = this.#scene.getTransformNodeByID(nodeId);
490
+ const nodeMeshes = transformNode.getChildMeshes();
491
+ if (nodeMeshes.length) {
492
+ nodeMeshes.forEach((mesh) => this.#highlightLayer.addMesh(mesh, this.#highlightColor));
493
+ }
494
+ }
495
+
496
+ /**
497
+ * Sets up pointer observers to highlight animated nodes on hover.
498
+ * @private
499
+ */
500
+ #setupPointerObservers() {
501
+ this.#scene.onPointerObservable.add((pointerInfo) => {
502
+ if (pointerInfo.type === PointerEventTypes.POINTERMOVE) {
503
+ const pickingInfo = this.#scene.pick(pointerInfo.event.clientX, pointerInfo.event.clientY);
504
+ this.#hightlightMeshesForAnimation(pickingInfo);
505
+ }
506
+ if (pointerInfo.type === PointerEventTypes.POINTERUP) {
507
+ // Eliminar cualquier Babylon GUI que se haya creado anteriormente
508
+ if (this.#advancedDynamicTexture) {
509
+ this.#advancedDynamicTexture.dispose();
510
+ this.#advancedDynamicTexture = null;
511
+ }
512
+ const pickingInfo = this.#scene.pick(pointerInfo.event.clientX, pointerInfo.event.clientY);
513
+ this.#showMenu(pickingInfo);
514
+ }
515
+ });
516
+ }
517
+
518
+ #showMenu(pickingInfo) {
519
+ if (!pickingInfo?.hit && !pickingInfo?.pickedMesh) {
520
+ return;
521
+ }
522
+
523
+ const nodeId = this.#getNodeAnimatedByMesh(pickingInfo.pickedMesh);
524
+ if (!nodeId) {
525
+ return;
526
+ }
527
+ const openingAnimation = this.#getOpeningAnimationByNode(nodeId);
528
+ if (!openingAnimation) {
529
+ return;
530
+ }
531
+ if (!this.#advancedDynamicTexture) {
532
+ this.#advancedDynamicTexture = AdvancedDynamicTexture.CreateFullscreenUI("UI_Animation");
533
+ }
534
+ openingAnimation.showControls(this.#advancedDynamicTexture, pickingInfo.pickedMesh);
535
+ }
536
+ }
@@ -3,7 +3,9 @@ import { DracoCompression } from "@babylonjs/core/Meshes/Compression/dracoCompre
3
3
  import "@babylonjs/loaders";
4
4
  import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_draco_mesh_compression";
5
5
  import { USDZExportAsync, GLTF2Export } from "@babylonjs/serializers";
6
+
6
7
  import GLTFResolver from "./gltf-resolver.js";
8
+ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
7
9
 
8
10
  /**
9
11
  * BabylonJSController - Main controller for managing Babylon.js 3D scenes, assets, and interactions.
@@ -76,6 +78,7 @@ export default class BabylonJSController {
76
78
  #options = {};
77
79
 
78
80
  #gltfResolver = null; // GLTFResolver instance
81
+ #babylonJSAnimationController = null; // AnimationController instance
79
82
 
80
83
  /**
81
84
  * Constructs a new BabylonJSController instance.
@@ -629,7 +632,7 @@ export default class BabylonJSController {
629
632
  * @private
630
633
  * @param {object} container - The container object containing asset state and metadata.
631
634
  * @param {object} newAssetContainer - The new asset container to add to the scene.
632
- * @returns {boolean} True if the container was replaced, false otherwise.
635
+ * @returns {boolean} True if the container was replaced and added, false otherwise.
633
636
  */
634
637
  #replaceContainer(container, newAssetContainer) {
635
638
  if (container.assetContainer) {
@@ -639,8 +642,7 @@ export default class BabylonJSController {
639
642
  }
640
643
  this.#scene.getEngine().releaseEffects();
641
644
  container.assetContainer = newAssetContainer;
642
- this.#addContainer(container);
643
- return true;
645
+ return this.#addContainer(container);
644
646
  }
645
647
  /**
646
648
  * Sets the visibility of wall and floor meshes in the model container based on the provided value or environment visibility.
@@ -663,7 +665,6 @@ export default class BabylonJSController {
663
665
  * @private
664
666
  * @returns {void}
665
667
  */
666
- #stopR;
667
668
  #stopRender() {
668
669
  this.#engine.stopRenderLoop(this.#renderLoop.bind(this));
669
670
  }
@@ -753,7 +754,10 @@ export default class BabylonJSController {
753
754
  if (container.state.name === "model") {
754
755
  assetContainer.lights = [];
755
756
  }
756
- this.#replaceContainer(container, assetContainer);
757
+ const replacedAndAdded = this.#replaceContainer(container, assetContainer);
758
+ if (replacedAndAdded && container.state.name === "model") {
759
+ this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#scene, assetContainer);
760
+ }
757
761
  container.state.setSuccess(true);
758
762
  } else {
759
763
  if (container.assetContainer && container.state.mustBeShown !== container.state.isVisible && container.state.name === "materials") {
@@ -0,0 +1 @@
1
+ <svg id="pause" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M14,19H18V5H14M6,19H10V5H6V19Z" /></svg>
@@ -0,0 +1 @@
1
+ <svg id="play_backwards" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M16,18.86V4.86l-11,7,11,7Z" /></svg>
@@ -0,0 +1 @@
1
+ <svg id="play" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M8,5.14v14l11-7-11-7Z" /></svg>
@@ -0,0 +1 @@
1
+ <svg id="skip-backward" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M20,5V19L13,12M6,5V19H4V5M13,5V19L6,12" /></svg>
@@ -0,0 +1 @@
1
+ <svg id="skip-forward" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M4,5V19L11,12M18,5V19H20V5M11,5V19L18,12" /></svg>
package/src/index.js CHANGED
@@ -270,7 +270,7 @@ class PrefViewer extends HTMLElement {
270
270
 
271
271
  const customEventOptions = {
272
272
  bubbles: true,
273
- cancelable: false,
273
+ cancelable: true,
274
274
  composed: true,
275
275
  };
276
276
  if (detail) {
@@ -294,7 +294,7 @@ class PrefViewer extends HTMLElement {
294
294
 
295
295
  const customEventOptions = {
296
296
  bubbles: true,
297
- cancelable: false,
297
+ cancelable: true,
298
298
  composed: true,
299
299
  };
300
300
  this.dispatchEvent(new CustomEvent("drawing-loading", customEventOptions));
@@ -316,7 +316,7 @@ class PrefViewer extends HTMLElement {
316
316
 
317
317
  const customEventOptions = {
318
318
  bubbles: true,
319
- cancelable: false,
319
+ cancelable: true,
320
320
  composed: true,
321
321
  };
322
322
  if (detail) {
@@ -333,9 +333,11 @@ class PrefViewer extends HTMLElement {
333
333
  * @returns {void}
334
334
  */
335
335
  #on2DZoomChanged(event) {
336
+ event.stopPropagation();
337
+ event.preventDefault();
336
338
  const customEventOptions = {
337
339
  bubbles: true,
338
- cancelable: false,
340
+ cancelable: true,
339
341
  composed: true,
340
342
  detail: event.detail,
341
343
  };
@@ -221,9 +221,9 @@ export class PrefViewer2D extends HTMLElement {
221
221
  const error = "PrefViewer2D: Error in SVG provided for loading. Ensure the SVG is a URL to an SVG file, a string containing a SVG, or a string containing a base64-encoded SVG.";
222
222
  const detail = { error: new Error(error) };
223
223
  const customEventOptions = {
224
- bubbles: true,
225
- cancelable: false,
226
- composed: true,
224
+ bubbles: true, // indicates whether the event bubbles up through the DOM tree or not
225
+ cancelable: true, // indicates whether the event can be canceled, and therefore prevented as if the event never happened
226
+ composed: false, // indicates whether or not the event will propagate across the shadow DOM boundary into the standard DOM
227
227
  detail: detail,
228
228
  };
229
229
  this.dispatchEvent(new CustomEvent("drawing-error", customEventOptions));
@@ -238,8 +238,8 @@ export class PrefViewer2D extends HTMLElement {
238
238
  #onLoaded() {
239
239
  const customEventOptions = {
240
240
  bubbles: true,
241
- cancelable: false,
242
- composed: true,
241
+ cancelable: true,
242
+ composed: false,
243
243
  };
244
244
  this.dispatchEvent(new CustomEvent("drawing-loaded", customEventOptions));
245
245
 
@@ -258,8 +258,8 @@ export class PrefViewer2D extends HTMLElement {
258
258
  #onLoading() {
259
259
  const customEventOptions = {
260
260
  bubbles: true,
261
- cancelable: false,
262
- composed: true,
261
+ cancelable: true,
262
+ composed: false,
263
263
  };
264
264
  this.dispatchEvent(new CustomEvent("drawing-loading", customEventOptions));
265
265
 
@@ -285,10 +285,11 @@ export class PrefViewer2D extends HTMLElement {
285
285
  #onPanzoomChanged(state) {
286
286
  const customEventOptions = {
287
287
  bubbles: true,
288
- cancelable: false,
289
- composed: true,
288
+ cancelable: true,
289
+ composed: false,
290
290
  detail: state,
291
291
  };
292
+ customEventOptions.detail.desde2d = "true";
292
293
  this.dispatchEvent(new CustomEvent("drawing-zoom-changed", customEventOptions));
293
294
  }
294
295
 
@@ -303,9 +303,9 @@ export class PrefViewer3D extends HTMLElement {
303
303
  */
304
304
  #onLoading() {
305
305
  const customEventOptions = {
306
- bubbles: true,
307
- cancelable: false,
308
- composed: true,
306
+ bubbles: true, // indicates whether the event bubbles up through the DOM tree or not
307
+ cancelable: true, // indicates whether the event can be canceled, and therefore prevented as if the event never happened
308
+ composed: false, // indicates whether or not the event will propagate across the shadow DOM boundary into the standard DOM
309
309
  };
310
310
  this.dispatchEvent(new CustomEvent("scene-loading", customEventOptions));
311
311
 
@@ -351,8 +351,8 @@ export class PrefViewer3D extends HTMLElement {
351
351
 
352
352
  const customEventOptions = {
353
353
  bubbles: true,
354
- cancelable: false,
355
- composed: true,
354
+ cancelable: true,
355
+ composed: false,
356
356
  detail: detail,
357
357
  };
358
358
  this.dispatchEvent(new CustomEvent("scene-loaded", customEventOptions));
@@ -375,8 +375,8 @@ export class PrefViewer3D extends HTMLElement {
375
375
  #onSettingOptions() {
376
376
  const customEventOptions = {
377
377
  bubbles: true,
378
- cancelable: false,
379
- composed: true,
378
+ cancelable: true,
379
+ composed: false,
380
380
  };
381
381
  this.dispatchEvent(new CustomEvent("scene-setting-options", customEventOptions));
382
382
 
@@ -416,8 +416,8 @@ export class PrefViewer3D extends HTMLElement {
416
416
 
417
417
  const customEventOptions = {
418
418
  bubbles: true,
419
- cancelable: false,
420
- composed: true,
419
+ cancelable: true,
420
+ composed: false,
421
421
  detail: detail,
422
422
  };
423
423
  this.dispatchEvent(new CustomEvent("scene-set-options", customEventOptions));