@preference-sl/pref-viewer 2.12.0 → 2.13.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { ArcRotateCamera, AssetContainer, Camera, Color4, DefaultRenderingPipeline, DirectionalLight, Engine, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, LoadAssetContainerAsync, MeshBuilder, PBRMaterial, PointerEventTypes, PointLight, RenderTargetTexture, Scene, ShadowGenerator, SpotLight, SSAORenderingPipeline, Tools, Vector3, WebXRDefaultExperience, WebXRFeatureName, WebXRSessionManager, WebXRState } from "@babylonjs/core";
1
+ import { ArcRotateCamera, AssetContainer, Camera, Color4, DefaultRenderingPipeline, DirectionalLight, Engine, FreeCamera, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, LoadAssetContainerAsync, MeshBuilder, PBRMaterial, PointerEventTypes, PointLight, RenderTargetTexture, Scene, ShadowGenerator, SpotLight, SSAORenderingPipeline, Tools, UniversalCamera, Vector3, WebXRDefaultExperience, WebXRFeatureName, WebXRSessionManager, WebXRState } from "@babylonjs/core";
2
2
  import { DracoCompression } from "@babylonjs/core/Meshes/Compression/dracoCompression.js";
3
3
  import "@babylonjs/loaders";
4
4
  import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_draco_mesh_compression.js";
@@ -8,99 +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, and resumes the render loop.
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
- * - #initializeShadows(): Sets up standard or IBL shadows for meshes.
66
- * - #initializeDefaultLightShadows(): Configures soft shadows when no HDR environment exists.
67
- * - #initializeEnvironmentShadows(): Rebuilds environment-provided shadow generators.
68
- * - #setMaxSimultaneousLights(): Updates max simultaneous lights for all materials.
69
- * - #onPointerObservable(info): Handles pointer events and dispatches to pointer/mouse handlers.
70
- * - #onPointerUp(event, pickInfo): Handles pointer up events (e.g., right-click for animation menu).
71
- * - #onPointerMove(event, pickInfo): Handles pointer move events (e.g., mesh highlighting).
72
- * - #onMouseWheel(event, pickInfo): Handles mouse wheel events for camera zoom.
73
- * - #onKeyUp(event): Handles keyup events for download dialog and shortcuts.
74
- * - #enableInteraction(): Adds canvas and scene interaction event listeners.
75
- * - #disableInteraction(): Removes canvas and scene interaction event listeners.
76
- * - #disposeAnimationController(): Disposes the animation controller if it exists.
77
- * - #disposeXRExperience(): Disposes the Babylon.js WebXR experience if it exists.
78
- * - #disposeEngine(): Disposes engine and releases all resources.
79
- * - #setOptionsMaterial(optionMaterial): Applies a material option to relevant meshes.
80
- * - #setOptions_Materials(): Applies all material options from configuration.
81
- * - #setOptions_Camera(): Applies camera options from configuration.
82
- * - #setOptions_IBL(): Applies pending HDR/IBL option updates and refreshes lights.
83
- * - #findContainerByName(name): Finds a container by its name.
84
- * - #addContainer(container, updateVisibility): Adds a container to the scene and updates visibility.
85
- * - #removeContainer(container, updateVisibility): Removes a container from the scene and updates visibility.
86
- * - #replaceContainer(container, newAssetContainer): Replaces a container in the scene.
87
- * - #getPrefViewer3DComponent(): Caches and retrieves the parent PREF-VIEWER-3D element.
88
- * - #getPrefViewerComponent(): Caches and retrieves the parent PREF-VIEWER element.
89
- * - #updateVisibilityAttributeInComponents(name, isVisible): Updates parent visibility attributes.
90
- * - #setVisibilityOfWallAndFloorInModel(show): Controls wall/floor mesh visibility.
91
- * - #stopRender(): Stops the Babylon.js render loop.
92
- * - #startRender(): Starts the Babylon.js render loop.
93
- * - #loadAssetContainer(container): Loads an asset container asynchronously.
94
- * - #loadContainers(): Loads all asset containers and adds them to the scene.
95
- * - #loadCameraDepentEffects(): Re-initializes camera-bound post-processes after reloads or option changes.
96
- * - #checkModelMetadata(oldMetadata, newMetadata): Processes metadata changes after loading.
97
- * - #checkInnerFloorTranslation(oldMetadata, newMetadata): Applies inner floor Y translations from metadata.
98
- * - #translateNodeY(name, deltaY): Adjusts the Y position of a scene node.
99
- * - #addDateToName(name): Appends the current date/time to a name string.
100
- * - #downloadZip(files, name, comment, addDateInName): Generates and downloads a ZIP file.
101
- * - #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.
102
61
  */
