@preference-sl/pref-viewer 2.11.0-beta.8 → 2.11.0

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,24 +1,31 @@
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";
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";
6
7
 
7
8
  import GLTFResolver from "./gltf-resolver.js";
8
9
  import { MaterialData } from "./pref-viewer-3d-data.js";
9
10
  import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
10
11
 
11
12
  /**
12
- * 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.
13
14
  *
14
- * Responsibilities:
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.
18
+ *
19
+ * Key Responsibilities:
15
20
  * - Initializes and manages the Babylon.js engine, scene, camera, lights, and asset containers.
16
21
  * - Handles loading, replacing, and disposing of 3D models, environments, and materials.
17
22
  * - Configures advanced rendering features such as Draco mesh compression, shadows, and image-based lighting (IBL).
18
23
  * - Integrates Babylon.js WebXR experience for augmented reality (AR) support.
19
- * - Provides methods for downloading models and scenes in GLB, GLTF, and USDZ formats.
24
+ * - Provides methods for downloading models and scenes in GLB, glTF (ZIP), and USDZ formats.
20
25
  * - Manages camera and material options, container visibility, and user interactions.
21
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.
22
29
  *
23
30
  * Usage:
24
31
  * - Instantiate: const controller = new BabylonJSController(canvas, containers, options);
@@ -26,7 +33,7 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
26
33
  * - Load assets: await controller.load();
27
34
  * - Set camera/material options: controller.setCameraOptions(), controller.setMaterialOptions();
28
35
  * - Control visibility: controller.setContainerVisibility(name, show);
29
- * - Download assets: controller.downloadModelGLB(), controller.downloadModelGLTF(), controller.downloadModelUSDZ(), controller.downloadModelAndSceneGLB(), controller.downloadModelAndSceneGLTF(), controller.downloadModelAndSceneUSDZ();
36
+ * - Download assets: controller.downloadGLB(), controller.downloadGLTF(), controller.downloadUSDZ();
30
37
  * - Disable rendering: controller.disable();
31
38
  *
32
39
  * Public Methods:
@@ -40,30 +47,46 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
40
47
  * - downloadGLTF(content): Downloads the current scene, model, or environment as a glTF ZIP file.
41
48
  * - downloadUSDZ(content): Downloads the current scene, model, or environment as a USDZ file.
42
49
  *
43
- * Private Methods:
50
+ * Private Methods (using ECMAScript private fields):
51
+ * - #bindHandlers(): Pre-binds reusable event handlers to preserve stable references.
44
52
  * - #configureDracoCompression(): Sets up Draco mesh compression.
45
53
  * - #renderLoop(): Babylon.js render loop callback.
46
54
  * - #addStylesToARButton(): Styles AR button.
47
55
  * - #createXRExperience(): Initializes WebXR AR experience.
48
- * - #createCamera(), #createLights(), #initializeEnvironmentTexture(), #initializeIBLShadows(), #initializeShadows(): Scene setup.
49
- * - #setMaxSimultaneousLights(): Updates max simultaneous lights for materials.
50
- * - #enableInteraction(), #disableInteraction(): Canvas interaction handlers.
51
- * - #disposeEngine(): Disposes engine and resources.
52
- * - #onMouseWheel(event), #onKeyUp(event): Canvas event handlers.
53
- * - #setOptionsMaterial(), #setOptions_Materials(), #setOptions_Camera(): Applies material/camera options.
54
- * - #findContainerByName(), #addContainer(), #removeContainer(), #replaceContainer(): Container management.
55
- * - #getPrefViewer3DComponent(), #getPrefViewerComponent(): Custom element references.
56
- * - #updateVisibilityAttributeInComponents(): Updates parent visibility attributes.
57
- * - #setVisibilityOfWallAndFloorInModel(): Controls wall/floor mesh visibility.
58
- * - #stopRender(), #startRender(): Render loop control.
59
- * - #loadAssetContainer(), #loadContainers(): Asset loading.
60
- * - #downloadZip(): Generates and downloads a ZIP file.
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.
61
89
  * - #openDownloadDialog(): Opens the modal download dialog.
62
- *
63
- * Notes:
64
- * - Designed for integration with PrefViewer and GLTFResolver.
65
- * - Supports advanced Babylon.js features for product visualization and configurators.
66
- * - All resource management and rendering operations are performed asynchronously for performance.
67
90
  */
