@preference-sl/pref-viewer 2.11.0-beta.7 → 2.11.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preference-sl/pref-viewer",
3
- "version": "2.11.0-beta.7",
3
+ "version": "2.11.0-beta.9",
4
4
  "description": "Web Component to preview GLTF models with Babylon.js",
5
5
  "author": "Alex Moreno Palacio <amoreno@preference.es>",
6
6
  "scripts": {
@@ -40,7 +40,8 @@
40
40
  "@panzoom/panzoom": "^4.6.0",
41
41
  "babylonjs-gltf2interface": "^8.36.1",
42
42
  "idb": "^8.0.3",
43
- "is-svg": "^6.1.0"
43
+ "is-svg": "^6.1.0",
44
+ "jszip": "^3.10.1"
44
45
  },
45
46
  "devDependencies": {
46
47
  "esbuild": "^0.25.10",
@@ -1,4 +1,4 @@
1
- import { ArcRotateCamera, AssetContainer, Color4, DirectionalLight, Engine, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, LoadAssetContainerAsync, MeshBuilder, PointLight, Scene, ShadowGenerator, Tools, Vector3, WebXRDefaultExperience, WebXRFeatureName, WebXRSessionManager } 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
2
  import { DracoCompression } from "@babylonjs/core/Meshes/Compression/dracoCompression";
3
3
  import "@babylonjs/loaders";
4
4
  import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_draco_mesh_compression";
@@ -16,7 +16,7 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
16
16
  * - Handles loading, replacing, and disposing of 3D models, environments, and materials.
17
17
  * - Configures advanced rendering features such as Draco mesh compression, shadows, and image-based lighting (IBL).
18
18
  * - Integrates Babylon.js WebXR experience for augmented reality (AR) support.
19
- * - 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.
20
20
  * - Manages camera and material options, container visibility, and user interactions.
21
21
  * - Observes canvas resize events and updates the engine accordingly.
22
22
  *
@@ -26,7 +26,7 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
26
26
  * - Load assets: await controller.load();
27
27
  * - Set camera/material options: controller.setCameraOptions(), controller.setMaterialOptions();
28
28
  * - Control visibility: controller.setContainerVisibility(name, show);
29
- * - Download assets: controller.downloadModelGLB(), controller.downloadModelUSDZ(), etc.
29
+ * - Download assets: controller.downloadModelGLB(), controller.downloadModelGLTF(), controller.downloadModelUSDZ(), controller.downloadModelAndSceneGLB(), controller.downloadModelAndSceneGLTF(), controller.downloadModelAndSceneUSDZ();
30
30
  * - Disable rendering: controller.disable();
31
31
  *
32
32
  * Public Methods:
@@ -36,20 +36,29 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
36
36
  * - setCameraOptions(): Applies camera options from configuration.
37
37
  * - setMaterialOptions(): Applies material options from configuration.
38
38
  * - setContainerVisibility(name, show): Shows or hides a container by name.
39
- * - 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.
40
42
  *
41
43
  * Private Methods:
42
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.
43
48
  * - #createCamera(), #createLights(), #initializeEnvironmentTexture(), #initializeIBLShadows(), #initializeShadows(): Scene setup.
44
- * - #setupInteraction(): Sets up canvas interaction handlers.
49
+ * - #setMaxSimultaneousLights(): Updates max simultaneous lights for materials.
50
+ * - #enableInteraction(), #disableInteraction(): Canvas interaction handlers.
45
51
  * - #disposeEngine(): Disposes engine and resources.
52
+ * - #onMouseWheel(event), #onKeyUp(event): Canvas event handlers.
46
53
  * - #setOptionsMaterial(), #setOptions_Materials(), #setOptions_Camera(): Applies material/camera options.
47
54
  * - #findContainerByName(), #addContainer(), #removeContainer(), #replaceContainer(): Container management.
55
+ * - #getPrefViewer3DComponent(), #getPrefViewerComponent(): Custom element references.
56
+ * - #updateVisibilityAttributeInComponents(): Updates parent visibility attributes.
48
57
  * - #setVisibilityOfWallAndFloorInModel(): Controls wall/floor mesh visibility.
49
58
  * - #stopRender(), #startRender(): Render loop control.
50
59
  * - #loadAssetContainer(), #loadContainers(): Asset loading.
51
- * - #addStylesToARButton(): Styles AR button.
52
- * - #createXRExperience(): Initializes WebXR AR experience.
60
+ * - #downloadZip(): Generates and downloads a ZIP file.
61
+ * - #openDownloadDialog(): Opens the modal download dialog.
53
62
  *
54
63
  * Notes:
55
64
  * - Designed for integration with PrefViewer and GLTFResolver.
@@ -393,23 +402,22 @@ export default class BabylonJSController {
393
402
 
394
403
  /**
395
404
  * Sets up interaction handlers for the Babylon.js canvas.
396
- * Adds a wheel event listener to control camera zoom based on mouse wheel input.
397
- * Prevents zoom if the active camera is locked.
398
405
  * @private
399
406
  * @returns {void}
400
407
  */
401
- #setupInteraction() {
402
- this.#canvas.addEventListener("wheel", (event) => {
403
- if (!this.#scene || !this.#camera) {
404
- return false;
405
- }
406
- //const pick = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
407
- //this.#camera.target = pick.hit ? pick.pickedPoint.clone() : this.#camera.target;
408
- if (!this.#scene.activeCamera.metadata?.locked) {
409
- this.#scene.activeCamera.inertialRadiusOffset -= event.deltaY * this.#scene.activeCamera.wheelPrecision * 0.001;
410
- }
411
- event.preventDefault();
412
- });
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));
413
421
  }
