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

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,9 +1,12 @@
1
- import { Engine, Scene, ArcRotateCamera, Vector3, Color4, HemisphericLight, DirectionalLight, PointLight, ShadowGenerator, LoadAssetContainerAsync, Tools, WebXRSessionManager, WebXRDefaultExperience, MeshBuilder, WebXRFeatureName, HDRCubeTexture, IblShadowsRenderPipeline } from "@babylonjs/core";
1
+ import { ArcRotateCamera, AssetContainer, Camera, Color4, DirectionalLight, Engine, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, LoadAssetContainerAsync, MeshBuilder, PointLight, Scene, ShadowGenerator, Tools, Vector3, WebXRDefaultExperience, WebXRFeatureName, WebXRSessionManager } 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 { MaterialData } from "./pref-viewer-3d-data.js";
9
+ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
7
10
 
8
11
  /**
9
12
  * BabylonJSController - Main controller for managing Babylon.js 3D scenes, assets, and interactions.
@@ -13,7 +16,7 @@ import GLTFResolver from "./gltf-resolver.js";
13
16
  * - Handles loading, replacing, and disposing of 3D models, environments, and materials.
14
17
  * - Configures advanced rendering features such as Draco mesh compression, shadows, and image-based lighting (IBL).
15
18
  * - Integrates Babylon.js WebXR experience for augmented reality (AR) support.
16
- * - Provides methods for downloading models and scenes in GLB and USDZ formats.
19
+ * - Provides methods for downloading models and scenes in GLB, GLTF, and USDZ formats.
17
20
  * - Manages camera and material options, container visibility, and user interactions.
18
21
  * - Observes canvas resize events and updates the engine accordingly.
19
22
  *
@@ -23,7 +26,7 @@ import GLTFResolver from "./gltf-resolver.js";
23
26
  * - Load assets: await controller.load();
24
27
  * - Set camera/material options: controller.setCameraOptions(), controller.setMaterialOptions();
25
28
  * - Control visibility: controller.setContainerVisibility(name, show);
26
- * - Download assets: controller.downloadModelGLB(), controller.downloadModelUSDZ(), etc.
29
+ * - Download assets: controller.downloadModelGLB(), controller.downloadModelGLTF(), controller.downloadModelUSDZ(), controller.downloadModelAndSceneGLB(), controller.downloadModelAndSceneGLTF(), controller.downloadModelAndSceneUSDZ();
27
30
  * - Disable rendering: controller.disable();
28
31
  *
29
32
  * Public Methods:
@@ -33,20 +36,29 @@ import GLTFResolver from "./gltf-resolver.js";
33
36
  * - setCameraOptions(): Applies camera options from configuration.
34
37
  * - setMaterialOptions(): Applies material options from configuration.
35
38
  * - setContainerVisibility(name, show): Shows or hides a container by name.
36
- * - downloadModelGLB(), downloadModelUSDZ(), downloadModelAndSceneGLB(), downloadModelAndSceneUSDZ(): Downloads assets.
39
+ * - downloadGLB(content): Downloads the current scene, model, or environment as a GLB file.
40
+ * - downloadGLTF(content): Downloads the current scene, model, or environment as a glTF ZIP file.
41
+ * - downloadUSDZ(content): Downloads the current scene, model, or environment as a USDZ file.
37
42
  *
38
43
  * Private Methods:
39
44
  * - #configureDracoCompression(): Sets up Draco mesh compression.
45
+ * - #renderLoop(): Babylon.js render loop callback.
46
+ * - #addStylesToARButton(): Styles AR button.
47
+ * - #createXRExperience(): Initializes WebXR AR experience.
40
48
  * - #createCamera(), #createLights(), #initializeEnvironmentTexture(), #initializeIBLShadows(), #initializeShadows(): Scene setup.
41
- * - #setupInteraction(): Sets up canvas interaction handlers.
49
+ * - #setMaxSimultaneousLights(): Updates max simultaneous lights for materials.
50
+ * - #enableInteraction(), #disableInteraction(): Canvas interaction handlers.
42
51
  * - #disposeEngine(): Disposes engine and resources.
52
+ * - #onMouseWheel(event), #onKeyUp(event): Canvas event handlers.
43
53
  * - #setOptionsMaterial(), #setOptions_Materials(), #setOptions_Camera(): Applies material/camera options.
44
54
  * - #findContainerByName(), #addContainer(), #removeContainer(), #replaceContainer(): Container management.
55
+ * - #getPrefViewer3DComponent(), #getPrefViewerComponent(): Custom element references.
56
+ * - #updateVisibilityAttributeInComponents(): Updates parent visibility attributes.
45
57
  * - #setVisibilityOfWallAndFloorInModel(): Controls wall/floor mesh visibility.
46
58
  * - #stopRender(), #startRender(): Render loop control.
47
59
  * - #loadAssetContainer(), #loadContainers(): Asset loading.
48
- * - #addStylesToARButton(): Styles AR button.
49
- * - #createXRExperience(): Initializes WebXR AR experience.
60
+ * - #downloadZip(): Generates and downloads a ZIP file.
61
+ * - #openDownloadDialog(): Opens the modal download dialog.
50
62
  *
51
63
  * Notes:
52
64
  * - Designed for integration with PrefViewer and GLTFResolver.
@@ -76,6 +88,7 @@ export default class BabylonJSController {
76
88
  #options = {};
77
89
 
78
90
  #gltfResolver = null; // GLTFResolver instance
91
+ #babylonJSAnimationController = null; // AnimationController instance
79
92
 
80
93
  /**
81
94
  * Constructs a new BabylonJSController instance.
@@ -290,29 +303,42 @@ export default class BabylonJSController {
290
303
  return false;
291
304
  }
292
305
 
293
- let createIBLShadowPipeline = function (scene) {
306
+ /**
307
+ * Creates and configures the IBL shadow render pipeline for the Babylon.js scene.
308
+ * Sets recommended options for resolution, sampling, opacity, and disables debug passes.
309
+ * Accepts an optional camera array for pipeline targeting.
310
+ * @private
311
+ * @param {Scene} scene - The Babylon.js scene instance.
312
+ * @param {Camera[]} [cameras] - Optional array of cameras to target with the pipeline.
313
+ * @returns {IblShadowsRenderPipeline} The configured IBL shadow pipeline.
314
+ */
315
+ let createIBLShadowPipeline = function (scene, cameras = [scene.activeCamera]) {
294
316
  const pipeline = new IblShadowsRenderPipeline(
295
317
  "iblShadowsPipeline",
296
318
  scene,
297
319
  {
298
- resolutionExp: 7,
299
- sampleDirections: 2,
320
+ resolutionExp: 8, // Higher resolution for better shadow quality
321
+ sampleDirections: 4, // More sample directions for smoother shadows
300
322
  ssShadowsEnabled: true,
301
- shadowRemanence: 0.8,
323
+ shadowRemanence: 0.85,
302
324
  triPlanarVoxelization: true,
303
- shadowOpacity: 0.8,
325
+ shadowOpacity: 0.85,
304
326
  },
305
- [scene.activeCamera]
327
+ cameras
306
328
  );
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;
329
+ // Disable all debug passes for performance
330
+ const pipelineProps = {
331
+ allowDebugPasses: false,
332
+ gbufferDebugEnabled: false,
333
+ importanceSamplingDebugEnabled: false,
334
+ voxelDebugEnabled: false,
335
+ voxelDebugDisplayMip: 0,
336
+ voxelDebugAxis: 0,
337
+ voxelTracingDebugEnabled: false,
338
+ spatialBlurPassDebugEnabled: false,
339
+ accumulationPassDebugEnabled: false,
340
+ };
341
+ Object.assign(pipeline, pipelineProps);
316
342
  return pipeline;
317
343
  };