103
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
+
104
76
  // Canvas HTML element
105
77
  #canvas = null;
106
78
 
@@ -131,12 +103,7 @@ export default class BabylonJSController {
131
103
  renderLoop: null,
132
104
  };
133
105
 
134
- #settings = {
135
- antiAliasingEnabled: true,
136
- ambientOcclusionEnabled: true,
137
- iblEnabled: true,
138
- shadowsEnabled: false,
139
- };
106
+ #settings = { ...BabylonJSController.DEFAULT_RENDER_SETTINGS };
140
107
 
141
108
  /**
142
109
  * Constructs a new BabylonJSController instance.
@@ -160,6 +127,7 @@ export default class BabylonJSController {
160
127
  };
161
128
  });
162
129
  this.#options = options;
130
+ this.#loadStoredRenderSettings();
163
131
  this.#bindHandlers();
164
132
  }
165
133
 
@@ -192,6 +160,114 @@ export default class BabylonJSController {
192
160
  };
193
161
  }
194
162
 
163
+ /**
164
+ * Merges boolean render toggles into the current configuration, clearing the environment texture when IBL turns off
165
+ * and persisting the updated state when at least one flag changed.
166
+ * @private
167
+ * @param {object} [settings={}] - Partial map of render settings (AA, SSAO, IBL, shadows, etc.).
168
+ * @returns {boolean} True when any setting changed and was saved.
169
+ */
170
+ #applyRenderSettings(settings = {}) {
171
+ if (!settings) {
172
+ return false;
173
+ }
174
+
175
+ let changed = false;
176
+ Object.keys(settings).forEach((key) => {
177
+ if (typeof settings[key] === "boolean" && this.#settings[key] !== settings[key]) {
178
+ this.#settings[key] = settings[key];
179
+ changed = true;
180
+ }
181
+ });
182
+
183
+ if (changed && settings.iblEnabled === false && this.#scene) {
184
+ this.#scene.environmentTexture = null;
185
+ }
186
+
187
+ if (changed) {
188
+ this.#saveRenderSettings();
189
+ }
190
+
191
+ return changed;
192
+ }
193
+
194
+ /**
195
+ * Restores previously persisted render toggles from localStorage, falling back to defaults when parsing fails or
196
+ * the browser environment is unavailable.
197
+ * @private
198
+ * @returns {void}
199
+ */
200
+ #loadStoredRenderSettings() {
201
+ if (typeof window === "undefined" || !window?.localStorage) {
202
+ return;
203
+ }
204
+ try {
205
+ const serialized = window.localStorage.getItem(this.#RENDER_SETTINGS_STORAGE_KEY);
206
+ if (!serialized) {
207
+ return;
208
+ }
209
+ const parsed = JSON.parse(serialized);
210
+ Object.keys(BabylonJSController.DEFAULT_RENDER_SETTINGS).forEach((key) => {
211
+ if (typeof parsed?.[key] === "boolean") {
212
+ this.#settings[key] = parsed[key];
213
+ }
214
+ });
215
+ } catch (error) {
216
+ console.warn("PrefViewer: unable to load render settings", error);
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Serializes the current render toggle object to localStorage, ignoring non-browser contexts and logging warnings
222
+ * if persistence fails.
223
+ * @private
224
+ * @returns {void}
225
+ */
226
+ #saveRenderSettings() {
227
+ if (typeof window === "undefined" || !window?.localStorage) {
228
+ return;
229
+ }
230
+ try {
231
+ window.localStorage.setItem(this.#RENDER_SETTINGS_STORAGE_KEY, JSON.stringify(this.#settings));
232
+ } catch (error) {
233
+ console.warn("PrefViewer: unable to save render settings", error);
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Walks every container state and marks it as pending using `setPendingWithCurrentStorage` so asset sources are
239
+ * re-requested on the next load cycle.
240
+ * @private
241
+ * @returns {boolean} True when at least one container flipped to pending.
242
+ */
243
+ #markContainersForReload() {
244
+ let marked = false;
245
+ Object.values(this.#containers).forEach((container) => {
246
+ if (container?.state?.setPendingWithCurrentStorage && container.state.setPendingWithCurrentStorage()) {
247
+ marked = true;
248
+ }
249
+ });
250
+ return marked;
251
+ }
252
+
253
+ /**
254
+ * Re-schedules option-driven resources (camera, materials, IBL) for application by calling their respective
255
+ * `setPendingWithCurrent`/`setPending` helpers when available.
256
+ * @private
257
+ * @returns {void}
258
+ */
259
+ #markOptionsForReload() {
260
+ if (this.#options?.camera?.setPendingWithCurrent) {
261
+ this.#options.camera.setPendingWithCurrent();
262
+ }
263
+ if (this.#options?.materials) {
264
+ Object.values(this.#options.materials).forEach((material) => material?.setPendingWithCurrent?.());
265
+ }
266
+ if (this.#options?.ibl?.setPending) {
267
+ this.#options.ibl.setPending();
268
+ }
269
+ }
270
+
195
271
  /**
196
272
  * Render loop callback for Babylon.js.
197
273
  * @private
@@ -284,7 +360,7 @@ export default class BabylonJSController {
284
360
  * @returns {void}
285
361
  */
286
362
  #createCamera() {
287
- this.#camera = new ArcRotateCamera("camera", (3 * Math.PI) / 2, Math.PI * 0.47, 10, Vector3.Zero(), this.#scene);
363
+ this.#camera = new ArcRotateCamera("defaultCamera", (3 * Math.PI) / 2, Math.PI * 0.47, 10, Vector3.Zero(), this.#scene);
288
364
  this.#camera.upperBetaLimit = Math.PI * 0.48;
289
365
  this.#camera.lowerBetaLimit = Math.PI * 0.25;
290
366
  this.#camera.lowerRadiusLimit = 5;
@@ -564,13 +640,18 @@ export default class BabylonJSController {
564
640
 
565
641
  this.#scene.meshes.forEach((mesh) => {
566
642
  const isRootMesh = mesh.id.startsWith("__root__");
567
- const isHDRIMesh = mesh.name?.toLowerCase() === "hdri";
568
- if (isRootMesh || isHDRIMesh) {
643
+ if (isRootMesh) {
569
644
  return false;
570
645
  }
571
- iblShadowsRenderPipeline.addShadowCastingMesh(mesh);
572
- iblShadowsRenderPipeline.updateSceneBounds();
573
- mesh.receiveShadows = true; // Not necessary for IBL shadows, but yes for standard shadows
646
+
647
+ const isHDRIMesh = mesh.name?.toLowerCase() === "hdri";
648
+ const extrasCastShadows = mesh.metadata?.gltf?.extras?.castShadows;
649
+ const meshGenerateShadows = typeof extrasCastShadows === "boolean" ? extrasCastShadows : isHDRIMesh ? false : true;
650
+
651
+ if (meshGenerateShadows) {
652
+ iblShadowsRenderPipeline.addShadowCastingMesh(mesh);
653
+ iblShadowsRenderPipeline.updateSceneBounds();
654
+ }
574
655
  });
575
656
 
576
657
  this.#scene.materials.forEach((material) => {
@@ -590,6 +671,29 @@ export default class BabylonJSController {
590
671
  }
591
672
  }
592
673
 
674
+ /**
675
+ * Adds the mesh to the provided shadow generator when it is eligible to cast shadows.
676
+ * Skips hidden Babylon root nodes, ignores HDRI domes, and honors the `castShadows` GLTF extra flag.
677
+ * @private
678
+ * @param {ShadowGenerator} shadowGenerator - Target generator that should receive the mesh.
679
+ * @param {AbstractMesh} mesh - Mesh to evaluate for shadow casting.
680
+ * @returns {boolean} True when the mesh is registered as a caster, otherwise false.
681
+ */
682
+ #addMeshToShadowGenerator(shadowGenerator, mesh) {
683
+ const isRootMesh = mesh.id.startsWith("__root__");
684
+ if (isRootMesh) {
685
+ return false;
686
+ }
687
+ const isHDRIMesh = mesh.name?.toLowerCase() === "hdri";
688
+ const extrasCastShadows = mesh.metadata?.gltf?.extras?.castShadows;
689
+ const meshGenerateShadows = typeof extrasCastShadows === "boolean" ? extrasCastShadows : isHDRIMesh ? false : true;
690
+ if (meshGenerateShadows) {
691
+ shadowGenerator.addShadowCaster(mesh, true);
692
+ return true;
693
+ }
694
+ return false;
695
+ }
696
+
593
697
  /**
594
698
  * Configures soft shadows for the built-in directional light used when no HDR environment is present.
595
699
  * @private
@@ -608,15 +712,7 @@ export default class BabylonJSController {
608
712
  shadowGenerator.bias = 0.0005;
609
713
  shadowGenerator.normalBias = 0.02;
610
714
  shadowGenerator.getShadowMap().refreshRate = RenderTargetTexture.REFRESHRATE_RENDER_ONCE;
611
- this.#scene.meshes.forEach((mesh) => {
612
- if (mesh.id.startsWith("__root__")) {
613
- return false;
614
- }
615
- if (mesh.name?.toLowerCase() !== "hdri") {
616
- shadowGenerator.addShadowCaster(mesh, true);
617
- }
618
- mesh.receiveShadows = true;
619
- });
715
+ this.#scene.meshes.forEach((mesh) => this.#addMeshToShadowGenerator(shadowGenerator, mesh));
620
716
  this.#shadowGen.push(shadowGenerator);
621
717
  }
622
718
 
@@ -648,18 +744,28 @@ export default class BabylonJSController {
648
744
  shadowGenerator.bias = 0.0005;
649
745
  shadowGenerator.normalBias = 0.02;
650
746
  shadowGenerator.getShadowMap().refreshRate = RenderTargetTexture.REFRESHRATE_RENDER_ONCE;
651
- this.#scene.meshes.forEach((mesh) => {
652
- if (mesh.id.startsWith("__root__")) {
653
- return false;
654
- }
655
- if (mesh.name?.toLowerCase() !== "hdri") {
656
- shadowGenerator.addShadowCaster(mesh, true);
657
- }
658
- });
747
+ this.#scene.meshes.forEach((mesh) => this.#addMeshToShadowGenerator(shadowGenerator, mesh));
659
748
  this.#shadowGen.push(shadowGenerator);
660
749
  });
661
750
  }
662
751
 
752
+ /**
753
+ * Marks every non-root mesh as shadow receiving, unless GLTF metadata explicitly disables it.
754
+ * Ensures standard shadow generators work even when extras are missing, while still honoring `receiveShadows` overrides.
755
+ * @private
756
+ * @returns {void}
757
+ */
758
+ #ensureMeshesReceiveShadows() {
759
+ this.#scene.meshes.forEach((mesh) => {
760
+ const isRootMesh = mesh.id.startsWith("__root__");
761
+ if (isRootMesh) {
762
+ return false;
763
+ }
764
+ const extrasReceiveShadows = mesh.metadata?.gltf?.extras?.receiveShadows;
765
+ mesh.receiveShadows = typeof extrasReceiveShadows === "boolean" ? extrasReceiveShadows : true; // Not necessary for IBL shadows, but yes for standard shadows
766
+ });
767
+ }
768
+
663
769
  /**
664
770
  * Initializes shadows for the Babylon.js scene.
665
771
  * @private
@@ -672,6 +778,9 @@ export default class BabylonJSController {
672
778
  if (!this.#settings.shadowsEnabled) {
673
779
  return false;
674
780
  }
781
+
782
+ this.#ensureMeshesReceiveShadows();
783
+
675
784
  if (this.#scene.environmentTexture) {
676
785
  if (this.#options.ibl.shadows) {
677
786
  if (this.#scene.environmentTexture.isReady()) {
@@ -834,12 +943,23 @@ export default class BabylonJSController {
834
943
  * @returns {void|false} Returns false if there is no active camera; otherwise, void.
835
944
  */
