@preference-sl/pref-viewer 2.13.0-beta.0 → 2.13.0-beta.2

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.
@@ -8,101 +8,71 @@ import JSZip from "jszip";
8
8
  import GLTFResolver from "./gltf-resolver.js";
9
9
  import { MaterialData } from "./pref-viewer-3d-data.js";
10
10
  import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
11
+ import { translate } from "./localization/i18n.js";
11
12
 
12
13
  /**
13
- * BabylonJSController - Main controller for managing Babylon.js 3D scenes, assets, rendering, and user interactions in PrefViewer.
14
+ * BabylonJSController is the PrefViewer 3D engine coordinator: it bootstraps Babylon.js, manages asset containers,
15
+ * rebuilds post-process pipelines on demand, brokers XR/download interactions, and persists user render toggles so UI
16
+ * components can stay declarative. Higher layers hand over container + option state, while this class turns it into a
17
+ * fully interactive scene with deterministic reloads and exports.
14
18
  *
15
- * Summary:
16
- * Provides a high-level API for initializing, loading, displaying, and exporting 3D models and environments using Babylon.js.
17
- * Manages advanced rendering features, AR integration, asset containers, and user interaction logic.
19
+ * Overview
20
+ * - Spins up the Babylon.js engine/scene/camera stack, configures Draco loaders, and wires resize + render loops.
21
+ * - Resolves GLTF/GLB sources through GLTFResolver, loads them into `AssetContainer`s, and toggles their visibility.
22
+ * - Applies render-setting switches (AA, SSAO, IBL, shadows), persists them in localStorage, and rehydrates on startup.
23
+ * - Rebuilds DefaultRenderingPipeline, SSAO, IBL, and shadow pipelines every time the active camera or assets change.
24
+ * - Connects keyboard, pointer, wheel, hover, and XR events so menus, animation controllers, and PrefViewer attributes stay in sync.
25
+ * - Generates GLB, glTF+ZIP, or USDZ exports with timestamped names and localized dialog copy.
26
+ * - Translates metadata (inner floor offsets, cast/receive shadows, camera locks) into scene adjustments after reloads.
18
27
  *
19
- * Key Responsibilities:
20
- * - Initializes and manages the Babylon.js engine, scene, camera, lights, and asset containers.
21
- * - Handles loading, replacing, and disposing of 3D models, environments, and materials.
22
- * - Configures advanced rendering features such as Draco mesh compression, shadows, and image-based lighting (IBL).
23
- * - Integrates Babylon.js WebXR experience for augmented reality (AR) support.
24
- * - Provides methods for downloading models and scenes in GLB, glTF (ZIP), and USDZ formats.
25
- * - Manages camera and material options, container visibility, and user interactions.
26
- * - Observes canvas resize events and updates the engine accordingly.
27
- * - Applies metadata-driven scene adjustments (e.g., inner floor translation) after asset reloads.
28
- * - Designed for integration with PrefViewer and GLTFResolver.
29
- * - All resource management and rendering operations are performed asynchronously for performance.
28
+ * Runtime Flow
29
+ * 1. Instantiate with `new BabylonJSController(canvas, containers, options)`.
30
+ * 2. Call `enable()` to configure Draco, create the engine/scene, observers, and XR hooks.
31
+ * 3. Whenever PrefViewer marks containers/options pending, invoke `load()` to fetch sources and rebuild pipelines.
32
+ * 4. Use `applyRenderSettings` helpers (via menu events) to merge toggles, persist them, and trigger reloads when needed.
33
+ * 5. Respond to PrefViewer tasks by calling `showModel()/hideModel()` equivalents through container state changes.
34
+ * 6. Surface downloads through `downloadGLB/GLTF/USDZ` or the private `#openDownloadDialog()` triggered by keyboard shortcuts.
35
+ * 7. Invoke `disable()` when the element disconnects to teardown scenes, XR sessions, and listeners.
30
36
  *
31
- * Usage:
32
- * - Instantiate: const controller = new BabylonJSController(canvas, containers, options);
33
- * - Enable rendering: await controller.enable();
34
- * - Load assets: await controller.load();
35
- * - Set camera/material options: controller.setCameraOptions(), controller.setMaterialOptions();
36
- * - Control visibility: controller.setContainerVisibility(name, show);
37
- * - Download assets: controller.downloadGLB(), controller.downloadGLTF(), controller.downloadUSDZ();
38
- * - Disable rendering: controller.disable();
37
+ * Public API Highlights
38
+ * - constructor(canvas, containers, options)
39
+ * - enable() / disable()
40
+ * - load()
41
+ * - downloadGLB(content) / downloadGLTF(content) / downloadUSDZ(content)
42
+ * - getRenderSettings() / applyRenderSettings(partial)
43
+ * - setRenderSettings(settings) [via PrefViewer menu integration]
39
44
  *
40
- * Public Methods:
41
- * - constructor(canvas, containers, options): Creates the controller, wires container state, and stores runtime options.
42
- * - enable(): Boots the Babylon.js engine, scene, camera, baseline lights, XR support, and the render loop.
43
- * - disable(): Stops rendering and disposes the engine, lights, XR experience, and observers.
44
- * - load(): Reloads every pending asset container, re-applies options, and resolves with the loading summary { success, error }.
45
- * - setCameraOptions(): Applies the pending camera selection, reinstalls dependent pipelines, and restarts rendering safely.
46
- * - setMaterialOptions(): Re-applies all configured material overrides across visible containers and restarts rendering.
47
- * - setIBLOptions(): Pushes pending HDR/IBL updates, refreshes dependent effects, resumes the render loop, and reports whether anything changed.
48
- * - setContainerVisibility(name, show): Toggles model/environment containers, syncing wall/floor helpers and component attributes.
49
- * - downloadGLB(content): Exports the selected scope (scene/model/environment) into a time-stamped GLB and triggers the download.
50
- * - downloadGLTF(content): Generates a glTF + BIN + textures ZIP for the requested scope, adding metadata comments for traceability.
51
- * - downloadUSDZ(content): Builds an Apple USDZ archive for the requested scope and downloads it via blob streaming.
45
+ * Key Subsystems
46
+ * - Persistence: #applyRenderSettings, #loadStoredRenderSettings, #saveRenderSettings keep AA/SSAO/IBL/shadow flags synced.
47
+ * - Loading pipeline: #markContainersForReload, #markOptionsForReload, #loadAssetContainer, #loadContainers orchestrate deterministic reloads.
48
+ * - Visual setup: #configureDracoCompression, #createCamera, #createLights, #initializeVisualImprovements, #initializeAmbientOcclussion,
49
+ * #initializeIBLShadows, #initializeDefaultLightShadows, #initializeEnvironmentShadows, #setMaxSimultaneousLights.
50
+ * - Interaction + XR: #bindHandlers, #enableInteraction, #onPointerObservable, #onMouseWheel, #onKeyUp, #createXRExperience,
51
+ * #addStylesToARButton, #disposeXRExperience.
52
+ * - Container helpers: #addContainer, #removeContainer, #replaceContainer, #setOptions_Materials, #setOptions_Camera,
53
+ * #setOptions_IBL, #setVisibilityOfWallAndFloorInModel, #getPrefViewerComponent.
54
+ * - Metadata + download utilities: #checkModelMetadata, #checkInnerFloorTranslation, #translateNodeY, #addDateToName,
55
+ * #downloadZip, #openDownloadDialog.
52
56
  *
53
- * Private Methods (using ECMAScript private fields):
54
- * - #bindHandlers(): Pre-binds reusable event handlers to preserve stable references.
55
- * - #configureDracoCompression(): Sets up Draco mesh compression.
56
- * - #renderLoop(): Babylon.js render loop callback.
57
- * - #addStylesToARButton(): Styles AR button.
58
- * - #createXRExperience(): Initializes WebXR AR experience.
59
- * - #createCamera(): Creates and configures the main camera.
60
- * - #createLights(): Creates and configures scene lights and shadows.
61
- * - #initializeAmbientOcclussion(): Rebuilds the SSAO pipeline for the active camera.
62
- * - #initializeVisualImprovements(): Reinstalls the default rendering pipeline (MSAA/FXAA/grain).
63
- * - #initializeEnvironmentTexture(): Loads and sets the HDR environment texture.
64
- * - #initializeIBLShadows(): Sets up IBL shadow pipeline and assigns meshes/materials.
65
- * - #addMeshToShadowGenerator(shadowGenerator, mesh): Adds eligible meshes to the provided shadow generator while honoring GLTF metadata.
66
- * - #initializeShadows(): Sets up standard or IBL shadows for meshes.
67
- * - #initializeDefaultLightShadows(): Configures soft shadows when no HDR environment exists.
68
- * - #initializeEnvironmentShadows(): Rebuilds environment-provided shadow generators.
69
- * - #ensureMeshesReceiveShadows(): Ensures every non-root mesh receives shadows unless metadata opts out.
70
- * - #setMaxSimultaneousLights(): Updates max simultaneous lights for all materials.
71
- * - #onPointerObservable(info): Handles pointer events and dispatches to pointer/mouse handlers.
72
- * - #onPointerUp(event, pickInfo): Handles pointer up events (e.g., right-click for animation menu).
73
- * - #onPointerMove(event, pickInfo): Handles pointer move events (e.g., mesh highlighting).
74
- * - #onMouseWheel(event, pickInfo): Handles mouse wheel events for camera zoom.
75
- * - #onKeyUp(event): Handles keyup events for download dialog and shortcuts.
76
- * - #enableInteraction(): Adds canvas and scene interaction event listeners.
77
- * - #disableInteraction(): Removes canvas and scene interaction event listeners.
78
- * - #disposeAnimationController(): Disposes the animation controller if it exists.
79
- * - #disposeXRExperience(): Disposes the Babylon.js WebXR experience if it exists.
80
- * - #disposeEngine(): Disposes engine and releases all resources.
81
- * - #setOptionsMaterial(optionMaterial): Applies a material option to relevant meshes.
82
- * - #setOptions_Materials(): Applies all material options from configuration.
83
- * - #setOptions_Camera(): Applies camera options from configuration.
84
- * - #setOptions_IBL(): Applies pending HDR/IBL option updates and refreshes lights.
85
- * - #findContainerByName(name): Finds a container by its name.
86
- * - #addContainer(container, updateVisibility): Adds a container to the scene and updates visibility.
87
- * - #removeContainer(container, updateVisibility): Removes a container from the scene and updates visibility.
88
- * - #replaceContainer(container, newAssetContainer): Replaces a container in the scene.
89
- * - #getPrefViewer3DComponent(): Caches and retrieves the parent PREF-VIEWER-3D element.
90
- * - #getPrefViewerComponent(): Caches and retrieves the parent PREF-VIEWER element.
91
- * - #updateVisibilityAttributeInComponents(name, isVisible): Updates parent visibility attributes.
92
- * - #setVisibilityOfWallAndFloorInModel(show): Controls wall/floor mesh visibility.
93
- * - #stopRender(): Stops the Babylon.js render loop.
94
- * - #startRender(): Starts the Babylon.js render loop.
95
- * - #loadAssetContainer(container): Loads an asset container asynchronously.
96
- * - #loadContainers(): Loads all asset containers and adds them to the scene.
97
- * - #loadCameraDepentEffects(): Re-initializes camera-bound post-processes after reloads or option changes.
98
- * - #checkModelMetadata(oldMetadata, newMetadata): Processes metadata changes after loading.
99
- * - #checkInnerFloorTranslation(oldMetadata, newMetadata): Applies inner floor Y translations from metadata.
100
- * - #translateNodeY(name, deltaY): Adjusts the Y position of a scene node.
101
- * - #addDateToName(name): Appends the current date/time to a name string.
102
- * - #downloadZip(files, name, comment, addDateInName): Generates and downloads a ZIP file.
103
- * - #openDownloadDialog(): Opens the modal download dialog.
57
+ * Notes
58
+ * - Designed to be long-lived per PrefViewer instance; it caches parent components to reflect `show-model/show-scene` attributes.
59
+ * - All browser-only features guard against SSR/Node usage by checking `window` before touching localStorage or XR APIs.
60
+ * - Relies on PrefViewerMenu3D events to trigger render-setting updates, ensuring UI and persisted state never drift apart.
104
61
  */
