@preference-sl/pref-viewer 2.11.0-beta.1 → 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.1",
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
+ }
@@ -1,9 +1,11 @@
1
1
  import { Engine, Scene, ArcRotateCamera, Vector3, Color4, HemisphericLight, DirectionalLight, PointLight, ShadowGenerator, LoadAssetContainerAsync, Tools, WebXRSessionManager, WebXRDefaultExperience, MeshBuilder, WebXRFeatureName, HDRCubeTexture, IblShadowsRenderPipeline } from "@babylonjs/core";
2
+ import { DracoCompression } from "@babylonjs/core/Meshes/Compression/dracoCompression";
2
3
  import "@babylonjs/loaders";
3
- import { USDZExportAsync, GLTF2Export } from "@babylonjs/serializers";
4
4
  import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_draco_mesh_compression";
5
- import { DracoCompression } from "@babylonjs/core/Meshes/Compression/dracoCompression";
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
  }
@@ -725,11 +726,11 @@ export default class BabylonJSController {
725
726
  /**
726
727
  * Loads all asset containers (model, environment, materials, etc.) and adds them to the scene.
727
728
  * @private
728
- * @returns {Promise<boolean>} Resolves to true if loading succeeds, false otherwise.
729
+ * @returns {Promise<{success: boolean, error: any}>} Resolves to an object indicating if loading succeeded and any error encountered.
729
730
  * @description
730
731
  * Waits for all containers to load in parallel, then replaces or adds them to the scene as needed.
731
732
  * Applies material and camera options, sets wall/floor visibility, and initializes lights and shadows.
732
- * Returns true if all containers loaded successfully, false otherwise.
733
+ * Returns an object with success status and error details.
733
734
  */
734
735
  async #loadContainers() {
735
736
  this.#stopRender();
@@ -739,7 +740,10 @@ export default class BabylonJSController {
739
740
  promiseArray.push(this.#loadAssetContainer(container));
740
741
  });
741
742
 
742
- let success = false;
743
+ let detail = {
744
+ success: false,
745
+ error: null,
746
+ };
743
747
 
744
748
  await Promise.allSettled(promiseArray)
745
749
  .then((values) => {
@@ -750,7 +754,10 @@ export default class BabylonJSController {
750
754
  if (container.state.name === "model") {
751
755
  assetContainer.lights = [];
752
756
  }
753
- 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
+ }
754
761
  container.state.setSuccess(true);
755
762
  } else {
756
763
  if (container.assetContainer && container.state.mustBeShown !== container.state.isVisible && container.state.name === "materials") {
@@ -763,21 +770,21 @@ export default class BabylonJSController {
763
770
  this.#setOptions_Materials();
764
771
  this.#setOptions_Camera();
765
772
  this.#setVisibilityOfWallAndFloorInModel();
766
- success = true;
773
+ detail.success = true;
767
774
  })
768
775
  .catch((error) => {
769
776
  this.loaded = true;
770
777
  console.error("PrefViewer: failed to load model", error);
771
- success = false;
778
+ detail.success = false;
779
+ detail.error = error;
772
780
  })
773
781
  .finally(async () => {
774
782
  this.#setMaxSimultaneousLights();
775
783
  this.#initializeShadows();
776
784
  this.#startRender();
777
- return success;
778
785
  });
779
786
 
780
- return success;
787
+ return detail;
781
788
  }
782
789
 
783
790
  /**