68
91
  export default class BabylonJSController {
69
92
  // Canvas HTML element
@@ -72,7 +95,6 @@ export default class BabylonJSController {
72
95
  // References to parent custom elements
73
96
  #prefViewer3D = undefined;
74
97
  #prefViewer = undefined;
75
- #prefViewerDialog = null;
76
98
 
77
99
  // Babylon.js core objects
78
100
  #engine = null;
@@ -91,6 +113,12 @@ export default class BabylonJSController {
91
113
  #gltfResolver = null; // GLTFResolver instance
92
114
  #babylonJSAnimationController = null; // AnimationController instance
93
115
 
116
+ #handlers = {
117
+ onKeyUp: null,
118
+ onPointerObservable: null,
119
+ renderLoop: null,
120
+ };
121
+
94
122
  /**
95
123
  * Constructs a new BabylonJSController instance.
96
124
  * Initializes the canvas, asset containers, and options for the Babylon.js scene.
@@ -113,6 +141,18 @@ export default class BabylonJSController {
113
141
  };
114
142
  });
115
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);
116
156
  }
117
157
 
118
158
  /**
@@ -402,13 +442,34 @@ export default class BabylonJSController {
402
442
  }
403
443
 
404
444
  /**
405
- * Sets up interaction handlers for the Babylon.js canvas.
445
+ * Handles pointer events observed on the Babylon.js scene.
446
+ * @private
447
+ * @param {PointerInfo} info - The pointer event information from Babylon.js.
448
+ * @returns {void}
449
+ */
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.
406
463
  * @private
407
464
  * @returns {void}
408
465
  */
409
466
  #enableInteraction() {
410
- this.#canvas.addEventListener("wheel", this.#onMouseWheel.bind(this));
411
- this.#canvas.addEventListener("keyup", this.#onKeyUp.bind(this));
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
+ }
412
473
  }
413
474
 
414
475
  /**
@@ -417,8 +478,52 @@ export default class BabylonJSController {
417
478
  * @returns {void}
418
479
  */
419
480
  #disableInteraction() {
420
- this.#canvas.removeEventListener("wheel", this.#onMouseWheel.bind(this));
421
- this.#canvas.removeEventListener("keyup", this.#onKeyUp.bind(this));
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
+ }
422
527
  }
423
528
 
424
529
  /**
@@ -434,21 +539,39 @@ export default class BabylonJSController {
434
539
  this.#engine = this.#scene = this.#camera = null;
435
540
  this.#hemiLight = this.#dirLight = this.#cameraLight = null;
436
541
  this.#shadowGen = null;
437
- 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
+ }
438
561
  }
439
562
 
440
563
  /**
441
564
  * Handles mouse wheel events on the Babylon.js canvas for zooming the camera.
442
565
  * @private
443
566
  * @param {WheelEvent} event - The mouse wheel event.
444
- * @returns {void|false}
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.
445
569
  */
446
- #onMouseWheel(event) {
447
- if (!this.#scene || !this.#camera) {
570
+ #onMouseWheel(event, pickInfo) {
571
+ if (!this.#scene?.activeCamera) {
448
572
  return false;
449
573
  }
450
- //const pick = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
451
- //this.#camera.target = pick.hit ? pick.pickedPoint.clone() : this.#camera.target;
574
+ //this.#scene.activeCamera.target = pickInfo.hit ? pickInfo.pickedPoint.clone() : this.#scene.activeCamera.target;
452
575
  if (!this.#scene.activeCamera.metadata?.locked) {
453
576
  this.#scene.activeCamera.inertialRadiusOffset -= event.deltaY * this.#scene.activeCamera.wheelPrecision * 0.001;
454
577
  }
@@ -456,24 +579,35 @@ export default class BabylonJSController {
456
579
  }
457
580
 
458
581
  /**
459
- * Handles keyup events on the Babylon.js canvas for triggering model and scene downloads.
582
+ * Handles pointer up events on the Babylon.js scene.
460
583
  * @private
461
- * @param {KeyboardEvent} event - The keyup event.
584
+ * @param {PointerEvent} event - The pointer up event.
585
+ * @param {PickInfo} pickInfo - The result of the scene pick operation.
462
586
  * @returns {void}
463
587
  */
464
- #onKeyUp(event) {
465
- // CTRL + ALT + letter
466
- if (event.ctrlKey && event.altKey && event.key !== undefined) {
467
- switch (event.key.toLowerCase()) {
468
- case "d":
469
- this.#openDownloadDialog();
470
- break;
471
- default:
472
- break;
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);
473
594
  }
474
595
  }
475
596
  }
476
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
+ }
609
+ }
610
+
477
611
  /**
478
612
  * Applies material options from the configuration to the relevant meshes.
479
613
  * @private
@@ -740,7 +874,7 @@ export default class BabylonJSController {
740
874
  * @returns {void}
741
875
  */
742
876
  #stopRender() {
743
- this.#engine.stopRenderLoop(this.#renderLoop.bind(this));
877
+ this.#engine.stopRenderLoop(this.#handlers.renderLoop);
744
878
  }
745
879
  /**
746
880
  * Starts the Babylon.js render loop for the current scene.
@@ -750,7 +884,7 @@ export default class BabylonJSController {
750
884
  */