318
344
 
@@ -376,23 +402,22 @@ export default class BabylonJSController {
376
402
 
377
403
  /**
378
404
  * 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.
381
405
  * @private
382
406
  * @returns {void}
383
407
  */
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
- });
408
+ #enableInteraction() {
409
+ this.#canvas.addEventListener("wheel", this.#onMouseWheel.bind(this));
410
+ this.#canvas.addEventListener("keyup", this.#onKeyUp.bind(this));
411
+ }
412
+
413
+ /**
414
+ * Removes interaction event listeners from the Babylon.js canvas.
415
+ * @private
416
+ * @returns {void}
417
+ */
418
+ #disableInteraction() {
419
+ this.#canvas.removeEventListener("wheel", this.#onMouseWheel.bind(this));
420
+ this.#canvas.removeEventListener("keyup", this.#onKeyUp.bind(this));
396
421
  }
397
422
 
398
423
  /**
@@ -411,10 +436,47 @@ export default class BabylonJSController {
411
436
  this.#XRExperience = null;
412
437
  }
413
438
 
439
+ /**
440
+ * Handles mouse wheel events on the Babylon.js canvas for zooming the camera.
441
+ * @private
442
+ * @param {WheelEvent} event - The mouse wheel event.
443
+ * @returns {void|false}
444
+ */
445
+ #onMouseWheel(event) {
446
+ if (!this.#scene || !this.#camera) {
447
+ return false;
448
+ }
449
+ //const pick = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
450
+ //this.#camera.target = pick.hit ? pick.pickedPoint.clone() : this.#camera.target;
451
+ if (!this.#scene.activeCamera.metadata?.locked) {
452
+ this.#scene.activeCamera.inertialRadiusOffset -= event.deltaY * this.#scene.activeCamera.wheelPrecision * 0.001;
453
+ }
454
+ event.preventDefault();
455
+ }
456
+
457
+ /**
458
+ * Handles keyup events on the Babylon.js canvas for triggering model and scene downloads.
459
+ * @private
460
+ * @param {KeyboardEvent} event - The keyup event.
461
+ * @returns {void}
462
+ */
463
+ #onKeyUp(event) {
464
+ // CTRL + ALT + letter
465
+ if (event.ctrlKey && event.altKey && event.key !== undefined) {
466
+ switch (event.key.toLowerCase()) {
467
+ case "d":
468
+ this.#openDownloadDialog();
469
+ break;
470
+ default:
471
+ break;
472
+ }
473
+ }
474
+ }
475
+
414
476
  /**
415
477
  * Applies material options from the configuration to the relevant meshes.
416
478
  * @private
417
- * @param {object} optionMaterial - Material option containing value, nodePrefixes, nodeNames, and state.
479
+ * @param {MaterialData} optionMaterial - Material option containing value, nodePrefixes, nodeNames, and state.
418
480
  * @returns {boolean} True if any mesh material was set, false otherwise.
419
481
  */
