@preference-sl/pref-viewer 2.16.2 → 2.17.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/README.md +184 -22
- package/index.d.ts +11 -1
- package/package.json +1 -1
- package/src/babylonjs-animation-controller.js +12 -163
- package/src/babylonjs-animation-opening-menu.js +8 -1
- package/src/babylonjs-animation-opening.js +21 -1
- package/src/babylonjs-animation-overlay-controller.js +145 -0
- package/src/babylonjs-camera-framing.js +171 -0
- package/src/babylonjs-camera-zoom-controller.js +272 -0
- package/src/babylonjs-controller.js +640 -378
- package/src/babylonjs-selection-controller.js +215 -0
- package/src/babylonjs-view-cube-controller.js +810 -0
- package/src/file-storage.js +13 -0
- package/src/gltf-storage.js +97 -0
- package/src/localization/i18n.js +147 -74
- package/src/localization/translations.js +18 -0
- package/src/material-utils.js +67 -60
- package/src/panzoom-controller.js +14 -1
- package/src/pref-viewer-2d.js +16 -4
- package/src/pref-viewer-3d-data.js +166 -0
- package/src/pref-viewer-3d.js +40 -5
- package/src/pref-viewer-dialog.js +7 -1
- package/src/pref-viewer-menu-3d.js +57 -37
- package/src/pref-viewer.js +155 -44
- package/src/render-math.js +130 -117
- package/src/styles.js +94 -3
- package/src/svg-resolver.js +5 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ArcRotateCamera, AssetContainer, BRDFTextureTools, Camera, Color3, Color4, DefaultRenderingPipeline, DirectionalLight, Engine, FreeCamera, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, ImageProcessingConfiguration, LoadAssetContainerAsync, Material, MeshBuilder, PBRMaterial, PointerEventTypes, PointLight, RenderTargetTexture, Scene, ShadowGenerator, SpotLight, SSAORenderingPipeline, Texture, Tools, UniversalCamera, Vector3, WebXRDefaultExperience, WebXRFeatureName, WebXRSessionManager, WebXRState, WhenTextureReadyAsync } from "@babylonjs/core";
|
|
1
|
+
import { ArcRotateCamera, AssetContainer, BRDFTextureTools, Camera, Color3, Color4, DefaultRenderingPipeline, DirectionalLight, Engine, FreeCamera, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, ImageProcessingConfiguration, LoadAssetContainerAsync, Material, Matrix, MeshBuilder, PBRMaterial, PointerEventTypes, PointLight, RenderTargetTexture, Scene, ShadowGenerator, SpotLight, SSAORenderingPipeline, Texture, Tools, UniversalCamera, Vector3, WebXRDefaultExperience, WebXRFeatureName, WebXRSessionManager, WebXRState, WhenTextureReadyAsync } 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,15 +8,15 @@ 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 BabylonJSViewCubeController from "./babylonjs-view-cube-controller.js";
|
|
12
|
+
import BabylonJSCameraFraming from "./babylonjs-camera-framing.js";
|
|
13
|
+
import BabylonJSCameraZoomController from "./babylonjs-camera-zoom-controller.js";
|
|
14
|
+
import BabylonJSSelectionController from "./babylonjs-selection-controller.js";
|
|
11
15
|
import OpeningAnimation from "./babylonjs-animation-opening.js";
|
|
12
|
-
import
|
|
16
|
+
import I18n from "./localization/i18n.js";
|
|
13
17
|
import { PrefViewerStyleVariables } from "./styles.js";
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
-
|
|
17
|
-
// Re-export so existing consumers (tests, other modules) can keep importing from this file.
|
|
18
|
-
export { getAdaptiveHardwareScalingLevel, getLightingCycleProfile } from "./render-math.js";
|
|
19
|
-
export { markPrefViewerSimplifiedTransmissionMaterial, isPrefViewerPhysicalGlassMaterial, isPrefViewerTransparentRenderOrderMaterial } from "./material-utils.js";
|
|
18
|
+
import RenderMath from "./render-math.js";
|
|
19
|
+
import MaterialUtils from "./material-utils.js";
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* BabylonJSController coordinates the PrefViewer 3D runtime: it bootstraps Babylon.js, manages asset containers,
|
|
@@ -34,11 +34,12 @@ export { markPrefViewerSimplifiedTransmissionMaterial, isPrefViewerPhysicalGlass
|
|
|
34
34
|
* - Manages keyboard/pointer/wheel handlers, animation menus, and WebXR so PrefViewer menus and DOM attributes stay
|
|
35
35
|
* synchronized with Babylon state, using sampled pointer picking and last-picked tracking to reduce raycast/highlight
|
|
36
36
|
* cost on dense scenes.
|
|
37
|
+
* - Lazily owns the ViewCube overlay and binds a model-only orbital zoom guard independently of cube visibility.
|
|
37
38
|
* - Generates GLB, glTF+ZIP, or USDZ exports with timestamped names and localized dialog copy.
|
|
38
39
|
* - Translates metadata (inner floor offsets, cast/receive shadows, camera locks) into scene adjustments after reloads.
|
|
39
40
|
*
|
|
40
41
|
* Runtime Flow
|
|
41
|
-
* 1. Instantiate with `new BabylonJSController(canvas, containers, options)`.
|
|
42
|
+
* 1. Instantiate with `new BabylonJSController(canvas, containers, options, viewCubeConfig?)`.
|
|
42
43
|
* 2. Call `enable()` to configure Draco, spin up the engine/scene, and attach interaction + XR hooks. The render loop
|
|
43
44
|
* does not start here; it starts automatically after the first `load()` completes.
|
|
44
45
|
* 3. When PrefViewer marks containers/options pending, invoke `load()` (or `reloadWithCurrentSettings()`) so the controller
|
|
@@ -52,7 +53,8 @@ export { markPrefViewerSimplifiedTransmissionMaterial, isPrefViewerPhysicalGlass
|
|
|
52
53
|
* shared GLTF resolver, which closes its internal IndexedDB handle.
|
|
53
54
|
*
|
|
54
55
|
* Public API Highlights
|
|
55
|
-
* - constructor(canvas, containers, options)
|
|
56
|
+
* - constructor(canvas, containers, options, viewCubeConfig?)
|
|
57
|
+
* - setViewCubeEnabled(enabled) / refreshViewCube()
|
|
56
58
|
* - enable() / disable()
|
|
57
59
|
* - load(force?) / reloadWithCurrentSettings()
|
|
58
60
|
* - downloadGLB(content) / downloadGLTF(content) / downloadUSDZ(content)
|
|
@@ -78,8 +80,9 @@ export { markPrefViewerSimplifiedTransmissionMaterial, isPrefViewerPhysicalGlass
|
|
|
78
80
|
* - Browser-only features guard `window`, localStorage, and XR APIs before use so the controller is safe to construct
|
|
79
81
|
* in SSR/Node contexts (though functionality activates only in browsers).
|
|
80
82
|
* - Pointer-pick lifecycle: hover/highlight raycasts are sampled on POINTERMOVE (time + distance thresholds), wheel
|
|
81
|
-
* input avoids picks
|
|
82
|
-
*
|
|
83
|
+
* input avoids interaction picks; post-input orbital zoom protection separately probes only model meshes.
|
|
84
|
+
* Right-click POINTERUP performs an on-demand interaction pick for context-menu targeting.
|
|
85
|
+
* - Runtime bookkeeping is split into mutable `#state` and shared tuning settings under `BabylonJSController.config` to keep behavior changes
|
|
83
86
|
* explicit and reduce cross-field drift.
|
|
84
87
|
* - Resize lifecycle: canvas resize notifications are throttled (with trailing execution) before calling `engine.resize()`
|
|
85
88
|
* and any queued resize callback is canceled during teardown.
|
|
@@ -87,25 +90,6 @@ export { markPrefViewerSimplifiedTransmissionMaterial, isPrefViewerPhysicalGlass
|
|
|
87
90
|
* during XR exit and engine disposal.
|
|
88
91
|
*/
|
|
89
92
|
export default class BabylonJSController {
|
|
90
|
-
#RENDER_SETTINGS_STORAGE_KEY = "pref-viewer/render-settings";
|
|
91
|
-
|
|
92
|
-
// Default render settings
|
|
93
|
-
// Add here new render toggles that require persistence here, and remember to update the matching labels
|
|
94
|
-
// under translations.prefViewer.menu3d.switches in ./localization/translations.js.
|
|
95
|
-
static DEFAULT_RENDER_SETTINGS = {
|
|
96
|
-
antiAliasingEnabled: true,
|
|
97
|
-
ambientOcclusionEnabled: true,
|
|
98
|
-
iblEnabled: true,
|
|
99
|
-
shadowsEnabled: false,
|
|
100
|
-
// Animation menu and highlight settings
|
|
101
|
-
showAnimationMenu: true,
|
|
102
|
-
highlightEnabled: true,
|
|
103
|
-
highlightColor: PrefViewerStyleVariables.colorPrimary,
|
|
104
|
-
// Lighting time of day (0=midnight, 0.5=noon, 1=midnight)
|
|
105
|
-
showLightingTimeOfDay: true,
|
|
106
|
-
lightingTimeOfDay: 0.5,
|
|
107
|
-
};
|
|
108
|
-
|
|
109
93
|
// Canvas HTML element
|
|
110
94
|
#canvas = null;
|
|
111
95
|
|
|
@@ -128,10 +112,18 @@ export default class BabylonJSController {
|
|
|
128
112
|
#hdrTexture = null; // Reusable in-memory HDR source cloned into scene.environmentTexture across reloads
|
|
129
113
|
|
|
130
114
|
#containers = {};
|
|
115
|
+
|
|
116
|
+
#viewCube = null;
|
|
117
|
+
#cameraZoom = null;
|
|
118
|
+
#viewCubeConfig = {};
|
|
119
|
+
#viewCubeEnabled = true;
|
|
120
|
+
#viewCubeSuspended = true;
|
|
121
|
+
|
|
131
122
|
#options = {};
|
|
132
123
|
|
|
133
124
|
#gltfResolver = null; // GLTFResolver instance
|
|
134
125
|
#babylonJSAnimationController = null; // AnimationController instance
|
|
126
|
+
#selectionController = null;
|
|
135
127
|
|
|
136
128
|
#renderPipelines = {
|
|
137
129
|
default: null,
|
|
@@ -142,12 +134,13 @@ export default class BabylonJSController {
|
|
|
142
134
|
#handlers = {
|
|
143
135
|
onAnimationGroupChanged: null,
|
|
144
136
|
onKeyUp: null,
|
|
137
|
+
onPointerCancel: null,
|
|
145
138
|
onPointerObservable: null,
|
|
146
139
|
onResize: null,
|
|
147
140
|
renderLoop: null,
|
|
148
141
|
};
|
|
149
142
|
|
|
150
|
-
#
|
|
143
|
+
#renderSettings = { ...BabylonJSController.config.render.defaultSettings };
|
|
151
144
|
|
|
152
145
|
// Runtime mutable state (changes while the app is running).
|
|
153
146
|
#state = {
|
|
@@ -157,6 +150,7 @@ export default class BabylonJSController {
|
|
|
157
150
|
lastMovePickX: NaN,
|
|
158
151
|
lastMovePickY: NaN,
|
|
159
152
|
lastPickedMeshId: null,
|
|
153
|
+
pointerDown: null,
|
|
160
154
|
},
|
|
161
155
|
// Render loop state balances performance with responsiveness.
|
|
162
156
|
render: {
|
|
@@ -175,38 +169,22 @@ export default class BabylonJSController {
|
|
|
175
169
|
},
|
|
176
170
|
};
|
|
177
171
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
},
|
|
192
|
-
resize: {
|
|
193
|
-
throttleMs: 50, // Cap resize work to ~20 Hz while dragging/resizing containers
|
|
194
|
-
},
|
|
195
|
-
defaultCameraFraming: {
|
|
196
|
-
margin: 1.25, // Breathing room so the model doesn't touch the viewport edges
|
|
197
|
-
minRadiusFactor: 0.25, // Zoom in on detail
|
|
172
|
+
/**
|
|
173
|
+
* Shared runtime tuning settings accessible to cooperating controller classes.
|
|
174
|
+
* All instances use this object, including the quality preset updated by hardware detection.
|
|
175
|
+
* render.defaultSettings supplies defaults for each instance's persisted render settings snapshot.
|
|
176
|
+
* Per-instance render and interaction bookkeeping remains in #state.
|
|
177
|
+
* @public
|
|
178
|
+
* @static
|
|
179
|
+
* @type {object}
|
|
180
|
+
*/
|
|
181
|
+
static config = {
|
|
182
|
+
camera: {
|
|
183
|
+
wheelDeltaPercentage: 0.015,
|
|
184
|
+
fitMarginFactor: 0.15, // Total free viewport fraction: 0.15 leaves 85% for the model
|
|
198
185
|
maxRadiusFactor: 4, // Pull back past the whole model
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
quality: {
|
|
202
|
-
// Settings for different hardware tiers: 'low', 'medium', 'high'
|
|
203
|
-
// Applied automatically based on GPU detected capabilities
|
|
204
|
-
tier: 'medium', // Default, will be updated after hardware detection
|
|
205
|
-
shadowMapSize: 1024, // Standard shadow generator resolution
|
|
206
|
-
iblShadowResolution: 1, // IBL shadow resolution exponent (0=1024, 1=2048, 2=4096)
|
|
207
|
-
iblSampleDirections: 4, // IBL shadow quality
|
|
208
|
-
shadowBlurKernel: 16, // Shadow blur amount
|
|
209
|
-
ssaoEnabled: true, // Screen Space Ambient Occlusion
|
|
186
|
+
minRadiusFactor: 0.25, // ViewCube minimum radius relative to transition distance
|
|
187
|
+
transitionDuration: 350, // ViewCube camera transition duration in milliseconds
|
|
210
188
|
},
|
|
211
189
|
look: {
|
|
212
190
|
showroom: {
|
|
@@ -238,6 +216,53 @@ export default class BabylonJSController {
|
|
|
238
216
|
backgroundDay: [1.424, 1.424, 1.424],
|
|
239
217
|
},
|
|
240
218
|
},
|
|
219
|
+
pointerPicking: {
|
|
220
|
+
movePickIntervalMs: 50, // Cap expensive scene.pick calls to ~20 Hz while moving the pointer
|
|
221
|
+
movePickMinDistancePx: 2, // Skip picks for sub-pixel jitter
|
|
222
|
+
clickMaxDistancePx: 6,
|
|
223
|
+
},
|
|
224
|
+
selection: {
|
|
225
|
+
selectableColor: [1, 1, 0],
|
|
226
|
+
selectedColor: [1, 0, 0],
|
|
227
|
+
overlayAlpha: 0.5,
|
|
228
|
+
},
|
|
229
|
+
// Hardware-adaptive quality settings (applied based on GPU capabilities)
|
|
230
|
+
quality: {
|
|
231
|
+
// Settings for different hardware tiers: 'low', 'medium', 'high'
|
|
232
|
+
// Applied automatically based on GPU detected capabilities
|
|
233
|
+
tier: 'medium', // Default, will be updated after hardware detection
|
|
234
|
+
shadowMapSize: 1024, // Standard shadow generator resolution
|
|
235
|
+
iblShadowResolution: 1, // IBL shadow resolution exponent (0=1024, 1=2048, 2=4096)
|
|
236
|
+
iblSampleDirections: 4, // IBL shadow quality
|
|
237
|
+
shadowBlurKernel: 16, // Shadow blur amount
|
|
238
|
+
ssaoEnabled: true, // Screen Space Ambient Occlusion
|
|
239
|
+
},
|
|
240
|
+
render: {
|
|
241
|
+
// Default render settings. Add new persisted render toggles here and update their translation labels.
|
|
242
|
+
defaultSettings: {
|
|
243
|
+
antiAliasingEnabled: true,
|
|
244
|
+
ambientOcclusionEnabled: true,
|
|
245
|
+
iblEnabled: true,
|
|
246
|
+
shadowsEnabled: false,
|
|
247
|
+
// Animation menu and highlight settings
|
|
248
|
+
showAnimationMenu: true,
|
|
249
|
+
highlightEnabled: true,
|
|
250
|
+
highlightColor: PrefViewerStyleVariables.colorPrimary,
|
|
251
|
+
// Lighting time of day (0=midnight, 0.5=noon, 1=midnight)
|
|
252
|
+
showLightingTimeOfDay: true,
|
|
253
|
+
lightingTimeOfDay: 0.5,
|
|
254
|
+
},
|
|
255
|
+
burstFramesBase: 2,
|
|
256
|
+
burstFramesEnhanced: 32, // When AA/SSAO/IBL is enabled, more frames are needed to reach stable output
|
|
257
|
+
interactionMs: 250,
|
|
258
|
+
animationMs: 200,
|
|
259
|
+
idleThrottleMs: 1000 / 8, // Reduced from 15 FPS to 8 FPS for idle - reduces GPU/CPU load
|
|
260
|
+
idleThresholdMs: 3000, // Time before going to true idle (no renders)
|
|
261
|
+
},
|
|
262
|
+
resize: {
|
|
263
|
+
throttleMs: 50, // Cap resize work to ~20 Hz while dragging/resizing containers
|
|
264
|
+
},
|
|
265
|
+
storageKey: "pref-viewer/render-settings",
|
|
241
266
|
};
|
|
242
267
|
|
|
243
268
|
/**
|
|
@@ -301,6 +326,7 @@ export default class BabylonJSController {
|
|
|
301
326
|
* @private
|
|
302
327
|
* @param {WebGLRenderingContext} gl - WebGL context
|
|
303
328
|
* @param {string} rendererName - GPU renderer name
|
|
329
|
+
* @param {boolean} [conservative=false] - Uses a cautious fallback when the GPU renderer is unknown.
|
|
304
330
|
* @returns {string} Hardware tier
|
|
305
331
|
*/
|
|
306
332
|
#detectDesktopTier(gl, rendererName, conservative = false) {
|
|
@@ -410,6 +436,7 @@ export default class BabylonJSController {
|
|
|
410
436
|
* Applies quality settings based on detected hardware tier.
|
|
411
437
|
* @private
|
|
412
438
|
* @param {string} tier - Hardware tier: 'low', 'medium', or 'high'
|
|
439
|
+
* @returns {void}
|
|
413
440
|
*/
|
|
414
441
|
#applyQualitySettings(tier) {
|
|
415
442
|
const qualityPresets = {
|
|
@@ -474,7 +501,7 @@ export default class BabylonJSController {
|
|
|
474
501
|
const preset = presetMap[tier] || qualityPresets.medium;
|
|
475
502
|
|
|
476
503
|
// Apply preset to config
|
|
477
|
-
|
|
504
|
+
BabylonJSController.config.quality = {
|
|
478
505
|
tier,
|
|
479
506
|
...preset,
|
|
480
507
|
// Legacy compatibility
|
|
@@ -498,14 +525,20 @@ export default class BabylonJSController {
|
|
|
498
525
|
* @param {HTMLCanvasElement|null} canvas - The canvas element to render the scene on.
|
|
499
526
|
* @param {object} containers - An object containing container states for model, environment, materials, etc.
|
|
500
527
|
* @param {object} options - Configuration options for the Babylon.js scene and assets.
|
|
528
|
+
* @param {object} [viewCubeConfig={}] - Internal overlay integration options.
|
|
529
|
+
* @param {boolean} [viewCubeConfig.enabled=true] - Desired activation; creation waits for a live scene and camera.
|
|
530
|
+
* @param {function(): string} [viewCubeConfig.getCulture] - Current UI culture for cube labels.
|
|
531
|
+
* @param {function(number, number): void} [viewCubeConfig.onLayout] - Measured cube SVG height and scene viewport height in CSS pixels; (0, 0) on disposal.
|
|
501
532
|
* @returns {BabylonJSController}
|
|
502
533
|
* @description
|
|
503
534
|
* - Assigns the provided canvas to the controller.
|
|
504
535
|
* - Initializes each container with its state and sets assetContainer to null.
|
|
505
536
|
* - Stores the provided options for later use in scene setup and asset loading.
|
|
506
537
|
*/
|
|
507
|
-
constructor(canvas = null, containers = {}, options = {}) {
|
|
538
|
+
constructor(canvas = null, containers = {}, options = {}, viewCubeConfig = {}) {
|
|
508
539
|
this.#canvas = canvas;
|
|
540
|
+
this.#viewCubeConfig = viewCubeConfig;
|
|
541
|
+
this.#viewCubeEnabled = viewCubeConfig.enabled !== false;
|
|
509
542
|
Object.keys(containers).forEach((key) => {
|
|
510
543
|
this.#containers[key] = {
|
|
511
544
|
assetContainer: null,
|
|
@@ -513,6 +546,16 @@ export default class BabylonJSController {
|
|
|
513
546
|
};
|
|
514
547
|
});
|
|
515
548
|
this.#options = options;
|
|
549
|
+
const selectionConfig = BabylonJSController.config.selection;
|
|
550
|
+
this.#selectionController = new BabylonJSSelectionController(
|
|
551
|
+
() => this.#containers.model?.assetContainer?.meshes ?? [],
|
|
552
|
+
{
|
|
553
|
+
selectableColor: new Color3(...selectionConfig.selectableColor),
|
|
554
|
+
selectedColor: new Color3(...selectionConfig.selectedColor),
|
|
555
|
+
overlayAlpha: selectionConfig.overlayAlpha,
|
|
556
|
+
requestRender: () => this.#requestRender({ frames: 1 }),
|
|
557
|
+
},
|
|
558
|
+
);
|
|
516
559
|
this.#loadStoredRenderSettings();
|
|
517
560
|
this.#bindHandlers();
|
|
518
561
|
}
|
|
@@ -525,6 +568,7 @@ export default class BabylonJSController {
|
|
|
525
568
|
#bindHandlers() {
|
|
526
569
|
this.#handlers.onAnimationGroupChanged = this.#onAnimationGroupChanged.bind(this);
|
|
527
570
|
this.#handlers.onKeyUp = this.#onKeyUp.bind(this);
|
|
571
|
+
this.#handlers.onPointerCancel = this.#onPointerCancel.bind(this);
|
|
528
572
|
this.#handlers.onPointerObservable = this.#onPointerObservable.bind(this);
|
|
529
573
|
this.#handlers.onResize = this.#onResize.bind(this);
|
|
530
574
|
this.#handlers.renderLoop = this.#renderLoop.bind(this);
|
|
@@ -563,18 +607,18 @@ export default class BabylonJSController {
|
|
|
563
607
|
let changed = false;
|
|
564
608
|
Object.keys(settings).forEach((key) => {
|
|
565
609
|
// Handle boolean settings
|
|
566
|
-
if (typeof settings[key] === "boolean" && this.#
|
|
567
|
-
this.#
|
|
610
|
+
if (typeof settings[key] === "boolean" && this.#renderSettings[key] !== settings[key]) {
|
|
611
|
+
this.#renderSettings[key] = settings[key];
|
|
568
612
|
changed = true;
|
|
569
613
|
}
|
|
570
614
|
// Handle string settings (like illuminationColor)
|
|
571
|
-
if (typeof settings[key] === "string" && this.#
|
|
572
|
-
this.#
|
|
615
|
+
if (typeof settings[key] === "string" && this.#renderSettings[key] !== settings[key]) {
|
|
616
|
+
this.#renderSettings[key] = settings[key];
|
|
573
617
|
changed = true;
|
|
574
618
|
}
|
|
575
619
|
// Handle numeric settings such as lightingTimeOfDay
|
|
576
|
-
if (typeof settings[key] === "number" && Number.isFinite(settings[key]) && this.#
|
|
577
|
-
this.#
|
|
620
|
+
if (typeof settings[key] === "number" && Number.isFinite(settings[key]) && this.#renderSettings[key] !== settings[key]) {
|
|
621
|
+
this.#renderSettings[key] = settings[key];
|
|
578
622
|
changed = true;
|
|
579
623
|
}
|
|
580
624
|
});
|
|
@@ -597,23 +641,23 @@ export default class BabylonJSController {
|
|
|
597
641
|
return;
|
|
598
642
|
}
|
|
599
643
|
try {
|
|
600
|
-
const serialized = window.localStorage.getItem(
|
|
644
|
+
const serialized = window.localStorage.getItem(BabylonJSController.config.storageKey);
|
|
601
645
|
if (!serialized) {
|
|
602
646
|
return;
|
|
603
647
|
}
|
|
604
648
|
const parsed = JSON.parse(serialized);
|
|
605
|
-
Object.keys(BabylonJSController.
|
|
649
|
+
Object.keys(BabylonJSController.config.render.defaultSettings).forEach((key) => {
|
|
606
650
|
// Handle boolean settings
|
|
607
651
|
if (typeof parsed?.[key] === "boolean") {
|
|
608
|
-
this.#
|
|
652
|
+
this.#renderSettings[key] = parsed[key];
|
|
609
653
|
}
|
|
610
654
|
// Handle string settings (like illuminationColor)
|
|
611
655
|
if (typeof parsed?.[key] === "string") {
|
|
612
|
-
this.#
|
|
656
|
+
this.#renderSettings[key] = parsed[key];
|
|
613
657
|
}
|
|
614
658
|
// Handle numeric settings such as lightingTimeOfDay
|
|
615
659
|
if (typeof parsed?.[key] === "number" && Number.isFinite(parsed[key])) {
|
|
616
|
-
this.#
|
|
660
|
+
this.#renderSettings[key] = parsed[key];
|
|
617
661
|
}
|
|
618
662
|
});
|
|
619
663
|
} catch (error) {
|
|
@@ -632,7 +676,7 @@ export default class BabylonJSController {
|
|
|
632
676
|
return;
|
|
633
677
|
}
|
|
634
678
|
try {
|
|
635
|
-
window.localStorage.setItem(
|
|
679
|
+
window.localStorage.setItem(BabylonJSController.config.storageKey, JSON.stringify(this.#renderSettings));
|
|
636
680
|
} catch (error) {
|
|
637
681
|
console.warn("PrefViewer: unable to store render settings", error);
|
|
638
682
|
}
|
|
@@ -687,10 +731,15 @@ export default class BabylonJSController {
|
|
|
687
731
|
|
|
688
732
|
const timeoutMs = 2000;
|
|
689
733
|
const startedAt = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
690
|
-
this.#requestRender({ frames:
|
|
734
|
+
this.#requestRender({ frames: BabylonJSController.config.render.burstFramesEnhanced, continuousMs: timeoutMs });
|
|
691
735
|
|
|
692
736
|
try {
|
|
693
737
|
await new Promise((resolve) => {
|
|
738
|
+
/**
|
|
739
|
+
* Polls BRDF readiness until ready, disposed or the preparation timeout elapses.
|
|
740
|
+
* @private
|
|
741
|
+
* @returns {void}
|
|
742
|
+
*/
|
|
694
743
|
const tick = () => {
|
|
695
744
|
if (!this.#scene || !this.#engine || brdfTexture.isDisposed?.()) {
|
|
696
745
|
resolve(false);
|
|
@@ -844,7 +893,7 @@ export default class BabylonJSController {
|
|
|
844
893
|
|
|
845
894
|
/**
|
|
846
895
|
* Evaluates whether the renderer should stay in continuous mode.
|
|
847
|
-
* XR
|
|
896
|
+
* Pending ViewCube transitions and active XR force continuous rendering; animation/camera motion extends the
|
|
848
897
|
* continuous deadline window to avoid abrupt stop-start behavior.
|
|
849
898
|
* OPTIMIZATION: Added true idle mode - stops rendering completely after idleThresholdMs of inactivity.
|
|
850
899
|
* @private
|
|
@@ -852,6 +901,9 @@ export default class BabylonJSController {
|
|
|
852
901
|
* @returns {boolean} True when continuous rendering should remain active.
|
|
853
902
|
*/
|
|
854
903
|
#shouldRenderContinuously(now) {
|
|
904
|
+
if (this.#viewCube?.isAnimating) {
|
|
905
|
+
return true;
|
|
906
|
+
}
|
|
855
907
|
const inXR = this.#XRExperience?.baseExperience?.state === WebXRState.IN_XR;
|
|
856
908
|
if (inXR) {
|
|
857
909
|
this.#state.render.isIdle = false;
|
|
@@ -863,19 +915,19 @@ export default class BabylonJSController {
|
|
|
863
915
|
const cameraInMotion = this.#isCameraInMotion();
|
|
864
916
|
|
|
865
917
|
if (animationRunning) {
|
|
866
|
-
this.#state.render.continuousUntil = Math.max(this.#state.render.continuousUntil, now +
|
|
918
|
+
this.#state.render.continuousUntil = Math.max(this.#state.render.continuousUntil, now + BabylonJSController.config.render.animationMs);
|
|
867
919
|
this.#state.render.isIdle = false;
|
|
868
920
|
this.#state.render.lastActivityAt = now;
|
|
869
921
|
}
|
|
870
922
|
if (cameraInMotion) {
|
|
871
|
-
this.#state.render.continuousUntil = Math.max(this.#state.render.continuousUntil, now +
|
|
923
|
+
this.#state.render.continuousUntil = Math.max(this.#state.render.continuousUntil, now + BabylonJSController.config.render.interactionMs);
|
|
872
924
|
this.#state.render.isIdle = false;
|
|
873
925
|
this.#state.render.lastActivityAt = now;
|
|
874
926
|
}
|
|
875
927
|
|
|
876
928
|
// OPTIMIZATION: True idle mode - if no activity for idleThresholdMs, stop rendering completely
|
|
877
929
|
const timeSinceActivity = now - this.#state.render.lastActivityAt;
|
|
878
|
-
if (timeSinceActivity >
|
|
930
|
+
if (timeSinceActivity > BabylonJSController.config.render.idleThresholdMs) {
|
|
879
931
|
this.#state.render.isIdle = true;
|
|
880
932
|
}
|
|
881
933
|
|
|
@@ -891,7 +943,8 @@ export default class BabylonJSController {
|
|
|
891
943
|
|
|
892
944
|
/**
|
|
893
945
|
* Render loop callback for Babylon.js.
|
|
894
|
-
* Runs
|
|
946
|
+
* Runs while scene state is dirty, interactive motion/animations are active, or XR/ViewCube navigation requires it.
|
|
947
|
+
* Suspends dynamic zoom constraints during cube framing, advances its transition, renders and synchronizes its indicator.
|
|
895
948
|
* It self-stops when the scene becomes idle (true idle mode - no renders at all).
|
|
896
949
|
* OPTIMIZATION: Added true idle mode that completely stops rendering after idleThresholdMs.
|
|
897
950
|
* @private
|
|
@@ -918,11 +971,16 @@ export default class BabylonJSController {
|
|
|
918
971
|
return;
|
|
919
972
|
}
|
|
920
973
|
|
|
921
|
-
if (!continuous && this.#state.render.lastRenderAt > 0 && now - this.#state.render.lastRenderAt <
|
|
974
|
+
if (!continuous && this.#state.render.lastRenderAt > 0 && now - this.#state.render.lastRenderAt < BabylonJSController.config.render.idleThrottleMs) {
|
|
922
975
|
return;
|
|
923
976
|
}
|
|
924
977
|
|
|
978
|
+
if (this.#viewCube?.isAnimating) {
|
|
979
|
+
this.#cameraZoom?.update();
|
|
980
|
+
}
|
|
981
|
+
this.#viewCube?.tick(now);
|
|
925
982
|
this.#scene.render();
|
|
983
|
+
this.#viewCube?.sync();
|
|
926
984
|
this.#state.render.lastRenderAt = now;
|
|
927
985
|
|
|
928
986
|
if (this.#state.render.dirtyFrames > 0) {
|
|
@@ -1005,15 +1063,20 @@ export default class BabylonJSController {
|
|
|
1005
1063
|
}
|
|
1006
1064
|
|
|
1007
1065
|
/**
|
|
1008
|
-
* Creates
|
|
1066
|
+
* Creates the default ArcRotateCamera with unrestricted beta, upright orbit and proportional native wheel zoom.
|
|
1067
|
+
* Starts at the exact Inner orientation (+Z, alpha/beta pi/2), matching the ViewCube before loading.
|
|
1068
|
+
* Starts without a fixed lower radius; the independent model zoom guard supplies the dynamic floor.
|
|
1069
|
+
* Imported-camera configuration is applied separately and is not reset here.
|
|
1009
1070
|
* @private
|
|
1010
1071
|
* @returns {void}
|
|
1011
1072
|
*/
|
|
1012
1073
|
#createCamera() {
|
|
1013
|
-
this.#camera = new ArcRotateCamera("defaultCamera",
|
|
1014
|
-
this.#camera.upperBetaLimit =
|
|
1015
|
-
this.#camera.lowerBetaLimit =
|
|
1016
|
-
this.#camera.
|
|
1074
|
+
this.#camera = new ArcRotateCamera("defaultCamera", Math.PI / 2, Math.PI / 2, 10, Vector3.Zero(), this.#scene);
|
|
1075
|
+
this.#camera.upperBetaLimit = null;
|
|
1076
|
+
this.#camera.lowerBetaLimit = null;
|
|
1077
|
+
this.#camera.allowUpsideDown = false;
|
|
1078
|
+
this.#camera.wheelDeltaPercentage = BabylonJSController.config.camera.wheelDeltaPercentage;
|
|
1079
|
+
this.#camera.lowerRadiusLimit = null;
|
|
1017
1080
|
this.#camera.upperRadiusLimit = 20;
|
|
1018
1081
|
this.#camera.metadata = { locked: false };
|
|
1019
1082
|
this.#applyShowroomCameraFraming(this.#camera);
|
|
@@ -1040,7 +1103,7 @@ export default class BabylonJSController {
|
|
|
1040
1103
|
|
|
1041
1104
|
let lightsChanged = false;
|
|
1042
1105
|
|
|
1043
|
-
const iblEnabled = this.#
|
|
1106
|
+
const iblEnabled = this.#renderSettings.iblEnabled && (this.#options.ibl?.valid === true || !!this.#options.ibl?.cachedUrl);
|
|
1044
1107
|
|
|
1045
1108
|
if (iblEnabled) {
|
|
1046
1109
|
lightsChanged = await this.#initializeEnvironmentTexture();
|
|
@@ -1120,7 +1183,7 @@ export default class BabylonJSController {
|
|
|
1120
1183
|
return false;
|
|
1121
1184
|
}
|
|
1122
1185
|
|
|
1123
|
-
if (!this.#
|
|
1186
|
+
if (!this.#renderSettings.ambientOcclusionEnabled) {
|
|
1124
1187
|
return false;
|
|
1125
1188
|
}
|
|
1126
1189
|
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
@@ -1221,7 +1284,7 @@ export default class BabylonJSController {
|
|
|
1221
1284
|
return false;
|
|
1222
1285
|
}
|
|
1223
1286
|
|
|
1224
|
-
if (!this.#
|
|
1287
|
+
if (!this.#renderSettings.antiAliasingEnabled) {
|
|
1225
1288
|
return false;
|
|
1226
1289
|
}
|
|
1227
1290
|
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
@@ -1283,7 +1346,7 @@ export default class BabylonJSController {
|
|
|
1283
1346
|
let hdrTexture = null;
|
|
1284
1347
|
if (this.#options.ibl?.cachedUrl) {
|
|
1285
1348
|
const hdrTextureURI = this.#options.ibl.cachedUrl;
|
|
1286
|
-
const envTextureSize =
|
|
1349
|
+
const envTextureSize = BabylonJSController.config.quality.environmentTextureSize || 1024;
|
|
1287
1350
|
hdrTexture = new HDRCubeTexture(hdrTextureURI, this.#scene, envTextureSize, false, false, false, true, undefined, undefined, false, true, true);
|
|
1288
1351
|
} else if (this.#hdrTexture && this.#options.ibl?.valid === true) {
|
|
1289
1352
|
hdrTexture = this.#hdrTexture.clone();
|
|
@@ -1356,7 +1419,7 @@ export default class BabylonJSController {
|
|
|
1356
1419
|
* Configures pipeline options for resolution, sampling, opacity, and debugging.
|
|
1357
1420
|
* Only applies if the scene has an environment texture set.
|
|
1358
1421
|
* @private
|
|
1359
|
-
* @returns {Promise<
|
|
1422
|
+
* @returns {Promise<boolean>} True when the IBL shadow pipeline is initialized; false when unavailable or setup fails.
|
|
1360
1423
|
*/
|
|
1361
1424
|
async #initializeIBLShadows() {
|
|
1362
1425
|
const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
|
|
@@ -1411,8 +1474,8 @@ export default class BabylonJSController {
|
|
|
1411
1474
|
|
|
1412
1475
|
// Use hardware-adaptive quality settings
|
|
1413
1476
|
const pipelineOptions = {
|
|
1414
|
-
resolutionExp:
|
|
1415
|
-
sampleDirections:
|
|
1477
|
+
resolutionExp: BabylonJSController.config.quality.iblShadowResolution, // Adaptive: 0=1024, 1=2048, 2=4096
|
|
1478
|
+
sampleDirections: BabylonJSController.config.quality.iblSampleDirections, // Adaptive: 2=fast, 4=quality
|
|
1416
1479
|
ssShadowsEnabled: true,
|
|
1417
1480
|
shadowRemanence: 0.85,
|
|
1418
1481
|
triPlanarVoxelization: true,
|
|
@@ -1485,7 +1548,7 @@ export default class BabylonJSController {
|
|
|
1485
1548
|
* Configures soft shadows for the built-in directional light used when no HDR environment is present.
|
|
1486
1549
|
* Uses hardware-adaptive quality settings.
|
|
1487
1550
|
* @private
|
|
1488
|
-
* @returns {void}
|
|
1551
|
+
* @returns {Promise<void>}
|
|
1489
1552
|
*/
|
|
1490
1553
|
async #initializeDefaultLightShadows() {
|
|
1491
1554
|
if (!this.#dirLight) {
|
|
@@ -1493,11 +1556,11 @@ export default class BabylonJSController {
|
|
|
1493
1556
|
}
|
|
1494
1557
|
this.#dirLight.autoUpdateExtends = false;
|
|
1495
1558
|
// Use quality-adaptive shadow map size
|
|
1496
|
-
const shadowMapSize =
|
|
1559
|
+
const shadowMapSize = BabylonJSController.config.quality.shadowMapSize;
|
|
1497
1560
|
const shadowGenerator = new ShadowGenerator(shadowMapSize, this.#dirLight);
|
|
1498
1561
|
shadowGenerator.useBlurExponentialShadowMap = true;
|
|
1499
1562
|
// Use quality-adaptive blur kernel
|
|
1500
|
-
shadowGenerator.blurKernel =
|
|
1563
|
+
shadowGenerator.blurKernel = BabylonJSController.config.quality.shadowBlurKernel;
|
|
1501
1564
|
shadowGenerator.darkness = 0.5;
|
|
1502
1565
|
shadowGenerator.bias = 0.0005;
|
|
1503
1566
|
shadowGenerator.normalBias = 0.02;
|
|
@@ -1511,7 +1574,7 @@ export default class BabylonJSController {
|
|
|
1511
1574
|
* Keeps only the generator bound to the built-in `PrefViewerDirLight` so its shadows persist,
|
|
1512
1575
|
* then adds generators for every directional or spot light coming from the environment asset container.
|
|
1513
1576
|
* @private
|
|
1514
|
-
* @returns {void}
|
|
1577
|
+
* @returns {Promise<void>}
|
|
1515
1578
|
*/
|
|
1516
1579
|
async #initializeEnvironmentShadows() {
|
|
1517
1580
|
this.#shadowGen = this.#shadowGen.filter((generator) => {
|
|
@@ -1560,7 +1623,7 @@ export default class BabylonJSController {
|
|
|
1560
1623
|
* Disposes every active shadow generator plus the IBL shadow pipeline to avoid stale casters across reloads.
|
|
1561
1624
|
* Clears the cached `#shadowGen` array so subsequent loads can rebuild fresh generators.
|
|
1562
1625
|
* @private
|
|
1563
|
-
* @returns {void}
|
|
1626
|
+
* @returns {Promise<void>}
|
|
1564
1627
|
*/
|
|
1565
1628
|
async #disableShadows() {
|
|
1566
1629
|
this.#shadowGen.forEach((shadowGenerator) => {
|
|
@@ -1573,19 +1636,19 @@ export default class BabylonJSController {
|
|
|
1573
1636
|
/**
|
|
1574
1637
|
* Initializes shadows for the Babylon.js scene.
|
|
1575
1638
|
* @private
|
|
1576
|
-
* @returns {void|
|
|
1639
|
+
* @returns {Promise<void|false>} Resolves after shadow setup, or false when shadows are disabled.
|
|
1577
1640
|
* @description
|
|
1578
|
-
*
|
|
1579
|
-
* Otherwise,
|
|
1641
|
+
* Uses IBL shadows only when IBL is enabled with a valid source and its shadow option is active.
|
|
1642
|
+
* Otherwise initializes default-light shadows, then adds shadow support for environment lights.
|
|
1580
1643
|
*/
|
|
1581
1644
|
async #initializeShadows() {
|
|
1582
|
-
if (!this.#
|
|
1645
|
+
if (!this.#renderSettings.shadowsEnabled) {
|
|
1583
1646
|
return false;
|
|
1584
1647
|
}
|
|
1585
1648
|
|
|
1586
1649
|
this.#ensureMeshesReceiveShadows();
|
|
1587
1650
|
|
|
1588
|
-
const iblEnabled = this.#
|
|
1651
|
+
const iblEnabled = this.#renderSettings.iblEnabled && (this.#options.ibl?.valid === true || !!this.#options.ibl?.cachedUrl);
|
|
1589
1652
|
const iblShadowsEnabled = iblEnabled && this.#options.ibl.shadows;
|
|
1590
1653
|
|
|
1591
1654
|
if (iblShadowsEnabled) {
|
|
@@ -1603,7 +1666,9 @@ export default class BabylonJSController {
|
|
|
1603
1666
|
* @returns {void}
|
|
1604
1667
|
*/
|
|
1605
1668
|
#setMaxSimultaneousLights() {
|
|
1606
|
-
if (!this.#scene)
|
|
1669
|
+
if (!this.#scene) {
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1607
1672
|
let lightsNumber = 1; // At least one light coming from the environment texture contribution
|
|
1608
1673
|
this.#scene.lights.forEach((light) => {
|
|
1609
1674
|
if (light.isEnabled()) {
|
|
@@ -1657,10 +1722,10 @@ export default class BabylonJSController {
|
|
|
1657
1722
|
return;
|
|
1658
1723
|
}
|
|
1659
1724
|
|
|
1660
|
-
const showroom =
|
|
1725
|
+
const showroom = BabylonJSController.config.look?.showroom || {};
|
|
1661
1726
|
const imageProcessing = this.#scene.imageProcessingConfiguration;
|
|
1662
1727
|
|
|
1663
|
-
const bg = showroom.backgroundDay ??
|
|
1728
|
+
const bg = showroom.backgroundDay ?? BabylonJSController.config.look.showroom.backgroundDay;
|
|
1664
1729
|
this.#scene.clearColor = new Color4(bg[0], bg[1], bg[2], 1);
|
|
1665
1730
|
imageProcessing.exposure = showroom.exposure ?? 0.95;
|
|
1666
1731
|
imageProcessing.contrast = showroom.contrast ?? 1.08;
|
|
@@ -1684,17 +1749,17 @@ export default class BabylonJSController {
|
|
|
1684
1749
|
return false;
|
|
1685
1750
|
}
|
|
1686
1751
|
|
|
1687
|
-
const showroom =
|
|
1688
|
-
const timeOfDay = clamp01(this.#
|
|
1689
|
-
const profile = getLightingCycleProfile(timeOfDay);
|
|
1752
|
+
const showroom = BabylonJSController.config.look?.showroom || {};
|
|
1753
|
+
const timeOfDay = RenderMath.clamp01(this.#renderSettings.lightingTimeOfDay ? this.#renderSettings.lightingTimeOfDay ?? 0.5 : 0.5);
|
|
1754
|
+
const profile = RenderMath.getLightingCycleProfile(timeOfDay);
|
|
1690
1755
|
const daylight = profile.dayFactor;
|
|
1691
1756
|
const peakLight = daylight > 0 ? Math.pow(daylight, 0.72) : 0;
|
|
1692
1757
|
const twilight = 1 - peakLight;
|
|
1693
1758
|
|
|
1694
1759
|
if (this.#scene.imageProcessingConfiguration) {
|
|
1695
1760
|
const imageProcessing = this.#scene.imageProcessingConfiguration;
|
|
1696
|
-
imageProcessing.exposure = lerp(0.78, showroom.exposure ?? 0.92, peakLight);
|
|
1697
|
-
imageProcessing.contrast = lerp(0.98, showroom.contrast ?? 1.02, peakLight);
|
|
1761
|
+
imageProcessing.exposure = RenderMath.lerp(0.78, showroom.exposure ?? 0.92, peakLight);
|
|
1762
|
+
imageProcessing.contrast = RenderMath.lerp(0.98, showroom.contrast ?? 1.02, peakLight);
|
|
1698
1763
|
imageProcessing.toneMappingEnabled = showroom.toneMappingEnabled ?? true;
|
|
1699
1764
|
imageProcessing.toneMappingType = showroom.toneMappingType ?? (ImageProcessingConfiguration?.TONEMAPPING_ACES ?? 0);
|
|
1700
1765
|
imageProcessing.vignetteEnabled = false;
|
|
@@ -1703,64 +1768,64 @@ export default class BabylonJSController {
|
|
|
1703
1768
|
|
|
1704
1769
|
// Night end stays in low linear values (a dark blue sky); the day end is the calibrated
|
|
1705
1770
|
// HDR off-white. Both are linear already, so no toLinearSpace() on either.
|
|
1706
|
-
const bgDay = showroom.backgroundDay ??
|
|
1771
|
+
const bgDay = showroom.backgroundDay ?? BabylonJSController.config.look.showroom.backgroundDay;
|
|
1707
1772
|
this.#scene.clearColor = new Color4(
|
|
1708
|
-
lerp(0.000627, bgDay[0], daylight),
|
|
1709
|
-
lerp(0.001089, bgDay[1], daylight),
|
|
1710
|
-
lerp(0.004413, bgDay[2], daylight),
|
|
1773
|
+
RenderMath.lerp(0.000627, bgDay[0], daylight),
|
|
1774
|
+
RenderMath.lerp(0.001089, bgDay[1], daylight),
|
|
1775
|
+
RenderMath.lerp(0.004413, bgDay[2], daylight),
|
|
1711
1776
|
1,
|
|
1712
1777
|
);
|
|
1713
1778
|
|
|
1714
1779
|
if ("environmentIntensity" in this.#scene) {
|
|
1715
|
-
this.#scene.environmentIntensity = lerp(0.42, showroom.environmentIntensity ?? 1.08, peakLight);
|
|
1780
|
+
this.#scene.environmentIntensity = RenderMath.lerp(0.42, showroom.environmentIntensity ?? 1.08, peakLight);
|
|
1716
1781
|
}
|
|
1717
1782
|
|
|
1718
1783
|
if (this.#hemiLight) {
|
|
1719
1784
|
this.#hemiLight.direction = new Vector3(
|
|
1720
|
-
lerp(-0.45, -0.8, peakLight),
|
|
1721
|
-
lerp(0.45, 0.95, peakLight),
|
|
1722
|
-
lerp(-0.75, -0.35, peakLight),
|
|
1785
|
+
RenderMath.lerp(-0.45, -0.8, peakLight),
|
|
1786
|
+
RenderMath.lerp(0.45, 0.95, peakLight),
|
|
1787
|
+
RenderMath.lerp(-0.75, -0.35, peakLight),
|
|
1723
1788
|
);
|
|
1724
|
-
this.#hemiLight.intensity = lerp(0.18, showroom.hemiLightIntensity ?? 0.52, peakLight);
|
|
1789
|
+
this.#hemiLight.intensity = RenderMath.lerp(0.18, showroom.hemiLightIntensity ?? 0.52, peakLight);
|
|
1725
1790
|
this.#hemiLight.diffuse = new Color3(
|
|
1726
|
-
lerp(0.42, 0.92, peakLight),
|
|
1727
|
-
lerp(0.48, 0.96, peakLight),
|
|
1728
|
-
lerp(0.62, 0.98, peakLight),
|
|
1791
|
+
RenderMath.lerp(0.42, 0.92, peakLight),
|
|
1792
|
+
RenderMath.lerp(0.48, 0.96, peakLight),
|
|
1793
|
+
RenderMath.lerp(0.62, 0.98, peakLight),
|
|
1729
1794
|
);
|
|
1730
1795
|
this.#hemiLight.groundColor = new Color3(
|
|
1731
|
-
lerp(0.08, 0.3, twilight),
|
|
1732
|
-
lerp(0.08, 0.28, twilight),
|
|
1733
|
-
lerp(0.1, 0.22, twilight),
|
|
1796
|
+
RenderMath.lerp(0.08, 0.3, twilight),
|
|
1797
|
+
RenderMath.lerp(0.08, 0.28, twilight),
|
|
1798
|
+
RenderMath.lerp(0.1, 0.22, twilight),
|
|
1734
1799
|
);
|
|
1735
1800
|
}
|
|
1736
1801
|
|
|
1737
1802
|
if (this.#dirLight) {
|
|
1738
1803
|
this.#dirLight.direction = new Vector3(profile.sunDirection.x, profile.sunDirection.y, profile.sunDirection.z);
|
|
1739
|
-
this.#dirLight.intensity = lerp(0.02, showroom.dirLightIntensity ?? 0.56, peakLight);
|
|
1804
|
+
this.#dirLight.intensity = RenderMath.lerp(0.02, showroom.dirLightIntensity ?? 0.56, peakLight);
|
|
1740
1805
|
this.#dirLight.diffuse = new Color3(
|
|
1741
|
-
lerp(0.28, 1.0, peakLight),
|
|
1742
|
-
lerp(0.3, 0.97, peakLight),
|
|
1743
|
-
lerp(0.34, 0.9, peakLight),
|
|
1806
|
+
RenderMath.lerp(0.28, 1.0, peakLight),
|
|
1807
|
+
RenderMath.lerp(0.3, 0.97, peakLight),
|
|
1808
|
+
RenderMath.lerp(0.34, 0.9, peakLight),
|
|
1744
1809
|
);
|
|
1745
1810
|
}
|
|
1746
1811
|
|
|
1747
1812
|
if (this.#moonLight) {
|
|
1748
1813
|
this.#moonLight.direction = new Vector3(profile.moonDirection.x, profile.moonDirection.y, profile.moonDirection.z);
|
|
1749
|
-
this.#moonLight.intensity = lerp(0.44, 0.03, peakLight);
|
|
1814
|
+
this.#moonLight.intensity = RenderMath.lerp(0.44, 0.03, peakLight);
|
|
1750
1815
|
this.#moonLight.diffuse = new Color3(0.58, 0.7, 1.0);
|
|
1751
1816
|
}
|
|
1752
1817
|
|
|
1753
1818
|
if (this.#cameraLight) {
|
|
1754
|
-
this.#cameraLight.intensity = lerp(0.06, showroom.cameraLightIntensity ?? 0.16, peakLight);
|
|
1819
|
+
this.#cameraLight.intensity = RenderMath.lerp(0.06, showroom.cameraLightIntensity ?? 0.16, peakLight);
|
|
1755
1820
|
this.#cameraLight.diffuse = new Color3(
|
|
1756
|
-
lerp(0.35, 1.0, peakLight),
|
|
1757
|
-
lerp(0.35, 0.97, peakLight),
|
|
1758
|
-
lerp(0.36, 0.92, peakLight),
|
|
1821
|
+
RenderMath.lerp(0.35, 1.0, peakLight),
|
|
1822
|
+
RenderMath.lerp(0.35, 0.97, peakLight),
|
|
1823
|
+
RenderMath.lerp(0.36, 0.92, peakLight),
|
|
1759
1824
|
);
|
|
1760
1825
|
}
|
|
1761
1826
|
|
|
1762
1827
|
this.#scene.markAllMaterialsAsDirty?.(Material.LightDirtyFlag ?? Material.TextureDirtyFlag);
|
|
1763
|
-
this.#requestRender({ frames: 2, continuousMs:
|
|
1828
|
+
this.#requestRender({ frames: 2, continuousMs: BabylonJSController.config.render.interactionMs });
|
|
1764
1829
|
return true;
|
|
1765
1830
|
}
|
|
1766
1831
|
|
|
@@ -1776,81 +1841,62 @@ export default class BabylonJSController {
|
|
|
1776
1841
|
}
|
|
1777
1842
|
|
|
1778
1843
|
const pixelRatio = typeof window !== "undefined" ? window.devicePixelRatio : 1;
|
|
1779
|
-
const baseScaling =
|
|
1780
|
-
const scalingLevel = getAdaptiveHardwareScalingLevel(baseScaling, pixelRatio);
|
|
1844
|
+
const baseScaling = BabylonJSController.config.quality.hardwareScaling || 1;
|
|
1845
|
+
const scalingLevel = RenderMath.getAdaptiveHardwareScalingLevel(baseScaling, pixelRatio);
|
|
1781
1846
|
this.#engine.setHardwareScalingLevel(scalingLevel);
|
|
1782
1847
|
}
|
|
1783
1848
|
|
|
1784
1849
|
/**
|
|
1785
|
-
*
|
|
1850
|
+
* Frames the default camera using the shared model-framing calculation.
|
|
1851
|
+
* Preserves its current orientation and projection; excludes environment geometry.
|
|
1852
|
+
* Applies the destination immediately, without a ViewCube transition.
|
|
1853
|
+
* The default camera has no parent; imported cameras retain their authored framing.
|
|
1786
1854
|
* @private
|
|
1787
|
-
* @
|
|
1788
|
-
*/
|
|
1789
|
-
#getModelBounds() {
|
|
1790
|
-
const meshes = (this.#containers?.model?.assetContainer?.meshes ?? []).filter((mesh) => (mesh?.getTotalVertices?.() ?? 0) > 0);
|
|
1791
|
-
if (meshes.length === 0) {
|
|
1792
|
-
return null;
|
|
1793
|
-
}
|
|
1794
|
-
|
|
1795
|
-
let min = null;
|
|
1796
|
-
let max = null;
|
|
1797
|
-
meshes.forEach((mesh) => {
|
|
1798
|
-
mesh.computeWorldMatrix(true);
|
|
1799
|
-
const box = mesh.getBoundingInfo?.()?.boundingBox;
|
|
1800
|
-
if (!box) {
|
|
1801
|
-
return;
|
|
1802
|
-
}
|
|
1803
|
-
min = min ? Vector3.Minimize(min, box.minimumWorld) : box.minimumWorld.clone();
|
|
1804
|
-
max = max ? Vector3.Maximize(max, box.maximumWorld) : box.maximumWorld.clone();
|
|
1805
|
-
});
|
|
1806
|
-
|
|
1807
|
-
if (!min || !max) {
|
|
1808
|
-
return null;
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
|
-
const radius = Vector3.Distance(min, max) / 2;
|
|
1812
|
-
if (!Number.isFinite(radius) || radius <= 0) {
|
|
1813
|
-
return null;
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
return { center: Vector3.Center(min, max), radius };
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
|
-
/**
|
|
1820
|
-
* Frames the default camera on the loaded model: aims it at the model's centre and pulls it
|
|
1821
|
-
* back far enough for the whole model to fit the viewport.
|
|
1822
|
-
*
|
|
1823
|
-
* Only ever applied to the viewer's own camera — a camera authored in the model or the scene
|
|
1824
|
-
* is a composition choice and is left untouched. Orbit angles are preserved so a model reload
|
|
1825
|
-
* (an option or dimension change) re-centres without snapping the user's viewpoint back.
|
|
1826
|
-
* @private
|
|
1827
|
-
* @param {Camera|null|undefined} camera - The camera about to become active.
|
|
1855
|
+
* @param {Camera|null|undefined} camera - Camera about to become active.
|
|
1828
1856
|
* @returns {void}
|
|
1829
1857
|
*/
|
|
1830
1858
|
#fitDefaultCameraToModel(camera) {
|
|
1831
1859
|
if (camera !== this.#camera || !(camera instanceof ArcRotateCamera)) {
|
|
1832
1860
|
return;
|
|
1833
1861
|
}
|
|
1834
|
-
|
|
1835
|
-
const bounds = this.#getModelBounds();
|
|
1862
|
+
const bounds = BabylonJSCameraFraming.getModelBounds(this.#containers.model?.assetContainer);
|
|
1836
1863
|
if (!bounds) {
|
|
1837
1864
|
return;
|
|
1838
1865
|
}
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
const
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
camera.
|
|
1866
|
+
camera.getViewMatrix(true);
|
|
1867
|
+
const direction = camera.globalPosition.subtract(camera.getTarget());
|
|
1868
|
+
if (direction.lengthSquared() <= 1e-12) {
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
direction.normalize();
|
|
1872
|
+
const fit = BabylonJSCameraFraming.calculateCameraFit(camera, bounds, direction, {
|
|
1873
|
+
width: this.#canvas.clientWidth,
|
|
1874
|
+
height: this.#canvas.clientHeight,
|
|
1875
|
+
});
|
|
1876
|
+
if (!fit) {
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
// Geometry protection supplies the minimum zoom radius.
|
|
1880
|
+
camera.lowerRadiusLimit = null;
|
|
1881
|
+
camera.upperRadiusLimit = fit.distance * BabylonJSController.config.camera.maxRadiusFactor;
|
|
1882
|
+
camera.upVector = Vector3.Up();
|
|
1883
|
+
camera.setTarget(fit.target);
|
|
1884
|
+
// Match ViewCube's destination orientation, including its small pole correction.
|
|
1885
|
+
const rotationMatrix = Matrix.Identity();
|
|
1886
|
+
fit.rotation.toRotationMatrix(rotationMatrix);
|
|
1887
|
+
const forward = Vector3.TransformNormal(
|
|
1888
|
+
new Vector3(0, 0, camera.getScene().useRightHandedSystem ? -1 : 1),
|
|
1889
|
+
rotationMatrix
|
|
1890
|
+
);
|
|
1891
|
+
camera.setPosition(fit.target.subtract(forward.scale(fit.distance)));
|
|
1892
|
+
if (camera.mode === Camera.ORTHOGRAPHIC_CAMERA) {
|
|
1893
|
+
camera.orthoLeft = -fit.halfWidth;
|
|
1894
|
+
camera.orthoRight = fit.halfWidth;
|
|
1895
|
+
camera.orthoBottom = -fit.halfHeight;
|
|
1896
|
+
camera.orthoTop = fit.halfHeight;
|
|
1897
|
+
}
|
|
1898
|
+
camera.minZ = Math.min(camera.minZ > 0 ? camera.minZ : Infinity, Math.max(fit.radius * 0.001, 0.00001));
|
|
1899
|
+
camera.maxZ = Math.max(camera.maxZ, fit.distance + fit.radius * 2);
|
|
1854
1900
|
}
|
|
1855
1901
|
|
|
1856
1902
|
/**
|
|
@@ -1879,6 +1925,7 @@ export default class BabylonJSController {
|
|
|
1879
1925
|
this.#state.pointerPicking.lastMovePickX = NaN;
|
|
1880
1926
|
this.#state.pointerPicking.lastMovePickY = NaN;
|
|
1881
1927
|
this.#state.pointerPicking.lastPickedMeshId = null;
|
|
1928
|
+
this.#state.pointerPicking.pointerDown = null;
|
|
1882
1929
|
}
|
|
1883
1930
|
|
|
1884
1931
|
/**
|
|
@@ -1896,7 +1943,7 @@ export default class BabylonJSController {
|
|
|
1896
1943
|
const x = this.#scene.pointerX;
|
|
1897
1944
|
const y = this.#scene.pointerY;
|
|
1898
1945
|
const state = this.#state.pointerPicking;
|
|
1899
|
-
const config =
|
|
1946
|
+
const config = BabylonJSController.config.pointerPicking;
|
|
1900
1947
|
|
|
1901
1948
|
if (!Number.isFinite(state.lastMovePickX) || !Number.isFinite(state.lastMovePickY)) {
|
|
1902
1949
|
state.lastMovePickX = x;
|
|
@@ -1926,6 +1973,7 @@ export default class BabylonJSController {
|
|
|
1926
1973
|
#enableInteraction() {
|
|
1927
1974
|
if (this.#canvas) {
|
|
1928
1975
|
this.#canvas.addEventListener("keyup", this.#handlers.onKeyUp);
|
|
1976
|
+
this.#canvas.addEventListener("pointercancel", this.#handlers.onPointerCancel);
|
|
1929
1977
|
}
|
|
1930
1978
|
if (this.#scene) {
|
|
1931
1979
|
this.#scene.onPointerObservable.add(this.#handlers.onPointerObservable);
|
|
@@ -1944,6 +1992,7 @@ export default class BabylonJSController {
|
|
|
1944
1992
|
#disableInteraction() {
|
|
1945
1993
|
if (this.#canvas) {
|
|
1946
1994
|
this.#canvas.removeEventListener("keyup", this.#handlers.onKeyUp);
|
|
1995
|
+
this.#canvas.removeEventListener("pointercancel", this.#handlers.onPointerCancel);
|
|
1947
1996
|
}
|
|
1948
1997
|
if (this.#scene !== null) {
|
|
1949
1998
|
this.#scene.onPointerObservable.removeCallback(this.#handlers.onPointerObservable);
|
|
@@ -2065,7 +2114,7 @@ export default class BabylonJSController {
|
|
|
2065
2114
|
* @returns {void}
|
|
2066
2115
|
*/
|
|
2067
2116
|
#onAnimationGroupPlay() {
|
|
2068
|
-
this.#requestRender({ frames: 1, continuousMs:
|
|
2117
|
+
this.#requestRender({ frames: 1, continuousMs: BabylonJSController.config.render.animationMs });
|
|
2069
2118
|
}
|
|
2070
2119
|
|
|
2071
2120
|
/**
|
|
@@ -2074,11 +2123,11 @@ export default class BabylonJSController {
|
|
|
2074
2123
|
* @returns {void}
|
|
2075
2124
|
*/
|
|
2076
2125
|
#onAnimationGroupStop() {
|
|
2077
|
-
if (this.#
|
|
2126
|
+
if (this.#renderSettings.iblEnabled && this.#renderPipelines.iblShadows) {
|
|
2078
2127
|
this.#renderPipelines.iblShadows.updateVoxelization();
|
|
2079
2128
|
this.#scene?.postProcessRenderPipelineManager?.update();
|
|
2080
2129
|
}
|
|
2081
|
-
const frames = this.#
|
|
2130
|
+
const frames = this.#renderSettings.antiAliasingEnabled || this.#renderSettings.ambientOcclusionEnabled || this.#renderSettings.iblEnabled ? BabylonJSController.config.render.burstFramesEnhanced : BabylonJSController.config.render.burstFramesBase;
|
|
2082
2131
|
this.#requestRender({ frames: frames });
|
|
2083
2132
|
}
|
|
2084
2133
|
|
|
@@ -2131,7 +2180,8 @@ export default class BabylonJSController {
|
|
|
2131
2180
|
}
|
|
2132
2181
|
|
|
2133
2182
|
/**
|
|
2134
|
-
*
|
|
2183
|
+
* Wakes rendering for native orbital wheel zoom and handles wheel movement for target cameras.
|
|
2184
|
+
* ArcRotateCamera owns proportional wheel zoom; do not add a second radius offset here.
|
|
2135
2185
|
* OPTIMIZATION: Tracks activity for idle mode.
|
|
2136
2186
|
* @private
|
|
2137
2187
|
* @param {WheelEvent} event - The mouse wheel event.
|
|
@@ -2151,10 +2201,7 @@ export default class BabylonJSController {
|
|
|
2151
2201
|
return false;
|
|
2152
2202
|
}
|
|
2153
2203
|
if (!camera.metadata?.locked) {
|
|
2154
|
-
if (camera instanceof
|
|
2155
|
-
camera.wheelPrecision = camera.wheelPrecision || 3.0;
|
|
2156
|
-
camera.inertialRadiusOffset -= event.deltaY * camera.wheelPrecision * 0.001;
|
|
2157
|
-
} else if (camera instanceof FreeCamera || camera instanceof UniversalCamera) {
|
|
2204
|
+
if (camera instanceof FreeCamera || camera instanceof UniversalCamera) {
|
|
2158
2205
|
camera.wheelPrecision = camera.wheelPrecision || 3.0;
|
|
2159
2206
|
const zoomSpeed = -event.deltaY * camera.wheelPrecision * 0.001;
|
|
2160
2207
|
|
|
@@ -2163,18 +2210,18 @@ export default class BabylonJSController {
|
|
|
2163
2210
|
const movementVector = direction.scale(zoomSpeed);
|
|
2164
2211
|
camera.position = camera.position.add(movementVector);
|
|
2165
2212
|
}
|
|
2166
|
-
this.#requestRender({ frames: 1, continuousMs:
|
|
2213
|
+
this.#requestRender({ frames: 1, continuousMs: BabylonJSController.config.render.interactionMs });
|
|
2167
2214
|
}
|
|
2168
2215
|
}
|
|
2169
2216
|
|
|
2170
2217
|
/**
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2218
|
+
* Handles pointer up events on the Babylon.js scene.
|
|
2219
|
+
* OPTIMIZATION: Tracks activity for idle mode.
|
|
2220
|
+
* @private
|
|
2221
|
+
* @param {PointerEvent} event - The pointer up event.
|
|
2222
|
+
* @param {PickInfo} pickInfo - The result of the scene pick operation.
|
|
2223
|
+
* @returns {void}
|
|
2224
|
+
*/
|
|
2178
2225
|
#onPointerUp(event, pickInfo) {
|
|
2179
2226
|
// OPTIMIZATION: Mark activity for idle mode
|
|
2180
2227
|
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
@@ -2208,19 +2255,62 @@ export default class BabylonJSController {
|
|
|
2208
2255
|
|
|
2209
2256
|
const camera = this.#scene?.activeCamera;
|
|
2210
2257
|
if (camera && !camera.metadata?.locked) {
|
|
2211
|
-
this.#requestRender({ frames: 1, continuousMs:
|
|
2258
|
+
this.#requestRender({ frames: 1, continuousMs: BabylonJSController.config.render.interactionMs });
|
|
2212
2259
|
}
|
|
2213
2260
|
if (this.#babylonJSAnimationController && pickInfo) {
|
|
2214
2261
|
const pickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
2215
2262
|
if (this.#state.pointerPicking.lastPickedMeshId !== pickedMeshId) {
|
|
2216
2263
|
const highlightResult = this.#babylonJSAnimationController.highlightMeshes(pickInfo);
|
|
2217
2264
|
if (highlightResult.changed) {
|
|
2218
|
-
this.#requestRender({ frames: 1, continuousMs:
|
|
2265
|
+
this.#requestRender({ frames: 1, continuousMs: BabylonJSController.config.render.interactionMs });
|
|
2219
2266
|
}
|
|
2220
2267
|
}
|
|
2221
2268
|
}
|
|
2222
2269
|
}
|
|
2223
2270
|
|
|
2271
|
+
/**
|
|
2272
|
+
* Invalidates an incomplete click gesture when the browser cancels its pointer.
|
|
2273
|
+
* @private
|
|
2274
|
+
* @returns {void}
|
|
2275
|
+
*/
|
|
2276
|
+
#onPointerCancel() {
|
|
2277
|
+
this.#state.pointerPicking.pointerDown = null;
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
/**
|
|
2281
|
+
* Emits the internal 3D click contract for every model mesh crossed by the pointer ray.
|
|
2282
|
+
* @private
|
|
2283
|
+
* @returns {void}
|
|
2284
|
+
*/
|
|
2285
|
+
#emitModelMeshClick() {
|
|
2286
|
+
const modelMeshes = this.#containers.model?.assetContainer?.meshes ?? [];
|
|
2287
|
+
const acceptedMeshes = BabylonJSSelectionController.getModelMeshSet(modelMeshes);
|
|
2288
|
+
if (!this.#scene || acceptedMeshes.size === 0) {
|
|
2289
|
+
return;
|
|
2290
|
+
}
|
|
2291
|
+
const ray = this.#scene.createPickingRay(
|
|
2292
|
+
this.#scene.pointerX,
|
|
2293
|
+
this.#scene.pointerY,
|
|
2294
|
+
Matrix.Identity(),
|
|
2295
|
+
this.#scene.activeCamera,
|
|
2296
|
+
false,
|
|
2297
|
+
);
|
|
2298
|
+
const picks = this.#scene.multiPickWithRay(ray, (mesh) => BabylonJSSelectionController.isPickableModelMesh(mesh, acceptedMeshes)) ?? [];
|
|
2299
|
+
const ids = BabylonJSSelectionController.collectPickedMeshIds(picks, modelMeshes);
|
|
2300
|
+
if (ids.length === 0) {
|
|
2301
|
+
return;
|
|
2302
|
+
}
|
|
2303
|
+
this.#getPrefViewer3DComponent();
|
|
2304
|
+
const CustomEventType = this.#prefViewer3D?.ownerDocument?.defaultView?.CustomEvent ?? globalThis.CustomEvent;
|
|
2305
|
+
if (this.#prefViewer3D && CustomEventType) {
|
|
2306
|
+
this.#prefViewer3D.dispatchEvent(new CustomEventType("pref-viewer-3d-element-click", {
|
|
2307
|
+
bubbles: true,
|
|
2308
|
+
composed: true,
|
|
2309
|
+
detail: { ids },
|
|
2310
|
+
}));
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2224
2314
|
/**
|
|
2225
2315
|
* Handles pointer events observed on the Babylon.js scene.
|
|
2226
2316
|
* Uses pointer-move sampling to avoid running expensive `scene.pick()` on every event.
|
|
@@ -2230,6 +2320,11 @@ export default class BabylonJSController {
|
|
|
2230
2320
|
*/
|
|
2231
2321
|
#onPointerObservable(info) {
|
|
2232
2322
|
if (info.type === PointerEventTypes.POINTERMOVE) {
|
|
2323
|
+
const start = this.#state.pointerPicking.pointerDown;
|
|
2324
|
+
if (start && (info.event?.pointerId !== start.pointerId || (info.event?.buttons & 1) === 0
|
|
2325
|
+
|| Math.hypot(info.event?.clientX - start.x, info.event?.clientY - start.y) > BabylonJSController.config.pointerPicking.clickMaxDistancePx)) {
|
|
2326
|
+
start.dragged = true;
|
|
2327
|
+
}
|
|
2233
2328
|
let pickInfo = null;
|
|
2234
2329
|
let lastPickedMeshId = null;
|
|
2235
2330
|
if (this.#shouldPickOnPointerMove()) {
|
|
@@ -2238,7 +2333,17 @@ export default class BabylonJSController {
|
|
|
2238
2333
|
}
|
|
2239
2334
|
this.#onPointerMove(info.event, pickInfo);
|
|
2240
2335
|
this.#state.pointerPicking.lastPickedMeshId = lastPickedMeshId;
|
|
2336
|
+
} else if (info.type === PointerEventTypes.POINTERDOWN) {
|
|
2337
|
+
this.#state.pointerPicking.pointerDown = info.event?.button === 0 && !this.#state.pointerPicking.pointerDown
|
|
2338
|
+
? { x: info.event.clientX, y: info.event.clientY, button: info.event.button, pointerId: info.event.pointerId }
|
|
2339
|
+
: null;
|
|
2241
2340
|
} else if (info.type === PointerEventTypes.POINTERUP) {
|
|
2341
|
+
const pointerDown = this.#state.pointerPicking.pointerDown;
|
|
2342
|
+
this.#state.pointerPicking.pointerDown = null;
|
|
2343
|
+
const pointerUp = { x: info.event?.clientX, y: info.event?.clientY, button: info.event?.button, pointerId: info.event?.pointerId };
|
|
2344
|
+
if (BabylonJSSelectionController.isPrimaryClickGesture(pointerDown, pointerUp, BabylonJSController.config.pointerPicking.clickMaxDistancePx)) {
|
|
2345
|
+
this.#emitModelMeshClick();
|
|
2346
|
+
}
|
|
2242
2347
|
const pickInfo = info.event?.button === 2 ? this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY) : null;
|
|
2243
2348
|
if (pickInfo) {
|
|
2244
2349
|
this.#state.pointerPicking.lastPickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
@@ -2249,6 +2354,149 @@ export default class BabylonJSController {
|
|
|
2249
2354
|
}
|
|
2250
2355
|
}
|
|
2251
2356
|
|
|
2357
|
+
/**
|
|
2358
|
+
* Applies a pending engine resize, cancels cube navigation and wakes rendering.
|
|
2359
|
+
* @private
|
|
2360
|
+
* @param {number} elapsed - Time since the previous resize, retained for diagnostics.
|
|
2361
|
+
* @returns {void}
|
|
2362
|
+
*/
|
|
2363
|
+
#applyResize(elapsed) {
|
|
2364
|
+
this.#state.resize.timeoutId = null;
|
|
2365
|
+
this.#state.resize.isScheduled = false;
|
|
2366
|
+
if (!this.#engine) {
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
2369
|
+
this.#state.resize.lastAppliedAt = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
2370
|
+
console.log(`PrefViewer: Applying resize after ${Math.round(elapsed)}ms`);
|
|
2371
|
+
this.#applyShowroomCameraFraming();
|
|
2372
|
+
this.#engine.resize();
|
|
2373
|
+
const needsEnhancedBurst = this.#renderSettings.antiAliasingEnabled || this.#renderSettings.ambientOcclusionEnabled || this.#renderSettings.iblEnabled || this.#renderSettings.shadowsEnabled;
|
|
2374
|
+
const frames = needsEnhancedBurst ? BabylonJSController.config.render.burstFramesEnhanced : BabylonJSController.config.render.burstFramesBase;
|
|
2375
|
+
this.#requestRender({ frames, continuousMs: BabylonJSController.config.render.interactionMs });
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
/**
|
|
2379
|
+
* Extracts the final segment of a slash-containing metadata path, returning false otherwise.
|
|
2380
|
+
* @private
|
|
2381
|
+
* @static
|
|
2382
|
+
* @param {unknown} data - Metadata path candidate.
|
|
2383
|
+
* @returns {string|false}
|
|
2384
|
+
*/
|
|
2385
|
+
static #getNodeName(data) {
|
|
2386
|
+
if (data && typeof data === "string" && data.includes("/")) {
|
|
2387
|
+
data = data.slice(data.lastIndexOf("/") + 1);
|
|
2388
|
+
return data;
|
|
2389
|
+
}
|
|
2390
|
+
return false;
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
/**
|
|
2394
|
+
* Converts a vector to serializable coordinates, returning null for an absent value.
|
|
2395
|
+
* @private
|
|
2396
|
+
* @static
|
|
2397
|
+
* @param {Vector3|null|undefined} value - Vector to summarize.
|
|
2398
|
+
* @returns {{x:number,y:number,z:number}|null}
|
|
2399
|
+
*/
|
|
2400
|
+
static #vectorToJSON(value) {
|
|
2401
|
+
return value ? { x: value.x, y: value.y, z: value.z } : null;
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
/**
|
|
2405
|
+
* Summarizes texture identity and readiness without including its contents.
|
|
2406
|
+
* @private
|
|
2407
|
+
* @static
|
|
2408
|
+
* @param {object|null|undefined} texture - Babylon texture to inspect.
|
|
2409
|
+
* @returns {object}
|
|
2410
|
+
*/
|
|
2411
|
+
static #textureToJSON(texture) {
|
|
2412
|
+
return {
|
|
2413
|
+
name: texture?.name ?? null,
|
|
2414
|
+
url: texture?.url ?? null,
|
|
2415
|
+
isReady: texture?.isReady?.() ?? null,
|
|
2416
|
+
hasTexture: Boolean(texture),
|
|
2417
|
+
className: texture?.getClassName?.() ?? texture?.constructor?.name ?? null,
|
|
2418
|
+
};
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
/**
|
|
2422
|
+
* Summarizes material readiness, related geometry and texture diagnostics, capturing readiness errors.
|
|
2423
|
+
* @private
|
|
2424
|
+
* @static
|
|
2425
|
+
* @param {Material} material - Material to inspect.
|
|
2426
|
+
* @param {Array<object>} sceneMeshes - Current scene mesh snapshot used to resolve material owners.
|
|
2427
|
+
* @returns {object}
|
|
2428
|
+
*/
|
|
2429
|
+
static #summarizeMaterial(material, sceneMeshes) {
|
|
2430
|
+
const relatedMeshes = sceneMeshes.filter((mesh) => mesh.material === material);
|
|
2431
|
+
const sampleMesh = relatedMeshes[0] ?? null;
|
|
2432
|
+
const activeTextures = material?.getActiveTextures?.() ?? [];
|
|
2433
|
+
let isReadyWithoutMesh = null;
|
|
2434
|
+
let isReadyWithMesh = null;
|
|
2435
|
+
try {
|
|
2436
|
+
isReadyWithoutMesh = typeof material?.isReady === "function" ? material.isReady() : null;
|
|
2437
|
+
} catch (error) {
|
|
2438
|
+
isReadyWithoutMesh = `error: ${error?.message ?? error}`;
|
|
2439
|
+
}
|
|
2440
|
+
try {
|
|
2441
|
+
isReadyWithMesh = typeof material?.isReady === "function" && sampleMesh ? material.isReady(sampleMesh) : null;
|
|
2442
|
+
} catch (error) {
|
|
2443
|
+
isReadyWithMesh = `error: ${error?.message ?? error}`;
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
return {
|
|
2447
|
+
name: material?.name ?? null,
|
|
2448
|
+
id: material?.id ?? null,
|
|
2449
|
+
className: material?.getClassName?.() ?? material?.constructor?.name ?? null,
|
|
2450
|
+
isReadyWithoutMesh,
|
|
2451
|
+
isReadyWithMesh,
|
|
2452
|
+
relatedMeshes: relatedMeshes.length,
|
|
2453
|
+
sampleMesh: sampleMesh ? {
|
|
2454
|
+
name: sampleMesh.name,
|
|
2455
|
+
id: sampleMesh.id,
|
|
2456
|
+
enabled: sampleMesh.isEnabled?.() ?? null,
|
|
2457
|
+
isVisible: sampleMesh.isVisible ?? null,
|
|
2458
|
+
hasVertices: sampleMesh.getTotalVertices?.() ?? null,
|
|
2459
|
+
hasIndices: sampleMesh.getTotalIndices?.() ?? null,
|
|
2460
|
+
} : null,
|
|
2461
|
+
textures: activeTextures.map(BabylonJSController.#textureToJSON),
|
|
2462
|
+
notReadyTextures: activeTextures.filter((texture) => texture?.isReady?.() === false).map(BabylonJSController.#textureToJSON),
|
|
2463
|
+
forceDepthWrite: material?.forceDepthWrite ?? null,
|
|
2464
|
+
transparencyMode: material?.transparencyMode ?? null,
|
|
2465
|
+
alpha: material?.alpha ?? null,
|
|
2466
|
+
backFaceCulling: material?.backFaceCulling ?? null,
|
|
2467
|
+
disableLighting: material?.disableLighting ?? null,
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
/**
|
|
2472
|
+
* Summarizes container state and counts of its assets.
|
|
2473
|
+
* @private
|
|
2474
|
+
* @static
|
|
2475
|
+
* @param {object|null|undefined} container - Viewer container and its state.
|
|
2476
|
+
* @returns {object}
|
|
2477
|
+
*/
|
|
2478
|
+
static #summarizeContainer(container) {
|
|
2479
|
+
return {
|
|
2480
|
+
show: container?.state?.show ?? null,
|
|
2481
|
+
visible: container?.state?.visible ?? null,
|
|
2482
|
+
mustBeShown: container?.state?.mustBeShown ?? null,
|
|
2483
|
+
isPending: container?.state?.isPending ?? null,
|
|
2484
|
+
isSuccess: container?.state?.isSuccess ?? null,
|
|
2485
|
+
hasAssetContainer: Boolean(container?.assetContainer),
|
|
2486
|
+
meshes: container?.assetContainer?.meshes?.length ?? 0,
|
|
2487
|
+
enabledMeshes: (container?.assetContainer?.meshes ?? []).filter((mesh) => mesh.isEnabled?.() !== false && mesh.isVisible !== false).length,
|
|
2488
|
+
materials: container?.assetContainer?.materials?.length ?? 0,
|
|
2489
|
+
cameras: container?.assetContainer?.cameras?.map((item) => ({
|
|
2490
|
+
name: item.name,
|
|
2491
|
+
id: item.id,
|
|
2492
|
+
className: item.getClassName?.() ?? item.constructor?.name ?? null,
|
|
2493
|
+
position: BabylonJSController.#vectorToJSON(item.position),
|
|
2494
|
+
target: BabylonJSController.#vectorToJSON(item.target),
|
|
2495
|
+
})) ?? [],
|
|
2496
|
+
animationGroups: container?.assetContainer?.animationGroups?.length ?? 0,
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2252
2500
|
/**
|
|
2253
2501
|
* Handles canvas resize notifications.
|
|
2254
2502
|
* Resizes the Babylon engine and requests a short on-demand render burst so camera-dependent
|
|
@@ -2262,23 +2510,10 @@ export default class BabylonJSController {
|
|
|
2262
2510
|
}
|
|
2263
2511
|
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
2264
2512
|
const elapsed = now - this.#state.resize.lastAppliedAt;
|
|
2265
|
-
const applyResize = () => {
|
|
2266
|
-
this.#state.resize.timeoutId = null;
|
|
2267
|
-
this.#state.resize.isScheduled = false;
|
|
2268
|
-
if (!this.#engine) {
|
|
2269
|
-
return;
|
|
2270
|
-
}
|
|
2271
|
-
this.#state.resize.lastAppliedAt = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
2272
|
-
console.log(`PrefViewer: Applying resize after ${Math.round(elapsed)}ms`);
|
|
2273
|
-
this.#applyShowroomCameraFraming();
|
|
2274
|
-
this.#engine.resize();
|
|
2275
|
-
const needsEnhancedBurst = this.#settings.antiAliasingEnabled || this.#settings.ambientOcclusionEnabled || this.#settings.iblEnabled || this.#settings.shadowsEnabled;
|
|
2276
|
-
const frames = needsEnhancedBurst ? this.#config.render.burstFramesEnhanced : this.#config.render.burstFramesBase;
|
|
2277
|
-
this.#requestRender({ frames, continuousMs: this.#config.render.interactionMs });
|
|
2278
|
-
};
|
|
2279
2513
|
|
|
2280
|
-
|
|
2281
|
-
|
|
2514
|
+
|
|
2515
|
+
if (elapsed >= BabylonJSController.config.resize.throttleMs) {
|
|
2516
|
+
this.#applyResize(elapsed);
|
|
2282
2517
|
return;
|
|
2283
2518
|
}
|
|
2284
2519
|
|
|
@@ -2287,9 +2522,9 @@ export default class BabylonJSController {
|
|
|
2287
2522
|
}
|
|
2288
2523
|
|
|
2289
2524
|
this.#state.resize.isScheduled = true;
|
|
2290
|
-
const waitMs = Math.max(0,
|
|
2525
|
+
const waitMs = Math.max(0, BabylonJSController.config.resize.throttleMs - elapsed);
|
|
2291
2526
|
this.#state.resize.timeoutId = setTimeout(() => {
|
|
2292
|
-
applyResize();
|
|
2527
|
+
this.#applyResize(elapsed);
|
|
2293
2528
|
}, waitMs);
|
|
2294
2529
|
}
|
|
2295
2530
|
|
|
@@ -2373,11 +2608,15 @@ export default class BabylonJSController {
|
|
|
2373
2608
|
}
|
|
2374
2609
|
|
|
2375
2610
|
/**
|
|
2376
|
-
*
|
|
2611
|
+
* Cancels pending cube navigation and applies configured camera selection and locking.
|
|
2612
|
+
* Configures unlocked orbital cameras for proportional native wheel input, rebinds the model zoom guard
|
|
2613
|
+
* and synchronizes the cube after selecting the active camera.
|
|
2614
|
+
* A missing named camera falls back to the unlocked default camera, rather than retaining an imported view.
|
|
2377
2615
|
* @private
|
|
2378
2616
|
* @returns {boolean} True if camera options were set successfully, false otherwise.
|
|
2379
2617
|
*/
|
|
2380
2618
|
#setOptions_Camera() {
|
|
2619
|
+
this.#viewCube?.cancel();
|
|
2381
2620
|
const cameraState = this.#options.camera;
|
|
2382
2621
|
const modelContainer = this.#containers.model;
|
|
2383
2622
|
const environmentContainer = this.#containers.environment;
|
|
@@ -2395,31 +2634,22 @@ export default class BabylonJSController {
|
|
|
2395
2634
|
}
|
|
2396
2635
|
|
|
2397
2636
|
if (!camera) {
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
// If the current camera (different from the default) was found, use it
|
|
2404
|
-
camera.metadata = { locked: cameraState.locked };
|
|
2405
|
-
cameraState.setSuccess(false);
|
|
2406
|
-
} else {
|
|
2407
|
-
// If no camera was found, use the default camera
|
|
2408
|
-
camera = this.#camera;
|
|
2409
|
-
cameraState.value = null;
|
|
2410
|
-
cameraState.locked = this.#camera.metadata.locked;
|
|
2411
|
-
cameraState.setSuccess(false);
|
|
2412
|
-
}
|
|
2637
|
+
camera = this.#camera;
|
|
2638
|
+
camera.metadata = { ...camera.metadata, locked: false };
|
|
2639
|
+
cameraState.value = null;
|
|
2640
|
+
cameraState.locked = false;
|
|
2641
|
+
cameraState.setSuccess(false);
|
|
2413
2642
|
} else {
|
|
2414
2643
|
// If the requested camera was found, use it
|
|
2415
2644
|
if (cameraName !== null) {
|
|
2416
2645
|
camera.metadata = { locked: cameraState.isPending ? cameraState.update.locked : cameraState.locked };
|
|
2417
2646
|
} else {
|
|
2418
|
-
//
|
|
2647
|
+
// Selecting the default camera always restores interactive navigation.
|
|
2648
|
+
camera.metadata = { ...camera.metadata, locked: false };
|
|
2419
2649
|
if (cameraState.isPending) {
|
|
2420
|
-
cameraState.update.locked =
|
|
2650
|
+
cameraState.update.locked = false;
|
|
2421
2651
|
} else {
|
|
2422
|
-
cameraState.locked =
|
|
2652
|
+
cameraState.locked = false;
|
|
2423
2653
|
}
|
|
2424
2654
|
}
|
|
2425
2655
|
if (cameraState.isPending) {
|
|
@@ -2431,9 +2661,14 @@ export default class BabylonJSController {
|
|
|
2431
2661
|
this.#scene.activeCamera?.detachControl();
|
|
2432
2662
|
camera.detachControl();
|
|
2433
2663
|
if (!cameraState.locked) {
|
|
2664
|
+
if (camera instanceof ArcRotateCamera) {
|
|
2665
|
+
camera.wheelDeltaPercentage = BabylonJSController.config.camera.wheelDeltaPercentage;
|
|
2666
|
+
}
|
|
2434
2667
|
camera.attachControl(this.#canvas, true);
|
|
2435
2668
|
}
|
|
2436
2669
|
this.#scene.activeCamera = camera;
|
|
2670
|
+
this.#cameraZoom?.setCamera(camera);
|
|
2671
|
+
this.#viewCube?.sync();
|
|
2437
2672
|
return true;
|
|
2438
2673
|
}
|
|
2439
2674
|
|
|
@@ -2441,7 +2676,7 @@ export default class BabylonJSController {
|
|
|
2441
2676
|
* Applies pending image-based lighting (IBL) option updates.
|
|
2442
2677
|
* Marks the IBL state as successful, recreates lights so the new environment takes effect, and reports whether anything changed.
|
|
2443
2678
|
* @private
|
|
2444
|
-
* @returns {boolean} True when lights were refreshed due to pending IBL changes, otherwise false.
|
|
2679
|
+
* @returns {Promise<boolean>} True when lights were refreshed due to pending IBL changes, otherwise false.
|
|
2445
2680
|
* @description
|
|
2446
2681
|
* Delegates to `#createLights()`, which executes the full IBL URL-to-texture lifecycle:
|
|
2447
2682
|
* `cachedUrl -> HDRCubeTexture -> #hdrTexture clone -> consumeCachedUrl(true)`.
|
|
@@ -2590,6 +2825,7 @@ export default class BabylonJSController {
|
|
|
2590
2825
|
* Stops every animation group on the provided asset container to guarantee new loads start from a clean state.
|
|
2591
2826
|
* @private
|
|
2592
2827
|
* @param {AssetContainer} assetContainer - Container whose animation groups should be halted.
|
|
2828
|
+
* @returns {void}
|
|
2593
2829
|
*/
|
|
2594
2830
|
#assetContainer_stopAnimations(assetContainer) {
|
|
2595
2831
|
if (!assetContainer.animationGroups || assetContainer.animationGroups.length === 0) {
|
|
@@ -2628,8 +2864,12 @@ export default class BabylonJSController {
|
|
|
2628
2864
|
*/
|
|
2629
2865
|
#assetContainer_simplifyTransmissiveMaterials(assetContainer) {
|
|
2630
2866
|
assetContainer.materials?.forEach((mat) => {
|
|
2631
|
-
if (!(mat instanceof PBRMaterial))
|
|
2632
|
-
|
|
2867
|
+
if (!(mat instanceof PBRMaterial)) {
|
|
2868
|
+
return;
|
|
2869
|
+
}
|
|
2870
|
+
if (!mat.subSurface?.isRefractionEnabled && !mat.subSurface?.isTranslucencyEnabled) {
|
|
2871
|
+
return;
|
|
2872
|
+
}
|
|
2633
2873
|
this.#simplifyTransmissiveMaterial(mat);
|
|
2634
2874
|
});
|
|
2635
2875
|
}
|
|
@@ -2647,7 +2887,7 @@ export default class BabylonJSController {
|
|
|
2647
2887
|
mat.subSurface.isTranslucencyEnabled = false;
|
|
2648
2888
|
mat.alpha = Math.max(0.28, 1.0 - intensity * 0.65);
|
|
2649
2889
|
mat.transparencyMode = PBRMaterial.PBRMATERIAL_ALPHABLEND;
|
|
2650
|
-
markPrefViewerSimplifiedTransmissionMaterial(mat);
|
|
2890
|
+
MaterialUtils.markPrefViewerSimplifiedTransmissionMaterial(mat);
|
|
2651
2891
|
}
|
|
2652
2892
|
|
|
2653
2893
|
/**
|
|
@@ -2685,7 +2925,7 @@ export default class BabylonJSController {
|
|
|
2685
2925
|
/**
|
|
2686
2926
|
* Stops the Babylon.js render loop for the current scene.
|
|
2687
2927
|
* @private
|
|
2688
|
-
* @returns {void}
|
|
2928
|
+
* @returns {Promise<void>}
|
|
2689
2929
|
*/
|
|
2690
2930
|
async #stopRender() {
|
|
2691
2931
|
this.#stopEngineRenderLoop();
|
|
@@ -2700,11 +2940,13 @@ export default class BabylonJSController {
|
|
|
2700
2940
|
* @returns {Promise<void>}
|
|
2701
2941
|
*/
|
|
2702
2942
|
async #startRender() {
|
|
2703
|
-
if (!this.#scene)
|
|
2943
|
+
if (!this.#scene) {
|
|
2944
|
+
return;
|
|
2945
|
+
}
|
|
2704
2946
|
await this.#loadCameraDependentEffects();
|
|
2705
2947
|
await this.#scene.whenReadyAsync();
|
|
2706
|
-
const frames = this.#
|
|
2707
|
-
this.#requestRender({ frames: frames, continuousMs:
|
|
2948
|
+
const frames = this.#renderSettings.antiAliasingEnabled || this.#renderSettings.ambientOcclusionEnabled || this.#renderSettings.iblEnabled ? BabylonJSController.config.render.burstFramesEnhanced : BabylonJSController.config.render.burstFramesBase;
|
|
2949
|
+
this.#requestRender({ frames: frames, continuousMs: BabylonJSController.config.render.interactionMs });
|
|
2708
2950
|
}
|
|
2709
2951
|
|
|
2710
2952
|
/**
|
|
@@ -2755,6 +2997,9 @@ export default class BabylonJSController {
|
|
|
2755
2997
|
compileMaterials: true,
|
|
2756
2998
|
loadAllMaterials: true,
|
|
2757
2999
|
loadOnlyMaterials: container.state.name === "materials",
|
|
3000
|
+
// InstancedMesh overlays render through their source mesh. Regular model
|
|
3001
|
+
// meshes keep shared materials while allowing independent semantic colors.
|
|
3002
|
+
createInstances: container.state.name !== "model",
|
|
2758
3003
|
extensionOptions: {
|
|
2759
3004
|
ExtrasAsMetadata: { enabled: true },
|
|
2760
3005
|
},
|
|
@@ -2776,15 +3021,19 @@ export default class BabylonJSController {
|
|
|
2776
3021
|
}
|
|
2777
3022
|
|
|
2778
3023
|
/**
|
|
2779
|
-
*
|
|
3024
|
+
* Cancels pending cube navigation, loads asset containers and adds replacements to the scene.
|
|
3025
|
+
* Current model selection is resolved by the zoom guard on subsequent input updates.
|
|
2780
3026
|
* @private
|
|
3027
|
+
* @param {boolean} [force=false] - Whether to bypass existing asset cache metadata.
|
|
2781
3028
|
* @returns {Promise<{success: boolean, error: any}>} Resolves to an object indicating if loading succeeded and any error encountered.
|
|
2782
3029
|
* @description
|
|
2783
3030
|
* Waits for all containers to load in parallel, then replaces or adds them to the scene as needed.
|
|
2784
|
-
* Applies material
|
|
3031
|
+
* Applies material options and wall/floor visibility before camera fitting, then initializes lights and shadows.
|
|
2785
3032
|
* Returns an object with success status and error details.
|
|
2786
3033
|
*/
|
|
2787
3034
|
async #loadContainers(force = false) {
|
|
3035
|
+
this.#viewCube?.cancel();
|
|
3036
|
+
this.#resetPointerPickingState();
|
|
2788
3037
|
this.#detachAnimationChangedListener();
|
|
2789
3038
|
await this.#stopRender();
|
|
2790
3039
|
|
|
@@ -2837,9 +3086,10 @@ export default class BabylonJSController {
|
|
|
2837
3086
|
});
|
|
2838
3087
|
|
|
2839
3088
|
this.#setOptions_Materials();
|
|
3089
|
+
// Apply effective geometry visibility before calculating the camera fit.
|
|
3090
|
+
this.#setVisibilityOfWallAndFloorInModel();
|
|
2840
3091
|
this.#setOptions_Camera();
|
|
2841
3092
|
await this.#setOptions_IBL();
|
|
2842
|
-
this.#setVisibilityOfWallAndFloorInModel();
|
|
2843
3093
|
detail.success = true;
|
|
2844
3094
|
})
|
|
2845
3095
|
.catch((error) => {
|
|
@@ -2853,6 +3103,7 @@ export default class BabylonJSController {
|
|
|
2853
3103
|
this.#setMaxSimultaneousLights();
|
|
2854
3104
|
this.#enhanceTextureQuality();
|
|
2855
3105
|
this.#syncAnimationControllerAfterModelLoad();
|
|
3106
|
+
this.#selectionController?.refresh();
|
|
2856
3107
|
|
|
2857
3108
|
await this.#startRender();
|
|
2858
3109
|
});
|
|
@@ -2865,12 +3116,12 @@ export default class BabylonJSController {
|
|
|
2865
3116
|
* @returns {void}
|
|
2866
3117
|
*/
|
|
2867
3118
|
#syncAnimationControllerAfterModelLoad() {
|
|
2868
|
-
if (this.#
|
|
3119
|
+
if (this.#renderSettings.showAnimationMenu && this.#containers.model.assetContainer) {
|
|
2869
3120
|
this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
|
|
2870
3121
|
// Apply stored highlight settings so fresh page loads respect persisted state
|
|
2871
3122
|
this.#setHighlightConfig({
|
|
2872
|
-
enabled: this.#
|
|
2873
|
-
color: this.#
|
|
3123
|
+
enabled: this.#renderSettings.highlightEnabled,
|
|
3124
|
+
color: this.#renderSettings.highlightColor,
|
|
2874
3125
|
});
|
|
2875
3126
|
if (this.#babylonJSAnimationController?.hasAnimations?.()) {
|
|
2876
3127
|
this.#attachAnimationChangedListener();
|
|
@@ -2884,7 +3135,7 @@ export default class BabylonJSController {
|
|
|
2884
3135
|
* Reinstalls every camera-sensitive post-process (default pipeline, SSAO, shadows) after loads or option changes.
|
|
2885
3136
|
* Ensures the active camera always owns fresh pipelines so render quality remains consistent across reload cycles.
|
|
2886
3137
|
* @private
|
|
2887
|
-
* @returns {void}
|
|
3138
|
+
* @returns {Promise<void>}
|
|
2888
3139
|
*/
|
|
2889
3140
|
async #loadCameraDependentEffects() {
|
|
2890
3141
|
await this.#initializeVisualImprovements();
|
|
@@ -2896,7 +3147,7 @@ export default class BabylonJSController {
|
|
|
2896
3147
|
* Shuts down every post-process tied to the active camera before stopping the render loop.
|
|
2897
3148
|
* Ensures AA, SSAO, and shadow resources detach cleanly so reloads rebuild from scratch.
|
|
2898
3149
|
* @private
|
|
2899
|
-
* @returns {void}
|
|
3150
|
+
* @returns {Promise<void>}
|
|
2900
3151
|
*/
|
|
2901
3152
|
async #unloadCameraDependentEffects() {
|
|
2902
3153
|
await this.#disableVisualImprovements();
|
|
@@ -2937,19 +3188,13 @@ export default class BabylonJSController {
|
|
|
2937
3188
|
return;
|
|
2938
3189
|
}
|
|
2939
3190
|
|
|
2940
|
-
|
|
2941
|
-
if (data && typeof data === "string" && data.includes("/")) {
|
|
2942
|
-
data = data.slice(data.lastIndexOf("/") + 1);
|
|
2943
|
-
return data;
|
|
2944
|
-
}
|
|
2945
|
-
return false;
|
|
2946
|
-
};
|
|
3191
|
+
|
|
2947
3192
|
|
|
2948
3193
|
let applyNewTranslation = false;
|
|
2949
3194
|
let applyOldTranslation = false;
|
|
2950
3195
|
let undoOldTranslation = false;
|
|
2951
|
-
const newTranslationNodeName = getNodeName(newMetadata?.customProperties?.innerFloorPath);
|
|
2952
|
-
const oldTranslationNodeName = getNodeName(oldMetadata?.customProperties?.innerFloorPath);
|
|
3196
|
+
const newTranslationNodeName = BabylonJSController.#getNodeName(newMetadata?.customProperties?.innerFloorPath);
|
|
3197
|
+
const oldTranslationNodeName = BabylonJSController.#getNodeName(oldMetadata?.customProperties?.innerFloorPath);
|
|
2953
3198
|
|
|
2954
3199
|
if (this.#containers.environment.state.isSuccess) {
|
|
2955
3200
|
// If environment is being reloaded:
|
|
@@ -3062,7 +3307,7 @@ export default class BabylonJSController {
|
|
|
3062
3307
|
}
|
|
3063
3308
|
|
|
3064
3309
|
const locale = this.#prefViewer.culture;
|
|
3065
|
-
const texts = translate("prefViewer.downloadDialog", locale ? { locale } : undefined) || {};
|
|
3310
|
+
const texts = I18n.translate("prefViewer.downloadDialog", locale ? { locale } : undefined) || {};
|
|
3066
3311
|
const header = texts.title || "Download 3D Scene";
|
|
3067
3312
|
const content = `
|
|
3068
3313
|
<form slot="content" id="download-dialog-form" style="display:flex;flex-direction:column;gap:16px;">
|
|
@@ -3122,10 +3367,34 @@ export default class BabylonJSController {
|
|
|
3122
3367
|
* ---------------------------
|
|
3123
3368
|
*/
|
|
3124
3369
|
|
|
3370
|
+
/**
|
|
3371
|
+
* Creates the enabled cube once a live scene/camera exists and teardown is not in progress.
|
|
3372
|
+
* Resolves camera/model state through callbacks; does not reload assets or camera options.
|
|
3373
|
+
* @private
|
|
3374
|
+
* @returns {void}
|
|
3375
|
+
*/
|
|
3376
|
+
#initializeViewCube() {
|
|
3377
|
+
if (!this.#viewCubeEnabled || this.#viewCubeSuspended || this.#viewCube || !this.#engine || !this.#scene?.activeCamera) {
|
|
3378
|
+
return;
|
|
3379
|
+
}
|
|
3380
|
+
this.#viewCube = new BabylonJSViewCubeController({
|
|
3381
|
+
canvas: this.#canvas,
|
|
3382
|
+
getCamera: () => this.#scene?.activeCamera,
|
|
3383
|
+
isLocked: (camera) => !camera || camera.metadata?.locked === true || this.#XRExperience?.baseExperience?.state === WebXRState.IN_XR,
|
|
3384
|
+
getBounds: () => BabylonJSCameraFraming.getModelBounds(this.#containers.model?.assetContainer),
|
|
3385
|
+
getViewport: () => ({ width: this.#canvas.clientWidth, height: this.#canvas.clientHeight }),
|
|
3386
|
+
getCulture: this.#viewCubeConfig.getCulture,
|
|
3387
|
+
onLayout: this.#viewCubeConfig.onLayout,
|
|
3388
|
+
requestRender: () => this.#requestRender({ frames: 2 }),
|
|
3389
|
+
});
|
|
3390
|
+
}
|
|
3391
|
+
|
|
3125
3392
|
/**
|
|
3126
3393
|
* Initializes the Babylon.js engine, scene, camera, lights, and interaction handlers.
|
|
3127
|
-
* Configures Draco compression
|
|
3128
|
-
* Observes canvas resize events
|
|
3394
|
+
* Configures Draco compression and sets up XR; rendering starts through the existing load/request scheduler.
|
|
3395
|
+
* Observes canvas resize events, binds model-only orbital zoom protection and adaptive default-camera near clipping,
|
|
3396
|
+
* and lazily creates the requested cube. Imported camera clipping is preserved by the host policy.
|
|
3397
|
+
* Aborts remaining initialization if the scene was replaced/disposed during texture readiness.
|
|
3129
3398
|
* @public
|
|
3130
3399
|
* @returns {Promise<void>}
|
|
3131
3400
|
*/
|
|
@@ -3161,26 +3430,46 @@ export default class BabylonJSController {
|
|
|
3161
3430
|
this.#scene.skipPointerUpPicking = true;
|
|
3162
3431
|
|
|
3163
3432
|
this.#createCamera();
|
|
3433
|
+
this.#cameraZoom = new BabylonJSCameraZoomController(
|
|
3434
|
+
() => this.#containers.model?.state?.isVisible ? this.#containers.model.assetContainer : null,
|
|
3435
|
+
(camera) => camera.metadata?.locked === true || this.#XRExperience?.baseExperience?.state === WebXRState.IN_XR,
|
|
3436
|
+
() => this.#viewCube?.isAnimating === true,
|
|
3437
|
+
(camera) => camera === this.#camera,
|
|
3438
|
+
);
|
|
3439
|
+
this.#cameraZoom.setCamera(this.#camera);
|
|
3440
|
+
this.#viewCubeSuspended = false;
|
|
3441
|
+
this.#initializeViewCube();
|
|
3442
|
+
const enabledScene = this.#scene;
|
|
3164
3443
|
await this.#ensureEnvironmentBRDFTextureReady();
|
|
3444
|
+
if (!this.#engine || this.#scene !== enabledScene) {
|
|
3445
|
+
return;
|
|
3446
|
+
}
|
|
3165
3447
|
this.#enableInteraction();
|
|
3166
3448
|
await this.#createXRExperience();
|
|
3167
3449
|
}
|
|
3168
3450
|
|
|
3169
3451
|
/**
|
|
3170
3452
|
* Disposes the Babylon.js engine and disconnects the canvas resize observer.
|
|
3171
|
-
*
|
|
3453
|
+
* Releases the zoom observer and cube overlay before scene teardown, restoring the zoom camera
|
|
3454
|
+
* floor and suspending cube creation until the next enable. Cleans up light, XR and resolver resources.
|
|
3172
3455
|
* The teardown is asynchronous: it waits for XR/session-dependent shutdown work
|
|
3173
3456
|
* before disposing the engine, and coalesces concurrent calls into one in-flight promise.
|
|
3174
3457
|
* @public
|
|
3175
3458
|
* @returns {Promise<void>}
|
|
3176
3459
|
*/
|
|
3177
3460
|
async disable() {
|
|
3461
|
+
this.#cameraZoom?.dispose();
|
|
3462
|
+
this.#cameraZoom = null;
|
|
3463
|
+
this.#viewCubeSuspended = true;
|
|
3464
|
+
this.#viewCube?.dispose();
|
|
3465
|
+
this.#viewCube = null;
|
|
3178
3466
|
if (this.#disablingPromises.general) {
|
|
3179
3467
|
return await this.#disablingPromises.general;
|
|
3180
3468
|
}
|
|
3181
3469
|
|
|
3182
3470
|
this.#disablingPromises.general = (async () => {
|
|
3183
3471
|
this.#disableInteraction();
|
|
3472
|
+
this.#selectionController?.dispose();
|
|
3184
3473
|
this.#disposeAnimationController();
|
|
3185
3474
|
this.#disposeGLTFResolver();
|
|
3186
3475
|
try {
|
|
@@ -3205,6 +3494,42 @@ export default class BabylonJSController {
|
|
|
3205
3494
|
}
|
|
3206
3495
|
}
|
|
3207
3496
|
|
|
3497
|
+
/**
|
|
3498
|
+
* Lazily creates the cube when enabled and releases all cube resources when disabled.
|
|
3499
|
+
* Stores activation until a live scene is available; never reloads assets or camera options.
|
|
3500
|
+
* @public
|
|
3501
|
+
* @param {boolean} enabled - Whether the visible 3D viewer should own a ViewCube.
|
|
3502
|
+
* @returns {void}
|
|
3503
|
+
*/
|
|
3504
|
+
setViewCubeEnabled(enabled) {
|
|
3505
|
+
this.#viewCubeEnabled = Boolean(enabled);
|
|
3506
|
+
if (this.#viewCubeEnabled) {
|
|
3507
|
+
this.#initializeViewCube();
|
|
3508
|
+
} else {
|
|
3509
|
+
this.#viewCube?.dispose();
|
|
3510
|
+
this.#viewCube = null;
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
/**
|
|
3515
|
+
* Synchronizes the existing cube after culture or camera-state changes without creating an inactive cube.
|
|
3516
|
+
* @public
|
|
3517
|
+
* @returns {void}
|
|
3518
|
+
*/
|
|
3519
|
+
refreshViewCube() {
|
|
3520
|
+
this.#viewCube?.sync();
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
/**
|
|
3524
|
+
* Applies the host-owned semantic selection state to the current model meshes.
|
|
3525
|
+
* @public
|
|
3526
|
+
* @param {object} state - Canonical PrefViewer selection snapshot.
|
|
3527
|
+
* @returns {void}
|
|
3528
|
+
*/
|
|
3529
|
+
applySelectionState(state = {}) {
|
|
3530
|
+
this.#selectionController?.applySelectionState(state);
|
|
3531
|
+
}
|
|
3532
|
+
|
|
3208
3533
|
/**
|
|
3209
3534
|
* Downloads the current scene, model, or environment as a GLB file.
|
|
3210
3535
|
* @public
|
|
@@ -3314,7 +3639,7 @@ export default class BabylonJSController {
|
|
|
3314
3639
|
* @returns {{antiAliasingEnabled:boolean, ambientOcclusionEnabled:boolean, iblEnabled:boolean, shadowsEnabled:boolean}}
|
|
3315
3640
|
*/
|
|
3316
3641
|
getRenderSettings() {
|
|
3317
|
-
return { ...this.#
|
|
3642
|
+
return { ...this.#renderSettings };
|
|
3318
3643
|
}
|
|
3319
3644
|
|
|
3320
3645
|
/**
|
|
@@ -3324,7 +3649,7 @@ export default class BabylonJSController {
|
|
|
3324
3649
|
* @returns {object} Debug information for canvas, engine, scene, containers, render loop and settings.
|
|
3325
3650
|
*/
|
|
3326
3651
|
getDebugInfo() {
|
|
3327
|
-
|
|
3652
|
+
|
|
3328
3653
|
const rect = this.#canvas?.getBoundingClientRect?.();
|
|
3329
3654
|
const camera = this.#scene?.activeCamera ?? null;
|
|
3330
3655
|
const sceneMeshes = this.#scene?.meshes ?? [];
|
|
@@ -3334,62 +3659,16 @@ export default class BabylonJSController {
|
|
|
3334
3659
|
const modelAssetContainer = this.#containers?.model?.assetContainer ?? null;
|
|
3335
3660
|
const modelMeshes = modelAssetContainer?.meshes ?? [];
|
|
3336
3661
|
const enabledModelMeshes = modelMeshes.filter((mesh) => mesh.isEnabled?.() !== false && mesh.isVisible !== false);
|
|
3337
|
-
const textureToJSON = (texture) => ({
|
|
3338
|
-
name: texture?.name ?? null,
|
|
3339
|
-
url: texture?.url ?? null,
|
|
3340
|
-
isReady: texture?.isReady?.() ?? null,
|
|
3341
|
-
hasTexture: Boolean(texture),
|
|
3342
|
-
className: texture?.getClassName?.() ?? texture?.constructor?.name ?? null,
|
|
3343
|
-
});
|
|
3344
|
-
const summarizeMaterial = (material) => {
|
|
3345
|
-
const relatedMeshes = sceneMeshes.filter((mesh) => mesh.material === material);
|
|
3346
|
-
const sampleMesh = relatedMeshes[0] ?? null;
|
|
3347
|
-
const activeTextures = material?.getActiveTextures?.() ?? [];
|
|
3348
|
-
let isReadyWithoutMesh = null;
|
|
3349
|
-
let isReadyWithMesh = null;
|
|
3350
|
-
try {
|
|
3351
|
-
isReadyWithoutMesh = typeof material?.isReady === "function" ? material.isReady() : null;
|
|
3352
|
-
} catch (error) {
|
|
3353
|
-
isReadyWithoutMesh = `error: ${error?.message ?? error}`;
|
|
3354
|
-
}
|
|
3355
|
-
try {
|
|
3356
|
-
isReadyWithMesh = typeof material?.isReady === "function" && sampleMesh ? material.isReady(sampleMesh) : null;
|
|
3357
|
-
} catch (error) {
|
|
3358
|
-
isReadyWithMesh = `error: ${error?.message ?? error}`;
|
|
3359
|
-
}
|
|
3360
3662
|
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
id: material?.id ?? null,
|
|
3364
|
-
className: material?.getClassName?.() ?? material?.constructor?.name ?? null,
|
|
3365
|
-
isReadyWithoutMesh,
|
|
3366
|
-
isReadyWithMesh,
|
|
3367
|
-
relatedMeshes: relatedMeshes.length,
|
|
3368
|
-
sampleMesh: sampleMesh ? {
|
|
3369
|
-
name: sampleMesh.name,
|
|
3370
|
-
id: sampleMesh.id,
|
|
3371
|
-
enabled: sampleMesh.isEnabled?.() ?? null,
|
|
3372
|
-
isVisible: sampleMesh.isVisible ?? null,
|
|
3373
|
-
hasVertices: sampleMesh.getTotalVertices?.() ?? null,
|
|
3374
|
-
hasIndices: sampleMesh.getTotalIndices?.() ?? null,
|
|
3375
|
-
} : null,
|
|
3376
|
-
textures: activeTextures.map(textureToJSON),
|
|
3377
|
-
notReadyTextures: activeTextures.filter((texture) => texture?.isReady?.() === false).map(textureToJSON),
|
|
3378
|
-
forceDepthWrite: material?.forceDepthWrite ?? null,
|
|
3379
|
-
transparencyMode: material?.transparencyMode ?? null,
|
|
3380
|
-
alpha: material?.alpha ?? null,
|
|
3381
|
-
backFaceCulling: material?.backFaceCulling ?? null,
|
|
3382
|
-
disableLighting: material?.disableLighting ?? null,
|
|
3383
|
-
};
|
|
3384
|
-
};
|
|
3385
|
-
const materialDiagnostics = sceneMaterials.slice(0, 25).map(summarizeMaterial);
|
|
3663
|
+
|
|
3664
|
+
const materialDiagnostics = sceneMaterials.slice(0, 25).map((material) => BabylonJSController.#summarizeMaterial(material, sceneMeshes));
|
|
3386
3665
|
const notReadyMaterials = materialDiagnostics
|
|
3387
3666
|
.filter((material) => material.isReadyWithoutMesh === false || material.isReadyWithMesh === false || material.notReadyTextures.length > 0)
|
|
3388
3667
|
.slice(0, 25);
|
|
3389
3668
|
const notReadyTextures = sceneTextures
|
|
3390
3669
|
.filter((texture) => texture?.isReady?.() === false)
|
|
3391
3670
|
.slice(0, 25)
|
|
3392
|
-
.map(textureToJSON);
|
|
3671
|
+
.map(BabylonJSController.#textureToJSON);
|
|
3393
3672
|
const notReadyMeshes = sceneMeshes
|
|
3394
3673
|
.filter((mesh) => typeof mesh.isReady === "function" && !mesh.isReady())
|
|
3395
3674
|
.slice(0, 25)
|
|
@@ -3403,25 +3682,7 @@ export default class BabylonJSController {
|
|
|
3403
3682
|
const modelBounds = enabledModelMeshes.length && this.#scene?.getWorldExtends
|
|
3404
3683
|
? this.#scene.getWorldExtends((mesh) => enabledModelMeshes.includes(mesh))
|
|
3405
3684
|
: null;
|
|
3406
|
-
|
|
3407
|
-
show: container?.state?.show ?? null,
|
|
3408
|
-
visible: container?.state?.visible ?? null,
|
|
3409
|
-
mustBeShown: container?.state?.mustBeShown ?? null,
|
|
3410
|
-
isPending: container?.state?.isPending ?? null,
|
|
3411
|
-
isSuccess: container?.state?.isSuccess ?? null,
|
|
3412
|
-
hasAssetContainer: Boolean(container?.assetContainer),
|
|
3413
|
-
meshes: container?.assetContainer?.meshes?.length ?? 0,
|
|
3414
|
-
enabledMeshes: (container?.assetContainer?.meshes ?? []).filter((mesh) => mesh.isEnabled?.() !== false && mesh.isVisible !== false).length,
|
|
3415
|
-
materials: container?.assetContainer?.materials?.length ?? 0,
|
|
3416
|
-
cameras: container?.assetContainer?.cameras?.map((item) => ({
|
|
3417
|
-
name: item.name,
|
|
3418
|
-
id: item.id,
|
|
3419
|
-
className: item.getClassName?.() ?? item.constructor?.name ?? null,
|
|
3420
|
-
position: vectorToJSON(item.position),
|
|
3421
|
-
target: vectorToJSON(item.target),
|
|
3422
|
-
})) ?? [],
|
|
3423
|
-
animationGroups: container?.assetContainer?.animationGroups?.length ?? 0,
|
|
3424
|
-
});
|
|
3685
|
+
|
|
3425
3686
|
|
|
3426
3687
|
return {
|
|
3427
3688
|
canvas: {
|
|
@@ -3453,8 +3714,8 @@ export default class BabylonJSController {
|
|
|
3453
3714
|
name: camera.name,
|
|
3454
3715
|
id: camera.id,
|
|
3455
3716
|
className: camera.getClassName?.() ?? camera.constructor?.name ?? null,
|
|
3456
|
-
position: vectorToJSON(camera.position),
|
|
3457
|
-
target: vectorToJSON(camera.target),
|
|
3717
|
+
position: BabylonJSController.#vectorToJSON(camera.position),
|
|
3718
|
+
target: BabylonJSController.#vectorToJSON(camera.target),
|
|
3458
3719
|
radius: camera.radius ?? null,
|
|
3459
3720
|
alpha: camera.alpha ?? null,
|
|
3460
3721
|
beta: camera.beta ?? null,
|
|
@@ -3475,16 +3736,16 @@ export default class BabylonJSController {
|
|
|
3475
3736
|
notReadyMeshes,
|
|
3476
3737
|
},
|
|
3477
3738
|
containers: {
|
|
3478
|
-
model: summarizeContainer(this.#containers.model),
|
|
3479
|
-
environment: summarizeContainer(this.#containers.environment),
|
|
3480
|
-
materials: summarizeContainer(this.#containers.materials),
|
|
3739
|
+
model: BabylonJSController.#summarizeContainer(this.#containers.model),
|
|
3740
|
+
environment: BabylonJSController.#summarizeContainer(this.#containers.environment),
|
|
3741
|
+
materials: BabylonJSController.#summarizeContainer(this.#containers.materials),
|
|
3481
3742
|
},
|
|
3482
3743
|
modelBounds: modelBounds ? {
|
|
3483
|
-
min: vectorToJSON(modelBounds.min),
|
|
3484
|
-
max: vectorToJSON(modelBounds.max),
|
|
3744
|
+
min: BabylonJSController.#vectorToJSON(modelBounds.min),
|
|
3745
|
+
max: BabylonJSController.#vectorToJSON(modelBounds.max),
|
|
3485
3746
|
} : null,
|
|
3486
3747
|
render: { ...this.#state.render },
|
|
3487
|
-
settings: { ...this.#
|
|
3748
|
+
settings: { ...this.#renderSettings },
|
|
3488
3749
|
pipelines: {
|
|
3489
3750
|
default: Boolean(this.#renderPipelines.default),
|
|
3490
3751
|
ssao: Boolean(this.#renderPipelines.ssao),
|
|
@@ -3497,7 +3758,8 @@ export default class BabylonJSController {
|
|
|
3497
3758
|
* Loads all asset containers (model, environment, materials, etc.) and adds them to the scene.
|
|
3498
3759
|
* Applies material and camera options, sets visibility, and initializes lights and shadows.
|
|
3499
3760
|
* @public
|
|
3500
|
-
* @
|
|
3761
|
+
* @param {boolean} [force=false] - Bypasses asset cache metadata checks when reloading.
|
|
3762
|
+
* @returns {Promise<{success:boolean,error:*}>} Result of loading and preparing the asset containers.
|
|
3501
3763
|
*/
|
|
3502
3764
|
async load(force = false) {
|
|
3503
3765
|
return await this.#loadContainers(force);
|
|
@@ -3533,7 +3795,7 @@ export default class BabylonJSController {
|
|
|
3533
3795
|
* Reapplies image-based lighting configuration (HDR URL, intensity, shadow mode).
|
|
3534
3796
|
* Stops rendering, pushes pending IBL state into the scene, rebuilds camera-dependent effects, then resumes rendering.
|
|
3535
3797
|
* @public
|
|
3536
|
-
* @returns {boolean} True if IBL options were set successfully, false otherwise.
|
|
3798
|
+
* @returns {Promise<boolean>} True if IBL options were set successfully, false otherwise.
|
|
3537
3799
|
*/
|
|
3538
3800
|
async setIBLOptions() {
|
|
3539
3801
|
await this.#stopRender();
|
|
@@ -3548,7 +3810,7 @@ export default class BabylonJSController {
|
|
|
3548
3810
|
* Applies animation-menu, highlight, and time-of-day lighting configuration without forcing a full scene reload.
|
|
3549
3811
|
* Values are merged into the persisted render settings snapshot unless `config.persist === false`.
|
|
3550
3812
|
* When `showLightingTimeOfDay` is set to false and no explicit `lightingTimeOfDay` is supplied,
|
|
3551
|
-
* the active lighting time is reset to `
|
|
3813
|
+
* the active lighting time is reset to `BabylonJSController.config.render.defaultSettings.lightingTimeOfDay` (0.5).
|
|
3552
3814
|
* @public
|
|
3553
3815
|
* @param {object} config - Immediate render/illumination configuration.
|
|
3554
3816
|
* @param {boolean|string} [config.showAnimationMenu] - Enables/disables the animation controls when animations are available.
|
|
@@ -3566,25 +3828,25 @@ export default class BabylonJSController {
|
|
|
3566
3828
|
const showLighting = typeof config.showLightingTimeOfDay === "string"
|
|
3567
3829
|
? config.showLightingTimeOfDay.toLowerCase() !== "false"
|
|
3568
3830
|
: Boolean(config.showLightingTimeOfDay);
|
|
3569
|
-
if (this.#
|
|
3831
|
+
if (this.#renderSettings.showLightingTimeOfDay !== showLighting) {
|
|
3570
3832
|
shouldApplyLighting = true;
|
|
3571
3833
|
}
|
|
3572
|
-
this.#
|
|
3834
|
+
this.#renderSettings.showLightingTimeOfDay = showLighting;
|
|
3573
3835
|
|
|
3574
3836
|
const hasExplicitLightingTime = typeof config.lightingTimeOfDay === "number" && Number.isFinite(config.lightingTimeOfDay);
|
|
3575
3837
|
if (!showLighting && !hasExplicitLightingTime) {
|
|
3576
|
-
const defaultTime = BabylonJSController.
|
|
3577
|
-
if (this.#
|
|
3578
|
-
this.#
|
|
3838
|
+
const defaultTime = BabylonJSController.config.render.defaultSettings.lightingTimeOfDay;
|
|
3839
|
+
if (this.#renderSettings.lightingTimeOfDay !== defaultTime) {
|
|
3840
|
+
this.#renderSettings.lightingTimeOfDay = defaultTime;
|
|
3579
3841
|
shouldApplyLighting = true;
|
|
3580
3842
|
}
|
|
3581
3843
|
}
|
|
3582
3844
|
}
|
|
3583
3845
|
|
|
3584
3846
|
if (typeof config.lightingTimeOfDay === "number" && Number.isFinite(config.lightingTimeOfDay)) {
|
|
3585
|
-
const normalizedTime = clamp01(config.lightingTimeOfDay);
|
|
3586
|
-
if (this.#
|
|
3587
|
-
this.#
|
|
3847
|
+
const normalizedTime = RenderMath.clamp01(config.lightingTimeOfDay);
|
|
3848
|
+
if (this.#renderSettings.lightingTimeOfDay !== normalizedTime) {
|
|
3849
|
+
this.#renderSettings.lightingTimeOfDay = normalizedTime;
|
|
3588
3850
|
shouldApplyLighting = true;
|
|
3589
3851
|
}
|
|
3590
3852
|
}
|
|
@@ -3593,7 +3855,7 @@ export default class BabylonJSController {
|
|
|
3593
3855
|
const showMenu = typeof config.showAnimationMenu === "string"
|
|
3594
3856
|
? config.showAnimationMenu.toLowerCase() !== "false"
|
|
3595
3857
|
: Boolean(config.showAnimationMenu);
|
|
3596
|
-
this.#
|
|
3858
|
+
this.#renderSettings.showAnimationMenu = showMenu;
|
|
3597
3859
|
this.#syncAnimationControllerAfterModelLoad();
|
|
3598
3860
|
}
|
|
3599
3861
|
|
|
@@ -3606,12 +3868,12 @@ export default class BabylonJSController {
|
|
|
3606
3868
|
} else if (enabled === null) {
|
|
3607
3869
|
enabled = undefined;
|
|
3608
3870
|
}
|
|
3609
|
-
// Keep #
|
|
3871
|
+
// Keep #renderSettings in sync so getRenderSettings() always reflects the real state
|
|
3610
3872
|
if (enabled !== undefined) {
|
|
3611
|
-
this.#
|
|
3873
|
+
this.#renderSettings.highlightEnabled = enabled;
|
|
3612
3874
|
}
|
|
3613
3875
|
if (config.highlightColor !== undefined && config.highlightColor !== null) {
|
|
3614
|
-
this.#
|
|
3876
|
+
this.#renderSettings.highlightColor = config.highlightColor;
|
|
3615
3877
|
}
|
|
3616
3878
|
this.#setHighlightConfig({
|
|
3617
3879
|
color: config.highlightColor ?? undefined,
|