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

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.
@@ -1,21 +1,31 @@
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";
1
+ import { ArcRotateCamera, AssetContainer, Camera, Color4, DirectionalLight, Engine, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, LoadAssetContainerAsync, MeshBuilder, PointerEventTypes, PointLight, Scene, ShadowGenerator, Tools, Vector3, WebXRDefaultExperience, WebXRFeatureName, WebXRSessionManager, WebXRState } from "@babylonjs/core";
2
+ import { DracoCompression } from "@babylonjs/core/Meshes/Compression/dracoCompression.js";
3
3
  import "@babylonjs/loaders";
4
- import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_draco_mesh_compression";
4
+ import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_draco_mesh_compression.js";
5
5
  import { USDZExportAsync, GLTF2Export } from "@babylonjs/serializers";
6
+ import JSZip from "jszip";
7
+
6
8
  import GLTFResolver from "./gltf-resolver.js";
9
+ import { MaterialData } from "./pref-viewer-3d-data.js";
10
+ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
7
11
 
8
12
  /**
9
- * BabylonJSController - Main controller for managing Babylon.js 3D scenes, assets, and interactions.
13
+ * BabylonJSController - Main controller for managing Babylon.js 3D scenes, assets, rendering, and user interactions in PrefViewer.
14
+ *
15
+ * Summary:
16
+ * Provides a high-level API for initializing, loading, displaying, and exporting 3D models and environments using Babylon.js.
17
+ * Manages advanced rendering features, AR integration, asset containers, and user interaction logic.
10
18
  *
11
- * Responsibilities:
19
+ * Key Responsibilities:
12
20
  * - Initializes and manages the Babylon.js engine, scene, camera, lights, and asset containers.
13
21
  * - Handles loading, replacing, and disposing of 3D models, environments, and materials.
14
22
  * - Configures advanced rendering features such as Draco mesh compression, shadows, and image-based lighting (IBL).
15
23
  * - Integrates Babylon.js WebXR experience for augmented reality (AR) support.
16
- * - Provides methods for downloading models and scenes in GLB and USDZ formats.
24
+ * - Provides methods for downloading models and scenes in GLB, glTF (ZIP), and USDZ formats.
17
25
  * - Manages camera and material options, container visibility, and user interactions.
18
26
  * - Observes canvas resize events and updates the engine accordingly.
27
+ * - Designed for integration with PrefViewer and GLTFResolver.
28
+ * - All resource management and rendering operations are performed asynchronously for performance.
19
29
  *
20
30
  * Usage:
21
31
  * - Instantiate: const controller = new BabylonJSController(canvas, containers, options);
@@ -23,7 +33,7 @@ import GLTFResolver from "./gltf-resolver.js";
23
33
  * - Load assets: await controller.load();
24
34
  * - Set camera/material options: controller.setCameraOptions(), controller.setMaterialOptions();
25
35
  * - Control visibility: controller.setContainerVisibility(name, show);
26
- * - Download assets: controller.downloadModelGLB(), controller.downloadModelUSDZ(), etc.
36
+ * - Download assets: controller.downloadGLB(), controller.downloadGLTF(), controller.downloadUSDZ();
27
37
  * - Disable rendering: controller.disable();
28
38
  *
29
39
  * Public Methods:
@@ -33,25 +43,50 @@ import GLTFResolver from "./gltf-resolver.js";
33
43
  * - setCameraOptions(): Applies camera options from configuration.
34
44
  * - setMaterialOptions(): Applies material options from configuration.
35
45
  * - setContainerVisibility(name, show): Shows or hides a container by name.
36
- * - downloadModelGLB(), downloadModelUSDZ(), downloadModelAndSceneGLB(), downloadModelAndSceneUSDZ(): Downloads assets.
46
+ * - downloadGLB(content): Downloads the current scene, model, or environment as a GLB file.
47
+ * - downloadGLTF(content): Downloads the current scene, model, or environment as a glTF ZIP file.
48
+ * - downloadUSDZ(content): Downloads the current scene, model, or environment as a USDZ file.
37
49
  *
38
- * Private Methods:
50
+ * Private Methods (using ECMAScript private fields):
51
+ * - #bindHandlers(): Pre-binds reusable event handlers to preserve stable references.
39
52
  * - #configureDracoCompression(): Sets up Draco mesh compression.
40
- * - #createCamera(), #createLights(), #initializeEnvironmentTexture(), #initializeIBLShadows(), #initializeShadows(): Scene setup.
41
- * - #setupInteraction(): Sets up canvas interaction handlers.
42
- * - #disposeEngine(): Disposes engine and resources.
43
- * - #setOptionsMaterial(), #setOptions_Materials(), #setOptions_Camera(): Applies material/camera options.
44
- * - #findContainerByName(), #addContainer(), #removeContainer(), #replaceContainer(): Container management.
45
- * - #setVisibilityOfWallAndFloorInModel(): Controls wall/floor mesh visibility.
46
- * - #stopRender(), #startRender(): Render loop control.
47
- * - #loadAssetContainer(), #loadContainers(): Asset loading.
53
+ * - #renderLoop(): Babylon.js render loop callback.
48
54
  * - #addStylesToARButton(): Styles AR button.
49
55
  * - #createXRExperience(): Initializes WebXR AR experience.
50
- *
51
- * Notes:
52
- * - Designed for integration with PrefViewer and GLTFResolver.
53
- * - Supports advanced Babylon.js features for product visualization and configurators.
54
- * - All resource management and rendering operations are performed asynchronously for performance.
56
+ * - #createCamera(): Creates and configures the main camera.
57
+ * - #createLights(): Creates and configures scene lights and shadows.
58
+ * - #initializeEnvironmentTexture(): Loads and sets the HDR environment texture.
59
+ * - #initializeIBLShadows(): Sets up IBL shadow pipeline and assigns meshes/materials.
60
+ * - #initializeShadows(): Sets up standard or IBL shadows for meshes.
61
+ * - #setMaxSimultaneousLights(): Updates max simultaneous lights for all materials.
62
+ * - #onPointerObservable(info): Handles pointer events and dispatches to pointer/mouse handlers.
63
+ * - #onPointerUp(event, pickInfo): Handles pointer up events (e.g., right-click for animation menu).
64
+ * - #onPointerMove(event, pickInfo): Handles pointer move events (e.g., mesh highlighting).
65
+ * - #onMouseWheel(event, pickInfo): Handles mouse wheel events for camera zoom.
66
+ * - #onKeyUp(event): Handles keyup events for download dialog and shortcuts.
67
+ * - #enableInteraction(): Adds canvas and scene interaction event listeners.
68
+ * - #disableInteraction(): Removes canvas and scene interaction event listeners.
69
+ * - #disposeAnimationController(): Disposes the animation controller if it exists.
70
+ * - #disposeXRExperience(): Disposes the Babylon.js WebXR experience if it exists.
71
+ * - #disposeEngine(): Disposes engine and releases all resources.
72
+ * - #setOptionsMaterial(optionMaterial): Applies a material option to relevant meshes.
73
+ * - #setOptions_Materials(): Applies all material options from configuration.
74
+ * - #setOptions_Camera(): Applies camera options from configuration.
75
+ * - #findContainerByName(name): Finds a container by its name.
76
+ * - #addContainer(container, updateVisibility): Adds a container to the scene and updates visibility.
77
+ * - #removeContainer(container, updateVisibility): Removes a container from the scene and updates visibility.
78
+ * - #replaceContainer(container, newAssetContainer): Replaces a container in the scene.
79
+ * - #getPrefViewer3DComponent(): Caches and retrieves the parent PREF-VIEWER-3D element.
80
+ * - #getPrefViewerComponent(): Caches and retrieves the parent PREF-VIEWER element.
81
+ * - #updateVisibilityAttributeInComponents(name, isVisible): Updates parent visibility attributes.
82
+ * - #setVisibilityOfWallAndFloorInModel(show): Controls wall/floor mesh visibility.
83
+ * - #stopRender(): Stops the Babylon.js render loop.
84
+ * - #startRender(): Starts the Babylon.js render loop.
85
+ * - #loadAssetContainer(container): Loads an asset container asynchronously.
86
+ * - #loadContainers(): Loads all asset containers and adds them to the scene.
87
+ * - #addDateToName(name): Appends the current date/time to a name string.
88
+ * - #downloadZip(files, name, comment, addDateInName): Generates and downloads a ZIP file.
89
+ * - #openDownloadDialog(): Opens the modal download dialog.
55
90
  */