420
482
  #setOptionsMaterial(optionMaterial) {
@@ -435,7 +497,7 @@ export default class BabylonJSController {
435
497
  if (container.state.name === "materials") {
436
498
  return;
437
499
  }
438
- if (container.assetContainer && (container.isPending || materialContainer.isPending || optionMaterial.isPending)) {
500
+ if (container.assetContainer && (container.state.isPending || materialContainer.state.isPending || optionMaterial.isPending)) {
439
501
  assetContainersToProcess.push(container.assetContainer);
440
502
  }
441
503
  });
@@ -487,7 +549,7 @@ export default class BabylonJSController {
487
549
  const modelContainer = this.#containers.model;
488
550
  const environmentContainer = this.#containers.environment;
489
551
 
490
- if (!cameraState.isPending && !modelContainer.isPending && !environmentContainer.isPending) {
552
+ if (!cameraState.isPending && !modelContainer.state.isPending && !environmentContainer.state.isPending) {
491
553
  return false;
492
554
  }
493
555
 
@@ -542,7 +604,7 @@ export default class BabylonJSController {
542
604
  * Finds and returns the asset container object by its name.
543
605
  * @private
544
606
  * @param {string} name - The name of the container to find.
545
- * @returns {object|null} The matching container object, or null if not found.
607
+ * @returns {Object|null} The matching container object, or null if not found.
546
608
  */
547
609
  #findContainerByName(name) {
548
610
  return Object.values(this.#containers).find((container) => container.state.name === name) || null;
@@ -567,6 +629,13 @@ export default class BabylonJSController {
567
629
  */
568
630
  #getPrefViewerComponent() {
569
631
  if (this.#prefViewer === undefined) {
632
+ if (this.#prefViewer3D === undefined) {
633
+ this.#getPrefViewer3DComponent();
634
+ }
635
+ if (!this.#prefViewer3D) {
636
+ this.#prefViewer = null;
637
+ return;
638
+ }
570
639
  const rootNode = this.#prefViewer3D ? this.#prefViewer3D.getRootNode().host : null;
571
640
  this.#prefViewer = rootNode && rootNode.nodeName === "PREF-VIEWER" ? rootNode : null;
572
641
  }
@@ -579,7 +648,7 @@ export default class BabylonJSController {
579
648
  * @param {boolean} isVisible - True to show the container, false to hide it.
580
649
  * @returns {void}
581
650
  */
582
- #updateVisibilityAttributeInComponentes(name, isVisible) {
651
+ #updateVisibilityAttributeInComponents(name, isVisible) {
583
652
  // Cache references to parent custom elements
584
653
  this.#getPrefViewer3DComponent();
585
654
  this.#getPrefViewerComponent();
@@ -596,13 +665,16 @@ export default class BabylonJSController {
596
665
  * Adds the asset container to the Babylon.js scene if it should be shown and is not already visible.
597
666
  * @private
598
667
  * @param {object} container - The container object containing asset state and metadata.
668
+ * @param {boolean} [updateVisibility=true] - If true, updates the visibility attribute in parent components.
599
669
  * @returns {boolean} True if the container was added, false otherwise.
600
670
  */
601
- #addContainer(container) {
671
+ #addContainer(container, updateVisibility = true) {
602
672
  if (!container.assetContainer || container.state.isVisible || !container.state.mustBeShown) {
603
673
  return false;
604
674
  }
605
- this.#updateVisibilityAttributeInComponentes(container.state.name, true);
675
+ if (updateVisibility) {
676
+ this.#updateVisibilityAttributeInComponents(container.state.name, true);
677
+ }
606
678
  container.assetContainer.addAllToScene();
607
679
  container.state.visible = true;
608
680
  return true;
@@ -612,13 +684,16 @@ export default class BabylonJSController {
612
684
  * Removes the asset container from the Babylon.js scene if it is currently visible.
613
685
  * @private
614
686
  * @param {object} container - The container object containing asset state and metadata.
687
+ * @param {boolean} [updateVisibility=true] - If true, updates the visibility attribute in parent components.
615
688
  * @returns {boolean} True if the container was removed, false otherwise.
616
689
  */
617
- #removeContainer(container) {
690
+ #removeContainer(container, updateVisibility = true) {
618
691
  if (!container.assetContainer || !container.state.isVisible) {
619
692
  return false;
620
693
  }
621
- this.#updateVisibilityAttributeInComponentes(container.state.name, false);
694
+ if (updateVisibility) {
695
+ this.#updateVisibilityAttributeInComponents(container.state.name, false);
696
+ }
622
697
  container.assetContainer.removeAllFromScene();
623
698
  container.state.visible = false;
624
699
  return true;
@@ -628,20 +703,20 @@ export default class BabylonJSController {
628
703
  * Replaces the asset container in the Babylon.js scene with a new one.
629
704
  * @private
630
705
  * @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.
706
+ * @param {AssetContainer} newAssetContainer - The new asset container to add to the scene.
707
+ * @returns {boolean} True if the container was replaced and added, false otherwise.
633
708
  */
634
709
  #replaceContainer(container, newAssetContainer) {
635
710
  if (container.assetContainer) {
636
- this.#removeContainer(container);
711
+ this.#removeContainer(container, false);
637
712
  container.assetContainer.dispose();
638
713
  container.assetContainer = null;
639
714
  }
640
715
  this.#scene.getEngine().releaseEffects();
641
716
  container.assetContainer = newAssetContainer;
642
- this.#addContainer(container);
643
- return true;
717
+ return this.#addContainer(container, false);
644
718
  }
719
+
645
720
  /**
646
721
  * Sets the visibility of wall and floor meshes in the model container based on the provided value or environment visibility.
647
722
  * @private
@@ -663,7 +738,6 @@ export default class BabylonJSController {
663
738
  * @private
664
739
  * @returns {void}
665
740
  */
666
- #stopR;
667
741
  #stopRender() {
668
742
  this.#engine.stopRenderLoop(this.#renderLoop.bind(this));
669
743
  }
@@ -725,11 +799,11 @@ export default class BabylonJSController {
725
799
  /**
726
800
  * Loads all asset containers (model, environment, materials, etc.) and adds them to the scene.
727
801
  * @private
728
- * @returns {Promise<boolean>} Resolves to true if loading succeeds, false otherwise.
802
+ * @returns {Promise<{success: boolean, error: any}>} Resolves to an object indicating if loading succeeded and any error encountered.
729
803
  * @description
730
804
  * Waits for all containers to load in parallel, then replaces or adds them to the scene as needed.
731
805
  * Applies material and camera options, sets wall/floor visibility, and initializes lights and shadows.
732
- * Returns true if all containers loaded successfully, false otherwise.
806
+ * Returns an object with success status and error details.
733
807
  */
734
808
  async #loadContainers() {
735
809
  this.#stopRender();
@@ -739,10 +813,17 @@ export default class BabylonJSController {
739
813
  promiseArray.push(this.#loadAssetContainer(container));
740
814
  });
741
815
 
742
- let success = false;
816
+ let detail = {
817
+ success: false,
818
+ error: null,
819
+ };
743
820
 
744
821
  await Promise.allSettled(promiseArray)
745
822
  .then((values) => {
823
+ if (this.#babylonJSAnimationController) {
824
+ this.#babylonJSAnimationController.dispose();
825
+ this.#babylonJSAnimationController = null;
826
+ }
746
827
  values.forEach((result) => {
747
828
  const container = result.value ? result.value[0] : null;
748
829
  const assetContainer = result.value ? result.value[1] : null;
@@ -763,21 +844,137 @@ export default class BabylonJSController {
763
844
  this.#setOptions_Materials();
764
845
  this.#setOptions_Camera();
765
846
  this.#setVisibilityOfWallAndFloorInModel();
766
- success = true;
847
+ detail.success = true;
767
848
  })
768
849
  .catch((error) => {
769
850
  this.loaded = true;
770
851
  console.error("PrefViewer: failed to load model", error);
771
- success = false;
852
+ detail.success = false;
853
+ detail.error = error;
772
854
  })
773
855
  .finally(async () => {
774
856
  this.#setMaxSimultaneousLights();
775
857
  this.#initializeShadows();
858
+ this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#scene);
776
859
  this.#startRender();
777
- return success;
778
860
  });
861
+ return detail;
862
+ }
779
863
 
780
- return success;
864
+ /**
865
+ * Appends the current date and time to the provided name string in the format: name_YYYY-MM-DD_HH.mm.ss
866
+ * @private
867
+ * @param {string} name - The base name to which the date and time will be appended.
868
+ * @returns {string} The name with the appended date and time.
869
+ */
870
+ #addDateToName(name) {
871
+ const now = new Date();
872
+ const year = now.getFullYear();
873
+ const month = (now.getMonth() + 1).toString().padStart(2, "0");
874
+ const day = now.getDate().toString().padStart(2, "0");
875
+ const hours = now.getHours().toString().padStart(2, "0");
876
+ const minutes = now.getMinutes().toString().padStart(2, "0");
877
+ const seconds = now.getSeconds().toString().padStart(2, "0");
878
+ name = `${name}_${year}-${month}-${day}_${hours}.${minutes}.${seconds}`;
879
+ return name;
880
+ }
881
+
882
+ /**
883
+ * Generates and downloads a ZIP file with the specified folder name and files.
884
+ * @private
885
+ * @param {Object} files - An object where keys are file names and values are file contents.
886
+ * @param {string} [name="files"] - The name for the root folder inside the ZIP.
887
+ * @param {string} [comment=""] - A comment to include in the ZIP file.
888
+ * @param {boolean} [addDateInName=false] - Whether to append the current date/time to the folder name.
889
+ * @returns {void}
890
+ * @description
891
+ * Uses JSZip to create the ZIP and triggers a browser download.
892
+ */
893
+ #downloadZip(files, name = "files", comment = "", addDateInName = false) {
894
+ name = addDateInName ? this.#addDateToName(name) : name;
895
+
896
+ const JSZip = require("jszip");
897
+ const zip = new JSZip();
898
+ zip.comment = comment;
899
+
900
+ const rootFolder = zip.folder(name);
901
+ Object.keys(files).forEach((key) => {
902
+ rootFolder.file(key, files[key]);
903
+ });
904
+
905
+ zip.generateAsync({ type: "blob" }).then((content) => {
906
+ const zipName = `${name}.zip`;
907
+ const a = document.createElement("a");
908
+ const url = window.URL.createObjectURL(content);
909
+ a.href = url;
910
+ a.download = zipName;
911
+ a.click();
912
+ window.URL.revokeObjectURL(url);
913
+ });
914
+ }
915
+
916
+ /**
917
+ * Opens a modal dialog for downloading the 3D scene, model, or environment.
918
+ * @private
919
+ * @returns {void}
920
+ */
921
+ #openDownloadDialog() {
922
+ this.#getPrefViewerComponent();
923
+ if (!this.#prefViewer) {
924
+ return;
925
+ }
926
+
927
+ const header = "Download 3D Scene";
928
+ const content = `
929
+ <form slot="content" id="download-dialog-form" style="display:flex;flex-direction:column;gap:16px;">
930
+ <h4>Content</h4>
931
+ <div style="display:flex;flex-direction:row;gap:16px;margin:0 8px 0 8px;">
932
+ <label><input type="radio" name="content" value="1"> Model</label>
933
+ <label><input type="radio" name="content" value="2"> Scene</label>
934
+ <label><input type="radio" name="content" value="0" checked> Both</label>
935
+ </div>
936
+ <h4>Format</h4>
937
+ <div style="display:flex;flex-direction:row;gap:16px;margin:0 8px 0 8px;">
938
+ <label><input type="radio" name="format" value="gltf" checked> glTF (ZIP)</label>
939
+ <label><input type="radio" name="format" value="glb"> GLB</label>
940
+ <label><input type="radio" name="format" value="usdz"> USDZ</label>
941
+ </div>
942
+ </form>`;
943
+ const footer = `
944
+ <button type="button" class="primary" id="download-dialog-download">Download</button>
945
+ <button type="button" id="download-dialog-cancel">Cancel</button>`;
946
+
947
+ const dialog = this.#prefViewer.openDialog(header, content, footer);
948
+
949
+ if (!dialog) {
950
+ return;
951
+ }
952
+
953
+ // Button event handlers
954
+ const form = dialog.querySelector("#download-dialog-form");
955
+ const downloadButton = dialog.querySelector("#download-dialog-download");
956
+ const cancelButton = dialog.querySelector("#download-dialog-cancel");
957
+
958
+ downloadButton.onclick = () => {
959
+ const contentValue = form.content.value;
960
+ const formatValue = form.format.value;
961
+ switch (formatValue) {
962
+ case "glb":
963
+ this.downloadGLB(Number(contentValue));
964
+ break;
965
+ case "gltf":
966
+ this.downloadGLTF(Number(contentValue));
967
+ break;
968
+ case "usdz":
969
+ this.downloadUSDZ(Number(contentValue));
970
+ break;
971
+ }
972
+ this.#prefViewer.closeDialog();
973
+ };
974
+
975
+ cancelButton.onclick = () => {
976
+ this.#prefViewer.closeDialog();
977
+ };
781
978
  }
782
979
 
783
980
  /**
@@ -801,7 +998,7 @@ export default class BabylonJSController {
801
998
  this.#scene.clearColor = new Color4(1, 1, 1, 1);
802
999
  this.#createCamera();
803
1000
  this.#createLights();
804
- this.#setupInteraction();
1001
+ this.#enableInteraction();
805
1002
  await this.#createXRExperience();
806
1003
  this.#startRender();
807
1004
  this.#canvasResizeObserver = new ResizeObserver(() => this.#engine && this.#engine.resize());
@@ -815,8 +1012,13 @@ export default class BabylonJSController {
815
1012
  * @returns {void}
816
1013
  */
817
1014
  disable() {
818
- this.#disposeEngine();
819
1015
  this.#canvasResizeObserver.disconnect();
1016
+ this.#disableInteraction();
1017
+ if (this.#babylonJSAnimationController) {
1018
+ this.#babylonJSAnimationController.dispose();
1019
+ this.#babylonJSAnimationController = null;
1020
+ }
1021
+ this.#disposeEngine();
820
1022
  }
821
1023
 
822
1024
  /**
@@ -880,50 +1082,105 @@ export default class BabylonJSController {
880
1082
  }
881
1083
 
882
1084
  /**
883
- * Initiates download of the current model as a GLB file.
1085
+ * Downloads the current scene, model, or environment as a GLB file.
884
1086
  * @public
1087
+ * @param {number} content - 0 for full scene, 1 for model, 2 for environment.
885
1088
  * @returns {void}
886
1089
  */
887
- downloadModelGLB() {
888
- const fileName = "model";
889
- GLTF2Export.GLBAsync(this.#containers.model.assetContainer, fileName, { exportWithoutWaitingForScene: true }).then((glb) => glb.downloadFiles());
1090
+ downloadGLB(content = 0) {
1091
+ let fileName = "";
1092
+ let container = null;
1093
+ switch (content) {
1094
+ case 0:
1095
+ fileName = "full-scene";
1096
+ container = this.#scene;
1097
+ break;
1098
+ case 1:
1099
+ fileName = "model";
1100
+ container = this.#containers.model.assetContainer;
1101
+ break;
1102
+ case 2:
1103
+ fileName = "scene";
1104
+ container = this.#containers.environment.assetContainer;
1105
+ break;
1106
+ default:
1107
+ break;
1108
+ }
1109
+ fileName = this.#addDateToName(fileName);
1110
+ GLTF2Export.GLBAsync(container, fileName, { exportWithoutWaitingForScene: true }).then((glb) => {
1111
+ if (glb) {
1112
+ glb.downloadFiles();
1113
+ }
1114
+ });
890
1115
  }
891
1116
 
892
1117
  /**
893
- * Initiates download of the current model as a USDZ file.
1118
+ * Downloads the current scene, model, or environment as a glTF ZIP file.
894
1119
  * @public
1120
+ * @param {number} content - 0 for full scene, 1 for model, 2 for environment.
895
1121
  * @returns {void}
896
1122
  */
897
- downloadModelUSDZ() {
898
- const fileName = "model";
899
- USDZExportAsync(this.#containers.model.assetContainer).then((response) => {
900
- if (response) {
901
- Tools.Download(new Blob([response], { type: "model/vnd.usdz+zip" }), `${fileName}.usdz`);
1123
+ downloadGLTF(content = 0) {
1124
+ let fileName = "";
1125
+ let container = null;
1126
+ let comment = "";
1127
+ switch (content) {
1128
+ case 0:
1129
+ fileName = "full-scene";
1130
+ comment = "Export of the complete scene in glTF format, including the model and the environment.";
1131
+ container = this.#scene;
1132
+ break;
1133
+ case 1:
1134
+ fileName = "model";
1135
+ comment = "Export of model scene in glTF format.";
1136
+ container = this.#containers.model.assetContainer;
1137
+ break;
1138
+ case 2:
1139
+ fileName = "scene";
1140
+ comment = "Export of the environment scene in glTF format.";
1141
+ container = this.#containers.environment.assetContainer;
1142
+ break;
1143
+ default:
1144
+ break;
1145
+ }
1146
+ comment += "\nPrefViewer by Preference, S.L.\npreference.com\n";
1147
+ GLTF2Export.GLTFAsync(container, fileName, { exportWithoutWaitingForScene: true }).then((gltf) => {
1148
+ if (gltf?.files) {
1149
+ this.#downloadZip(gltf.files, fileName, comment, true);
902
1150
  }
903
1151
  });
904
1152
  }
905
1153
 
906
1154
  /**
907
- * Initiates download of the entire scene (model and environment) as a USDZ file.
1155
+ * Downloads the current scene, model, or environment as a USDZ file.
908
1156
  * @public
1157
+ * @param {number} content - 0 for full scene, 1 for model, 2 for environment.
909
1158
  * @returns {void}
910
1159
  */
911
- downloadModelAndSceneUSDZ() {
912
- const fileName = "scene";
913
- USDZExportAsync(this.#scene).then((response) => {
1160
+ downloadUSDZ(content = 0) {
1161
+ let fileName = "";
1162
+ let container = null;
1163
+ switch (content) {
1164
+ case 0:
1165
+ fileName = "full-scene";
1166
+ container = this.#scene;
1167
+ break;
1168
+ case 1:
1169
+ fileName = "model";
1170
+ container = this.#containers.model.assetContainer;
1171
+ break;
1172
+ case 2:
1173
+ fileName = "scene";
1174
+ container = this.#containers.environment.assetContainer;
1175
+ break;
1176
+ default:
1177
+ break;
1178
+ }
1179
+ fileName = this.#addDateToName(fileName);
1180
+ USDZExportAsync(container).then((response) => {
914
1181
  if (response) {
915
1182
  Tools.Download(new Blob([response], { type: "model/vnd.usdz+zip" }), `${fileName}.usdz`);
916
1183
  }
917
1184
  });
918
1185
  }
919
-
920
- /**
921
- * Initiates download of the entire scene (model and environment) as a GLB file.
922
- * @public
923
- * @returns {void}
924
- */
925
- downloadModelAndSceneGLB() {
926
- const fileName = "scene";
927
- GLTF2Export.GLBAsync(this.#scene, fileName, { exportWithoutWaitingForScene: true }).then((glb) => glb.downloadFiles());
928
- }
929
1186
  }