414
422
 
415
423
  /**
@@ -428,6 +436,43 @@ export default class BabylonJSController {
428
436
  this.#XRExperience = null;
429
437
  }
430
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
+
431
476
  /**
432
477
  * Applies material options from the configuration to the relevant meshes.
433
478
  * @private
@@ -584,6 +629,13 @@ export default class BabylonJSController {
584
629
  */
585
630
  #getPrefViewerComponent() {
586
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
+ }
587
639
  const rootNode = this.#prefViewer3D ? this.#prefViewer3D.getRootNode().host : null;
588
640
  this.#prefViewer = rootNode && rootNode.nodeName === "PREF-VIEWER" ? rootNode : null;
589
641
  }
@@ -771,7 +823,7 @@ export default class BabylonJSController {
771
823
  if (this.#babylonJSAnimationController) {
772
824
  this.#babylonJSAnimationController.dispose();
773
825
  this.#babylonJSAnimationController = null;
774
- };
826
+ }
775
827
  values.forEach((result) => {
776
828
  const container = result.value ? result.value[0] : null;
777
829
  const assetContainer = result.value ? result.value[1] : null;
@@ -788,7 +840,7 @@ export default class BabylonJSController {
788
840
  container.state.setSuccess(false);
789
841
  }
790
842
  });
791
-
843
+
792
844
  this.#setOptions_Materials();
793
845
  this.#setOptions_Camera();
794
846
  this.#setVisibilityOfWallAndFloorInModel();
@@ -809,6 +861,122 @@ export default class BabylonJSController {
809
861
  return detail;
810
862
  }
811
863
 
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
+ };
978
+ }
979
+
812
980
  /**
813
981
  * ---------------------------
814
982
  * Public methods
@@ -830,7 +998,7 @@ export default class BabylonJSController {
830
998
  this.#scene.clearColor = new Color4(1, 1, 1, 1);
831
999
  this.#createCamera();
832
1000
  this.#createLights();
833
- this.#setupInteraction();
1001
+ this.#enableInteraction();
834
1002
  await this.#createXRExperience();
835
1003
  this.#startRender();
836
1004
  this.#canvasResizeObserver = new ResizeObserver(() => this.#engine && this.#engine.resize());
@@ -844,8 +1012,13 @@ export default class BabylonJSController {
844
1012
  * @returns {void}
845
1013
  */
846
1014
  disable() {
847
- this.#disposeEngine();
848
1015
  this.#canvasResizeObserver.disconnect();
1016
+ this.#disableInteraction();
1017
+ if (this.#babylonJSAnimationController) {
1018
+ this.#babylonJSAnimationController.dispose();
1019
+ this.#babylonJSAnimationController = null;
1020
+ }
1021
+ this.#disposeEngine();
849
1022
  }
850
1023
 
851
1024
  /**
@@ -909,50 +1082,105 @@ export default class BabylonJSController {
909
1082
  }
910
1083
 
911
1084
  /**
912
- * Initiates download of the current model as a GLB file.
1085
+ * Downloads the current scene, model, or environment as a GLB file.
913
1086
  * @public
1087
+ * @param {number} content - 0 for full scene, 1 for model, 2 for environment.
914
1088
  * @returns {void}
915
1089
  */
916
- downloadModelGLB() {
917
- const fileName = "model";
918
- 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
+ });
919
1115
  }
920
1116
 
921
1117
  /**
922
- * Initiates download of the current model as a USDZ file.
1118
+ * Downloads the current scene, model, or environment as a glTF ZIP file.
923
1119
  * @public
1120
+ * @param {number} content - 0 for full scene, 1 for model, 2 for environment.
924
1121
  * @returns {void}
925
1122
  */
926
- downloadModelUSDZ() {
927
- const fileName = "model";
928
- USDZExportAsync(this.#containers.model.assetContainer).then((response) => {
929
- if (response) {
930
- 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);
931
1150
  }
932
1151
  });
933
1152
  }
934
1153
 
935
1154
  /**
936
- * 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.
937
1156
  * @public
1157
+ * @param {number} content - 0 for full scene, 1 for model, 2 for environment.
938
1158
  * @returns {void}
939
1159
  */
940
- downloadModelAndSceneUSDZ() {
941
- const fileName = "scene";
942
- 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) => {
943
1181
  if (response) {
944
1182
  Tools.Download(new Blob([response], { type: "model/vnd.usdz+zip" }), `${fileName}.usdz`);
945
1183
  }
946
1184
  });