56
91
  export default class BabylonJSController {
57
92
  // Canvas HTML element
@@ -76,6 +111,13 @@ export default class BabylonJSController {
76
111
  #options = {};
77
112
 
78
113
  #gltfResolver = null; // GLTFResolver instance
114
+ #babylonJSAnimationController = null; // AnimationController instance
115
+
116
+ #handlers = {
117
+ onKeyUp: null,
118
+ onPointerObservable: null,
119
+ renderLoop: null,
120
+ };
79
121
 
80
122
  /**
81
123
  * Constructs a new BabylonJSController instance.
@@ -99,6 +141,18 @@ export default class BabylonJSController {
99
141
  };
100
142
  });
101
143
  this.#options = options;
144
+ this.#bindHandlers();
145
+ }
146
+
147
+ /**
148
+ * Pre-binds reusable event handlers to preserve stable references.
149
+ * @private
150
+ * @returns {void}
151
+ */
152
+ #bindHandlers() {
153
+ this.#handlers.onKeyUp = this.#onKeyUp.bind(this);
154
+ this.#handlers.onPointerObservable = this.#onPointerObservable.bind(this);
155
+ this.#handlers.renderLoop = this.#renderLoop.bind(this);
102
156
  }
103
157
 
104
158
  /**
@@ -290,29 +344,42 @@ export default class BabylonJSController {
290
344
  return false;
291
345
  }
292
346
 
293
- let createIBLShadowPipeline = function (scene) {
347
+ /**
348
+ * Creates and configures the IBL shadow render pipeline for the Babylon.js scene.
349
+ * Sets recommended options for resolution, sampling, opacity, and disables debug passes.
350
+ * Accepts an optional camera array for pipeline targeting.
351
+ * @private
352
+ * @param {Scene} scene - The Babylon.js scene instance.
353
+ * @param {Camera[]} [cameras] - Optional array of cameras to target with the pipeline.
354
+ * @returns {IblShadowsRenderPipeline} The configured IBL shadow pipeline.
355
+ */
356
+ let createIBLShadowPipeline = function (scene, cameras = [scene.activeCamera]) {
294
357
  const pipeline = new IblShadowsRenderPipeline(
295
358
  "iblShadowsPipeline",
296
359
  scene,
297
360
  {
298
- resolutionExp: 7,
299
- sampleDirections: 2,
361
+ resolutionExp: 8, // Higher resolution for better shadow quality
362
+ sampleDirections: 4, // More sample directions for smoother shadows
300
363
  ssShadowsEnabled: true,
301
- shadowRemanence: 0.8,
364
+ shadowRemanence: 0.85,
302
365
  triPlanarVoxelization: true,
303
- shadowOpacity: 0.8,
366
+ shadowOpacity: 0.85,
304
367
  },
305
- [scene.activeCamera]
368
+ cameras
306
369
  );
307
- pipeline.allowDebugPasses = false;
308
- pipeline.gbufferDebugEnabled = true;
309
- pipeline.importanceSamplingDebugEnabled = false;
310
- pipeline.voxelDebugEnabled = false;
311
- pipeline.voxelDebugDisplayMip = 1;
312
- pipeline.voxelDebugAxis = 2;
313
- pipeline.voxelTracingDebugEnabled = false;
314
- pipeline.spatialBlurPassDebugEnabled = false;
315
- pipeline.accumulationPassDebugEnabled = false;
370
+ // Disable all debug passes for performance
371
+ const pipelineProps = {
372
+ allowDebugPasses: false,
373
+ gbufferDebugEnabled: false,
374
+ importanceSamplingDebugEnabled: false,
375
+ voxelDebugEnabled: false,
376
+ voxelDebugDisplayMip: 0,
377
+ voxelDebugAxis: 0,
378
+ voxelTracingDebugEnabled: false,
379
+ spatialBlurPassDebugEnabled: false,
380
+ accumulationPassDebugEnabled: false,
381
+ };
382
+ Object.assign(pipeline, pipelineProps);
316
383
  return pipeline;
317
384
  };