751
885
  async #startRender() {
752
886
  await this.#scene.whenReadyAsync();
753
- this.#engine.runRenderLoop(this.#renderLoop.bind(this));
887
+ this.#engine.runRenderLoop(this.#handlers.renderLoop);
754
888
  }
755
889
 
756
890
  /**
@@ -821,10 +955,7 @@ export default class BabylonJSController {
821
955
 
822
956
  await Promise.allSettled(promiseArray)
823
957
  .then((values) => {
824
- if (this.#babylonJSAnimationController) {
825
- this.#babylonJSAnimationController.dispose();
826
- this.#babylonJSAnimationController = null;
827
- }
958
+ this.#disposeAnimationController();
828
959
  values.forEach((result) => {
829
960
  const container = result.value ? result.value[0] : null;
830
961
  const assetContainer = result.value ? result.value[1] : null;
@@ -862,6 +993,24 @@ export default class BabylonJSController {
862
993
  return detail;
863
994
  }
864
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
+
865
1014
  /**
866
1015
  * Generates and downloads a ZIP file with the specified folder name and files.
867
1016
  * @private
@@ -874,18 +1023,8 @@ export default class BabylonJSController {
874
1023
  * Uses JSZip to create the ZIP and triggers a browser download.
875
1024
  */
876
1025
  #downloadZip(files, name = "files", comment = "", addDateInName = false) {
877
- if (addDateInName) {
878
- const now = new Date();
879
- const year = now.getFullYear();
880
- const month = (now.getMonth() + 1).toString().padStart(2, "0");
881
- const day = now.getDate().toString().padStart(2, "0");
882
- const hours = now.getHours().toString().padStart(2, "0");
883
- const minutes = now.getMinutes().toString().padStart(2, "0");
884
- const seconds = now.getSeconds().toString().padStart(2, "0");
885
- name = `${name}_${year}-${month}-${day}_${hours}.${minutes}.${seconds}`;
886
- }
1026
+ name = addDateInName ? this.#addDateToName(name) : name;
887
1027
 
888
- const JSZip = require("jszip");
889
1028
  const zip = new JSZip();
890
1029
  zip.comment = comment;
891
1030
 