836
945
  #onMouseWheel(event, pickInfo) {
837
- if (!this.#scene?.activeCamera) {
946
+ const camera = this.#scene?.activeCamera;
947
+ if (!camera) {
838
948
  return false;
839
949
  }
840
- //this.#scene.activeCamera.target = pickInfo.hit ? pickInfo.pickedPoint.clone() : this.#scene.activeCamera.target;
841
- if (!this.#scene.activeCamera.metadata?.locked) {
842
- this.#scene.activeCamera.inertialRadiusOffset -= event.deltaY * this.#scene.activeCamera.wheelPrecision * 0.001;
950
+ if (!camera.metadata?.locked) {
951
+ if (camera instanceof ArcRotateCamera) {
952
+ camera.wheelPrecision = camera.wheelPrecision || 3.0;
953
+ camera.inertialRadiusOffset -= event.deltaY * camera.wheelPrecision * 0.001;
954
+ } else if (camera instanceof FreeCamera || camera instanceof UniversalCamera) {
955
+ camera.wheelPrecision = camera.wheelPrecision || 3.0;
956
+ const zoomSpeed = -event.deltaY * camera.wheelPrecision * 0.001;
957
+
958
+ const target = camera.target || Vector3.Zero();
959
+ const direction = target.subtract(camera.position).normalize();
960
+ const movementVector = direction.scale(zoomSpeed);
961
+ camera.position = camera.position.add(movementVector);
962
+ }
843
963
  }
844
964
  event.preventDefault();
845
965
  }
@@ -995,7 +1115,8 @@ export default class BabylonJSController {
995
1115
  }
996
1116
  }