318
385
 
@@ -375,24 +442,88 @@ export default class BabylonJSController {
375
442
  }
376
443
 
377
444
  /**
378
- * Sets up interaction handlers for the Babylon.js canvas.
379
- * Adds a wheel event listener to control camera zoom based on mouse wheel input.
380
- * Prevents zoom if the active camera is locked.
445
+ * Handles pointer events observed on the Babylon.js scene.
381
446
  * @private
447
+ * @param {PointerInfo} info - The pointer event information from Babylon.js.
382
448
  * @returns {void}
383
449
  */
384
- #setupInteraction() {
385
- this.#canvas.addEventListener("wheel", (event) => {
386
- if (!this.#scene || !this.#camera) {
387
- return false;
388
- }
389
- //const pick = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
390
- //this.#camera.target = pick.hit ? pick.pickedPoint.clone() : this.#camera.target;
391
- if (!this.#scene.activeCamera.metadata?.locked) {
392
- this.#scene.activeCamera.inertialRadiusOffset -= event.deltaY * this.#scene.activeCamera.wheelPrecision * 0.001;
393
- }
394
- event.preventDefault();
395
- });
450
+ #onPointerObservable(info) {
451
+ const pickInfo = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
452
+ if (info.type === PointerEventTypes.POINTERUP) {
453
+ this.#onPointerUp(info.event, pickInfo);
454
+ } else if (info.type === PointerEventTypes.POINTERMOVE) {
455
+ this.#onPointerMove(info.event, pickInfo);
456
+ } else if (info.type === PointerEventTypes.POINTERWHEEL) {
457
+ this.#onMouseWheel(info.event, pickInfo);
458
+ }
459
+ }
460
+
461
+ /**
462
+ * Sets up interaction handlers for the Babylon.js canvas and scene.
463
+ * @private
464
+ * @returns {void}
465
+ */
466
+ #enableInteraction() {
467
+ if (this.#canvas) {
468
+ this.#canvas.addEventListener("keyup", this.#handlers.onKeyUp);
469
+ }
470
+ if (this.#scene) {
471
+ this.#scene.onPointerObservable.add(this.#handlers.onPointerObservable);
472
+ }
473
+ }
474
+
475
+ /**
476
+ * Removes interaction event listeners from the Babylon.js canvas.
477
+ * @private
478
+ * @returns {void}
479
+ */
480
+ #disableInteraction() {
481
+ if (this.#canvas) {
482
+ this.#canvas.removeEventListener("keyup", this.#handlers.onKeyUp);
483
+ }
484
+ if (this.#scene !== null) {
485
+ this.#scene.onPointerObservable.removeCallback(this.#handlers.onPointerObservable);
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Disposes the BabylonJSAnimationController instance if it exists.
491
+ * @private
492
+ * @returns {void}
493
+ */
494
+ #disposeAnimationController() {
495
+ if (this.#babylonJSAnimationController) {
496
+ this.#babylonJSAnimationController.dispose();
497
+ this.#babylonJSAnimationController = null;
498
+ }
499
+ }
500
+
501
+ /**
502
+ * Disposes the Babylon.js WebXR experience if it exists.
503
+ * @private
504
+ * @returns {void}
505
+ */
506
+ #disposeXRExperience() {
507
+ if (!this.#XRExperience) {
508
+ return;
509
+ }
510
+
511
+ if (this.#XRExperience.baseExperience.state === WebXRState.IN_XR) {
512
+ this.#XRExperience.baseExperience
513
+ .exitXRAsync()
514
+ .then(() => {
515
+ this.#XRExperience.dispose();
516
+ this.#XRExperience = null;
517
+ })
518
+ .catch((error) => {
519
+ console.warn("Error exiting XR experience:", error);
520
+ this.#XRExperience.dispose();
521
+ this.#XRExperience = null;
522
+ });
523
+ } else {
524
+ this.#XRExperience.dispose();
525
+ this.#XRExperience = null;
526
+ }
396
527
  }
