@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.
- package/package.json +5 -5
- package/src/babylonjs-animation-controller.js +2 -2
- package/src/babylonjs-controller.js +367 -225
- package/src/index.js +2 -0
- package/src/localization/i18n.js +94 -0
- package/src/localization/translations.js +104 -0
- package/src/pref-viewer-3d-data.js +31 -2
- package/src/pref-viewer-3d.js +46 -10
- package/src/pref-viewer-menu-3d.js +557 -0
- package/src/pref-viewer-task.js +1 -0
- package/src/pref-viewer.js +532 -200
- package/src/styles.js +302 -10
|
@@ -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
|
|
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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
-
*
|
|
32
|
-
* -
|
|
33
|
-
* -
|
|
34
|
-
* -
|
|
35
|
-
* -
|
|
36
|
-
* -
|
|
37
|
-
* -
|
|
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
|
-
*
|
|
41
|
-
* -
|
|
42
|
-
* -
|
|
43
|
-
* -
|
|
44
|
-
*
|
|
45
|
-
* -
|
|
46
|
-
*
|
|
47
|
-
* -
|
|
48
|
-
*
|
|
49
|
-
* -
|
|
50
|
-
*
|
|
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
|
-
*
|
|
54
|
-
* -
|
|
55
|
-
* -
|
|
56
|
-
* -
|
|
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("
|
|
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
|
-
|
|
568
|
-
if (isRootMesh || isHDRIMesh) {
|
|
643
|
+
if (isRootMesh) {
|
|
569
644
|
return false;
|
|
570
645
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
-
|
|
946
|
+
const camera = this.#scene?.activeCamera;
|
|
947
|
+
if (!camera) {
|
|
838
948
|
return false;
|
|
839
949
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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"
|
|
1506
|
-
<button type="button" id="download-dialog-cancel"
|
|
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
|
}
|