997
1117
  this.#scene.activeCamera?.detachControl();
998
- if (!cameraState.locked && cameraState.value !== null) {
1118
+ camera.detachControl();
1119
+ if (!cameraState.locked) {
999
1120
  camera.attachControl(this.#canvas, true);
1000
1121
  }
1001
1122
  this.#scene.activeCamera = camera;
@@ -1210,6 +1331,9 @@ export default class BabylonJSController {
1210
1331
  compileMaterials: true,
1211
1332
  loadAllMaterials: true,
1212
1333
  loadOnlyMaterials: container.state.name === "materials",
1334
+ extensionOptions: {
1335
+ ExtrasAsMetadata: { enabled: true },
1336
+ },
1213
1337
  },
1214
1338
  },
1215
1339
  };
@@ -1288,7 +1412,6 @@ export default class BabylonJSController {
1288
1412
  this.#setMaxSimultaneousLights();
1289
1413
  this.#loadCameraDepentEffects();
1290
1414
  this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
1291
- this.#forceReflectionsInModelGlasses();
1292
1415
  this.#startRender();
1293
1416
  });
1294
1417
  return detail;
@@ -1382,28 +1505,6 @@ export default class BabylonJSController {
1382
1505
  }
1383
1506
  }
1384
1507
 
1385
- /**
1386
- * Configures realistic glass material properties for all materials whose names include "Glass".
1387
- * @private
1388
- * @param {AssetContainer} [assetContainer] - The asset container with materials to process. If undefined, uses the model container.
1389
- * @returns {void}
1390
- * @note This method assumes that glass materials are named with the substring "Glass".
1391
- */
1392
- #forceReflectionsInModelGlasses(assetContainer) {
1393
- if (assetContainer === undefined) {
1394
- assetContainer = this.#containers.model.assetContainer;
1395
- }
1396
- if (!this.#scene || !assetContainer) {
1397
- return;
1398
- }
1399
- assetContainer.materials?.forEach(material => {
1400
- if (material && material.name && material.name.includes("Glass")) {
1401
- material.metallic = 0.25;
1402
- material.environmentIntensity = 1.0;
1403
- }
1404
- });
1405
- }
1406
-
1407
1508
  /**
1408
1509
  * Translates a node along the scene's vertical (Y) axis by the provided value.
1409
1510
  * @private
@@ -1485,16 +1586,18 @@ export default class BabylonJSController {
1485
1586
  return;
1486
1587
  }
1487
1588
 
1488
- const header = "Download 3D Scene";
1589
+ const locale = this.#prefViewer.culture;
1590
+ const texts = translate("prefViewer.downloadDialog", locale ? { locale } : undefined) || {};
1591
+ const header = texts.title || "Download 3D Scene";
1489
1592
  const content = `
1490
1593
  <form slot="content" id="download-dialog-form" style="display:flex;flex-direction:column;gap:16px;">
1491
- <h4>Content</h4>
1594
+ <h4>${texts.sections?.content || "Content"}</h4>
1492
1595
  <div style="display:flex;flex-direction:row;gap:16px;margin:0 8px 0 8px;">
1493
- <label><input type="radio" name="content" value="1"> Model</label>
1494
- <label><input type="radio" name="content" value="2"> Scene</label>
1495
- <label><input type="radio" name="content" value="0" checked> Both</label>
1596
+ <label><input type="radio" name="content" value="1"> ${texts.options?.model || "Model"}</label>
1597
+ <label><input type="radio" name="content" value="2"> ${texts.options?.scene || "Scene"}</label>
1598
+ <label><input type="radio" name="content" value="0" checked> ${texts.options?.both || "Both"}</label>
1496
1599
  </div>
1497
- <h4>Format</h4>
1600
+ <h4>${texts.sections?.format || "Format"}</h4>
1498
1601
  <div style="display:flex;flex-direction:row;gap:16px;margin:0 8px 0 8px;">
1499
1602
  <label><input type="radio" name="format" value="gltf" checked> glTF (ZIP)</label>
1500
1603
  <label><input type="radio" name="format" value="glb"> GLB</label>
@@ -1502,8 +1605,8 @@ export default class BabylonJSController {
1502
1605
  </div>
1503
1606
  </form>`;
1504
1607
  const footer = `
1505
- <button type="button" class="primary" id="download-dialog-download">Download</button>
1506
- <button type="button" id="download-dialog-cancel">Cancel</button>`;
1608
+ <button type="button" class="primary" id="download-dialog-download">${texts.buttons?.download || "Download"}</button>
1609
+ <button type="button" id="download-dialog-cancel">${texts.buttons?.cancel || "Cancel"}</button>`;
1507
1610
 
1508
1611
  const dialog = this.#prefViewer.openDialog(header, content, footer);
1509
1612
 
@@ -1590,81 +1693,6 @@ export default class BabylonJSController {
1590
1693
  this.#disposeEngine();
1591
1694
  }
1592
1695
 
1593
- /**
1594
- * Loads all asset containers (model, environment, materials, etc.) and adds them to the scene.
1595
- * Applies material and camera options, sets visibility, and initializes lights and shadows.
1596
- * @public
1597
- * @returns {Promise<boolean>} Resolves to true if loading succeeds, false otherwise.
1598
- */
1599
- async load() {
1600
- return await this.#loadContainers();
1601
- }
1602
-
1603
- /**
1604
- * Applies camera options from the configuration to the active camera.
1605
- * Stops and restarts the render loop to apply changes.
1606
- * @public
1607
- * @returns {boolean} True if camera options were set successfully, false otherwise.
1608
- */
1609
- setCameraOptions() {
1610
- this.#stopRender();
1611
- const cameraOptionsSetted = this.#setOptions_Camera();
1612
- this.#loadCameraDepentEffects();
1613
- this.#startRender();
1614
- return cameraOptionsSetted;
1615
- }
1616
-
1617
- /**
1618
- * Applies material options from the configuration to the relevant meshes.
1619
- * Stops and restarts the render loop to apply changes.
1620
- * @public
1621
- * @returns {boolean} True if material options were set successfully, false otherwise.
1622
- */
1623
- setMaterialOptions() {
1624
- this.#stopRender();
1625
- const materialsOptionsSetted = this.#setOptions_Materials();
1626
- this.#startRender();
1627
- return materialsOptionsSetted;
1628
- }
1629
-
1630
- /**
1631
- * Reapplies image-based lighting configuration (HDR URL, intensity, shadow mode).
1632
- * Stops rendering, pushes pending IBL state into the scene, rebuilds camera-dependent effects, then resumes rendering.
1633
- * @public
1634
- * @returns {void}
1635
- */
1636
- setIBLOptions() {
1637
- this.#stopRender();
1638
- const IBLOptionsSetted = this.#setOptions_IBL();
1639
- this.#loadCameraDepentEffects();
1640
- this.#startRender();
1641
- return IBLOptionsSetted;
1642
- }
1643
-
1644
- /**
1645
- * Sets the visibility of a container (model, environment, etc.) by name.
1646
- * Adds or removes the container from the scene and updates wall/floor visibility.
1647
- * Restarts the render loop to apply changes.
1648
- * @public
1649
- * @param {string} name - The name of the container to show or hide.
1650
- * @param {boolean} show - True to show the container, false to hide it.
1651
- * @returns {void}
1652
- */
1653
- setContainerVisibility(name, show) {
1654
- const container = this.#findContainerByName(name);
1655
- if (!container) {
1656
- return;
1657
- }
1658
- if (container.state.show === show && container.state.visible === show) {
1659
- return;
1660
- }
1661
- container.state.show = show;
1662
- this.#stopRender();
1663
- show ? this.#addContainer(container) : this.#removeContainer(container);
1664
- this.#setVisibilityOfWallAndFloorInModel();
1665
- this.#startRender();
1666
- }
1667
-
1668
1696
  /**
1669
1697
  * Downloads the current scene, model, or environment as a GLB file.
1670
1698
  * @public
@@ -1767,4 +1795,118 @@ export default class BabylonJSController {
1767
1795
  }
1768
1796
  });
1769
1797
  }
1798
+
1799
+ /**
1800
+ * Returns a shallow copy of the current render settings so callers can read the active state without mutating the controller internals.
1801
+ * @public
1802
+ * @returns {{antiAliasingEnabled:boolean, ambientOcclusionEnabled:boolean, iblEnabled:boolean, shadowsEnabled:boolean}}
1803
+ */
1804
+ getRenderSettings() {
1805
+ return { ...this.#settings };
1806
+ }
1807
+
1808
+ /**
1809
+ * Loads all asset containers (model, environment, materials, etc.) and adds them to the scene.
1810
+ * Applies material and camera options, sets visibility, and initializes lights and shadows.
1811
+ * @public
1812
+ * @returns {Promise<boolean>} Resolves to true if loading succeeds, false otherwise.
1813
+ */
1814
+ async load() {
1815
+ return await this.#loadContainers();
1816
+ }
1817
+
1818
+ /**
1819
+ * Merges incoming render flags with the current configuration, persists them, and marks
1820
+ * all dependent loaders/options as pending when something actually changed.
1821
+ * @public
1822
+ * @param {{antiAliasingEnabled?:boolean, ambientOcclusionEnabled?:boolean, iblEnabled?:boolean, shadowsEnabled?:boolean}} settings Partial set of render toggles to apply.
1823
+ * @returns {{changed:boolean, settings:{antiAliasingEnabled:boolean, ambientOcclusionEnabled:boolean, iblEnabled:boolean, shadowsEnabled:boolean}}}
1824
+ * @description
1825
+ * Callers can inspect the `changed` flag to decide whether to trigger a reload with
1826
+ * `reloadWithCurrentSettings()` or simply reuse the returned snapshot.
1827
+ */
1828
+ scheduleRenderSettingsReload(settings = {}) {
1829
+ const changed = this.#applyRenderSettings(settings);
1830
+ if (!changed) {
1831
+ return { changed: false, settings: this.getRenderSettings() };
1832
+ }
1833
+ this.#markContainersForReload();
1834
+ this.#markOptionsForReload();
1835
+ return { changed: true, settings: this.getRenderSettings() };
1836
+ }
1837
+
1838
+ /**
1839
+ * Applies camera options from the configuration to the active camera.
1840
+ * Stops and restarts the render loop to apply changes.
1841
+ * @public
1842
+ * @returns {boolean} True if camera options were set successfully, false otherwise.
1843
+ */
1844
+ setCameraOptions() {
1845
+ this.#stopRender();
1846
+ const cameraOptionsSetted = this.#setOptions_Camera();
1847
+ this.#loadCameraDepentEffects();
1848
+ this.#startRender();
1849
+ return cameraOptionsSetted;
1850
+ }
1851
+
1852
+ /**
1853
+ * Applies material options from the configuration to the relevant meshes.
1854
+ * Stops and restarts the render loop to apply changes.
1855
+ * @public
1856
+ * @returns {boolean} True if material options were set successfully, false otherwise.
1857
+ */
1858
+ setMaterialOptions() {
1859
+ this.#stopRender();
1860
+ const materialsOptionsSetted = this.#setOptions_Materials();
1861
+ this.#startRender();
1862
+ return materialsOptionsSetted;
1863
+ }
1864
+
1865
+ /**
1866
+ * Reapplies image-based lighting configuration (HDR URL, intensity, shadow mode).
1867
+ * Stops rendering, pushes pending IBL state into the scene, rebuilds camera-dependent effects, then resumes rendering.
1868
+ * @public
1869
+ * @returns {void}
1870
+ */
1871
+ setIBLOptions() {
1872
+ this.#stopRender();
1873
+ const IBLOptionsSetted = this.#setOptions_IBL();
1874
+ this.#loadCameraDepentEffects();
1875
+ this.#startRender();
1876
+ return IBLOptionsSetted;
1877
+ }
1878
+
1879
+ /**
1880
+ * Sets the visibility of a container (model, environment, etc.) by name.
1881
+ * Adds or removes the container from the scene and updates wall/floor visibility.
1882
+ * Restarts the render loop to apply changes.
1883
+ * @public
1884
+ * @param {string} name - The name of the container to show or hide.
1885
+ * @param {boolean} show - True to show the container, false to hide it.
1886
+ * @returns {void}
1887
+ */
1888
+ setContainerVisibility(name, show) {
1889
+ const container = this.#findContainerByName(name);
1890
+ if (!container) {
1891
+ return;
1892
+ }
1893
+ if (container.state.show === show && container.state.visible === show) {
1894
+ return;
1895
+ }
1896
+ container.state.show = show;
1897
+ this.#stopRender();
1898
+ show ? this.#addContainer(container) : this.#removeContainer(container);
1899
+ this.#setVisibilityOfWallAndFloorInModel();
1900
+ this.#startRender();
1901
+ }
1902
+
1903
+ /**
1904
+ * Reloads every asset container using the latest staged render settings.
1905
+ * Intended to be called after `scheduleRenderSettingsReload()` marks data as pending.
1906
+ * @public
1907
+ * @returns {Promise<{success:boolean,error:any}>} Resolves with the same status object returned by `load()`.
1908
+ */
1909
+ async reloadWithCurrentSettings() {
1910
+ return await this.#loadContainers();
1911
+ }
1770
1912
  }