397
528
 
398
529
  /**
@@ -408,13 +539,79 @@ export default class BabylonJSController {
408
539
  this.#engine = this.#scene = this.#camera = null;
409
540
  this.#hemiLight = this.#dirLight = this.#cameraLight = null;
410
541
  this.#shadowGen = null;
411
- this.#XRExperience = null;
542
+ }
543
+
544
+ /**
545
+ * Handles keyup events on the Babylon.js canvas for triggering model and scene downloads.
546
+ * @private
547
+ * @param {KeyboardEvent} event - The keyup event.
548
+ * @returns {void}
549
+ */
550
+ #onKeyUp(event) {
551
+ // CTRL + ALT + letter
552
+ if (event.ctrlKey && event.altKey && event.key !== undefined) {
553
+ switch (event.key.toLowerCase()) {
554
+ case "d":
555
+ this.#openDownloadDialog();
556
+ break;
557
+ default:
558
+ break;
559
+ }
560
+ }
561
+ }
562
+
563
+ /**
564
+ * Handles mouse wheel events on the Babylon.js canvas for zooming the camera.
565
+ * @private
566
+ * @param {WheelEvent} event - The mouse wheel event.
567
+ * @param {Object} pickInfo - The result of the scene pick operation (not used in this method).
568
+ * @returns {void|false} Returns false if there is no active camera; otherwise, void.
569
+ */
570
+ #onMouseWheel(event, pickInfo) {
571
+ if (!this.#scene?.activeCamera) {
572
+ return false;
573
+ }
574
+ //this.#scene.activeCamera.target = pickInfo.hit ? pickInfo.pickedPoint.clone() : this.#scene.activeCamera.target;
575
+ if (!this.#scene.activeCamera.metadata?.locked) {
576
+ this.#scene.activeCamera.inertialRadiusOffset -= event.deltaY * this.#scene.activeCamera.wheelPrecision * 0.001;
577
+ }
578
+ event.preventDefault();
579
+ }
580
+
581
+ /**
582
+ * Handles pointer up events on the Babylon.js scene.
583
+ * @private
584
+ * @param {PointerEvent} event - The pointer up event.
585
+ * @param {PickInfo} pickInfo - The result of the scene pick operation.
586
+ * @returns {void}
587
+ */
588
+ #onPointerUp(event, pickInfo) {
589
+ if (this.#babylonJSAnimationController) {
590
+ this.#babylonJSAnimationController.hideMenu();
591
+ // Right click for showing animation menu
592
+ if (event.button === 2) {
593
+ this.#babylonJSAnimationController.showMenu(pickInfo);
594
+ }
595
+ }
596
+ }
597
+
598
+ /**
599
+ * Handles pointer move events on the Babylon.js scene.
600
+ * @private
601
+ * @param {PointerEvent} event - The pointer move event.
602
+ * @param {PickInfo} pickInfo - The result of the scene pick operation.
603
+ * @returns {void}
604
+ */
605
+ #onPointerMove(event, pickInfo) {
606
+ if (this.#babylonJSAnimationController) {
607
+ this.#babylonJSAnimationController.hightlightMeshes(pickInfo);
608
+ }
412
609
  }
413
610
 