947
1185
  }
948
-
949
- /**
950
- * Initiates download of the entire scene (model and environment) as a GLB file.
951
- * @public
952
- * @returns {void}
953
- */
954
- downloadModelAndSceneGLB() {
955
- const fileName = "scene";
956
- GLTF2Export.GLBAsync(this.#scene, fileName, { exportWithoutWaitingForScene: true }).then((glb) => glb.downloadFiles());
957
- }
958
1186
  }
@@ -0,0 +1,39 @@
1
+ /* Variables */
2
+ pref-viewer-2d {
3
+ --pref-viewer-2d-bg-color: #ffffff;
4
+ --pref-viewer-2d-svg-padding: 10px;
5
+ }
6
+
7
+ pref-viewer-2d[visible="true"] {
8
+ display: block;
9
+ }
10
+
11
+ pref-viewer-2d[visible="false"] {
12
+ display: none;
13
+ }
14
+
15
+ pref-viewer-2d {
16
+ grid-column: 1;
17
+ grid-row: 1;
18
+ overflow: hidden;
19
+ min-width: 0;
20
+ min-height: 0;
21
+ align-self: stretch;
22
+ justify-self: stretch;
23
+ background: var(--pref-viewer-2d-bg-color);
24
+ }
25
+
26
+ pref-viewer-2d,
27
+ pref-viewer-2d>div,
28
+ pref-viewer-2d>div>svg {
29
+ width: 100%;
30
+ height: 100%;
31
+ display: block;
32
+ position: relative;
33
+ outline: none;
34
+ box-sizing: border-box;
35
+ }
36
+
37
+ pref-viewer-2d>div>svg {
38
+ padding: var(--pref-viewer-2d-svg-padding);
39
+ }
@@ -0,0 +1,28 @@
1
+ pref-viewer-3d[visible="true"] {
2
+ display: block;
3
+ }
4
+
5
+ pref-viewer-3d[visible="false"] {
6
+ display: none;
7
+ }
8
+
9
+ pref-viewer-3d {
10
+ grid-column: 1;
11
+ grid-row: 1;
12
+ overflow: hidden;
13
+ min-width: 0;
14
+ min-height: 0;
15
+ align-self: stretch;
16
+ justify-self: stretch;
17
+ }
18
+
19
+ pref-viewer-3d,
20
+ pref-viewer-3d>div,
21
+ pref-viewer-3d>div>canvas {
22
+ width: 100%;
23
+ height: 100%;
24
+ display: block;
25
+ position: relative;
26
+ outline: none;
27
+ box-sizing: border-box;
28
+ }
@@ -0,0 +1,105 @@
1
+ /* Variables */
2
+ pref-viewer-dialog {
3
+ --brand-color: #ff6700;
4
+ --dialog-general-space: 16px;
5
+ --dialog-bg-color: #ffffff;
6
+ --dialog-backdrop-color: rgba(0, 0, 0, 0.25);
7
+ --dialog-border-color: #e7e7e7;
8
+ --dialog-border-radius: 8px;
9
+ --dialog-box-shadow: 0 4px 24px rgba(0, 0, 0, 0.25);
10
+ --button-default-bg-color: #bbbbbb;
11
+ --button-default-bg-color-hover: #a1a1a1;
12
+ --button-primary-bg-color: color-mix(in oklab, var(--brand-color), white 25%);
13
+ --button-primary-bg-color-hover: var(--brand-color);
14
+ --button-border-radius: 4px;
15
+ --button-padding-horizontal: 16px;
16
+ --button-padding-vertical: 8px;
17
+ }
18
+
19
+ pref-viewer-dialog:not {
20
+ display: none;
21
+ }
22
+
23
+ pref-viewer-dialog[open] {
24
+ font-family: 'Roboto', ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
25
+ grid-row: 1;
26
+ grid-column: 1;
27
+ overflow: hidden;
28
+ min-width: 0;
29
+ min-height: 0;
30
+ align-self: stretch;
31
+ justify-self: stretch;
32
+ display: flex;
33
+ align-items: center;
34
+ justify-content: center;
35
+ background-color: var(--dialog-backdrop-color);
36
+ position: relative;
37
+ z-index: 1000;
38
+ }
39
+
40
+ pref-viewer-dialog>.dialog-wrapper {
41
+ display: flex;
42
+ flex-direction: column;
43
+ align-items: stretch;
44
+ background: var(--dialog-bg-color);
45
+ border: 1px solid var(--dialog-border-color);
46
+ border-radius: var(--dialog-border-radius);
47
+ box-shadow: var(--dialog-box-shadow);
48
+ padding: 0;
49
+ min-width: 320px;
50
+ max-width: 90%;
51
+ max-height: 90%;
52
+ overflow: auto;
53
+ }
54
+
55
+ pref-viewer-dialog .dialog-header {
56
+ padding: var(--dialog-general-space);
57
+ border-bottom: 1px solid var(--dialog-border-color);
58
+ }
59
+
60
+ pref-viewer-dialog .dialog-header h3 {
61
+ margin: 0;
62
+ font-weight: 500;
63
+ font-size: 1.1em;
64
+ }
65
+
66
+ pref-viewer-dialog .dialog-content {
67
+ padding: var(--dialog-general-space);
68
+ }
69
+
70
+ pref-viewer-dialog .dialog-content h4 {
71
+ margin: 0;
72
+ font-weight: 500;
73
+ font-size: 1.05em;
74
+ }
75
+
76
+ pref-viewer-dialog .dialog-footer {
77
+ border-top: 1px solid var(--dialog-border-color);
78
+ padding: var(--dialog-general-space);
79
+ display: flex;
80
+ gap: var(--dialog-general-space);
81
+ justify-content: stretch;
82
+ }
83
+
84
+ pref-viewer-dialog .dialog-footer button {
85
+ width: 100%;
86
+ font-size: 1em;
87
+ padding: var(--button-padding-vertical) var(--button-padding-horizontal);
88
+ border-radius: var(--button-border-radius);
89
+ border: none;
90
+ background: var(--button-default-bg-color);
91
+ cursor: pointer;
92
+ transition: background 0.2s;
93
+ }
94
+
95
+ pref-viewer-dialog .dialog-footer button.primary {
96
+ background: var(--button-primary-bg-color);
97
+ }
98
+
99
+ pref-viewer-dialog .dialog-footer button:hover {
100
+ background: var(--button-default-bg-color-hover);
101
+ }
102
+
103
+ pref-viewer-dialog .dialog-footer button.primary:hover {
104
+ background: var(--button-primary-bg-color-hover);
105
+ }
@@ -0,0 +1,11 @@
1
+ :host .pref-viewer-wrapper {
2
+ display: grid !important;
3
+ position: relative;
4
+ width: 100%;
5
+ height: 100%;
6
+ grid-template-columns: 1fr;
7
+ grid-template-rows: 1fr;
8
+ grid-gap: 0;
9
+ min-width: 0;
10
+ min-height: 0;
11
+ }
package/src/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { PrefViewer2D } from "./pref-viewer-2d.js";
2
2
  import { PrefViewer3D } from "./pref-viewer-3d.js";
