@preference-sl/pref-viewer 2.11.0-beta.0 → 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.
@@ -0,0 +1,1186 @@
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";
3
+ import "@babylonjs/loaders";
4
+ import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_draco_mesh_compression";
5
+ import { USDZExportAsync, GLTF2Export } from "@babylonjs/serializers";
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";
10
+
11
+ /**
12
+ * BabylonJSController - Main controller for managing Babylon.js 3D scenes, assets, and interactions.
13
+ *
14
+ * Responsibilities:
15
+ * - Initializes and manages the Babylon.js engine, scene, camera, lights, and asset containers.
16
+ * - Handles loading, replacing, and disposing of 3D models, environments, and materials.
17
+ * - Configures advanced rendering features such as Draco mesh compression, shadows, and image-based lighting (IBL).
18
+ * - Integrates Babylon.js WebXR experience for augmented reality (AR) support.
19
+ * - Provides methods for downloading models and scenes in GLB, GLTF, and USDZ formats.
20
+ * - Manages camera and material options, container visibility, and user interactions.
21
+ * - Observes canvas resize events and updates the engine accordingly.
22
+ *
23
+ * Usage:
24
+ * - Instantiate: const controller = new BabylonJSController(canvas, containers, options);
25
+ * - Enable rendering: await controller.enable();
26
+ * - Load assets: await controller.load();
27
+ * - Set camera/material options: controller.setCameraOptions(), controller.setMaterialOptions();
28
+ * - Control visibility: controller.setContainerVisibility(name, show);
29
+ * - Download assets: controller.downloadModelGLB(), controller.downloadModelGLTF(), controller.downloadModelUSDZ(), controller.downloadModelAndSceneGLB(), controller.downloadModelAndSceneGLTF(), controller.downloadModelAndSceneUSDZ();
30
+ * - Disable rendering: controller.disable();
31
+ *
32
+ * Public Methods:
33
+ * - enable(): Initializes engine, scene, camera, lights, XR, and starts rendering.
34
+ * - disable(): Disposes engine and disconnects resize observer.
35
+ * - load(): Loads all asset containers and adds them to the scene.
36
+ * - setCameraOptions(): Applies camera options from configuration.
37
+ * - setMaterialOptions(): Applies material options from configuration.
38
+ * - setContainerVisibility(name, show): Shows or hides a container by name.
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.
42
+ *
43
+ * Private Methods:
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.
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.
61
+ * - #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
+ */
68
+ export default class BabylonJSController {
69
+ // Canvas HTML element
70
+ #canvas = null;
71
+
72
+ // References to parent custom elements
73
+ #prefViewer3D = undefined;
74
+ #prefViewer = undefined;
75
+
76
+ // Babylon.js core objects
77
+ #engine = null;
78
+ #scene = null;
79
+ #camera = null;
80
+ #hemiLight = null;
81
+ #dirLight = null;
82
+ #cameraLight = null;
83
+ #shadowGen = null;
84
+ #XRExperience = null;
85
+ #canvasResizeObserver = null;
86
+
87
+ #containers = {};
88
+ #options = {};
89
+
90
+ #gltfResolver = null; // GLTFResolver instance
91
+ #babylonJSAnimationController = null; // AnimationController instance
92
+
93
+ /**
94
+ * Constructs a new BabylonJSController instance.
95
+ * Initializes the canvas, asset containers, and options for the Babylon.js scene.
96
+ * @public
97
+ * @param {HTMLCanvasElement|null} canvas - The canvas element to render the scene on.
98
+ * @param {object} containers - An object containing container states for model, environment, materials, etc.
99
+ * @param {object} options - Configuration options for the Babylon.js scene and assets.
100
+ * @returns {BabylonJSController}
101
+ * @description
102
+ * - Assigns the provided canvas to the controller.
103
+ * - Initializes each container with its state and sets assetContainer to null.
104
+ * - Stores the provided options for later use in scene setup and asset loading.
105
+ */
106
+ constructor(canvas = null, containers = {}, options = {}) {
107
+ this.#canvas = canvas;
108
+ Object.keys(containers).forEach((key) => {
109
+ this.#containers[key] = {
110
+ assetContainer: null,
111
+ state: containers[key],
112
+ };
113
+ });
114
+ this.#options = options;
115
+ }
116
+
117
+ /**
118
+ * Configures the Draco mesh compression decoder for Babylon.js.
119
+ * @private
120
+ * @returns {void}
121
+ */
122
+ #configureDracoCompression() {
123
+ // Point to whichever version you packaged or want to use:
124
+ const DRACO_BASE = "https://www.gstatic.com/draco/versioned/decoders/1.5.7";
125
+ DracoCompression.Configuration.decoder = {
126
+ // loader for the “wrapper” that pulls in the real WASM
127
+ wasmUrl: `${DRACO_BASE}/draco_wasm_wrapper_gltf.js`,
128
+ // the raw WebAssembly binary
129
+ wasmBinaryUrl: `${DRACO_BASE}/draco_decoder_gltf.wasm`,
130
+ // JS fallback if WASM isn’t available
131
+ fallbackUrl: `${DRACO_BASE}/draco_decoder_gltf.js`,
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Render loop callback for Babylon.js.
137
+ * @private
138
+ * @returns {void}
139
+ * @description
140
+ * Continuously renders the current scene if it exists.
141
+ * Used by the engine's runRenderLoop method to update the view.
142
+ */
143
+ #renderLoop() {
144
+ this.#scene && this.#scene.render();
145
+ }
146
+
147
+ /**
148
+ * Adds custom CSS styles to the Babylon.js AR button for consistent appearance and interaction.
149
+ * @private
150
+ * @returns {void}
151
+ */
152
+ #addStylesToARButton() {
153
+ const css = '.babylonVRicon { color: #868686; border-color: #868686; border-style: solid; margin-left: 10px; height: 50px; width: 80px; background-color: rgba(51,51,51,0.7); background-image: url(data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%222048%22%20height%3D%221152%22%20viewBox%3D%220%200%202048%201152%22%20version%3D%221.1%22%3E%3Cpath%20transform%3D%22rotate%28180%201024%2C576.0000000000001%29%22%20d%3D%22m1109%2C896q17%2C0%2030%2C-12t13%2C-30t-12.5%2C-30.5t-30.5%2C-12.5l-170%2C0q-18%2C0%20-30.5%2C12.5t-12.5%2C30.5t13%2C30t30%2C12l170%2C0zm-85%2C256q59%2C0%20132.5%2C-1.5t154.5%2C-5.5t164.5%2C-11.5t163%2C-20t150%2C-30t124.5%2C-41.5q23%2C-11%2042%2C-24t38%2C-30q27%2C-25%2041%2C-61.5t14%2C-72.5l0%2C-257q0%2C-123%20-47%2C-232t-128%2C-190t-190%2C-128t-232%2C-47l-81%2C0q-37%2C0%20-68.5%2C14t-60.5%2C34.5t-55.5%2C45t-53%2C45t-53%2C34.5t-55.5%2C14t-55.5%2C-14t-53%2C-34.5t-53%2C-45t-55.5%2C-45t-60.5%2C-34.5t-68.5%2C-14l-81%2C0q-123%2C0%20-232%2C47t-190%2C128t-128%2C190t-47%2C232l0%2C257q0%2C68%2038%2C115t97%2C73q54%2C24%20124.5%2C41.5t150%2C30t163%2C20t164.5%2C11.5t154.5%2C5.5t132.5%2C1.5zm939%2C-298q0%2C39%20-24.5%2C67t-58.5%2C42q-54%2C23%20-122%2C39.5t-143.5%2C28t-155.5%2C19t-157%2C11t-148.5%2C5t-129.5%2C1.5q-59%2C0%20-130%2C-1.5t-148%2C-5t-157%2C-11t-155.5%2C-19t-143.5%2C-28t-122%2C-39.5q-34%2C-14%20-58.5%2C-42t-24.5%2C-67l0%2C-257q0%2C-106%2040.5%2C-199t110%2C-162.5t162.5%2C-109.5t199%2C-40l81%2C0q27%2C0%2052%2C14t50%2C34.5t51%2C44.5t55.5%2C44.5t63.5%2C34.5t74%2C14t74%2C-14t63.5%2C-34.5t55.5%2C-44.5t51%2C-44.5t50%2C-34.5t52%2C-14l14%2C0q37%2C0%2070%2C0.5t64.5%2C4.5t63.5%2C12t68%2C23q71%2C30%20128.5%2C78.5t98.5%2C110t63.5%2C133.5t22.5%2C149l0%2C257z%22%20fill%3D%22white%22%20/%3E%3C/svg%3E%0A); background-size: 80%; background-repeat:no-repeat; background-position: center; border: none; outline: none; transition: transform 0.125s ease-out } .babylonVRicon:hover { transform: scale(1.05) } .babylonVRicon:active {background-color: rgba(51,51,51,1) } .babylonVRicon:focus {background-color: rgba(51,51,51,1) }.babylonVRicon.vrdisplaypresenting { background-image: none;} .vrdisplaypresenting::after { content: "EXIT"} .xr-error::after { content: "ERROR"}';
154
+ let style = this.#canvas.parentElement.parentElement.querySelector("style");
155
+ if (!style) {
156
+ style = document.createElement("style");
157
+ this.#canvas.parentElement.parentElement.appendChild(style);
158
+ }
159
+ style.appendChild(document.createTextNode(css));
160
+ }
161
+
162
+ /**
163
+ * Creates and initializes the Babylon.js WebXR experience for augmented reality (AR).
164
+ * @private
165
+ * @returns {Promise<boolean|void>} Resolves to true if XR experience is created, false if not supported, or void on error.
166
+ * @description
167
+ * Checks for AR session support, creates a hidden ground mesh, and configures session and UI options.
168
+ * Enables teleportation feature and sets the initial XR camera pose when the session starts.
169
+ * Adds custom styles to the AR button for consistent appearance.
170
+ * If AR is not supported or initialization fails, logs a warning and sets XRExperience to null.
171
+ */
172
+ async #createXRExperience() {
173
+ if (this.#XRExperience) {
174
+ return true;
175
+ }
176
+
177
+ const sessionMode = "immersive-ar";
178
+ const sessionSupported = await WebXRSessionManager.IsSessionSupportedAsync(sessionMode);
179
+ if (!sessionSupported) {
180
+ console.info("PrefViewer: WebXR in mode AR is not supported");
181
+ return false;
182
+ }
183
+
184
+ try {
185
+ const ground = MeshBuilder.CreateGround("ground", { width: 1000, height: 1000 }, this.#scene);
186
+ ground.isVisible = false;
187
+
188
+ const options = {
189
+ floorMeshes: [ground],
190
+ uiOptions: {
191
+ sessionMode: sessionMode,
192
+ renderTarget: "xrLayer",
193
+ referenceSpaceType: "local",
194
+ },
195
+ optionalFeatures: true,
196
+ };
197
+
198
+ this.#XRExperience = await WebXRDefaultExperience.CreateAsync(this.#scene, options);
199
+
200
+ const featuresManager = this.#XRExperience.baseExperience.featuresManager;
201
+ featuresManager.enableFeature(WebXRFeatureName.TELEPORTATION, "stable", {
202
+ xrInput: this.#XRExperience.input,
203
+ floorMeshes: [ground],
204
+ timeToTeleport: 1500,
205
+ });
206
+
207
+ this.#XRExperience.baseExperience.sessionManager.onXRReady.add(() => {
208
+ // Set the initial position of xrCamera: use nonVRCamera, which contains a copy of the original this.#scene.activeCamera before entering XR
209
+ this.#XRExperience.baseExperience.camera.setTransformationFromNonVRCamera(this.#XRExperience.baseExperience._nonVRCamera);
210
+ this.#XRExperience.baseExperience.camera.setTarget(Vector3.Zero());
211
+ this.#XRExperience.baseExperience.onInitialXRPoseSetObservable.notifyObservers(this.#XRExperience.baseExperience.camera);
212
+ });
213
+
214
+ this.#addStylesToARButton();
215
+ } catch (error) {
216
+ console.warn("PrefViewer: failed to create WebXR experience", error);
217
+ this.#XRExperience = null;
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Creates and configures the main ArcRotateCamera for the Babylon.js scene.
223
+ * @private
224
+ * @returns {void}
225
+ */
226
+ #createCamera() {
227
+ this.#camera = new ArcRotateCamera("camera", (3 * Math.PI) / 2, Math.PI * 0.47, 10, Vector3.Zero(), this.#scene);
228
+ this.#camera.upperBetaLimit = Math.PI * 0.48;
229
+ this.#camera.lowerBetaLimit = Math.PI * 0.25;
230
+ this.#camera.lowerRadiusLimit = 5;
231
+ this.#camera.upperRadiusLimit = 20;
232
+ this.#camera.metadata = { locked: false };
233
+ this.#camera.attachControl(this.#canvas, true);
234
+ this.#scene.activeCamera = this.#camera;
235
+ }
236
+
237
+ /**
238
+ * Creates and configures the main lights for the Babylon.js scene.
239
+ * Initializes the environment texture if needed.
240
+ * Adds a hemispheric ambient light, a directional light for shadows, a shadow generator, and a camera-attached point light.
241
+ * Sets light intensities and shadow properties for realistic rendering.
242
+ * @private
243
+ * @returns {void}
244
+ */
245
+ #createLights() {
246
+ this.#initializeEnvironmentTexture();
247
+
248
+ if (this.#scene.environmentTexture) {
249
+ return true;
250
+ }
251
+
252
+ // 1) Stronger ambient fill
253
+ this.#hemiLight = new HemisphericLight("hemiLight", new Vector3(-10, 10, -10), this.#scene);
254
+ this.#hemiLight.intensity = 0.6;
255
+
256
+ // 2) Directional light from the front-right, angled slightly down
257
+ this.#dirLight = new DirectionalLight("dirLight", new Vector3(-10, 10, -10), this.#scene);
258
+ this.#dirLight.position = new Vector3(5, 4, 5); // light is IN FRONT + ABOVE + to the RIGHT
259
+ this.#dirLight.intensity = 0.6;
260
+
261
+ // // 3) Soft shadows
262
+ this.#shadowGen = new ShadowGenerator(1024, this.#dirLight);
263
+ this.#shadowGen.useBlurExponentialShadowMap = true;
264
+ this.#shadowGen.blurKernel = 16;
265
+ this.#shadowGen.darkness = 0.5;
266
+
267
+ // 4) Camera‐attached headlight
268
+ this.#cameraLight = new PointLight("pl", this.#camera.position, this.#scene);
269
+ this.#cameraLight.parent = this.#camera;
270
+ this.#cameraLight.intensity = 0.3;
271
+ }
272
+
273
+ /**
274
+ * Initializes the environment texture for the Babylon.js scene.
275
+ * Loads an HDR texture from a predefined URI and assigns it to the scene's environmentTexture property.
276
+ * Configures gamma space, mipmaps, and intensity level for realistic lighting.
277
+ * @private
278
+ * @returns {boolean}
279
+ */
280
+ #initializeEnvironmentTexture() {
281
+ return false;
282
+ if (this.#scene.environmentTexture) {
283
+ return;
284
+ }
285
+ const hdrTextureURI = "../src/environments/noon_grass.hdr";
286
+ const hdrTexture = new HDRCubeTexture(hdrTextureURI, this.#scene, 128);
287
+ hdrTexture.gammaSpace = true;
288
+ hdrTexture._noMipmap = false;
289
+ hdrTexture.level = 2.0;
290
+ this.#scene.environmentTexture = hdrTexture;
291
+ }
292
+
293
+ /**
294
+ * Initializes the Image-Based Lighting (IBL) shadows for the Babylon.js scene.
295
+ * Creates an IBL shadow render pipeline and adds all relevant meshes and materials for shadow casting and receiving.
296
+ * Configures pipeline options for resolution, sampling, opacity, and debugging.
297
+ * Only applies if the scene has an environment texture set.
298
+ * @private
299
+ * @returns {void|false} Returns false if no environment texture is set; otherwise void.
300
+ */
301
+ #initializeIBLShadows() {
302
+ if (!this.#scene.environmentTexture) {
303
+ return false;
304
+ }
305
+
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]) {
316
+ const pipeline = new IblShadowsRenderPipeline(
317
+ "iblShadowsPipeline",
318
+ scene,
319
+ {
320
+ resolutionExp: 8, // Higher resolution for better shadow quality
321
+ sampleDirections: 4, // More sample directions for smoother shadows
322
+ ssShadowsEnabled: true,
323
+ shadowRemanence: 0.85,
324
+ triPlanarVoxelization: true,
325
+ shadowOpacity: 0.85,
326
+ },
327
+ cameras
328
+ );
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);
342
+ return pipeline;
343
+ };
344
+
345
+ let iblShadowsPipeline = createIBLShadowPipeline(this.#scene);
346
+
347
+ this.#scene.meshes.forEach((mesh) => {
348
+ if (mesh.id.startsWith("__root__") || mesh.name === "hdri") {
349
+ return false;
350
+ }
351
+ iblShadowsPipeline.addShadowCastingMesh(mesh);
352
+ iblShadowsPipeline.updateSceneBounds();
353
+ });
354
+
355
+ this.#scene.materials.forEach((material) => {
356
+ iblShadowsPipeline.addShadowReceivingMaterial(material);
357
+ });
358
+ }
359
+
360
+ /**
361
+ * Initializes shadows for the Babylon.js scene.
362
+ * @private
363
+ * @returns {void|true} Returns true if IBL shadows are initialized, otherwise void.
364
+ * @description
365
+ * If no environment texture is set, initializes IBL shadows.
366
+ * Otherwise, sets up shadow casting and receiving for all relevant meshes using the shadow generator.
367
+ */
368
+ #initializeShadows() {
369
+ if (!this.#scene.environmentTexture) {
370
+ this.#initializeIBLShadows();
371
+ return true;
372
+ }
373
+
374
+ this.#scene.meshes.forEach((mesh) => {
375
+ if (mesh.id.startsWith("__root__")) {
376
+ return false;
377
+ }
378
+ mesh.receiveShadows = true;
379
+ if (!mesh.name === "hdri") {
380
+ this.#shadowGen.addShadowCaster(mesh, true);
381
+ }
382
+ });
383
+ }
384
+
385
+ /**
386
+ * Sets the maximum number of simultaneous lights for all materials in the scene.
387
+ * Counts enabled lights and updates the maxSimultaneousLights property for each material.
388
+ * @private
389
+ * @returns {void}
390
+ */
391
+ #setMaxSimultaneousLights() {
392
+ let lightsNumber = 1; // Como mínimo una luz correspondiente a la textura de environmentTexture
393
+ this.#scene.lights.forEach((light) => {
394
+ if (light.isEnabled()) {
395
+ ++lightsNumber;
396
+ }
397
+ });
398
+ if (this.#scene.materials) {
399
+ this.#scene.materials.forEach((material) => (material.maxSimultaneousLights = lightsNumber));
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Sets up interaction handlers for the Babylon.js canvas.
405
+ * @private
406
+ * @returns {void}
407
+ */
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));
421
+ }
422
+
423
+ /**
424
+ * Disposes the Babylon.js engine and releases all associated resources.
425
+ * @private
426
+ * @returns {void}
427
+ */
428
+ #disposeEngine() {
429
+ if (!this.#engine) {
430
+ return;
431
+ }
432
+ this.#engine.dispose();
433
+ this.#engine = this.#scene = this.#camera = null;
434
+ this.#hemiLight = this.#dirLight = this.#cameraLight = null;
435
+ this.#shadowGen = null;
436
+ this.#XRExperience = null;
437
+ }
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
+
476
+ /**
477
+ * Applies material options from the configuration to the relevant meshes.
478
+ * @private
479
+ * @param {MaterialData} optionMaterial - Material option containing value, nodePrefixes, nodeNames, and state.
480
+ * @returns {boolean} True if any mesh material was set, false otherwise.
481
+ */
482
+ #setOptionsMaterial(optionMaterial) {
483
+ if (!optionMaterial || !(optionMaterial.value || (optionMaterial.isPending && optionMaterial.update.value)) || !(optionMaterial.nodePrefixes.length || optionMaterial.nodeNames.length)) {
484
+ return false;
485
+ }
486
+
487
+ const materialContainer = this.#containers.materials;
488
+ const materialName = optionMaterial.isPending && optionMaterial.update.value ? optionMaterial.update.value : optionMaterial.value;
489
+
490
+ const material = materialContainer.assetContainer?.materials.find((mat) => mat.name === materialName) || null;
491
+ if (!material) {
492
+ return false;
493
+ }
494
+
495
+ const assetContainersToProcess = [];
496
+ Object.values(this.#containers).forEach((container) => {
497
+ if (container.state.name === "materials") {
498
+ return;
499
+ }
500
+ if (container.assetContainer && (container.state.isPending || materialContainer.state.isPending || optionMaterial.isPending)) {
501
+ assetContainersToProcess.push(container.assetContainer);
502
+ }
503
+ });
504
+ if (assetContainersToProcess.length === 0) {
505
+ return false;
506
+ }
507
+
508
+ let someSetted = false;
509
+ assetContainersToProcess.forEach((assetContainer) =>
510
+ assetContainer.meshes
511
+ .filter((meshToFilter) => optionMaterial.nodePrefixes.some((prefix) => meshToFilter.name.startsWith(prefix)) || optionMaterial.nodeNames.includes(meshToFilter.name))
512
+ .forEach((mesh) => {
513
+ mesh.material = material;
514
+ someSetted = true;
515
+ })
516
+ );
517
+
518
+ if (someSetted) {
519
+ optionMaterial.setSuccess(true);
520
+ } else if (optionMaterial.isPending) {
521
+ optionMaterial.setSuccess(false);
522
+ }
523
+
524
+ return someSetted;
525
+ }
526
+
527
+ /**
528
+ * Applies all material options from the configuration to the relevant meshes.
529
+ * Iterates through each material option and applies it using #setOptionsMaterial.
530
+ * @private
531
+ * @returns {boolean} True if any material option was set, false otherwise.
532
+ */
533
+ #setOptions_Materials() {
534
+ let someSetted = false;
535
+ Object.values(this.#options.materials).forEach((material) => {
536
+ let settedMaterial = this.#setOptionsMaterial(material);
537
+ someSetted = someSetted || settedMaterial;
538
+ });
539
+ return someSetted;
540
+ }
541
+
542
+ /**
543
+ * Applies camera options from the configuration to the active camera.
544
+ * @private
545
+ * @returns {boolean} True if camera options were set successfully, false otherwise.
546
+ */
547
+ #setOptions_Camera() {
548
+ const cameraState = this.#options.camera;
549
+ const modelContainer = this.#containers.model;
550
+ const environmentContainer = this.#containers.environment;
551
+
552
+ if (!cameraState.isPending && !modelContainer.state.isPending && !environmentContainer.state.isPending) {
553
+ return false;
554
+ }
555
+
556
+ const cameraName = cameraState.isPending ? cameraState.update.value : cameraState.value;
557
+ let camera = null;
558
+ if (cameraName !== null) {
559
+ camera = modelContainer.assetContainer?.cameras.find((thisCamera) => thisCamera.name === cameraName) || environmentContainer.assetContainer?.cameras.find((thisCamera) => thisCamera.name === cameraName) || null;
560
+ } else {
561
+ camera = this.#camera;
562
+ }
563
+
564
+ if (!camera) {
565
+ // If a new camera (different from the default) was tried and not found, search for the current one
566
+ if (cameraState.isPending && cameraState.update.value !== null && cameraState.value !== null && cameraState.update.value !== cameraState.value) {
567
+ camera = modelContainer.assetContainer?.cameras.find((thisCamera) => thisCamera.name === cameraState.value) || environmentContainer.assetContainer?.cameras.find((thisCamera) => thisCamera.name === cameraState.value) || null;
568
+ }
569
+ if (camera) {
570
+ // If the current camera (different from the default) was found, use it
571
+ camera.metadata = { locked: cameraState.locked };
572
+ cameraState.setSuccess(false);
573
+ } else {
574
+ // If no camera was found, use the default camera
575
+ camera = this.#camera;
576
+ cameraState.value = null;
577
+ cameraState.locked = this.#camera.metadata.locked;
578
+ cameraState.setSuccess(false);
579
+ }
580
+ } else {
581
+ // If the requested camera was found, use it
582
+ if (cameraName !== null) {
583
+ camera.metadata = { locked: cameraState.isPending ? cameraState.update.locked : cameraState.locked };
584
+ } else {
585
+ // If it's the default camera, maintain its lock state
586
+ if (cameraState.isPending) {
587
+ cameraState.update.locked = this.#camera.metadata.locked;
588
+ } else {
589
+ cameraState.locked = this.#camera.metadata.locked;
590
+ }
591
+ }
592
+ if (cameraState.isPending) {
593
+ cameraState.setSuccess(true);
594
+ }
595
+ }
596
+ if (!cameraState.locked && cameraState.value !== null) {
597
+ camera.attachControl(this.#canvas, true);
598
+ }
599
+ this.#scene.activeCamera = camera;
600
+ return true;
601
+ }
602
+
603
+ /**
604
+ * Finds and returns the asset container object by its name.
605
+ * @private
606
+ * @param {string} name - The name of the container to find.
607
+ * @returns {Object|null} The matching container object, or null if not found.
608
+ */
609
+ #findContainerByName(name) {
610
+ return Object.values(this.#containers).find((container) => container.state.name === name) || null;
611
+ }
612
+
613
+ /**
614
+ * Caches and retrieves the parent custom element "PREF-VIEWER-3D" for efficient access.
615
+ * @private
616
+ * @returns {void}
617
+ */
618
+ #getPrefViewer3DComponent() {
619
+ if (this.#prefViewer3D === undefined) {
620
+ const grandParentElement = this.#canvas.parentElement.parentElement;
621
+ this.#prefViewer3D = grandParentElement && grandParentElement.nodeName === "PREF-VIEWER-3D" ? grandParentElement : null;
622
+ }
623
+ }
624
+
625
+ /**
626
+ * Caches and retrieves the parent custom element "PREF-VIEWER" for efficient access.
627
+ * @private
628
+ * @returns {void}
629
+ */
630
+ #getPrefViewerComponent() {
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
+ }
639
+ const rootNode = this.#prefViewer3D ? this.#prefViewer3D.getRootNode().host : null;
640
+ this.#prefViewer = rootNode && rootNode.nodeName === "PREF-VIEWER" ? rootNode : null;
641
+ }
642
+ }
643
+
644
+ /**
645
+ * Updates the visibility attributes "show-model" or "show-scene" of parent custom elements ("PREF-VIEWER-3D" and "PREF-VIEWER") based on the container name and visibility state.
646
+ * @private
647
+ * @param {string} name - The name of the container ("model" or "environment").
648
+ * @param {boolean} isVisible - True to show the container, false to hide it.
649
+ * @returns {void}
650
+ */
651
+ #updateVisibilityAttributeInComponents(name, isVisible) {
652
+ // Cache references to parent custom elements
653
+ this.#getPrefViewer3DComponent();
654
+ this.#getPrefViewerComponent();
655
+ if (name === "model") {
656
+ this.#prefViewer3D?.setAttribute("show-model", isVisible ? "true" : "false");
657
+ this.#prefViewer?.setAttribute("show-model", isVisible ? "true" : "false");
658
+ } else if (name === "environment") {
659
+ this.#prefViewer3D?.setAttribute("show-scene", isVisible ? "true" : "false");
660
+ this.#prefViewer?.setAttribute("show-scene", isVisible ? "true" : "false");
661
+ }
662
+ }
663
+
664
+ /**
665
+ * Adds the asset container to the Babylon.js scene if it should be shown and is not already visible.
666
+ * @private
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.
669
+ * @returns {boolean} True if the container was added, false otherwise.
670
+ */
671
+ #addContainer(container, updateVisibility = true) {
672
+ if (!container.assetContainer || container.state.isVisible || !container.state.mustBeShown) {
673
+ return false;
674
+ }
675
+ if (updateVisibility) {
676
+ this.#updateVisibilityAttributeInComponents(container.state.name, true);
677
+ }
678
+ container.assetContainer.addAllToScene();
679
+ container.state.visible = true;
680
+ return true;
681
+ }
682
+
683
+ /**
684
+ * Removes the asset container from the Babylon.js scene if it is currently visible.
685
+ * @private
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.
688
+ * @returns {boolean} True if the container was removed, false otherwise.
689
+ */
690
+ #removeContainer(container, updateVisibility = true) {
691
+ if (!container.assetContainer || !container.state.isVisible) {
692
+ return false;
693
+ }
694
+ if (updateVisibility) {
695
+ this.#updateVisibilityAttributeInComponents(container.state.name, false);
696
+ }
697
+ container.assetContainer.removeAllFromScene();
698
+ container.state.visible = false;
699
+ return true;
700
+ }
701
+
702
+ /**
703
+ * Replaces the asset container in the Babylon.js scene with a new one.
704
+ * @private
705
+ * @param {object} container - The container object containing asset state and metadata.
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.
708
+ */
709
+ #replaceContainer(container, newAssetContainer) {
710
+ if (container.assetContainer) {
711
+ this.#removeContainer(container, false);
712
+ container.assetContainer.dispose();
713
+ container.assetContainer = null;
714
+ }
715
+ this.#scene.getEngine().releaseEffects();
716
+ container.assetContainer = newAssetContainer;
717
+ return this.#addContainer(container, false);
718
+ }
719
+
720
+ /**
721
+ * Sets the visibility of wall and floor meshes in the model container based on the provided value or environment visibility.
722
+ * @private
723
+ * @param {boolean} [show] - Optional. True to show wall/floor meshes, false to hide. Defaults to environment visibility.
724
+ * @returns {void|false} Returns false if the model container is not available or not visible; otherwise void.
725
+ */
726
+ #setVisibilityOfWallAndFloorInModel(show) {
727
+ if (!this.#containers.model.assetContainer || !this.#containers.model.state.isVisible) {
728
+ return false;
729
+ }
730
+ show = show !== undefined ? show : this.#containers.environment.state.isVisible;
731
+ const nodePrefixes = Object.values(this.#options.materials).flatMap((material) => material.nodePrefixes);
732
+ const nodeNames = Object.values(this.#options.materials).flatMap((material) => material.nodeNames);
733
+ this.#containers.model.assetContainer.meshes.filter((meshToFilter) => nodePrefixes.some((prefix) => meshToFilter.name.startsWith(prefix)) || nodeNames.includes(meshToFilter.name)).forEach((mesh) => mesh.setEnabled(show));
734
+ }
735
+
736
+ /**
737
+ * Stops the Babylon.js render loop for the current scene.
738
+ * @private
739
+ * @returns {void}
740
+ */
741
+ #stopRender() {
742
+ this.#engine.stopRenderLoop(this.#renderLoop.bind(this));
743
+ }
744
+ /**
745
+ * Starts the Babylon.js render loop for the current scene.
746
+ * Waits until the scene is ready before beginning continuous rendering.
747
+ * @private
748
+ * @returns {Promise<void>}
749
+ */
750
+ async #startRender() {
751
+ await this.#scene.whenReadyAsync();
752
+ this.#engine.runRenderLoop(this.#renderLoop.bind(this));
753
+ }
754
+
755
+ /**
756
+ * Loads an asset container (model, environment, materials, etc.) using the provided container state.
757
+ * @private
758
+ * @param {object} container - The container object containing asset state and metadata.
759
+ * @returns {Promise<[object, AssetContainer|boolean]>} Resolves to an array with the container and the loaded asset container, or false if loading fails.
760
+ * @description
761
+ * Resolves the asset source using GLTFResolver, prepares plugin options, and loads the asset into the Babylon.js scene.
762
+ * Updates the container's cache data and returns the container along with the loaded asset container or false if loading fails.
763
+ */
764
+ async #loadAssetContainer(container) {
765
+ if (container?.state?.update?.storage === undefined || container?.state?.size === undefined || container?.state?.timeStamp === undefined) {
766
+ return [container, false];
767
+ }
768
+
769
+ if (container.state.isPending === false) {
770
+ return [container, false];
771
+ }
772
+
773
+ if (!this.#gltfResolver) {
774
+ this.#gltfResolver = new GLTFResolver();
775
+ }
776
+
777
+ let sourceData = await this.#gltfResolver.getSource(container.state.update.storage, container.state.size, container.state.timeStamp);
778
+ if (!sourceData) {
779
+ return [container, false];
780
+ }
781
+
782
+ container.state.setPendingCacheData(sourceData.size, sourceData.timeStamp);
783
+
784
+ // https://doc.babylonjs.com/typedoc/interfaces/BABYLON.LoadAssetContainerOptions
785
+ let options = {
786
+ pluginExtension: sourceData.extension,
787
+ pluginOptions: {
788
+ gltf: {
789
+ compileMaterials: true,
790
+ loadAllMaterials: true,
791
+ loadOnlyMaterials: container.state.name === "materials",
792
+ },
793
+ },
794
+ };
795
+
796
+ return [container, await LoadAssetContainerAsync(sourceData.source, this.#scene, options)];
797
+ }
798
+
799
+ /**
800
+ * Loads all asset containers (model, environment, materials, etc.) and adds them to the scene.
801
+ * @private
802
+ * @returns {Promise<{success: boolean, error: any}>} Resolves to an object indicating if loading succeeded and any error encountered.
803
+ * @description
804
+ * Waits for all containers to load in parallel, then replaces or adds them to the scene as needed.
805
+ * Applies material and camera options, sets wall/floor visibility, and initializes lights and shadows.
806
+ * Returns an object with success status and error details.
807
+ */
808
+ async #loadContainers() {
809
+ this.#stopRender();
810
+
811
+ const promiseArray = [];
812
+ Object.values(this.#containers).forEach((container) => {
813
+ promiseArray.push(this.#loadAssetContainer(container));
814
+ });
815
+
816
+ let detail = {
817
+ success: false,
818
+ error: null,
819
+ };
820
+
821
+ await Promise.allSettled(promiseArray)
822
+ .then((values) => {
823
+ if (this.#babylonJSAnimationController) {
824
+ this.#babylonJSAnimationController.dispose();
825
+ this.#babylonJSAnimationController = null;
826
+ }
827
+ values.forEach((result) => {
828
+ const container = result.value ? result.value[0] : null;
829
+ const assetContainer = result.value ? result.value[1] : null;
830
+ if (result.status === "fulfilled" && assetContainer) {
831
+ if (container.state.name === "model") {
832
+ assetContainer.lights = [];
833
+ }
834
+ this.#replaceContainer(container, assetContainer);
835
+ container.state.setSuccess(true);
836
+ } else {
837
+ if (container.assetContainer && container.state.mustBeShown !== container.state.isVisible && container.state.name === "materials") {
838
+ container.state.mustBeShown ? this.#addContainer(container) : this.#removeContainer(container);
839
+ }
840
+ container.state.setSuccess(false);
841
+ }
842
+ });
843
+
844
+ this.#setOptions_Materials();
845
+ this.#setOptions_Camera();
846
+ this.#setVisibilityOfWallAndFloorInModel();
847
+ detail.success = true;
848
+ })
849
+ .catch((error) => {
850
+ this.loaded = true;
851
+ console.error("PrefViewer: failed to load model", error);
852
+ detail.success = false;
853
+ detail.error = error;
854
+ })
855
+ .finally(async () => {
856
+ this.#setMaxSimultaneousLights();
857
+ this.#initializeShadows();
858
+ this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#scene);
859
+ this.#startRender();
860
+ });
861
+ return detail;
862
+ }
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
+
980
+ /**
981
+ * ---------------------------
982
+ * Public methods
983
+ * ---------------------------
984
+ */
985
+
986
+ /**
987
+ * Initializes the Babylon.js engine, scene, camera, lights, and interaction handlers.
988
+ * Configures Draco compression, sets up XR experience, and starts the render loop.
989
+ * Observes canvas resize events to update the engine.
990
+ * @public
991
+ * @returns {Promise<void>}
992
+ */
993
+ async enable() {
994
+ this.#configureDracoCompression();
995
+ this.#engine = new Engine(this.#canvas, true, { alpha: true });
996
+ this.#engine.disableUniformBuffers = true;
997
+ this.#scene = new Scene(this.#engine);
998
+ this.#scene.clearColor = new Color4(1, 1, 1, 1);
999
+ this.#createCamera();
1000
+ this.#createLights();
1001
+ this.#enableInteraction();
1002
+ await this.#createXRExperience();
1003
+ this.#startRender();
1004
+ this.#canvasResizeObserver = new ResizeObserver(() => this.#engine && this.#engine.resize());
1005
+ this.#canvasResizeObserver.observe(this.#canvas);
1006
+ }
1007
+
1008
+ /**
1009
+ * Disposes the Babylon.js engine and disconnects the canvas resize observer.
1010
+ * Cleans up all scene, camera, light, and XR resources.
1011
+ * @public
1012
+ * @returns {void}
1013
+ */
1014
+ disable() {
1015
+ this.#canvasResizeObserver.disconnect();
1016
+ this.#disableInteraction();
1017
+ if (this.#babylonJSAnimationController) {
1018
+ this.#babylonJSAnimationController.dispose();
1019
+ this.#babylonJSAnimationController = null;
1020
+ }
1021
+ this.#disposeEngine();
1022
+ }
1023
+
1024
+ /**
1025
+ * Loads all asset containers (model, environment, materials, etc.) and adds them to the scene.
1026
+ * Applies material and camera options, sets visibility, and initializes lights and shadows.
1027
+ * @public
1028
+ * @returns {Promise<boolean>} Resolves to true if loading succeeds, false otherwise.
1029
+ */
1030
+ async load() {
1031
+ return await this.#loadContainers();
1032
+ }
1033
+
1034
+ /**
1035
+ * Applies camera options from the configuration to the active camera.
1036
+ * Stops and restarts the render loop to apply changes.
1037
+ * @public
1038
+ * @returns {boolean} True if camera options were set successfully, false otherwise.
1039
+ */
1040
+ setCameraOptions() {
1041
+ this.#stopRender();
1042
+ const cameraOptionsSetted = this.#setOptions_Camera();
1043
+ this.#startRender();
1044
+ return cameraOptionsSetted;
1045
+ }
1046
+
1047
+ /**
1048
+ * Applies material options from the configuration to the relevant meshes.
1049
+ * Stops and restarts the render loop to apply changes.
1050
+ * @public
1051
+ * @returns {boolean} True if material options were set successfully, false otherwise.
1052
+ */
1053
+ setMaterialOptions() {
1054
+ this.#stopRender();
1055
+ const materialsOptionsSetted = this.#setOptions_Materials();
1056
+ this.#startRender();
1057
+ return materialsOptionsSetted;
1058
+ }
1059
+
1060
+ /**
1061
+ * Sets the visibility of a container (model, environment, etc.) by name.
1062
+ * Adds or removes the container from the scene and updates wall/floor visibility.
1063
+ * Restarts the render loop to apply changes.
1064
+ * @public
1065
+ * @param {string} name - The name of the container to show or hide.
1066
+ * @param {boolean} show - True to show the container, false to hide it.
1067
+ * @returns {void}
1068
+ */
1069
+ setContainerVisibility(name, show) {
1070
+ const container = this.#findContainerByName(name);
1071
+ if (!container) {
1072
+ return;
1073
+ }
1074
+ if (container.state.show === show && container.state.visible === show) {
1075
+ return;
1076
+ }
1077
+ container.state.show = show;
1078
+ this.#stopRender();
1079
+ show ? this.#addContainer(container) : this.#removeContainer(container);
1080
+ this.#setVisibilityOfWallAndFloorInModel();
1081
+ this.#startRender();
1082
+ }
1083
+
1084
+ /**
1085
+ * Downloads the current scene, model, or environment as a GLB file.
1086
+ * @public
1087
+ * @param {number} content - 0 for full scene, 1 for model, 2 for environment.
1088
+ * @returns {void}
1089
+ */
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
+ });
1115
+ }
1116
+
1117
+ /**
1118
+ * Downloads the current scene, model, or environment as a glTF ZIP file.
1119
+ * @public
1120
+ * @param {number} content - 0 for full scene, 1 for model, 2 for environment.
1121
+ * @returns {void}
1122
+ */
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);
1150
+ }
1151
+ });
1152
+ }
1153
+
1154
+ /**
1155
+ * Downloads the current scene, model, or environment as a USDZ file.
1156
+ * @public
1157
+ * @param {number} content - 0 for full scene, 1 for model, 2 for environment.
1158
+ * @returns {void}
1159
+ */
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) => {
1181
+ if (response) {
1182
+ Tools.Download(new Blob([response], { type: "model/vnd.usdz+zip" }), `${fileName}.usdz`);
1183
+ }
1184
+ });
1185
+ }
1186
+ }