105
62
  export default class BabylonJSController {
63
+
64
+ #RENDER_SETTINGS_STORAGE_KEY = "pref-viewer/render-settings";
65
+
66
+ // Default render settings
67
+ // Add here new render toggles that require persistence here, and remember to update the matching labels
68
+ // under translations.prefViewer.menu3d.switches in ./localization/translations.js.
69
+ static DEFAULT_RENDER_SETTINGS = {
70
+ antiAliasingEnabled: true,
71
+ ambientOcclusionEnabled: true,
72
+ iblEnabled: true,
73
+ shadowsEnabled: false,
74
+ };
75
+
106
76
  // Canvas HTML element
107
77
  #canvas = null;
108
78
 
@@ -126,19 +96,20 @@ export default class BabylonJSController {
126
96
 
127
97
  #gltfResolver = null; // GLTFResolver instance
128
98
  #babylonJSAnimationController = null; // AnimationController instance
129
-
99
+
100
+ #renderPipelines = {
101
+ default: null,
102
+ ssao: null,
103
+ iblShadows: null,
104
+ };
105
+
130
106
  #handlers = {
131
107
  onKeyUp: null,
132
108
  onPointerObservable: null,
133
109
  renderLoop: null,
134
110
  };
135
111
 
136
- #settings = {
137
- antiAliasingEnabled: true,
138
- ambientOcclusionEnabled: true,
139
- iblEnabled: true,
140
- shadowsEnabled: false,
141
- };
112
+ #settings = { ...BabylonJSController.DEFAULT_RENDER_SETTINGS };
142
113
 