@@ -915,56 +1054,39 @@ export default class BabylonJSController {
915
1054
  if (!this.#prefViewer) {
916
1055
  return;
917
1056
  }
918
- this.#prefViewerDialog = document.createElement("pref-viewer-dialog");
919
- this.#prefViewer.shadowRoot.appendChild(this.#prefViewerDialog);
920
-
921
- const content = document.createElement("div");
922
- content.innerHTML = `
923
- <form id="download-dialog-form" style="display:flex;flex-direction:column;gap:15px;">
924
- <h3 style="margin: 0 0 5px; 0">Download 3D Scene</h3>
925
- <h4 style="margin:0;">Content</h4>
926
- <div style="display:flex;flex-direction:row;gap:15px;margin:0 10px 0 10px;">
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;">
927
1063
  <label><input type="radio" name="content" value="1"> Model</label>
928
1064
  <label><input type="radio" name="content" value="2"> Scene</label>
929
1065
  <label><input type="radio" name="content" value="0" checked> Both</label>
930
1066
  </div>
931
- <h4 style="margin:0;">Format</h4>
932
- <div style="display:flex;flex-direction:row;gap:15px;margin: 0 10px 0 10px;">
1067
+ <h4>Format</h4>
1068
+ <div style="display:flex;flex-direction:row;gap:16px;margin:0 8px 0 8px;">
933
1069
  <label><input type="radio" name="format" value="gltf" checked> glTF (ZIP)</label>
934
1070
  <label><input type="radio" name="format" value="glb"> GLB</label>
935
1071
  <label><input type="radio" name="format" value="usdz"> USDZ</label>
936
1072
  </div>
937
- <div style="display:flex;gap:15px;justify-content:stretch;margin-top:10px;">
938
- <button type="button" id="download-dialog-download">Download</button>
939
- <button type="button" id="download-dialog-cancel">Cancel</button>
940
- </div>
941
- <style>
942
- #download-dialog-download,
943
- #download-dialog-cancel {
944
- width: 100%;
945
- padding: 8px 20px;
946
- font-size: 1.1rem;
947
- border-radius: 6px;
948
- border: none;
949
- background: #eee;
950
- cursor: pointer;
951
- transition: background 0.2s;
952
- }
953
- #download-dialog-download:hover,
954
- #download-dialog-cancel:hover {
955
- background: #e0e0e0;
956
- }
957
- </style>
958
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>`;
959
1077
 
960
- this.#prefViewerDialog.open(content);
1078
+ const dialog = this.#prefViewer.openDialog(header, content, footer);
1079
+
1080
+ if (!dialog) {
1081
+ return;
1082
+ }
961
1083
 
962
1084
  // Button event handlers
963
- const form = content.querySelector("#download-dialog-form");
964
- const downloadBtn = content.querySelector("#download-dialog-download");
965
- const cancelBtn = content.querySelector("#download-dialog-cancel");
1085
+ const form = dialog.querySelector("#download-dialog-form");
1086
+ const downloadButton = dialog.querySelector("#download-dialog-download");
1087
+ const cancelButton = dialog.querySelector("#download-dialog-cancel");
966
1088
 
967
- downloadBtn.onclick = () => {
1089
+ downloadButton.onclick = () => {
968
1090
  const contentValue = form.content.value;
969
1091
  const formatValue = form.format.value;
970
1092
  switch (formatValue) {
@@ -978,14 +1100,12 @@ export default class BabylonJSController {
978
1100
  this.downloadUSDZ(Number(contentValue));
979
1101
  break;
980
1102
  }
981
- this.#prefViewerDialog.close();
1103
+ this.#prefViewer.closeDialog();
982
1104
  };
983
1105
 
984
- cancelBtn.onclick = () => {
985
- this.#prefViewerDialog.close();
1106
+ cancelButton.onclick = () => {
1107
+ this.#prefViewer.closeDialog();
986
1108
  };
987
-
988
- this.#prefViewerDialog.open(content);
989
1109
  }
990
1110
 
991
1111
  /**
@@ -1003,7 +1123,7 @@ export default class BabylonJSController {
1003
1123
  */
1004
1124
  async enable() {
1005
1125
  this.#configureDracoCompression();
1006
- this.#engine = new Engine(this.#canvas, true, { alpha: true });
1126
+ this.#engine = new Engine(this.#canvas, true, { alpha: true, stencil: true, preserveDrawingBuffer: false });
1007
1127
  this.#engine.disableUniformBuffers = true;
1008
1128
  this.#scene = new Scene(this.#engine);
1009
1129
  this.#scene.clearColor = new Color4(1, 1, 1, 1);
@@ -1025,10 +1145,8 @@ export default class BabylonJSController {
1025
1145
  disable() {
1026
1146
  this.#canvasResizeObserver.disconnect();
1027
1147
  this.#disableInteraction();
1028
- if (this.#babylonJSAnimationController) {
1029
- this.#babylonJSAnimationController.dispose();
1030
- this.#babylonJSAnimationController = null;
1031
- }
1148
+ this.#disposeAnimationController();
1149
+ this.#disposeXRExperience();
1032
1150
  this.#disposeEngine();
1033
1151
  }
1034
1152
 
@@ -1117,6 +1235,7 @@ export default class BabylonJSController {
1117
1235
  default:
1118
1236
  break;
1119
1237
  }
1238
+ fileName = this.#addDateToName(fileName);
1120
1239
  GLTF2Export.GLBAsync(container, fileName, { exportWithoutWaitingForScene: true }).then((glb) => {
1121
1240
  if (glb) {
1122
1241
  glb.downloadFiles();
@@ -1186,6 +1305,7 @@ export default class BabylonJSController {
1186
1305
  default:
1187
1306
  break;
1188
1307
  }
1308
+ fileName = this.#addDateToName(fileName);
1189
1309
  USDZExportAsync(container).then((response) => {
1190
1310
  if (response) {
1191
1311
  Tools.Download(new Blob([response], { type: "model/vnd.usdz+zip" }), `${fileName}.usdz`);
@@ -174,7 +174,12 @@ export class FileStorage {
174
174
  xhr.onload = () => {
175
175
  if (xhr.status === 200) {
176
176
  const blob = xhr.response;
177
- const timeStamp = new Date(xhr.getResponseHeader("Last-Modified")).toISOString();
177
+ const lastModified = xhr.getResponseHeader("Last-Modified");
178
+ let timeStamp = null;
179
+ if (lastModified) {
180
+ const parsed = new Date(lastModified);
181
+ timeStamp = Number.isNaN(parsed.valueOf()) ? null : parsed.toISOString();
182
+ }
178
183
  file = { blob: blob, timeStamp: timeStamp };
179
184
  resolve(file);
180
185
  } else {
@@ -204,7 +209,11 @@ export class FileStorage {
204
209
  xhr.responseType = "blob";
205
210
  xhr.onload = () => {
206
211
  if (xhr.status === 200) {
207
- timeStamp = new Date(xhr.getResponseHeader("Last-Modified")).toISOString();
212
+ const lastModified = xhr.getResponseHeader("Last-Modified");
213
+ if (lastModified) {
214
+ const parsed = new Date(lastModified);
215
+ timeStamp = Number.isNaN(parsed.valueOf()) ? null : parsed.toISOString();
216
+ }
208
217
  resolve(timeStamp);
209
218
  } else {
210
219
  resolve(timeStamp);