3
+ import { PrefViewerDialog } from "./pref-viewer-dialog.js";
3
4
  import { PrefViewerTask } from "./pref-viewer-task.js";
4
5
 
5
6
  /**
@@ -29,12 +30,32 @@ import { PrefViewerTask } from "./pref-viewer-task.js";
29
30
  * - mode: Viewer mode ("2d" or "3d").
30
31
  *
31
32
  * Public Methods:
32
- * - loadConfig(config), loadModel(model), loadScene(scene), loadMaterials(materials), loadDrawing(drawing)
33
- * - setOptions(options)
33
+ * - loadConfig(config): Loads a configuration object or JSON string.
34
+ * - loadModel(model): Loads a model object or JSON string.
35
+ * - loadScene(scene): Loads a scene/environment object or JSON string.
36
+ * - loadMaterials(materials): Loads materials object or JSON string.
37
+ * - loadDrawing(drawing): Loads a drawing object or JSON string.
38
+ * - setOptions(options): Sets viewer options from an object or JSON string.
34
39
  * - setMode(mode): Sets the viewer mode to "2d" or "3d" and updates component visibility.
35
- * - showModel(), hideModel(), showScene(), hideScene()
36
- * - downloadModelGLB(), downloadModelUSDZ(), downloadModelAndSceneGLB(), downloadModelAndSceneUSDZ()
37
- * - zoomCenter(), zoomExtentsAll(), zoomIn(), zoomOut()
40
+ * - showModel(): Shows the 3D model.
41
+ * - hideModel(): Hides the 3D model.
42
+ * - showScene(): Shows the 3D environment/scene.
43
+ * - hideScene(): Hides the 3D environment/scene.
44
+ * - zoomCenter(): Centers the 2D drawing view.
45
+ * - zoomExtentsAll(): Zooms the 2D drawing to fit all content.
46
+ * - zoomIn(): Zooms in on the 2D drawing.
47
+ * - zoomOut(): Zooms out of the 2D drawing.
48
+ * - downloadModelGLB(): Downloads the current 3D model as a GLB file.
49
+ * - downloadModelGLTF(): Downloads the current 3D model as a glTF ZIP file.
50
+ * - downloadModelUSDZ(): Downloads the current 3D model as a USDZ file.
51
+ * - downloadModelAndSceneGLB(): Downloads both the model and scene as a GLB file.
52
+ * - downloadModelAndSceneGLTF(): Downloads both the model and scene as a glTF ZIP file.
53
+ * - downloadModelAndSceneUSDZ(): Downloads both the model and scene as a USDZ file.
54
+ * - downloadSceneGLB(): Downloads the environment as a GLB file.
55
+ * - downloadSceneGLTF(): Downloads the environment as a glTF ZIP file.
56
+ * - downloadSceneUSDZ(): Downloads the environment as a USDZ file.
57
+ * - openDialog(title, content, footer): Opens a modal dialog with the specified title, content, and footer.
58
+ * - closeDialog(): Closes the currently open dialog, if any, and removes it from the DOM.
38
59
  *
39
60
  * Public Properties:
40
61
  * - isInitialized: Indicates if the viewer is initialized.
@@ -63,8 +84,10 @@ class PrefViewer extends HTMLElement {
63
84
 
64
85
  #taskQueue = [];
65
86
 
87
+ #wrapper = null;
66
88
  #component2D = null;
67
89
  #component3D = null;
90
+ #dialog = null;
68
91
 
69
92
  /**
70
93
  * Creates a new PrefViewer instance and attaches a shadow DOM.
@@ -145,6 +168,14 @@ class PrefViewer extends HTMLElement {
145
168
  * @returns {void|boolean} Returns false if initialization fails; otherwise void.
146
169
  */