414
611
  /**
415
612
  * Applies material options from the configuration to the relevant meshes.
416
613
  * @private
417
- * @param {object} optionMaterial - Material option containing value, nodePrefixes, nodeNames, and state.
614
+ * @param {MaterialData} optionMaterial - Material option containing value, nodePrefixes, nodeNames, and state.
418
615
  * @returns {boolean} True if any mesh material was set, false otherwise.
419
616
  */
420
617
  #setOptionsMaterial(optionMaterial) {
@@ -435,7 +632,7 @@ export default class BabylonJSController {
435
632
  if (container.state.name === "materials") {
436
633
  return;
437
634
  }
438
- if (container.assetContainer && (container.isPending || materialContainer.isPending || optionMaterial.isPending)) {
635
+ if (container.assetContainer && (container.state.isPending || materialContainer.state.isPending || optionMaterial.isPending)) {
439
636
  assetContainersToProcess.push(container.assetContainer);
440
637
  }
441
638
  });
@@ -487,7 +684,7 @@ export default class BabylonJSController {
487
684
  const modelContainer = this.#containers.model;
488
685
  const environmentContainer = this.#containers.environment;
489
686
 
490
- if (!cameraState.isPending && !modelContainer.isPending && !environmentContainer.isPending) {
687
+ if (!cameraState.isPending && !modelContainer.state.isPending && !environmentContainer.state.isPending) {
491
688
  return false;
492
689
  }
493
690
 
@@ -542,7 +739,7 @@ export default class BabylonJSController {
542
739
  * Finds and returns the asset container object by its name.
543
740
  * @private
544
741
  * @param {string} name - The name of the container to find.
545
- * @returns {object|null} The matching container object, or null if not found.
742
+ * @returns {Object|null} The matching container object, or null if not found.
546
743
  */
547
744
  #findContainerByName(name) {
548
745
  return Object.values(this.#containers).find((container) => container.state.name === name) || null;
@@ -567,6 +764,13 @@ export default class BabylonJSController {
567
764
  */
568
765
  #getPrefViewerComponent() {
569
766
  if (this.#prefViewer === undefined) {
767
+ if (this.#prefViewer3D === undefined) {
768
+ this.#getPrefViewer3DComponent();
769
+ }
770
+ if (!this.#prefViewer3D) {
771
+ this.#prefViewer = null;
772
+ return;
773
+ }
570
774
  const rootNode = this.#prefViewer3D ? this.#prefViewer3D.getRootNode().host : null;
571
775
  this.#prefViewer = rootNode && rootNode.nodeName === "PREF-VIEWER" ? rootNode : null;
572
776
  }
@@ -579,7 +783,7 @@ export default class BabylonJSController {
579
783
  * @param {boolean} isVisible - True to show the container, false to hide it.
580
784
  * @returns {void}
581
785
  */
582
- #updateVisibilityAttributeInComponentes(name, isVisible) {
786
+ #updateVisibilityAttributeInComponents(name, isVisible) {
583
787
  // Cache references to parent custom elements
584
788
  this.#getPrefViewer3DComponent();
585
789
  this.#getPrefViewerComponent();
@@ -596,13 +800,16 @@ export default class BabylonJSController {
596
800
  * Adds the asset container to the Babylon.js scene if it should be shown and is not already visible.
597
801
  * @private
598
802
  * @param {object} container - The container object containing asset state and metadata.
803
+ * @param {boolean} [updateVisibility=true] - If true, updates the visibility attribute in parent components.
599
804
  * @returns {boolean} True if the container was added, false otherwise.
600
805
  */
601
- #addContainer(container) {
806
+ #addContainer(container, updateVisibility = true) {
602
807
  if (!container.assetContainer || container.state.isVisible || !container.state.mustBeShown) {
603
808
  return false;
604
809
  }
605
- this.#updateVisibilityAttributeInComponentes(container.state.name, true);
810
+ if (updateVisibility) {
811
+ this.#updateVisibilityAttributeInComponents(container.state.name, true);
812
+ }
606
813
  container.assetContainer.addAllToScene();
607
814
  container.state.visible = true;
608
815
  return true;
@@ -612,13 +819,16 @@ export default class BabylonJSController {
612
819
  * Removes the asset container from the Babylon.js scene if it is currently visible.
613
820
  * @private
614
821
  * @param {object} container - The container object containing asset state and metadata.
822
+ * @param {boolean} [updateVisibility=true] - If true, updates the visibility attribute in parent components.
615
823
  * @returns {boolean} True if the container was removed, false otherwise.
616
824
  */
617
- #removeContainer(container) {
825
+ #removeContainer(container, updateVisibility = true) {
618
826
  if (!container.assetContainer || !container.state.isVisible) {
619
827
  return false;
620
828
  }
621
- this.#updateVisibilityAttributeInComponentes(container.state.name, false);
829
+ if (updateVisibility) {
830
+ this.#updateVisibilityAttributeInComponents(container.state.name, false);
831
+ }
622
832
  container.assetContainer.removeAllFromScene();
623
833
  container.state.visible = false;
624
834
  return true;
@@ -628,20 +838,20 @@ export default class BabylonJSController {
628
838
  * Replaces the asset container in the Babylon.js scene with a new one.
629
839
  * @private
630
840
  * @param {object} container - The container object containing asset state and metadata.
631
- * @param {object} newAssetContainer - The new asset container to add to the scene.
632
- * @returns {boolean} True if the container was replaced, false otherwise.
841
+ * @param {AssetContainer} newAssetContainer - The new asset container to add to the scene.
842
+ * @returns {boolean} True if the container was replaced and added, false otherwise.
633
843
  */
634
844
  #replaceContainer(container, newAssetContainer) {
635
845
  if (container.assetContainer) {
636
- this.#removeContainer(container);
846
+ this.#removeContainer(container, false);
637
847
  container.assetContainer.dispose();
638
848
  container.assetContainer = null;
639
849
  }
640
850
  this.#scene.getEngine().releaseEffects();
641
851
  container.assetContainer = newAssetContainer;
642
- this.#addContainer(container);
643
- return true;
852
+ return this.#addContainer(container, false);
644
853
  }
854
+
645
855
  /**
646
856
  * Sets the visibility of wall and floor meshes in the model container based on the provided value or environment visibility.
647
857
  * @private
@@ -663,9 +873,8 @@ export default class BabylonJSController {
663
873
  * @private
664
874
  * @returns {void}
665
875
  */
666
- #stopR;
667
876
  #stopRender() {
668
- this.#engine.stopRenderLoop(this.#renderLoop.bind(this));
877
+ this.#engine.stopRenderLoop(this.#handlers.renderLoop);
669
878
  }
670
879
  /**
671
880
  * Starts the Babylon.js render loop for the current scene.
@@ -675,7 +884,7 @@ export default class BabylonJSController {
675
884
  */
676
885
  async #startRender() {
677
886
  await this.#scene.whenReadyAsync();
678
- this.#engine.runRenderLoop(this.#renderLoop.bind(this));
887
+ this.#engine.runRenderLoop(this.#handlers.renderLoop);
679
888
  }
680
889
 
681
890
  /**
@@ -746,6 +955,7 @@ export default class BabylonJSController {
746
955
 
747
956
  await Promise.allSettled(promiseArray)
748
957
  .then((values) => {
958
+ this.#disposeAnimationController();
749
959
  values.forEach((result) => {
750
960
  const container = result.value ? result.value[0] : null;
751
961
  const assetContainer = result.value ? result.value[1] : null;
@@ -777,12 +987,127 @@ export default class BabylonJSController {
777
987
  .finally(async () => {
778
988
  this.#setMaxSimultaneousLights();
779
989
  this.#initializeShadows();
990
+ this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#scene);
780
991
  this.#startRender();
781
992
  });
782
-
783
993
  return detail;
784
994
  }
785
995
 
996
+ /**
997
+ * Appends the current date and time to the provided name string in the format: name_YYYY-MM-DD_HH.mm.ss
998
+ * @private
999
+ * @param {string} name - The base name to which the date and time will be appended.
1000
+ * @returns {string} The name with the appended date and time.
1001
+ */
1002
+ #addDateToName(name) {
1003
+ const now = new Date();
1004
+ const year = now.getFullYear();
1005
+ const month = (now.getMonth() + 1).toString().padStart(2, "0");
1006
+ const day = now.getDate().toString().padStart(2, "0");
1007
+ const hours = now.getHours().toString().padStart(2, "0");
1008
+ const minutes = now.getMinutes().toString().padStart(2, "0");
1009
+ const seconds = now.getSeconds().toString().padStart(2, "0");
1010
+ name = `${name}_${year}-${month}-${day}_${hours}.${minutes}.${seconds}`;
1011
+ return name;
1012
+ }
1013
+
1014
+ /**
1015
+ * Generates and downloads a ZIP file with the specified folder name and files.
1016
+ * @private
1017
+ * @param {Object} files - An object where keys are file names and values are file contents.
1018
+ * @param {string} [name="files"] - The name for the root folder inside the ZIP.
1019
+ * @param {string} [comment=""] - A comment to include in the ZIP file.
1020
+ * @param {boolean} [addDateInName=false] - Whether to append the current date/time to the folder name.
1021
+ * @returns {void}
1022
+ * @description
1023
+ * Uses JSZip to create the ZIP and triggers a browser download.
1024
+ */
1025
+ #downloadZip(files, name = "files", comment = "", addDateInName = false) {
1026
+ name = addDateInName ? this.#addDateToName(name) : name;
1027
+
1028
+ const zip = new JSZip();
1029
+ zip.comment = comment;
1030
+
1031
+ const rootFolder = zip.folder(name);
1032
+ Object.keys(files).forEach((key) => {
1033
+ rootFolder.file(key, files[key]);
1034
+ });
1035
+
1036
+ zip.generateAsync({ type: "blob" }).then((content) => {
1037
+ const zipName = `${name}.zip`;
1038
+ const a = document.createElement("a");
1039
+ const url = window.URL.createObjectURL(content);
1040
+ a.href = url;
1041
+ a.download = zipName;
1042
+ a.click();
1043
+ window.URL.revokeObjectURL(url);
1044
+ });
1045
+ }
1046
+
1047
+ /**
1048
+ * Opens a modal dialog for downloading the 3D scene, model, or environment.
1049
+ * @private
1050
+ * @returns {void}
1051
+ */
1052
+ #openDownloadDialog() {
1053
+ this.#getPrefViewerComponent();
1054
+ if (!this.#prefViewer) {
1055
+ return;
1056
+ }
1057
+
1058
+ const header = "Download 3D Scene";
1059
+ const content = `
1060
+ <form slot="content" id="download-dialog-form" style="display:flex;flex-direction:column;gap:16px;">
1061
+ <h4>Content</h4>
1062
+ <div style="display:flex;flex-direction:row;gap:16px;margin:0 8px 0 8px;">
1063
+ <label><input type="radio" name="content" value="1"> Model</label>
1064
+ <label><input type="radio" name="content" value="2"> Scene</label>
1065
+ <label><input type="radio" name="content" value="0" checked> Both</label>
1066
+ </div>
1067
+ <h4>Format</h4>
1068
+ <div style="display:flex;flex-direction:row;gap:16px;margin:0 8px 0 8px;">
1069
+ <label><input type="radio" name="format" value="gltf" checked> glTF (ZIP)</label>
1070
+ <label><input type="radio" name="format" value="glb"> GLB</label>
1071
+ <label><input type="radio" name="format" value="usdz"> USDZ</label>
1072
+ </div>
1073
+ </form>`;
1074
+ const footer = `
1075
+ <button type="button" class="primary" id="download-dialog-download">Download</button>
1076
+ <button type="button" id="download-dialog-cancel">Cancel</button>`;
1077
+
1078
+ const dialog = this.#prefViewer.openDialog(header, content, footer);
1079
+
1080
+ if (!dialog) {
1081
+ return;
1082
+ }
1083
+
1084
+ // Button event handlers
1085
+ const form = dialog.querySelector("#download-dialog-form");
1086
+ const downloadButton = dialog.querySelector("#download-dialog-download");
1087
+ const cancelButton = dialog.querySelector("#download-dialog-cancel");
1088
+
1089
+ downloadButton.onclick = () => {
1090
+ const contentValue = form.content.value;
1091
+ const formatValue = form.format.value;
1092
+ switch (formatValue) {
1093
+ case "glb":
1094
+ this.downloadGLB(Number(contentValue));
1095
+ break;
1096
+ case "gltf":
1097
+ this.downloadGLTF(Number(contentValue));
1098
+ break;
1099
+ case "usdz":
1100
+ this.downloadUSDZ(Number(contentValue));
1101
+ break;
1102
+ }
1103
+ this.#prefViewer.closeDialog();
1104
+ };
1105
+
1106
+ cancelButton.onclick = () => {
1107
+ this.#prefViewer.closeDialog();
1108
+ };
1109
+ }
1110
+
786
1111
  /**
787
1112
  * ---------------------------
788
1113
  * Public methods
@@ -798,13 +1123,13 @@ export default class BabylonJSController {
798
1123
  */
799
1124
  async enable() {
800
1125
  this.#configureDracoCompression();
801
- this.#engine = new Engine(this.#canvas, true, { alpha: true });
1126
+ this.#engine = new Engine(this.#canvas, true, { alpha: true, stencil: true, preserveDrawingBuffer: false });
802
1127
  this.#engine.disableUniformBuffers = true;
803
1128
  this.#scene = new Scene(this.#engine);
804
1129
  this.#scene.clearColor = new Color4(1, 1, 1, 1);
805
1130
  this.#createCamera();
806
1131
  this.#createLights();
807
- this.#setupInteraction();
1132
+ this.#enableInteraction();
808
1133
  await this.#createXRExperience();
809
1134
  this.#startRender();
810
1135
  this.#canvasResizeObserver = new ResizeObserver(() => this.#engine && this.#engine.resize());
@@ -818,8 +1143,11 @@ export default class BabylonJSController {
818
1143
  * @returns {void}
819
1144
  */
820
1145
  disable() {
821
- this.#disposeEngine();
822
1146
  this.#canvasResizeObserver.disconnect();
1147
+ this.#disableInteraction();
1148
+ this.#disposeAnimationController();
1149
+ this.#disposeXRExperience();
1150
+ this.#disposeEngine();
823
1151
  }
824
1152
 
825
1153
  /**
@@ -883,50 +1211,105 @@ export default class BabylonJSController {
883
1211
  }
884
1212
 
885
1213
  /**
886
- * Initiates download of the current model as a GLB file.
1214
+ * Downloads the current scene, model, or environment as a GLB file.
887
1215
  * @public
1216
+ * @param {number} content - 0 for full scene, 1 for model, 2 for environment.
888
1217
  * @returns {void}
889
1218
  */
890
- downloadModelGLB() {
891
- const fileName = "model";
892
- GLTF2Export.GLBAsync(this.#containers.model.assetContainer, fileName, { exportWithoutWaitingForScene: true }).then((glb) => glb.downloadFiles());
1219
+ downloadGLB(content = 0) {
1220
+ let fileName = "";
1221
+ let container = null;
1222
+ switch (content) {
1223
+ case 0:
1224
+ fileName = "full-scene";
1225
+ container = this.#scene;
1226
+ break;
1227
+ case 1:
1228
+ fileName = "model";
1229
+ container = this.#containers.model.assetContainer;
1230
+ break;
1231
+ case 2:
1232
+ fileName = "scene";
1233
+ container = this.#containers.environment.assetContainer;
1234
+ break;
1235
+ default:
1236
+ break;
1237
+ }
1238
+ fileName = this.#addDateToName(fileName);
1239
+ GLTF2Export.GLBAsync(container, fileName, { exportWithoutWaitingForScene: true }).then((glb) => {
1240
+ if (glb) {
1241
+ glb.downloadFiles();
1242
+ }
1243
+ });
893
1244
  }
894
1245
 
895
1246
  /**
896
- * Initiates download of the current model as a USDZ file.
1247
+ * Downloads the current scene, model, or environment as a glTF ZIP file.
897
1248
  * @public
1249
+ * @param {number} content - 0 for full scene, 1 for model, 2 for environment.
898
1250
  * @returns {void}
899
1251
  */
900
- downloadModelUSDZ() {
901
- const fileName = "model";
902
- USDZExportAsync(this.#containers.model.assetContainer).then((response) => {
903
- if (response) {
904
- Tools.Download(new Blob([response], { type: "model/vnd.usdz+zip" }), `${fileName}.usdz`);
1252
+ downloadGLTF(content = 0) {
1253
+ let fileName = "";
1254
+ let container = null;
1255
+ let comment = "";
1256
+ switch (content) {
1257
+ case 0:
1258
+ fileName = "full-scene";
1259
+ comment = "Export of the complete scene in glTF format, including the model and the environment.";
1260
+ container = this.#scene;
1261
+ break;
1262
+ case 1:
1263
+ fileName = "model";
1264
+ comment = "Export of model scene in glTF format.";
1265
+ container = this.#containers.model.assetContainer;
1266
+ break;
1267
+ case 2:
1268
+ fileName = "scene";
1269
+ comment = "Export of the environment scene in glTF format.";
1270
+ container = this.#containers.environment.assetContainer;
1271
+ break;
1272
+ default:
1273
+ break;
1274
+ }
1275
+ comment += "\nPrefViewer by Preference, S.L.\npreference.com\n";
1276
+ GLTF2Export.GLTFAsync(container, fileName, { exportWithoutWaitingForScene: true }).then((gltf) => {
1277
+ if (gltf?.files) {
1278
+ this.#downloadZip(gltf.files, fileName, comment, true);
905
1279
  }
906
1280
  });
907
1281
  }
908
1282
 
909
1283
  /**
910
- * Initiates download of the entire scene (model and environment) as a USDZ file.
1284
+ * Downloads the current scene, model, or environment as a USDZ file.
911
1285
  * @public
1286
+ * @param {number} content - 0 for full scene, 1 for model, 2 for environment.
912
1287
  * @returns {void}
913
1288
  */
914
- downloadModelAndSceneUSDZ() {
915
- const fileName = "scene";
916
- USDZExportAsync(this.#scene).then((response) => {
1289
+ downloadUSDZ(content = 0) {
1290
+ let fileName = "";
1291
+ let container = null;
1292
+ switch (content) {
1293
+ case 0:
1294
+ fileName = "full-scene";
1295
+ container = this.#scene;
1296
+ break;
1297
+ case 1:
1298
+ fileName = "model";
1299
+ container = this.#containers.model.assetContainer;
1300
+ break;
1301
+ case 2:
1302
+ fileName = "scene";
1303
+ container = this.#containers.environment.assetContainer;
1304
+ break;
1305
+ default:
1306
+ break;
1307
+ }
1308
+ fileName = this.#addDateToName(fileName);
1309
+ USDZExportAsync(container).then((response) => {
917
1310
  if (response) {
918
1311
  Tools.Download(new Blob([response], { type: "model/vnd.usdz+zip" }), `${fileName}.usdz`);
919
1312
  }
920
1313
  });
921
1314
  }
922
-
923
- /**
924
- * Initiates download of the entire scene (model and environment) as a GLB file.
925
- * @public
926
- * @returns {void}
927
- */
928
- downloadModelAndSceneGLB() {
929
- const fileName = "scene";
930
- GLTF2Export.GLBAsync(this.#scene, fileName, { exportWithoutWaitingForScene: true }).then((glb) => glb.downloadFiles());
931
- }
932
1315
  }