143
114
  /**
144
115
  * Constructs a new BabylonJSController instance.
@@ -162,6 +133,7 @@ export default class BabylonJSController {
162
133
  };
163
134
  });
164
135
  this.#options = options;
136
+ this.#loadStoredRenderSettings();
165
137
  this.#bindHandlers();
166
138
  }
167
139
 
@@ -194,6 +166,114 @@ export default class BabylonJSController {
194
166
  };
195
167
  }
196
168
 
169
+ /**
170
+ * Merges boolean render toggles into the current configuration, clearing the environment texture when IBL turns off
171
+ * and persisting the updated state when at least one flag changed.
172
+ * @private
173
+ * @param {object} [settings={}] - Partial map of render settings (AA, SSAO, IBL, shadows, etc.).
174
+ * @returns {boolean} True when any setting changed and was saved.
175
+ */
176
+ #applyRenderSettings(settings = {}) {
177
+ if (!settings) {
178
+ return false;
179
+ }
180
+
181
+ let changed = false;
182
+ Object.keys(settings).forEach((key) => {
183
+ if (typeof settings[key] === "boolean" && this.#settings[key] !== settings[key]) {
184
+ this.#settings[key] = settings[key];
185
+ changed = true;
186
+ }
187
+ });
188
+
189
+ if (changed && settings.iblEnabled === false && this.#scene) {
190
+ this.#scene.environmentTexture = null;
191
+ }
192
+
193
+ if (changed) {
194
+ this.#saveRenderSettings();
195
+ }
196
+
197
+ return changed;
198
+ }
199
+
200
+ /**
201
+ * Restores previously persisted render toggles from localStorage, falling back to defaults when parsing fails or
202
+ * the browser environment is unavailable.
203
+ * @private
204
+ * @returns {void}
205
+ */
206
+ #loadStoredRenderSettings() {
207
+ if (typeof window === "undefined" || !window?.localStorage) {
208
+ return;
209
+ }
210
+ try {
211
+ const serialized = window.localStorage.getItem(this.#RENDER_SETTINGS_STORAGE_KEY);
212
+ if (!serialized) {
213
+ return;
214
+ }
215
+ const parsed = JSON.parse(serialized);
216
+ Object.keys(BabylonJSController.DEFAULT_RENDER_SETTINGS).forEach((key) => {
217
+ if (typeof parsed?.[key] === "boolean") {
218
+ this.#settings[key] = parsed[key];
219
+ }
220
+ });
221
+ } catch (error) {
222
+ console.warn("PrefViewer: unable to load render settings", error);
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Serializes the current render toggle object to localStorage, ignoring non-browser contexts and logging warnings
228
+ * if persistence fails.
229
+ * @private
230
+ * @returns {void}
231
+ */
232
+ #saveRenderSettings() {
233
+ if (typeof window === "undefined" || !window?.localStorage) {
234
+ return;
235
+ }
236
+ try {
237
+ window.localStorage.setItem(this.#RENDER_SETTINGS_STORAGE_KEY, JSON.stringify(this.#settings));
238
+ } catch (error) {
239
+ console.warn("PrefViewer: unable to save render settings", error);
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Walks every container state and marks it as pending using `setPendingWithCurrentStorage` so asset sources are
245
+ * re-requested on the next load cycle.
246
+ * @private
247
+ * @returns {boolean} True when at least one container flipped to pending.
248
+ */
249
+ #markContainersForReload() {
250
+ let marked = false;
251
+ Object.values(this.#containers).forEach((container) => {
252
+ if (container?.state?.setPendingWithCurrentStorage && container.state.setPendingWithCurrentStorage()) {
253
+ marked = true;
254
+ }
255
+ });
256
+ return marked;
257
+ }
258
+
259
+ /**
260
+ * Re-schedules option-driven resources (camera, materials, IBL) for application by calling their respective
261
+ * `setPendingWithCurrent`/`setPending` helpers when available.
262
+ * @private
263
+ * @returns {void}
264
+ */
265
+ #markOptionsForReload() {
266
+ if (this.#options?.camera?.setPendingWithCurrent) {
267
+ this.#options.camera.setPendingWithCurrent();
268
+ }
269
+ if (this.#options?.materials) {
270
+ Object.values(this.#options.materials).forEach((material) => material?.setPendingWithCurrent?.());
271
+ }
272
+ if (this.#options?.ibl?.setPending) {
273
+ this.#options.ibl.setPending();
274
+ }
275
+ }
276
+
197
277
  /**
198
278
  * Render loop callback for Babylon.js.
199
279
  * @private
@@ -338,6 +418,43 @@ export default class BabylonJSController {
338
418
  }
339
419
  }
340
420
 
421
+ /**
422
+ * Detaches and disposes the SSAO render pipeline from the active camera when it exists.
423
+ * Guards against missing scene resources or absent pipelines, returning false when no cleanup is needed.
424
+ * @private
425
+ * @returns {boolean} Returns true when the SSAO pipeline was disabled, false otherwise.
426
+ */
427
+ #disableAmbientOcclusion() {
428
+ const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
429
+
430
+ if (!this.#scene || !pipelineManager || this.#scene?.activeCamera === null) {
431
+ return false;
432
+ }
433
+
434
+ const supportedPipelines = pipelineManager.supportedPipelines;
435
+
436
+ if (supportedPipelines === undefined) {
437
+ return false;
438
+ }
439
+
440
+ if (!this.#renderPipelines.ssao) {
441
+ return false;
442
+ }
443
+
444
+ const pipelineName = this.#renderPipelines.ssao.name;
445
+ const ssaoPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
446
+
447
+ if (ssaoPipeline) {
448
+ pipelineManager.detachCamerasFromRenderPipeline(pipelineName, [this.#scene.activeCamera]);
449
+ ssaoPipeline.dispose();
450
+ pipelineManager.removePipeline(pipelineName);
451
+ pipelineManager.update();
452
+ this.#renderPipelines.ssao = null;
453
+ }
454
+
455
+ return true;
456
+ }
457
+
341
458
  /**
342
459
  * Rebuilds the SSAO post-process pipeline to inject screenspace ambient occlusion on the active camera.
343
460
  * Disposes previous SSAO pipelines, instantiates a tuned `SSAORenderingPipeline`, and attaches it to the
@@ -346,138 +463,166 @@ export default class BabylonJSController {
346
463
  * @returns {boolean} True if the SSAO pipeline is supported and enabled, otherwise false.
347
464
  */
348
465
  #initializeAmbientOcclussion() {
349
- if (!this.#scene || !this.#scene.postProcessRenderPipelineManager || this.#scene.activeCamera === null) {
466
+ const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
467
+ if (!this.#scene || !pipelineManager || this.#scene?.activeCamera === null) {
350
468
  return false;
351
469
  }
352
470
 
353
471
  if (!this.#settings.ambientOcclusionEnabled) {
354
472
  return false;
355
473
  }
356
-
357
- const pipelineName = "PrefViewerSSAORenderingPipeline";
358
-
359
- const supportedPipelines = this.#scene.postProcessRenderPipelineManager.supportedPipelines;
360
-
361
- if (!supportedPipelines) {
474
+ const supportedPipelines = pipelineManager.supportedPipelines;
475
+
476
+ if (supportedPipelines === undefined) {
362
477
  return false;
363
478
  }
364
-
365
- const oldSsaoPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
366
-
367
- if (oldSsaoPipeline) {
368
- oldSsaoPipeline.dispose();
369
- this.#scene.postProcessRenderPipelineManager.update();
370
- }
479
+
480
+ const pipelineName = "PrefViewerSSAORenderingPipeline";
371
481
 
372
482
  const ssaoRatio = {
373
483
  ssaoRatio: 0.5,
374
484
  combineRatio: 1.0
375
485
  };
376
486
 
377
- const ssaoPipeline = new SSAORenderingPipeline(pipelineName, this.#scene, ssaoRatio, [this.#scene.activeCamera]);
487
+ this.#renderPipelines.ssao = new SSAORenderingPipeline(pipelineName, this.#scene, ssaoRatio, [this.#scene.activeCamera]);
378
488
 
379
- if (!ssaoPipeline){
489
+ if (!this.#renderPipelines.ssao){
380
490
  return false;
381
491
  }
382
492
 
383
- if (ssaoPipeline.isSupported) {
384
- ssaoPipeline.fallOff = 0.000001;
385
- ssaoPipeline.area = 1;
386
- ssaoPipeline.radius = 0.0001;
387
- ssaoPipeline.totalStrength = 1;
388
- ssaoPipeline.base = 0.6;
493
+ if (this.#renderPipelines.ssao.isSupported) {
494
+ this.#renderPipelines.ssao.fallOff = 0.000001;
495
+ this.#renderPipelines.ssao.area = 1;
496
+ this.#renderPipelines.ssao.radius = 0.0001;
497
+ this.#renderPipelines.ssao.totalStrength = 1;
498
+ this.#renderPipelines.ssao.base = 0.6;
389
499
 
390
500
  // Configure SSAO to calculate only once instead of every frame for better performance
391
- if (ssaoPipeline._ssaoPostProcess) {
392
- ssaoPipeline._ssaoPostProcess.autoClear = false;
393
- ssaoPipeline._ssaoPostProcess.samples = 1;
501
+ if (this.#renderPipelines.ssao._ssaoPostProcess) {
502
+ this.#renderPipelines.ssao._ssaoPostProcess.autoClear = false;
503
+ this.#renderPipelines.ssao._ssaoPostProcess.samples = 1;
504
+ }
505
+ if (this.#renderPipelines.ssao._combinePostProcess) {
506
+ this.#renderPipelines.ssao._combinePostProcess.autoClear = false;
507
+ this.#renderPipelines.ssao._combinePostProcess.samples = 1;
394
508
  }
395
509
 
396
- this.#scene.postProcessRenderPipelineManager.update();
510
+ pipelineManager.update();
397
511
  return true;
398
512
  } else {
399
- ssaoPipeline.dispose();
400
- this.#scene.postProcessRenderPipelineManager.update();
513
+ this.#renderPipelines.ssao.dispose();
514
+ this.#renderPipelines.ssao = null;
515
+ pipelineManager.update();
401
516
  return false;
402
517
  }
403
518
  }
404
519
 
405
520
  /**
406
- * Rebuilds the custom default rendering pipeline (MSAA, FXAA, film grain) for the active camera.
407
- * Disposes any previous pipeline instance to avoid duplicates, then attaches a fresh
408
- * `DefaultRenderingPipeline` with tuned settings for sharper anti-aliasing and subtle grain.
521
+ * Tears down the default rendering pipeline (MSAA/FXAA/grain) for the active camera when present.
522
+ * Ensures stale pipelines detach cleanly so a fresh one can be installed on the next load.
409
523
  * @private
410
- * @returns {boolean} True when the pipeline is supported and active, otherwise false.
411
- * @see {@link https://doc.babylonjs.com/features/featuresDeepDive/postProcesses/defaultRenderingPipeline|Using the Default Rendering Pipeline | Babylon.js Documentation}
524
+ * @returns {boolean} Returns true when the pipeline was removed, false otherwise.
412
525
  */
413
- #initializeVisualImprovements() {
414
- if (!this.#scene || !this.#scene.postProcessRenderPipelineManager || this.#scene.activeCamera === null) {
526
+ #disableVisualImprovements() {
527
+ const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
528
+
529
+ if (!this.#scene || !pipelineManager || this.#scene?.activeCamera === null) {
415
530
  return false;
416
531
  }
417
532
 
418
- const pipelineName = "PrefViewerDefaultRenderingPipeline";
419
- const supportedPipelines = this.#scene.postProcessRenderPipelineManager.supportedPipelines;
533
+ const supportedPipelines = pipelineManager.supportedPipelines;
534
+
535
+ if (supportedPipelines === undefined) {
536
+ return false;
537
+ }
420
538
 
421
- if (!supportedPipelines) {
539
+ if (!this.#renderPipelines.default) {
422
540
  return false;
423
541
  }
424
542
 
425
- const oldDefaultPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
543
+ const pipelineName = this.#renderPipelines.default.name;
544
+ const defaultPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
545
+
546
+ if (defaultPipeline) {
547
+ pipelineManager.detachCamerasFromRenderPipeline(pipelineName, [this.#scene.activeCamera]);
548
+ defaultPipeline.dispose();
549
+ pipelineManager.removePipeline(pipelineName);
550
+ pipelineManager.update();
551
+ this.#renderPipelines.default = null;
552
+ }
553
+
554
+ return true;
555
+ }
426
556
 
427
- if (oldDefaultPipeline) {
428
- oldDefaultPipeline.dispose();
429
- this.#scene.postProcessRenderPipelineManager.update();
557
+ /**
558
+ * Rebuilds the custom default rendering pipeline (MSAA, FXAA, film grain) for the active camera.
559
+ * Disposes any previous pipeline instance to avoid duplicates, then attaches a fresh
560
+ * `DefaultRenderingPipeline` with tuned settings for sharper anti-aliasing and subtle grain.
561
+ * @private
562
+ * @returns {boolean} True when the pipeline is supported and active, otherwise false.
563
+ * @see {@link https://doc.babylonjs.com/features/featuresDeepDive/postProcesses/defaultRenderingPipeline|Using the Default Rendering Pipeline | Babylon.js Documentation}
564
+ */
565
+ #initializeVisualImprovements() {
566
+ const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
567
+ if (!this.#scene || !pipelineManager || this.#scene?.activeCamera === null) {
568
+ return false;
430
569
  }
431
570
 
432
571
  if (!this.#settings.antiAliasingEnabled) {
433
572
  return false;
434
573
  }
574
+ const supportedPipelines = pipelineManager.supportedPipelines;
575
+
576
+ if (supportedPipelines === undefined) {
577
+ return false;
578
+ }
579
+
580
+ const pipelineName = "PrefViewerDefaultRenderingPipeline";
435
581
 
436
- const defaultPipeline = new DefaultRenderingPipeline(pipelineName, true, this.#scene, [this.#scene.activeCamera], true);
582
+ this.#renderPipelines.default = new DefaultRenderingPipeline(pipelineName, true, this.#scene, [this.#scene.activeCamera], true);
437
583
 
438
- if (!defaultPipeline){
584
+ if (!this.#renderPipelines.default){
439
585
  return false;
440
586
  }
441
587
 
442
- if (defaultPipeline.isSupported) {
588
+ if (this.#renderPipelines.default.isSupported) {
443
589
  // MSAA - Multisample Anti-Aliasing
444
590
  const caps = this.#scene.getEngine()?.getCaps?.() || {};
445
591
  const maxSamples = typeof caps.maxMSAASamples === "number" ? caps.maxMSAASamples : 4;
446
- defaultPipeline.samples = Math.max(1, Math.min(8, maxSamples));
447
-
592
+ this.#renderPipelines.default.samples = Math.max(1, Math.min(8, maxSamples));
448
593
  // FXAA - Fast Approximate Anti-Aliasing
449
- defaultPipeline.fxaaEnabled = true;
450
- defaultPipeline.fxaa.samples = 8;
451
- defaultPipeline.fxaa.adaptScaleToCurrentViewport = true;
452
- if (defaultPipeline.fxaa.edgeThreshold !== undefined) {
453
- defaultPipeline.fxaa.edgeThreshold = 0.125;
594
+ this.#renderPipelines.default.fxaaEnabled = true;
595
+ this.#renderPipelines.default.fxaa.samples = 8;
596
+ this.#renderPipelines.default.fxaa.adaptScaleToCurrentViewport = true;
597
+ if (this.#renderPipelines.default.fxaa.edgeThreshold !== undefined) {
598
+ this.#renderPipelines.default.fxaa.edgeThreshold = 0.125;
454
599
  }
455
- if (defaultPipeline.fxaa.edgeThresholdMin !== undefined) {
456
- defaultPipeline.fxaa.edgeThresholdMin = 0.0625;
600
+ if (this.#renderPipelines.default.fxaa.edgeThresholdMin !== undefined) {
601
+ this.#renderPipelines.default.fxaa.edgeThresholdMin = 0.0625;
457
602
  }
458
- if (defaultPipeline.fxaa.subPixelQuality !== undefined) {
459
- defaultPipeline.fxaa.subPixelQuality = 0.75;
603
+ if (this.#renderPipelines.default.fxaa.subPixelQuality !== undefined) {
604
+ this.#renderPipelines.default.fxaa.subPixelQuality = 0.75;
460
605
  }
461
606
 
462
607
  // Grain
463
- defaultPipeline.grainEnabled = true;
464
- defaultPipeline.grain.adaptScaleToCurrentViewport = true;
465
- defaultPipeline.grain.animated = false;
466
- defaultPipeline.grain.intensity = 3;
608
+ this.#renderPipelines.default.grainEnabled = true;
609
+ this.#renderPipelines.default.grain.adaptScaleToCurrentViewport = true;
610
+ this.#renderPipelines.default.grain.animated = false;
611
+ this.#renderPipelines.default.grain.intensity = 3;
467
612
 
468
613
  // Configure post-processes to calculate only once instead of every frame for better performance
469
- if (defaultPipeline.fxaa?._postProcess) {
470
- defaultPipeline.fxaa._postProcess.autoClear = false;
614
+ if (this.#renderPipelines.default.fxaa?._postProcess) {
615
+ this.#renderPipelines.default.fxaa._postProcess.autoClear = false;
471
616
  }
472
- if (defaultPipeline.grain?._postProcess) {
473
- defaultPipeline.grain._postProcess.autoClear = false;
617
+ if (this.#renderPipelines.default.grain?._postProcess) {
618
+ this.#renderPipelines.default.grain._postProcess.autoClear = false;
474
619
  }
475
620
 
476
- this.#scene.postProcessRenderPipelineManager.update();
621
+ pipelineManager.update();
477
622
  return true;
478
623
  } else {
479
- defaultPipeline.dispose();
480
- this.#scene.postProcessRenderPipelineManager.update();
624
+ this.#renderPipelines.default.dispose();
625
+ pipelineManager.update();
481
626
  return false;
482
627
  }
483
628
  }
@@ -497,6 +642,43 @@ export default class BabylonJSController {
497
642
  return true;
498
643
  }
499
644
 
645
+ /**
646
+ * Removes the IBL shadow render pipeline from the active camera when present.
647
+ * Ensures voxelized shadow data is disposed so reloading environments installs a clean pipeline.
648
+ * @private
649
+ * @returns {boolean} Returns true when the pipeline was removed, false otherwise.
650
+ */
651
+ #disableIBLShadows() {
652
+ const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
653
+
654
+ if (!this.#scene || !pipelineManager || this.#scene?.activeCamera === null) {
655
+ return false;
656
+ }
657
+
658
+ const supportedPipelines = pipelineManager.supportedPipelines;
659
+
660
+ if (supportedPipelines === undefined) {
661
+ return false;
662
+ }
663
+
664
+ if (!this.#renderPipelines.iblShadows) {
665
+ return false;
666
+ }
667
+
668
+ const pipelineName = this.#renderPipelines.iblShadows.name;
669
+ const defaultPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
670
+
671
+ if (defaultPipeline) {
672
+ pipelineManager.detachCamerasFromRenderPipeline(pipelineName, [this.#scene.activeCamera]);
673
+ defaultPipeline.dispose();
674
+ pipelineManager.removePipeline(pipelineName);
675
+ pipelineManager.update();
676
+ this.#renderPipelines.iblShadows = null;
677
+ }
678
+
679
+ return true;
680
+ }
681
+
500
682
  /**
501
683
  * Initializes the Image-Based Lighting (IBL) shadows for the Babylon.js scene.
502
684
  * Creates an IBL shadow render pipeline and adds all relevant meshes and materials for shadow casting and receiving.
@@ -506,27 +688,22 @@ export default class BabylonJSController {
506
688
  * @returns {void|false} Returns false if no environment texture is set; otherwise void.
507
689
  */
508
690
  #initializeIBLShadows() {
509
- if (!this.#scene || !this.#scene.postProcessRenderPipelineManager || this.#scene.activeCamera === null) {
691
+ const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
692
+
693
+ if (!this.#scene || !pipelineManager || this.#scene?.activeCamera === null) {
510
694
  return false;
511
695
  }
512
696
 
513
- // if (!this.#scene.environmentTexture || !this.#scene.environmentTexture.isReady()) {
514
697
  if (!this.#scene.environmentTexture) {
515
698
  return false;
516
699
  }
517
- const pipelineName = "PrefViewerIblShadowsRenderPipeline";
518
- const supportedPipelines = this.#scene.postProcessRenderPipelineManager.supportedPipelines;
519
-
700
+ const supportedPipelines = pipelineManager.supportedPipelines;
701
+
520
702
  if (!supportedPipelines) {
521
703
  return false;
522
704
  }
523
-
524
- const oldIblShadowsRenderPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
525
-
526
- if (oldIblShadowsRenderPipeline) {
527
- oldIblShadowsRenderPipeline.dispose();
528
- this.#scene.postProcessRenderPipelineManager.update();
529
- }
705
+
706
+ const pipelineName = "PrefViewerIblShadowsRenderPipeline";
530
707
 
531
708
  const pipelineOptions = {
532
709
  resolutionExp: 1, // Higher resolution for better shadow quality (recomended 8)
@@ -537,13 +714,13 @@ export default class BabylonJSController {
537
714
  shadowOpacity: 0.85,
538
715
  };
539
716
 
540
- const iblShadowsRenderPipeline = new IblShadowsRenderPipeline(pipelineName, this.#scene, pipelineOptions, [this.#scene.activeCamera]);
717
+ this.#renderPipelines.iblShadows = new IblShadowsRenderPipeline(pipelineName, this.#scene, pipelineOptions, [this.#scene.activeCamera]);
541
718
 
542
- if (!iblShadowsRenderPipeline) {
719
+ if (!this.#renderPipelines.iblShadows) {
543
720
  return false;
544
721
  }
545
722
 
546
- if (iblShadowsRenderPipeline.isSupported) {
723
+ if (this.#renderPipelines.iblShadows.isSupported) {
547
724
  // Disable all debug passes for performance
548
725
  const pipelineProps = {
549
726
  allowDebugPasses: false,
@@ -557,11 +734,11 @@ export default class BabylonJSController {
557
734
  accumulationPassDebugEnabled: false,
558
735
  };
559
736
 
560
- Object.assign(iblShadowsRenderPipeline, pipelineProps);
737
+ Object.assign(this.#renderPipelines.iblShadows, pipelineProps);
561
738
 
562
- if (iblShadowsRenderPipeline._ssaoPostProcess) {
563
- iblShadowsRenderPipeline._ssaoPostProcess.autoClear = false;
564
- iblShadowsRenderPipeline._ssaoPostProcess.samples = 1;
739
+ if (this.#renderPipelines.iblShadows._ssaoPostProcess) {
740
+ this.#renderPipelines.iblShadows._ssaoPostProcess.autoClear = false;
741
+ this.#renderPipelines.iblShadows._ssaoPostProcess.samples = 1;
565
742
  }
566
743
 
567
744
  this.#scene.meshes.forEach((mesh) => {
@@ -575,8 +752,8 @@ export default class BabylonJSController {
575
752
  const meshGenerateShadows = typeof extrasCastShadows === "boolean" ? extrasCastShadows : isHDRIMesh ? false : true;
576
753
 
577
754
  if (meshGenerateShadows) {
578
- iblShadowsRenderPipeline.addShadowCastingMesh(mesh);
579
- iblShadowsRenderPipeline.updateSceneBounds();
755
+ this.#renderPipelines.iblShadows.addShadowCastingMesh(mesh);
756
+ this.#renderPipelines.iblShadows.updateSceneBounds();
580
757
  }
581
758
  });
582
759
 
@@ -584,15 +761,15 @@ export default class BabylonJSController {
584
761
  if (material instanceof PBRMaterial) {
585
762
  material.enableSpecularAntiAliasing = false;
586
763
  }
587
- iblShadowsRenderPipeline.addShadowReceivingMaterial(material);
764
+ this.#renderPipelines.iblShadows.addShadowReceivingMaterial(material);
588
765
  });
589
766
 
590
- iblShadowsRenderPipeline.updateVoxelization();
591
- this.#scene.postProcessRenderPipelineManager.update();
767
+ this.#renderPipelines.iblShadows.updateVoxelization();
768
+ pipelineManager.update();
592
769
  return true;
593
770
  } else {
594
- iblShadowsRenderPipeline.dispose();
595
- this.#scene.postProcessRenderPipelineManager.update();
771
+ this.#renderPipelines.iblShadows.dispose();
772
+ pipelineManager.update();
596
773
  return false;
597
774
  }
598
775
  }
@@ -626,7 +803,6 @@ export default class BabylonJSController {
626
803
  * @returns {void}
627
804
  */
628
805
  #initializeDefaultLightShadows() {
629
- this.#shadowGen = [];
630
806
  if (!this.#dirLight) {
631
807
  return;
632
808
  }
@@ -692,6 +868,20 @@ export default class BabylonJSController {
692
868
  });
693
869
  }
694
870
 
871
+ /**
872
+ * Disposes every active shadow generator plus the IBL shadow pipeline to avoid stale casters across reloads.
873
+ * Clears the cached `#shadowGen` array so subsequent loads can rebuild fresh generators.
874
+ * @private
875
+ * @returns {void}
876
+ */
877
+ #disableShadows() {
878
+ this.#shadowGen.forEach((shadowGenerator) => {
879
+ shadowGenerator.dispose();
880
+ });
881
+ this.#shadowGen = [];
882
+ this.#disableIBLShadows();
883
+ }
884
+
695
885
  /**
696
886
  * Initializes shadows for the Babylon.js scene.
697
887
  * @private
@@ -839,7 +1029,6 @@ export default class BabylonJSController {
839
1029
  this.#engine.dispose();
840
1030
  this.#engine = this.#scene = this.#camera = null;
841
1031
  this.#hemiLight = this.#dirLight = this.#cameraLight = null;
842
- this.#shadowGen = [];
843
1032
  }
844
1033
 
845
1034
  /**
@@ -1207,6 +1396,7 @@ export default class BabylonJSController {
1207
1396
  */
1208
1397
  #stopRender() {
1209
1398
  this.#engine.stopRenderLoop(this.#handlers.renderLoop);
1399
+ this.#unloadCameraDependentEffects();
1210
1400
  }
1211
1401
  /**
1212
1402
  * Starts the Babylon.js render loop for the current scene.
@@ -1215,6 +1405,7 @@ export default class BabylonJSController {
1215
1405
  * @returns {Promise<void>}
1216
1406
  */
1217
1407
  async #startRender() {
1408
+ this.#loadCameraDependentEffects();
1218
1409
  await this.#scene.whenReadyAsync();
1219
1410
  this.#engine.runRenderLoop(this.#handlers.renderLoop);
1220
1411
  }
@@ -1285,7 +1476,6 @@ export default class BabylonJSController {
1285
1476
  */
1286
1477
  async #loadContainers() {
1287
1478
  this.#stopRender();
1288
- this.#scene.postProcessRenderPipelineManager?.dispose();
1289
1479
 
1290
1480
  let oldModelMetadata = { ...(this.#containers.model?.state?.metadata ?? {}) };
1291
1481
  let newModelMetadata = {};
@@ -1311,6 +1501,13 @@ export default class BabylonJSController {
1311
1501
  assetContainer.lights = [];
1312
1502
  newModelMetadata = { ...(container.state.update.metadata ?? {}) };
1313
1503
  }
1504
+ if (container.state.name === "model" || container.state.name === "environment") {
1505
+ assetContainer.cameras.forEach((camera) => {
1506
+ // To avoid conflicts when reloading the model we rename the id because Babylon.js caches the camera's SSAO effect by id.
1507
+ const sufix = "_" + Date.now();
1508
+ camera.id = `${camera.id || camera.name || "camera"}${sufix}`;
1509
+ });
1510
+ }
1314
1511
  this.#replaceContainer(container, assetContainer);
1315
1512
  container.state.setSuccess(true);
1316
1513
  } else {
@@ -1336,7 +1533,6 @@ export default class BabylonJSController {
1336
1533
  .finally(async () => {
1337
1534
  this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
1338
1535
  this.#setMaxSimultaneousLights();
1339
- this.#loadCameraDepentEffects();
1340
1536
  this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
1341
1537
  this.#startRender();
1342
1538
  });
@@ -1349,12 +1545,24 @@ export default class BabylonJSController {
1349
1545
  * @private
1350
1546
  * @returns {void}
1351
1547
  */
1352
- #loadCameraDepentEffects() {
1548
+ #loadCameraDependentEffects() {
1353
1549
  this.#initializeVisualImprovements();
1354
1550
  this.#initializeAmbientOcclussion();
1355
1551
  this.#initializeShadows();
1356
1552
  }
1357
1553
 
1554
+ /**
1555
+ * Shuts down every post-process tied to the active camera before stopping the render loop.
1556
+ * Ensures AA, SSAO, and shadow resources detach cleanly so reloads rebuild from scratch.
1557
+ * @private
1558
+ * @returns {void}
1559
+ */
1560
+ #unloadCameraDependentEffects() {
1561
+ this.#disableVisualImprovements();
1562
+ this.#disableAmbientOcclusion();
1563
+ this.#disableShadows();
1564
+ }
1565
+
1358
1566
  /**
1359
1567
  * Checks and applies model metadata changes after asset loading.
1360
1568
  * @private
@@ -1512,16 +1720,18 @@ export default class BabylonJSController {
1512
1720
  return;
1513
1721
  }
1514
1722
 
1515
- const header = "Download 3D Scene";
1723
+ const locale = this.#prefViewer.culture;
1724
+ const texts = translate("prefViewer.downloadDialog", locale ? { locale } : undefined) || {};
1725
+ const header = texts.title || "Download 3D Scene";
1516
1726
  const content = `
1517
1727
  <form slot="content" id="download-dialog-form" style="display:flex;flex-direction:column;gap:16px;">
1518
- <h4>Content</h4>
1728
+ <h4>${texts.sections?.content || "Content"}</h4>
1519
1729
  <div style="display:flex;flex-direction:row;gap:16px;margin:0 8px 0 8px;">
1520
- <label><input type="radio" name="content" value="1"> Model</label>
1521
- <label><input type="radio" name="content" value="2"> Scene</label>
1522
- <label><input type="radio" name="content" value="0" checked> Both</label>
1730
+ <label><input type="radio" name="content" value="1"> ${texts.options?.model || "Model"}</label>
1731
+ <label><input type="radio" name="content" value="2"> ${texts.options?.scene || "Scene"}</label>
1732
+ <label><input type="radio" name="content" value="0" checked> ${texts.options?.both || "Both"}</label>
1523
1733
  </div>
1524
- <h4>Format</h4>
1734
+ <h4>${texts.sections?.format || "Format"}</h4>
1525
1735
  <div style="display:flex;flex-direction:row;gap:16px;margin:0 8px 0 8px;">
1526
1736
  <label><input type="radio" name="format" value="gltf" checked> glTF (ZIP)</label>
1527
1737
  <label><input type="radio" name="format" value="glb"> GLB</label>
@@ -1529,8 +1739,8 @@ export default class BabylonJSController {
1529
1739
  </div>
1530
1740
  </form>`;
1531
1741
  const footer = `
1532
- <button type="button" class="primary" id="download-dialog-download">Download</button>
1533
- <button type="button" id="download-dialog-cancel">Cancel</button>`;
1742
+ <button type="button" class="primary" id="download-dialog-download">${texts.buttons?.download || "Download"}</button>
1743
+ <button type="button" id="download-dialog-cancel">${texts.buttons?.cancel || "Cancel"}</button>`;
1534
1744
 
1535
1745
  const dialog = this.#prefViewer.openDialog(header, content, footer);
1536
1746
 
@@ -1614,84 +1824,10 @@ export default class BabylonJSController {
1614
1824
  this.#disableInteraction();
1615
1825
  this.#disposeAnimationController();
1616
1826
  this.#disposeXRExperience();
1827
+ this.#unloadCameraDependentEffects();
1617
1828
  this.#disposeEngine();
1618
1829
  }
1619
1830
 
1620
- /**
1621
- * Loads all asset containers (model, environment, materials, etc.) and adds them to the scene.
1622
- * Applies material and camera options, sets visibility, and initializes lights and shadows.
1623
- * @public
1624
- * @returns {Promise<boolean>} Resolves to true if loading succeeds, false otherwise.
1625
- */
1626
- async load() {
1627
- return await this.#loadContainers();
1628
- }
1629
-
1630
- /**
1631
- * Applies camera options from the configuration to the active camera.
1632
- * Stops and restarts the render loop to apply changes.
1633
- * @public
1634
- * @returns {boolean} True if camera options were set successfully, false otherwise.
1635
- */
1636
- setCameraOptions() {
1637
- this.#stopRender();
1638
- const cameraOptionsSetted = this.#setOptions_Camera();
1639
- this.#loadCameraDepentEffects();
1640
- this.#startRender();
1641
- return cameraOptionsSetted;
1642
- }
1643
-
1644
- /**
1645
- * Applies material options from the configuration to the relevant meshes.
1646
- * Stops and restarts the render loop to apply changes.
1647
- * @public
1648
- * @returns {boolean} True if material options were set successfully, false otherwise.
1649
- */
1650
- setMaterialOptions() {
1651
- this.#stopRender();
1652
- const materialsOptionsSetted = this.#setOptions_Materials();
1653
- this.#startRender();
1654
- return materialsOptionsSetted;
1655
- }
1656
-
1657
- /**
1658
- * Reapplies image-based lighting configuration (HDR URL, intensity, shadow mode).
1659
- * Stops rendering, pushes pending IBL state into the scene, rebuilds camera-dependent effects, then resumes rendering.
1660
- * @public
1661
- * @returns {void}
1662
- */
1663
- setIBLOptions() {
1664
- this.#stopRender();
1665
- const IBLOptionsSetted = this.#setOptions_IBL();
1666
- this.#loadCameraDepentEffects();
1667
- this.#startRender();
1668
- return IBLOptionsSetted;
1669
- }
1670
-
1671
- /**
1672
- * Sets the visibility of a container (model, environment, etc.) by name.
1673
- * Adds or removes the container from the scene and updates wall/floor visibility.
1674
- * Restarts the render loop to apply changes.
1675
- * @public
1676
- * @param {string} name - The name of the container to show or hide.
1677
- * @param {boolean} show - True to show the container, false to hide it.
1678
- * @returns {void}
1679
- */
1680
- setContainerVisibility(name, show) {
1681
- const container = this.#findContainerByName(name);
1682
- if (!container) {
1683
- return;
1684
- }
1685
- if (container.state.show === show && container.state.visible === show) {
1686
- return;
1687
- }
1688
- container.state.show = show;
1689
- this.#stopRender();
1690
- show ? this.#addContainer(container) : this.#removeContainer(container);
1691
- this.#setVisibilityOfWallAndFloorInModel();
1692
- this.#startRender();
1693
- }
1694
-
1695
1831
  /**
1696
1832
  * Downloads the current scene, model, or environment as a GLB file.
1697
1833
  * @public
@@ -1794,4 +1930,116 @@ export default class BabylonJSController {
1794
1930
  }
1795
1931
  });
1796
1932
  }
1933
+
1934
+ /**
1935
+ * Returns a shallow copy of the current render settings so callers can read the active state without mutating the controller internals.
1936
+ * @public
1937
+ * @returns {{antiAliasingEnabled:boolean, ambientOcclusionEnabled:boolean, iblEnabled:boolean, shadowsEnabled:boolean}}
1938
+ */
1939
+ getRenderSettings() {
1940
+ return { ...this.#settings };
1941
+ }
1942
+
1943
+ /**
1944
+ * Loads all asset containers (model, environment, materials, etc.) and adds them to the scene.
1945
+ * Applies material and camera options, sets visibility, and initializes lights and shadows.
1946
+ * @public
1947
+ * @returns {Promise<boolean>} Resolves to true if loading succeeds, false otherwise.
1948
+ */
1949
+ async load() {
1950
+ return await this.#loadContainers();
1951
+ }
1952
+
1953
+ /**
1954
+ * Merges incoming render flags with the current configuration, persists them, and marks
1955
+ * all dependent loaders/options as pending when something actually changed.
1956
+ * @public
1957
+ * @param {{antiAliasingEnabled?:boolean, ambientOcclusionEnabled?:boolean, iblEnabled?:boolean, shadowsEnabled?:boolean}} settings Partial set of render toggles to apply.
1958
+ * @returns {{changed:boolean, settings:{antiAliasingEnabled:boolean, ambientOcclusionEnabled:boolean, iblEnabled:boolean, shadowsEnabled:boolean}}}
1959
+ * @description
1960
+ * Callers can inspect the `changed` flag to decide whether to trigger a reload with
1961
+ * `reloadWithCurrentSettings()` or simply reuse the returned snapshot.
1962
+ */
1963
+ scheduleRenderSettingsReload(settings = {}) {
1964
+ const changed = this.#applyRenderSettings(settings);
1965
+ if (!changed) {
1966
+ return { changed: false, settings: this.getRenderSettings() };
1967
+ }
1968
+ this.#markContainersForReload();
1969
+ this.#markOptionsForReload();
1970
+ return { changed: true, settings: this.getRenderSettings() };
1971
+ }
1972
+
1973
+ /**
1974
+ * Applies camera options from the configuration to the active camera.
1975
+ * Stops and restarts the render loop to apply changes.
1976
+ * @public
1977
+ * @returns {boolean} True if camera options were set successfully, false otherwise.
1978
+ */
1979
+ setCameraOptions() {
1980
+ this.#stopRender();
1981
+ const cameraOptionsSetted = this.#setOptions_Camera();
1982
+ this.#startRender();
1983
+ return cameraOptionsSetted;
1984
+ }
1985
+
1986
+ /**
1987
+ * Applies material options from the configuration to the relevant meshes.
1988
+ * Stops and restarts the render loop to apply changes.
1989
+ * @public
1990
+ * @returns {boolean} True if material options were set successfully, false otherwise.
1991
+ */
1992
+ setMaterialOptions() {
1993
+ this.#stopRender();
1994
+ const materialsOptionsSetted = this.#setOptions_Materials();
1995
+ this.#startRender();
1996
+ return materialsOptionsSetted;
1997
+ }
1998
+
1999
+ /**
2000
+ * Reapplies image-based lighting configuration (HDR URL, intensity, shadow mode).
2001
+ * Stops rendering, pushes pending IBL state into the scene, rebuilds camera-dependent effects, then resumes rendering.
2002
+ * @public
2003
+ * @returns {void}
2004
+ */
2005
+ setIBLOptions() {
2006
+ this.#stopRender();
2007
+ const IBLOptionsSetted = this.#setOptions_IBL();
2008
+ this.#startRender();
2009
+ return IBLOptionsSetted;
2010
+ }
2011
+
2012
+ /**
2013
+ * Sets the visibility of a container (model, environment, etc.) by name.
2014
+ * Adds or removes the container from the scene and updates wall/floor visibility.
2015
+ * Restarts the render loop to apply changes.
2016
+ * @public
2017
+ * @param {string} name - The name of the container to show or hide.
2018
+ * @param {boolean} show - True to show the container, false to hide it.
2019
+ * @returns {void}
2020
+ */
2021
+ setContainerVisibility(name, show) {
2022
+ const container = this.#findContainerByName(name);
2023
+ if (!container) {
2024
+ return;
2025
+ }
2026
+ if (container.state.show === show && container.state.visible === show) {
2027
+ return;
2028
+ }
2029
+ container.state.show = show;
2030
+ this.#stopRender();
2031
+ show ? this.#addContainer(container) : this.#removeContainer(container);
2032
+ this.#setVisibilityOfWallAndFloorInModel();
2033
+ this.#startRender();
2034
+ }
2035
+
2036
+ /**
2037
+ * Reloads every asset container using the latest staged render settings.
2038
+ * Intended to be called after `scheduleRenderSettingsReload()` marks data as pending.
2039
+ * @public
2040
+ * @returns {Promise<{success:boolean,error:any}>} Resolves with the same status object returned by `load()`.
2041
+ */
2042
+ async reloadWithCurrentSettings() {
2043
+ return await this.#loadContainers();
2044
+ }
1797
2045
  }