147
170
  connectedCallback() {
171
+ this.#wrapper = document.createElement("div");
172
+ this.#wrapper.classList.add("pref-viewer-wrapper");
173
+ this.shadowRoot.append(this.#wrapper);
174
+
175
+ const style = document.createElement("style");
176
+ style.textContent = `@import "../src/css/pref-viewer.css";`;
177
+ this.shadowRoot.append(style);
178
+
148
179
  this.#createComponent3D();
149
180
  this.#createComponent2D();
150
181
 
@@ -166,7 +197,7 @@ class PrefViewer extends HTMLElement {
166
197
  this.#component2D = document.createElement("pref-viewer-2d");
167
198
  this.#component2D.setAttribute("visible", "false");
168
199
  this.#component2D.addEventListener("drawing-zoom-changed", this.#on2DZoomChanged.bind(this));
169
- this.shadowRoot.appendChild(this.#component2D);
200
+ this.#wrapper.appendChild(this.#component2D);
170
201
  }
171
202
 
172
203
  /**
@@ -178,7 +209,7 @@ class PrefViewer extends HTMLElement {
178
209
  #createComponent3D() {
179
210
  this.#component3D = document.createElement("pref-viewer-3d");
180
211
  this.#component3D.setAttribute("visible", "false");
181
- this.shadowRoot.appendChild(this.#component3D);
212
+ this.#wrapper.appendChild(this.#component3D);
182
213
  }
183
214
 
184
215
  /**
@@ -480,11 +511,14 @@ class PrefViewer extends HTMLElement {
480
511
  this.#component3D?.hide();
481
512
  this.#component2D?.show();
482
513
  } else {
483
- this.#component3D?.show();
484
514
  this.#component2D?.hide();
515
+ this.#component3D?.show();
485
516
  }
486
517
  if (this.getAttribute("mode") !== mode) {
487
518
  this.setAttribute("mode", mode);
519
+ if (this.#dialog) {
520
+ this.closeDialog();
521
+ }
488
522
  }
489
523
  }
490
524
 
@@ -689,6 +723,19 @@ class PrefViewer extends HTMLElement {
689
723
  this.#component3D.downloadModelGLB();
690
724
  }
691
725
 
726
+ /**
727
+ * Initiates download of the current 3D model in GLTF format.
728
+ * @public
729
+ * @returns {void}
730
+ */
731
+ downloadModelGLTF() {
732
+ if (!this.#component3D) {
733
+ return;
734
+ }
735
+
736
+ this.#component3D.downloadModelGLTF();
737
+ }
738
+
692
739
  /**
693
740
  * Initiates download of the current 3D model in USDZ format.
694
741
  * @public
@@ -703,7 +750,33 @@ class PrefViewer extends HTMLElement {
703
750
  }
704
751
 
705
752
  /**
706
- * Initiates download of both the 3D model and scene in USDZ format.
753
+ * Initiates download of the current complete scene (3D model and environment) in GLB format.
754
+ * @public
755
+ * @returns {void}
756
+ */
757
+ downloadModelAndSceneGLB() {
758
+ if (!this.#component3D) {
759
+ return;
760
+ }
761
+
762
+ this.#component3D.downloadModelAndSceneGLB();
763
+ }
764
+
765
+ /**
766
+ * Initiates download of the current complete scene (3D model and environment) in GLTF format.
767
+ * @public
768
+ * @returns {void}
769
+ */
770
+ downloadModelAndSceneGLTF() {
771
+ if (!this.#component3D) {
772
+ return;
773
+ }
774
+
775
+ this.#component3D.downloadModelAndSceneGLTF();
776
+ }
777
+
778
+ /**
779
+ * Initiates download of the current complete scene (3D model and environment) in USDZ format.
707
780
  * @public
708
781
  * @returns {void}
709
782
  */
@@ -716,18 +789,82 @@ class PrefViewer extends HTMLElement {
716
789
  }
717
790
 
718
791
  /**
719
- * Initiates download of both the 3D model and scene in GLB format.
792
+ * Initiates download of the current 3D environment in GLB format.
720
793
  * @public
721
794
  * @returns {void}
722
795
  */
723
- downloadModelAndSceneGLB() {
796
+ downloadSceneGLB() {
724
797
  if (!this.#component3D) {
725
798
  return;
726
799
  }
727
800
 
728
- this.#component3D.downloadModelAndSceneGLB();
801
+ this.#component3D.downloadSceneGLB();
729
802
  }
730
803
 
804
+ /**
805
+ * Initiates download of the current 3D environment in GLTF format.
806
+ * @public
807
+ * @returns {void}
808
+ */
809
+ downloadSceneGLTF() {
810
+ if (!this.#component3D) {
811
+ return;
812
+ }
813
+
814
+ this.#component3D.downloadSceneGLTF();
815
+ }
816
+
817
+ /**
818
+ * Initiates download of the current 3D environment in USDZ format.
819
+ * @public
820
+ * @returns {void}
821
+ */
822
+ downloadSceneUSDZ() {
823
+ if (!this.#component3D) {
824
+ return;
825
+ }
826
+ this.#component3D.downloadSceneUSDZ();
827
+ }
828
+
829
+ /**
830
+ * Opens a modal dialog with the specified title, content, and footer.
831
+ * @public
832
+ * @param {string} title - The dialog title to display in the header.
833
+ * @param {string} content - The HTML content to display in the dialog body.
834
+ * @param {string} footer - The HTML content to display in the dialog footer (e.g., action buttons).
835
+ * @returns {HTMLElement} The created dialog element.
836
+ * @description
837
+ * If a dialog is already open, it is closed before opening the new one.
838
+ * The dialog is appended to the viewer's shadow DOM and returned for further manipulation.
839
+ */
840
+ openDialog(title, content, footer) {
841
+ if (this.#dialog && this.#dialog.hasAttribute("open")) {
842
+ this.#dialog.close();
843
+ }
844
+ this.#dialog = document.createElement("pref-viewer-dialog");
845
+ this.shadowRoot.querySelector(".pref-viewer-wrapper").appendChild(this.#dialog);
846
+ const opened = this.#dialog.open(title, content, footer);
847
+ return opened ? this.#dialog : null;
848
+ }
849
+
850
+ /**
851
+ * Closes the currently open dialog, if any, and removes it from the DOM.
852
+ * @public
853
+ * @returns {void}
854
+ */
855
+ closeDialog() {
856
+ if (this.#dialog) {
857
+ this.#dialog.close();
858
+ this.#dialog = null;
859
+ }
860
+ }
861
+
862
+ /**
863
+ * ---------------------------
864
+ * Public properties
865
+ * ---------------------------
866
+ */
867
+
731
868
  /**
732
869
  * Indicates whether the viewer has been initialized.
733
870
  * @public
@@ -776,4 +913,5 @@ class PrefViewer extends HTMLElement {
776
913
 
777
914
  customElements.define("pref-viewer-2d", PrefViewer2D);
778
915
  customElements.define("pref-viewer-3d", PrefViewer3D);
916
+ customElements.define("pref-viewer-dialog", PrefViewerDialog);
779
917
  customElements.define("pref-viewer", PrefViewer);
@@ -144,7 +144,7 @@ export class PrefViewer2D extends HTMLElement {
144
144
  this.#wrapper = document.createElement("div");
145
145
  this.append(this.#wrapper);
146
146
  const style = document.createElement("style");
147
- style.textContent = `pref-viewer-2d[visible="true"] { display: block; } pref-viewer-2d[visible="false"] { display: none; } pref-viewer-2d, pref-viewer-2d > div, pref-viewer-2d > div > svg { width: 100%; height: 100%; display: block; position: relative; outline: none; box-sizing: border-box; } pref-viewer-2d > div > svg { padding: 10px; }`;
147
+ style.textContent = `@import "../src/css/pref-viewer-2d.css";`;
148
148
  this.append(style);
149
149
  }
150
150
 
@@ -31,9 +31,14 @@ import BabylonJSController from "./babylonjs-controller.js";
31
31
  * - showEnvironment(): Shows the 3D environment/scene.
32
32
  * - hideEnvironment(): Hides the 3D environment/scene.
33
33
  * - downloadModelGLB(): Downloads the current 3D model as a GLB file.
34
+ * - downloadModelGLTF(): Downloads the current 3D model as a glTF ZIP file.
34
35
  * - downloadModelUSDZ(): Downloads the current 3D model as a USDZ file.
35
36
  * - downloadModelAndSceneGLB(): Downloads both the model and scene as a GLB file.
37
+ * - downloadModelAndSceneGLTF(): Downloads both the model and scene as a glTF ZIP file.
36
38
  * - downloadModelAndSceneUSDZ(): Downloads both the model and scene as a USDZ file.
39
+ * - downloadSceneGLB(): Downloads the environment as a GLB file.
40
+ * - downloadSceneGLTF(): Downloads the environment as a glTF ZIP file.
41
+ * - downloadSceneUSDZ(): Downloads the environment as a USDZ file.
37
42
  *
38
43
  * Public Properties:
39
44
  * - isInitialized: Indicates whether the component has completed initialization.
@@ -155,7 +160,7 @@ export class PrefViewer3D extends HTMLElement {
155
160
  this.#canvas = document.createElement("canvas");
156
161
  this.#wrapper.appendChild(this.#canvas);
157
162
  const style = document.createElement("style");
158
- style.textContent = `pref-viewer-3d[visible="true"] { display: block; } pref-viewer-3d[visible="false"] { display: none; } pref-viewer-3d, pref-viewer-3d > div, pref-viewer-3d > div > canvas { width: 100%; height: 100%; display: block; position: relative; outline: none; box-sizing: border-box; }`;
163
+ style.textContent = `@import "../src/css/pref-viewer-3d.css";`;
159
164
  this.append(style);
160
165
  }
161
166
 
@@ -567,7 +572,19 @@ export class PrefViewer3D extends HTMLElement {
567
572
  if (!this.#babylonJSController) {
568
573
  return;
569
574
  }
570
- this.#babylonJSController.downloadModelGLB();
575
+ this.#babylonJSController.downloadGLB(1);
576
+ }
577
+
578
+ /**
579
+ * Downloads the current 3D model as a glTF file (ZIP with gltf and all resources (textures, buffers, etc.)).
580
+ * @public
581
+ * @returns {void}
582
+ */
583
+ downloadModelGLTF() {
584
+ if (!this.#babylonJSController) {
585
+ return;
586
+ }
587
+ this.#babylonJSController.downloadGLTF(1);
571
588
  }
572
589
 
573
590
  /**
@@ -579,11 +596,35 @@ export class PrefViewer3D extends HTMLElement {
579
596
  if (!this.#babylonJSController) {
580
597
  return;
581
598
  }
582
- this.#babylonJSController.downloadModelUSDZ();
599
+ this.#babylonJSController.downloadUSDZ(1);
600
+ }
601
+
602
+ /**
603
+ * Downloads the current 3D complete scene (model and environment) as a GLB file.
604
+ * @public
605
+ * @returns {void}
606
+ */
607
+ downloadModelAndSceneGLB() {
608
+ if (!this.#babylonJSController) {
609
+ return;
610
+ }
611
+ this.#babylonJSController.downloadModelGLB(0);
583
612
  }
584
613
 
585
614
  /**
586
- * Downloads both the 3D model and scene as a USDZ file.
615
+ * Downloads the current 3D complete scene (model and environment) as a glTF file (ZIP with gltf and all resources (textures, buffers, etc.)).
616
+ * @public
617
+ * @returns {void}
618
+ */
619
+ downloadModelAndSceneGLTF() {
620
+ if (!this.#babylonJSController) {
621
+ return;
622
+ }
623
+ this.#babylonJSController.downloadGLTF(0);
624
+ }
625
+
626
+ /**
627
+ * Downloads the current 3D complete scene (model and environment) as a USDZ file.
587
628
  * @public
588
629
  * @returns {void}
589
630
  */
@@ -591,19 +632,43 @@ export class PrefViewer3D extends HTMLElement {
591
632
  if (!this.#babylonJSController) {
592
633
  return;
593
634
  }
594
- this.#babylonJSController.downloadModelAndSceneUSDZ();
635
+ this.#babylonJSController.downloadUSDZ(0);
595
636
  }
596
637
 
597
638
  /**
598
- * Downloads both the 3D model and scene as a GLB file.
639
+ * Downloads the current 3D environment as a GLB file.
599
640
  * @public
600
641
  * @returns {void}
601
642
  */
602
- downloadModelAndSceneGLB() {
643
+ downloadSceneGLB() {
644
+ if (!this.#babylonJSController) {
645
+ return;
646
+ }
647
+ this.#babylonJSController.downloadGLB(2);
648
+ }
649
+
650
+ /**
651
+ * Downloads the current 3D environment as a glTF file (ZIP with gltf and all resources (textures, buffers, etc.)).
652
+ * @public
653
+ * @returns {void}
654
+ */
655
+ downloadSceneGLTF() {
656
+ if (!this.#babylonJSController) {
657
+ return;
658
+ }
659
+ this.#babylonJSController.downloadGLTF(2);
660
+ }
661
+
662
+ /**
663
+ * Downloads the current 3D environment as a USDZ file.
664
+ * @public
665
+ * @returns {void}
666
+ */
667
+ downloadSceneUSDZ() {
603
668
  if (!this.#babylonJSController) {
604
669
  return;
605
670
  }
606
- this.#babylonJSController.downloadModelAndSceneGLB();
671
+ this.#babylonJSController.downloadUSDZ(2);
607
672
  }
608
673
 
609
674
  /**
@@ -0,0 +1,139 @@
1
+ /**
2
+ * PrefViewerDialog - Custom Web Component for displaying modal dialogs in PrefViewer.
3
+ *
4
+ * Overview:
5
+ * - Provides a modal dialog overlay for user interactions such as downloads or confirmations.
6
+ * - Handles DOM creation, styling, and open/close logic.
7
+ * - Ensures dialog content is centered and styled appropriately.
8
+ * - Automatically focuses the canvas when closed for improved accessibility.
9
+ *
10
+ * Usage:
11
+ * - Use as a custom HTML element: <pref-viewer-dialog></pref-viewer-dialog>
12
+ * - Call open(title, content, footer) to display the dialog with a header, content, and footer.
13
+ * - Call close() to hide and remove the dialog.
14
+ *
15
+ * Methods:
16
+ * - constructor(): Initializes the custom element.
17
+ * - connectedCallback(): Called when the element is added to the DOM; sets up DOM and styles.
18
+ * - disconnectedCallback(): Called when the element is removed from the DOM; cleans up resources.
19
+ * - open(title, content, footer): Displays the dialog and sets its header, content, and footer.
20
+ * - close(): Hides and removes the dialog, refocuses the canvas if available.
21
+ * - #createDOMElements(): Creates the dialog structure and applies styles.
22
+ */
23
+ export class PrefViewerDialog extends HTMLElement {
24
+ #wrapper = null;
25
+ #header = null;
26
+ #content = null;
27
+ #footer = null;
28
+
29
+ /**
30
+ * Initializes the custom dialog element.
31
+ * Only calls super(); heavy initialization is deferred to connectedCallback.
32
+ */
33
+ constructor() {
34
+ super();
35
+ }
36
+
37
+ /**
38
+ * Called when the element is inserted into the DOM.
39
+ * Sets up the dialog's DOM structure and styles.
40
+ * @returns {void}
41
+ */
42
+ connectedCallback() {
43
+ this.#createDOMElements();
44
+ }
45
+
46
+ /**
47
+ * Called when the element is removed from the DOM.
48
+ * Cleans up resources and event listeners.
49
+ * @returns {void}
50
+ */
51
+ disconnectedCallback() {
52
+ this.removeEventListener("click", this.#closeOnBackdropClick.bind(this));
53
+ }
54
+
55
+ /**
56
+ * Creates the dialog's DOM structure and applies CSS styles.
57
+ * Adds a click handler to close the dialog when clicking outside the content.
58
+ * @private
59
+ * @returns {void}
60
+ */
61
+ #createDOMElements() {
62
+ this.#wrapper = document.createElement("div");
63
+ this.#wrapper.classList.add("dialog-wrapper");
64
+ this.#wrapper.innerHTML = `
65
+ <div class="dialog-header"><h3 class="dialog-header-title"></h3></div>
66
+ <div class="dialog-content"></div>
67
+ <div class="dialog-footer"></div>`;
68
+ this.append(this.#wrapper);
69
+
70
+ const style = document.createElement("style");
71
+ style.textContent = `@import "../src/css/pref-viewer-dialog.css";`;
72
+ this.append(style);
73
+
74
+ this.addEventListener("click", this.#closeOnBackdropClick.bind(this));
75
+
76
+ this.#header = this.#wrapper.querySelector(".dialog-header");
77
+ this.#content = this.#wrapper.querySelector(".dialog-content");
78
+ this.#footer = this.#wrapper.querySelector(".dialog-footer");
79
+ }
80
+
81
+ #closeOnBackdropClick(event) {
82
+ if (event.target === this) {
83
+ this.close();
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Opens the dialog and sets its header, content, and footer.
89
+ * @param {string} title - The dialog title to display in the header.
90
+ * @param {string} content - The HTML content to display in the dialog body.
91
+ * @param {string} footer - The HTML content to display in the dialog footer (e.g., action buttons).
92
+ * @returns {boolean} True if the dialog was opened, false if no content was provided.
93
+ */
94
+ open(title = "", content = "", footer = "") {
95
+ if (this.#wrapper === null || this.#header === null || this.#content === null || this.#footer === null) {
96
+ return false;
97
+ }
98
+ if (title === "" && content === "" && footer === "") {
99
+ return false;
100
+ }
101
+
102
+ if (title === "") {
103
+ this.#header.style.display = "none";
104
+ }
105
+ if (footer === "") {
106
+ this.#footer.style.display = "none";
107
+ }
108
+
109
+ this.#header.querySelector(".dialog-header-title").innerHTML = title;
110
+ this.#content.innerHTML = content;
111
+ this.#footer.innerHTML = footer;
112
+ this.setAttribute("open", "");
113
+ return true;
114
+ }
115
+
116
+ /**
117
+ * Closes and removes the dialog from the DOM.
118
+ * @returns {void}
119
+ */
120
+ close() {
121
+ this.removeAttribute("open");
122
+ const parent = this.getRootNode().host;
123
+ if (parent) {
124
+ // Refocus 3D canvas or 2D component for accessibility
125
+ const canvas = parent.shadowRoot.querySelector("pref-viewer-3d[visible='true'] canvas");
126
+ if (canvas) {
127
+ canvas.focus();
128
+ } else {
129
+ const component2D = parent.shadowRoot.querySelector("pref-viewer-2d[visible='true']");
130
+ if (component2D) {
131
+ component2D.focus();
132
+ }
133
+ }
134
+
135
+ }
136
+ this.#wrapper = this.#header = this.#content = this.#footer = null;
137
+ this.remove();
138
+ }
139
+ }