@preference-sl/pref-viewer 2.16.2-beta.1 → 2.17.0-beta.0
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 +137 -7
- package/index.d.ts +9 -0
- package/package.json +1 -1
- package/src/babylonjs-camera-framing.js +171 -0
- package/src/babylonjs-camera-zoom-controller.js +272 -0
- package/src/babylonjs-controller.js +312 -205
- package/src/babylonjs-view-cube-controller.js +718 -0
- package/src/localization/translations.js +18 -0
- package/src/pref-viewer-3d.js +29 -5
- package/src/pref-viewer-menu-3d.js +2 -2
- package/src/pref-viewer.js +99 -24
- package/src/styles.js +94 -3
|
@@ -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,6 +8,9 @@ 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";
|
|
11
14
|
import OpeningAnimation from "./babylonjs-animation-opening.js";
|
|
12
15
|
import { translate } from "./localization/i18n.js";
|
|
13
16
|
import { PrefViewerStyleVariables } from "./styles.js";
|
|
@@ -34,11 +37,12 @@ export { markPrefViewerSimplifiedTransmissionMaterial, isPrefViewerPhysicalGlass
|
|
|
34
37
|
* - Manages keyboard/pointer/wheel handlers, animation menus, and WebXR so PrefViewer menus and DOM attributes stay
|
|
35
38
|
* synchronized with Babylon state, using sampled pointer picking and last-picked tracking to reduce raycast/highlight
|
|
36
39
|
* cost on dense scenes.
|
|
40
|
+
* - Lazily owns the ViewCube overlay and binds a model-only orbital zoom guard independently of cube visibility.
|
|
37
41
|
* - Generates GLB, glTF+ZIP, or USDZ exports with timestamped names and localized dialog copy.
|
|
38
42
|
* - Translates metadata (inner floor offsets, cast/receive shadows, camera locks) into scene adjustments after reloads.
|
|
39
43
|
*
|
|
40
44
|
* Runtime Flow
|
|
41
|
-
* 1. Instantiate with `new BabylonJSController(canvas, containers, options)`.
|
|
45
|
+
* 1. Instantiate with `new BabylonJSController(canvas, containers, options, viewCubeConfig?)`.
|
|
42
46
|
* 2. Call `enable()` to configure Draco, spin up the engine/scene, and attach interaction + XR hooks. The render loop
|
|
43
47
|
* does not start here; it starts automatically after the first `load()` completes.
|
|
44
48
|
* 3. When PrefViewer marks containers/options pending, invoke `load()` (or `reloadWithCurrentSettings()`) so the controller
|
|
@@ -52,7 +56,8 @@ export { markPrefViewerSimplifiedTransmissionMaterial, isPrefViewerPhysicalGlass
|
|
|
52
56
|
* shared GLTF resolver, which closes its internal IndexedDB handle.
|
|
53
57
|
*
|
|
54
58
|
* Public API Highlights
|
|
55
|
-
* - constructor(canvas, containers, options)
|
|
59
|
+
* - constructor(canvas, containers, options, viewCubeConfig?)
|
|
60
|
+
* - setViewCubeEnabled(enabled) / refreshViewCube()
|
|
56
61
|
* - enable() / disable()
|
|
57
62
|
* - load(force?) / reloadWithCurrentSettings()
|
|
58
63
|
* - downloadGLB(content) / downloadGLTF(content) / downloadUSDZ(content)
|
|
@@ -78,8 +83,9 @@ export { markPrefViewerSimplifiedTransmissionMaterial, isPrefViewerPhysicalGlass
|
|
|
78
83
|
* - Browser-only features guard `window`, localStorage, and XR APIs before use so the controller is safe to construct
|
|
79
84
|
* in SSR/Node contexts (though functionality activates only in browsers).
|
|
80
85
|
* - Pointer-pick lifecycle: hover/highlight raycasts are sampled on POINTERMOVE (time + distance thresholds), wheel
|
|
81
|
-
* input avoids picks
|
|
82
|
-
*
|
|
86
|
+
* input avoids interaction picks; post-input orbital zoom protection separately probes only model meshes.
|
|
87
|
+
* Right-click POINTERUP performs an on-demand interaction pick for context-menu targeting.
|
|
88
|
+
* - Runtime bookkeeping is split into mutable `#state` and shared tuning settings under `BabylonJSController.config` to keep behavior changes
|
|
83
89
|
* explicit and reduce cross-field drift.
|
|
84
90
|
* - Resize lifecycle: canvas resize notifications are throttled (with trailing execution) before calling `engine.resize()`
|
|
85
91
|
* and any queued resize callback is canceled during teardown.
|
|
@@ -87,25 +93,6 @@ export { markPrefViewerSimplifiedTransmissionMaterial, isPrefViewerPhysicalGlass
|
|
|
87
93
|
* during XR exit and engine disposal.
|
|
88
94
|
*/
|
|
89
95
|
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
96
|
// Canvas HTML element
|
|
110
97
|
#canvas = null;
|
|
111
98
|
|
|
@@ -128,6 +115,13 @@ export default class BabylonJSController {
|
|
|
128
115
|
#hdrTexture = null; // Reusable in-memory HDR source cloned into scene.environmentTexture across reloads
|
|
129
116
|
|
|
130
117
|
#containers = {};
|
|
118
|
+
|
|
119
|
+
#viewCube = null;
|
|
120
|
+
#cameraZoom = null;
|
|
121
|
+
#viewCubeConfig = {};
|
|
122
|
+
#viewCubeEnabled = true;
|
|
123
|
+
#viewCubeSuspended = true;
|
|
124
|
+
|
|
131
125
|
#options = {};
|
|
132
126
|
|
|
133
127
|
#gltfResolver = null; // GLTFResolver instance
|
|
@@ -147,7 +141,7 @@ export default class BabylonJSController {
|
|
|
147
141
|
renderLoop: null,
|
|
148
142
|
};
|
|
149
143
|
|
|
150
|
-
#
|
|
144
|
+
#renderSettings = { ...BabylonJSController.config.render.defaultSettings };
|
|
151
145
|
|
|
152
146
|
// Runtime mutable state (changes while the app is running).
|
|
153
147
|
#state = {
|
|
@@ -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,47 @@ 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
|
+
},
|
|
223
|
+
// Hardware-adaptive quality settings (applied based on GPU capabilities)
|
|
224
|
+
quality: {
|
|
225
|
+
// Settings for different hardware tiers: 'low', 'medium', 'high'
|
|
226
|
+
// Applied automatically based on GPU detected capabilities
|
|
227
|
+
tier: 'medium', // Default, will be updated after hardware detection
|
|
228
|
+
shadowMapSize: 1024, // Standard shadow generator resolution
|
|
229
|
+
iblShadowResolution: 1, // IBL shadow resolution exponent (0=1024, 1=2048, 2=4096)
|
|
230
|
+
iblSampleDirections: 4, // IBL shadow quality
|
|
231
|
+
shadowBlurKernel: 16, // Shadow blur amount
|
|
232
|
+
ssaoEnabled: true, // Screen Space Ambient Occlusion
|
|
233
|
+
},
|
|
234
|
+
render: {
|
|
235
|
+
// Default render settings. Add new persisted render toggles here and update their translation labels.
|
|
236
|
+
defaultSettings: {
|
|
237
|
+
antiAliasingEnabled: true,
|
|
238
|
+
ambientOcclusionEnabled: true,
|
|
239
|
+
iblEnabled: true,
|
|
240
|
+
shadowsEnabled: false,
|
|
241
|
+
// Animation menu and highlight settings
|
|
242
|
+
showAnimationMenu: true,
|
|
243
|
+
highlightEnabled: true,
|
|
244
|
+
highlightColor: PrefViewerStyleVariables.colorPrimary,
|
|
245
|
+
// Lighting time of day (0=midnight, 0.5=noon, 1=midnight)
|
|
246
|
+
showLightingTimeOfDay: true,
|
|
247
|
+
lightingTimeOfDay: 0.5,
|
|
248
|
+
},
|
|
249
|
+
burstFramesBase: 2,
|
|
250
|
+
burstFramesEnhanced: 32, // When AA/SSAO/IBL is enabled, more frames are needed to reach stable output
|
|
251
|
+
interactionMs: 250,
|
|
252
|
+
animationMs: 200,
|
|
253
|
+
idleThrottleMs: 1000 / 8, // Reduced from 15 FPS to 8 FPS for idle - reduces GPU/CPU load
|
|
254
|
+
idleThresholdMs: 3000, // Time before going to true idle (no renders)
|
|
255
|
+
},
|
|
256
|
+
resize: {
|
|
257
|
+
throttleMs: 50, // Cap resize work to ~20 Hz while dragging/resizing containers
|
|
258
|
+
},
|
|
259
|
+
storageKey: "pref-viewer/render-settings",
|
|
241
260
|
};
|
|
242
261
|
|
|
243
262
|
/**
|
|
@@ -474,7 +493,7 @@ export default class BabylonJSController {
|
|
|
474
493
|
const preset = presetMap[tier] || qualityPresets.medium;
|
|
475
494
|
|
|
476
495
|
// Apply preset to config
|
|
477
|
-
|
|
496
|
+
BabylonJSController.config.quality = {
|
|
478
497
|
tier,
|
|
479
498
|
...preset,
|
|
480
499
|
// Legacy compatibility
|
|
@@ -498,14 +517,20 @@ export default class BabylonJSController {
|
|
|
498
517
|
* @param {HTMLCanvasElement|null} canvas - The canvas element to render the scene on.
|
|
499
518
|
* @param {object} containers - An object containing container states for model, environment, materials, etc.
|
|
500
519
|
* @param {object} options - Configuration options for the Babylon.js scene and assets.
|
|
520
|
+
* @param {object} [viewCubeConfig={}] - Internal overlay integration options.
|
|
521
|
+
* @param {boolean} [viewCubeConfig.enabled=true] - Desired activation; creation waits for a live scene and camera.
|
|
522
|
+
* @param {function(): string} [viewCubeConfig.getCulture] - Current UI culture for cube labels.
|
|
523
|
+
* @param {function(number, number): void} [viewCubeConfig.onLayout] - Measured cube SVG height and scene viewport height in CSS pixels; (0, 0) on disposal.
|
|
501
524
|
* @returns {BabylonJSController}
|
|
502
525
|
* @description
|
|
503
526
|
* - Assigns the provided canvas to the controller.
|
|
504
527
|
* - Initializes each container with its state and sets assetContainer to null.
|
|
505
528
|
* - Stores the provided options for later use in scene setup and asset loading.
|
|
506
529
|
*/
|
|
507
|
-
constructor(canvas = null, containers = {}, options = {}) {
|
|
530
|
+
constructor(canvas = null, containers = {}, options = {}, viewCubeConfig = {}) {
|
|
508
531
|
this.#canvas = canvas;
|
|
532
|
+
this.#viewCubeConfig = viewCubeConfig;
|
|
533
|
+
this.#viewCubeEnabled = viewCubeConfig.enabled !== false;
|
|
509
534
|
Object.keys(containers).forEach((key) => {
|
|
510
535
|
this.#containers[key] = {
|
|
511
536
|
assetContainer: null,
|
|
@@ -563,18 +588,18 @@ export default class BabylonJSController {
|
|
|
563
588
|
let changed = false;
|
|
564
589
|
Object.keys(settings).forEach((key) => {
|
|
565
590
|
// Handle boolean settings
|
|
566
|
-
if (typeof settings[key] === "boolean" && this.#
|
|
567
|
-
this.#
|
|
591
|
+
if (typeof settings[key] === "boolean" && this.#renderSettings[key] !== settings[key]) {
|
|
592
|
+
this.#renderSettings[key] = settings[key];
|
|
568
593
|
changed = true;
|
|
569
594
|
}
|
|
570
595
|
// Handle string settings (like illuminationColor)
|
|
571
|
-
if (typeof settings[key] === "string" && this.#
|
|
572
|
-
this.#
|
|
596
|
+
if (typeof settings[key] === "string" && this.#renderSettings[key] !== settings[key]) {
|
|
597
|
+
this.#renderSettings[key] = settings[key];
|
|
573
598
|
changed = true;
|
|
574
599
|
}
|
|
575
600
|
// Handle numeric settings such as lightingTimeOfDay
|
|
576
|
-
if (typeof settings[key] === "number" && Number.isFinite(settings[key]) && this.#
|
|
577
|
-
this.#
|
|
601
|
+
if (typeof settings[key] === "number" && Number.isFinite(settings[key]) && this.#renderSettings[key] !== settings[key]) {
|
|
602
|
+
this.#renderSettings[key] = settings[key];
|
|
578
603
|
changed = true;
|
|
579
604
|
}
|
|
580
605
|
});
|
|
@@ -597,23 +622,23 @@ export default class BabylonJSController {
|
|
|
597
622
|
return;
|
|
598
623
|
}
|
|
599
624
|
try {
|
|
600
|
-
const serialized = window.localStorage.getItem(
|
|
625
|
+
const serialized = window.localStorage.getItem(BabylonJSController.config.storageKey);
|
|
601
626
|
if (!serialized) {
|
|
602
627
|
return;
|
|
603
628
|
}
|
|
604
629
|
const parsed = JSON.parse(serialized);
|
|
605
|
-
Object.keys(BabylonJSController.
|
|
630
|
+
Object.keys(BabylonJSController.config.render.defaultSettings).forEach((key) => {
|
|
606
631
|
// Handle boolean settings
|
|
607
632
|
if (typeof parsed?.[key] === "boolean") {
|
|
608
|
-
this.#
|
|
633
|
+
this.#renderSettings[key] = parsed[key];
|
|
609
634
|
}
|
|
610
635
|
// Handle string settings (like illuminationColor)
|
|
611
636
|
if (typeof parsed?.[key] === "string") {
|
|
612
|
-
this.#
|
|
637
|
+
this.#renderSettings[key] = parsed[key];
|
|
613
638
|
}
|
|
614
639
|
// Handle numeric settings such as lightingTimeOfDay
|
|
615
640
|
if (typeof parsed?.[key] === "number" && Number.isFinite(parsed[key])) {
|
|
616
|
-
this.#
|
|
641
|
+
this.#renderSettings[key] = parsed[key];
|
|
617
642
|
}
|
|
618
643
|
});
|
|
619
644
|
} catch (error) {
|
|
@@ -632,7 +657,7 @@ export default class BabylonJSController {
|
|
|
632
657
|
return;
|
|
633
658
|
}
|
|
634
659
|
try {
|
|
635
|
-
window.localStorage.setItem(
|
|
660
|
+
window.localStorage.setItem(BabylonJSController.config.storageKey, JSON.stringify(this.#renderSettings));
|
|
636
661
|
} catch (error) {
|
|
637
662
|
console.warn("PrefViewer: unable to store render settings", error);
|
|
638
663
|
}
|
|
@@ -687,7 +712,7 @@ export default class BabylonJSController {
|
|
|
687
712
|
|
|
688
713
|
const timeoutMs = 2000;
|
|
689
714
|
const startedAt = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
690
|
-
this.#requestRender({ frames:
|
|
715
|
+
this.#requestRender({ frames: BabylonJSController.config.render.burstFramesEnhanced, continuousMs: timeoutMs });
|
|
691
716
|
|
|
692
717
|
try {
|
|
693
718
|
await new Promise((resolve) => {
|
|
@@ -844,7 +869,7 @@ export default class BabylonJSController {
|
|
|
844
869
|
|
|
845
870
|
/**
|
|
846
871
|
* Evaluates whether the renderer should stay in continuous mode.
|
|
847
|
-
* XR
|
|
872
|
+
* Pending ViewCube transitions and active XR force continuous rendering; animation/camera motion extends the
|
|
848
873
|
* continuous deadline window to avoid abrupt stop-start behavior.
|
|
849
874
|
* OPTIMIZATION: Added true idle mode - stops rendering completely after idleThresholdMs of inactivity.
|
|
850
875
|
* @private
|
|
@@ -852,6 +877,9 @@ export default class BabylonJSController {
|
|
|
852
877
|
* @returns {boolean} True when continuous rendering should remain active.
|
|
853
878
|
*/
|
|
854
879
|
#shouldRenderContinuously(now) {
|
|
880
|
+
if (this.#viewCube?.isAnimating) {
|
|
881
|
+
return true;
|
|
882
|
+
}
|
|
855
883
|
const inXR = this.#XRExperience?.baseExperience?.state === WebXRState.IN_XR;
|
|
856
884
|
if (inXR) {
|
|
857
885
|
this.#state.render.isIdle = false;
|
|
@@ -863,19 +891,19 @@ export default class BabylonJSController {
|
|
|
863
891
|
const cameraInMotion = this.#isCameraInMotion();
|
|
864
892
|
|
|
865
893
|
if (animationRunning) {
|
|
866
|
-
this.#state.render.continuousUntil = Math.max(this.#state.render.continuousUntil, now +
|
|
894
|
+
this.#state.render.continuousUntil = Math.max(this.#state.render.continuousUntil, now + BabylonJSController.config.render.animationMs);
|
|
867
895
|
this.#state.render.isIdle = false;
|
|
868
896
|
this.#state.render.lastActivityAt = now;
|
|
869
897
|
}
|
|
870
898
|
if (cameraInMotion) {
|
|
871
|
-
this.#state.render.continuousUntil = Math.max(this.#state.render.continuousUntil, now +
|
|
899
|
+
this.#state.render.continuousUntil = Math.max(this.#state.render.continuousUntil, now + BabylonJSController.config.render.interactionMs);
|
|
872
900
|
this.#state.render.isIdle = false;
|
|
873
901
|
this.#state.render.lastActivityAt = now;
|
|
874
902
|
}
|
|
875
903
|
|
|
876
904
|
// OPTIMIZATION: True idle mode - if no activity for idleThresholdMs, stop rendering completely
|
|
877
905
|
const timeSinceActivity = now - this.#state.render.lastActivityAt;
|
|
878
|
-
if (timeSinceActivity >
|
|
906
|
+
if (timeSinceActivity > BabylonJSController.config.render.idleThresholdMs) {
|
|
879
907
|
this.#state.render.isIdle = true;
|
|
880
908
|
}
|
|
881
909
|
|
|
@@ -891,7 +919,8 @@ export default class BabylonJSController {
|
|
|
891
919
|
|
|
892
920
|
/**
|
|
893
921
|
* Render loop callback for Babylon.js.
|
|
894
|
-
* Runs
|
|
922
|
+
* Runs while scene state is dirty, interactive motion/animations are active, or XR/ViewCube navigation requires it.
|
|
923
|
+
* Suspends dynamic zoom constraints during cube framing, advances its transition, renders and synchronizes its indicator.
|
|
895
924
|
* It self-stops when the scene becomes idle (true idle mode - no renders at all).
|
|
896
925
|
* OPTIMIZATION: Added true idle mode that completely stops rendering after idleThresholdMs.
|
|
897
926
|
* @private
|
|
@@ -918,11 +947,16 @@ export default class BabylonJSController {
|
|
|
918
947
|
return;
|
|
919
948
|
}
|
|
920
949
|
|
|
921
|
-
if (!continuous && this.#state.render.lastRenderAt > 0 && now - this.#state.render.lastRenderAt <
|
|
950
|
+
if (!continuous && this.#state.render.lastRenderAt > 0 && now - this.#state.render.lastRenderAt < BabylonJSController.config.render.idleThrottleMs) {
|
|
922
951
|
return;
|
|
923
952
|
}
|
|
924
953
|
|
|
954
|
+
if (this.#viewCube?.isAnimating) {
|
|
955
|
+
this.#cameraZoom?.update();
|
|
956
|
+
}
|
|
957
|
+
this.#viewCube?.tick(now);
|
|
925
958
|
this.#scene.render();
|
|
959
|
+
this.#viewCube?.sync();
|
|
926
960
|
this.#state.render.lastRenderAt = now;
|
|
927
961
|
|
|
928
962
|
if (this.#state.render.dirtyFrames > 0) {
|
|
@@ -1005,15 +1039,20 @@ export default class BabylonJSController {
|
|
|
1005
1039
|
}
|
|
1006
1040
|
|
|
1007
1041
|
/**
|
|
1008
|
-
* Creates
|
|
1042
|
+
* Creates the default ArcRotateCamera with unrestricted beta, upright orbit and proportional native wheel zoom.
|
|
1043
|
+
* Starts at the exact Inner orientation (+Z, alpha/beta pi/2), matching the ViewCube before loading.
|
|
1044
|
+
* Starts without a fixed lower radius; the independent model zoom guard supplies the dynamic floor.
|
|
1045
|
+
* Imported-camera configuration is applied separately and is not reset here.
|
|
1009
1046
|
* @private
|
|
1010
1047
|
* @returns {void}
|
|
1011
1048
|
*/
|
|
1012
1049
|
#createCamera() {
|
|
1013
|
-
this.#camera = new ArcRotateCamera("defaultCamera",
|
|
1014
|
-
this.#camera.upperBetaLimit =
|
|
1015
|
-
this.#camera.lowerBetaLimit =
|
|
1016
|
-
this.#camera.
|
|
1050
|
+
this.#camera = new ArcRotateCamera("defaultCamera", Math.PI / 2, Math.PI / 2, 10, Vector3.Zero(), this.#scene);
|
|
1051
|
+
this.#camera.upperBetaLimit = null;
|
|
1052
|
+
this.#camera.lowerBetaLimit = null;
|
|
1053
|
+
this.#camera.allowUpsideDown = false;
|
|
1054
|
+
this.#camera.wheelDeltaPercentage = BabylonJSController.config.camera.wheelDeltaPercentage;
|
|
1055
|
+
this.#camera.lowerRadiusLimit = null;
|
|
1017
1056
|
this.#camera.upperRadiusLimit = 20;
|
|
1018
1057
|
this.#camera.metadata = { locked: false };
|
|
1019
1058
|
this.#applyShowroomCameraFraming(this.#camera);
|
|
@@ -1040,7 +1079,7 @@ export default class BabylonJSController {
|
|
|
1040
1079
|
|
|
1041
1080
|
let lightsChanged = false;
|
|
1042
1081
|
|
|
1043
|
-
const iblEnabled = this.#
|
|
1082
|
+
const iblEnabled = this.#renderSettings.iblEnabled && (this.#options.ibl?.valid === true || !!this.#options.ibl?.cachedUrl);
|
|
1044
1083
|
|
|
1045
1084
|
if (iblEnabled) {
|
|
1046
1085
|
lightsChanged = await this.#initializeEnvironmentTexture();
|
|
@@ -1120,7 +1159,7 @@ export default class BabylonJSController {
|
|
|
1120
1159
|
return false;
|
|
1121
1160
|
}
|
|
1122
1161
|
|
|
1123
|
-
if (!this.#
|
|
1162
|
+
if (!this.#renderSettings.ambientOcclusionEnabled) {
|
|
1124
1163
|
return false;
|
|
1125
1164
|
}
|
|
1126
1165
|
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
@@ -1221,7 +1260,7 @@ export default class BabylonJSController {
|
|
|
1221
1260
|
return false;
|
|
1222
1261
|
}
|
|
1223
1262
|
|
|
1224
|
-
if (!this.#
|
|
1263
|
+
if (!this.#renderSettings.antiAliasingEnabled) {
|
|
1225
1264
|
return false;
|
|
1226
1265
|
}
|
|
1227
1266
|
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
@@ -1283,7 +1322,7 @@ export default class BabylonJSController {
|
|
|
1283
1322
|
let hdrTexture = null;
|
|
1284
1323
|
if (this.#options.ibl?.cachedUrl) {
|
|
1285
1324
|
const hdrTextureURI = this.#options.ibl.cachedUrl;
|
|
1286
|
-
const envTextureSize =
|
|
1325
|
+
const envTextureSize = BabylonJSController.config.quality.environmentTextureSize || 1024;
|
|
1287
1326
|
hdrTexture = new HDRCubeTexture(hdrTextureURI, this.#scene, envTextureSize, false, false, false, true, undefined, undefined, false, true, true);
|
|
1288
1327
|
} else if (this.#hdrTexture && this.#options.ibl?.valid === true) {
|
|
1289
1328
|
hdrTexture = this.#hdrTexture.clone();
|
|
@@ -1411,8 +1450,8 @@ export default class BabylonJSController {
|
|
|
1411
1450
|
|
|
1412
1451
|
// Use hardware-adaptive quality settings
|
|
1413
1452
|
const pipelineOptions = {
|
|
1414
|
-
resolutionExp:
|
|
1415
|
-
sampleDirections:
|
|
1453
|
+
resolutionExp: BabylonJSController.config.quality.iblShadowResolution, // Adaptive: 0=1024, 1=2048, 2=4096
|
|
1454
|
+
sampleDirections: BabylonJSController.config.quality.iblSampleDirections, // Adaptive: 2=fast, 4=quality
|
|
1416
1455
|
ssShadowsEnabled: true,
|
|
1417
1456
|
shadowRemanence: 0.85,
|
|
1418
1457
|
triPlanarVoxelization: true,
|
|
@@ -1493,11 +1532,11 @@ export default class BabylonJSController {
|
|
|
1493
1532
|
}
|
|
1494
1533
|
this.#dirLight.autoUpdateExtends = false;
|
|
1495
1534
|
// Use quality-adaptive shadow map size
|
|
1496
|
-
const shadowMapSize =
|
|
1535
|
+
const shadowMapSize = BabylonJSController.config.quality.shadowMapSize;
|
|
1497
1536
|
const shadowGenerator = new ShadowGenerator(shadowMapSize, this.#dirLight);
|
|
1498
1537
|
shadowGenerator.useBlurExponentialShadowMap = true;
|
|
1499
1538
|
// Use quality-adaptive blur kernel
|
|
1500
|
-
shadowGenerator.blurKernel =
|
|
1539
|
+
shadowGenerator.blurKernel = BabylonJSController.config.quality.shadowBlurKernel;
|
|
1501
1540
|
shadowGenerator.darkness = 0.5;
|
|
1502
1541
|
shadowGenerator.bias = 0.0005;
|
|
1503
1542
|
shadowGenerator.normalBias = 0.02;
|
|
@@ -1579,13 +1618,13 @@ export default class BabylonJSController {
|
|
|
1579
1618
|
* Otherwise, sets up shadow casting and receiving for all relevant meshes using the shadow generator.
|
|
1580
1619
|
*/
|
|
1581
1620
|
async #initializeShadows() {
|
|
1582
|
-
if (!this.#
|
|
1621
|
+
if (!this.#renderSettings.shadowsEnabled) {
|
|
1583
1622
|
return false;
|
|
1584
1623
|
}
|
|
1585
1624
|
|
|
1586
1625
|
this.#ensureMeshesReceiveShadows();
|
|
1587
1626
|
|
|
1588
|
-
const iblEnabled = this.#
|
|
1627
|
+
const iblEnabled = this.#renderSettings.iblEnabled && (this.#options.ibl?.valid === true || !!this.#options.ibl?.cachedUrl);
|
|
1589
1628
|
const iblShadowsEnabled = iblEnabled && this.#options.ibl.shadows;
|
|
1590
1629
|
|
|
1591
1630
|
if (iblShadowsEnabled) {
|
|
@@ -1603,7 +1642,9 @@ export default class BabylonJSController {
|
|
|
1603
1642
|
* @returns {void}
|
|
1604
1643
|
*/
|
|
1605
1644
|
#setMaxSimultaneousLights() {
|
|
1606
|
-
if (!this.#scene)
|
|
1645
|
+
if (!this.#scene) {
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1607
1648
|
let lightsNumber = 1; // At least one light coming from the environment texture contribution
|
|
1608
1649
|
this.#scene.lights.forEach((light) => {
|
|
1609
1650
|
if (light.isEnabled()) {
|
|
@@ -1657,10 +1698,10 @@ export default class BabylonJSController {
|
|
|
1657
1698
|
return;
|
|
1658
1699
|
}
|
|
1659
1700
|
|
|
1660
|
-
const showroom =
|
|
1701
|
+
const showroom = BabylonJSController.config.look?.showroom || {};
|
|
1661
1702
|
const imageProcessing = this.#scene.imageProcessingConfiguration;
|
|
1662
1703
|
|
|
1663
|
-
const bg = showroom.backgroundDay ??
|
|
1704
|
+
const bg = showroom.backgroundDay ?? BabylonJSController.config.look.showroom.backgroundDay;
|
|
1664
1705
|
this.#scene.clearColor = new Color4(bg[0], bg[1], bg[2], 1);
|
|
1665
1706
|
imageProcessing.exposure = showroom.exposure ?? 0.95;
|
|
1666
1707
|
imageProcessing.contrast = showroom.contrast ?? 1.08;
|
|
@@ -1684,8 +1725,8 @@ export default class BabylonJSController {
|
|
|
1684
1725
|
return false;
|
|
1685
1726
|
}
|
|
1686
1727
|
|
|
1687
|
-
const showroom =
|
|
1688
|
-
const timeOfDay = clamp01(this.#
|
|
1728
|
+
const showroom = BabylonJSController.config.look?.showroom || {};
|
|
1729
|
+
const timeOfDay = clamp01(this.#renderSettings.lightingTimeOfDay ? this.#renderSettings.lightingTimeOfDay ?? 0.5 : 0.5);
|
|
1689
1730
|
const profile = getLightingCycleProfile(timeOfDay);
|
|
1690
1731
|
const daylight = profile.dayFactor;
|
|
1691
1732
|
const peakLight = daylight > 0 ? Math.pow(daylight, 0.72) : 0;
|
|
@@ -1703,7 +1744,7 @@ export default class BabylonJSController {
|
|
|
1703
1744
|
|
|
1704
1745
|
// Night end stays in low linear values (a dark blue sky); the day end is the calibrated
|
|
1705
1746
|
// HDR off-white. Both are linear already, so no toLinearSpace() on either.
|
|
1706
|
-
const bgDay = showroom.backgroundDay ??
|
|
1747
|
+
const bgDay = showroom.backgroundDay ?? BabylonJSController.config.look.showroom.backgroundDay;
|
|
1707
1748
|
this.#scene.clearColor = new Color4(
|
|
1708
1749
|
lerp(0.000627, bgDay[0], daylight),
|
|
1709
1750
|
lerp(0.001089, bgDay[1], daylight),
|
|
@@ -1760,7 +1801,7 @@ export default class BabylonJSController {
|
|
|
1760
1801
|
}
|
|
1761
1802
|
|
|
1762
1803
|
this.#scene.markAllMaterialsAsDirty?.(Material.LightDirtyFlag ?? Material.TextureDirtyFlag);
|
|
1763
|
-
this.#requestRender({ frames: 2, continuousMs:
|
|
1804
|
+
this.#requestRender({ frames: 2, continuousMs: BabylonJSController.config.render.interactionMs });
|
|
1764
1805
|
return true;
|
|
1765
1806
|
}
|
|
1766
1807
|
|
|
@@ -1776,81 +1817,62 @@ export default class BabylonJSController {
|
|
|
1776
1817
|
}
|
|
1777
1818
|
|
|
1778
1819
|
const pixelRatio = typeof window !== "undefined" ? window.devicePixelRatio : 1;
|
|
1779
|
-
const baseScaling =
|
|
1820
|
+
const baseScaling = BabylonJSController.config.quality.hardwareScaling || 1;
|
|
1780
1821
|
const scalingLevel = getAdaptiveHardwareScalingLevel(baseScaling, pixelRatio);
|
|
1781
1822
|
this.#engine.setHardwareScalingLevel(scalingLevel);
|
|
1782
1823
|
}
|
|
1783
1824
|
|
|
1784
1825
|
/**
|
|
1785
|
-
*
|
|
1826
|
+
* Frames the default camera using the shared model-framing calculation.
|
|
1827
|
+
* Preserves its current orientation and projection; excludes environment geometry.
|
|
1828
|
+
* Applies the destination immediately, without a ViewCube transition.
|
|
1829
|
+
* The default camera has no parent; imported cameras retain their authored framing.
|
|
1786
1830
|
* @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.
|
|
1831
|
+
* @param {Camera|null|undefined} camera - Camera about to become active.
|
|
1828
1832
|
* @returns {void}
|
|
1829
1833
|
*/
|
|
1830
1834
|
#fitDefaultCameraToModel(camera) {
|
|
1831
1835
|
if (camera !== this.#camera || !(camera instanceof ArcRotateCamera)) {
|
|
1832
1836
|
return;
|
|
1833
1837
|
}
|
|
1834
|
-
|
|
1835
|
-
const bounds = this.#getModelBounds();
|
|
1838
|
+
const bounds = BabylonJSCameraFraming.getModelBounds(this.#containers.model?.assetContainer);
|
|
1836
1839
|
if (!bounds) {
|
|
1837
1840
|
return;
|
|
1838
1841
|
}
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
const
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
camera.
|
|
1842
|
+
camera.getViewMatrix(true);
|
|
1843
|
+
const direction = camera.globalPosition.subtract(camera.getTarget());
|
|
1844
|
+
if (direction.lengthSquared() <= 1e-12) {
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
1847
|
+
direction.normalize();
|
|
1848
|
+
const fit = BabylonJSCameraFraming.calculateCameraFit(camera, bounds, direction, {
|
|
1849
|
+
width: this.#canvas.clientWidth,
|
|
1850
|
+
height: this.#canvas.clientHeight,
|
|
1851
|
+
});
|
|
1852
|
+
if (!fit) {
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
// Geometry protection supplies the minimum zoom radius.
|
|
1856
|
+
camera.lowerRadiusLimit = null;
|
|
1857
|
+
camera.upperRadiusLimit = fit.distance * BabylonJSController.config.camera.maxRadiusFactor;
|
|
1858
|
+
camera.upVector = Vector3.Up();
|
|
1859
|
+
camera.setTarget(fit.target);
|
|
1860
|
+
// Match ViewCube's destination orientation, including its small pole correction.
|
|
1861
|
+
const rotationMatrix = Matrix.Identity();
|
|
1862
|
+
fit.rotation.toRotationMatrix(rotationMatrix);
|
|
1863
|
+
const forward = Vector3.TransformNormal(
|
|
1864
|
+
new Vector3(0, 0, camera.getScene().useRightHandedSystem ? -1 : 1),
|
|
1865
|
+
rotationMatrix
|
|
1866
|
+
);
|
|
1867
|
+
camera.setPosition(fit.target.subtract(forward.scale(fit.distance)));
|
|
1868
|
+
if (camera.mode === Camera.ORTHOGRAPHIC_CAMERA) {
|
|
1869
|
+
camera.orthoLeft = -fit.halfWidth;
|
|
1870
|
+
camera.orthoRight = fit.halfWidth;
|
|
1871
|
+
camera.orthoBottom = -fit.halfHeight;
|
|
1872
|
+
camera.orthoTop = fit.halfHeight;
|
|
1873
|
+
}
|
|
1874
|
+
camera.minZ = Math.min(camera.minZ > 0 ? camera.minZ : Infinity, Math.max(fit.radius * 0.001, 0.00001));
|
|
1875
|
+
camera.maxZ = Math.max(camera.maxZ, fit.distance + fit.radius * 2);
|
|
1854
1876
|
}
|
|
1855
1877
|
|
|
1856
1878
|
/**
|
|
@@ -1896,7 +1918,7 @@ export default class BabylonJSController {
|
|
|
1896
1918
|
const x = this.#scene.pointerX;
|
|
1897
1919
|
const y = this.#scene.pointerY;
|
|
1898
1920
|
const state = this.#state.pointerPicking;
|
|
1899
|
-
const config =
|
|
1921
|
+
const config = BabylonJSController.config.pointerPicking;
|
|
1900
1922
|
|
|
1901
1923
|
if (!Number.isFinite(state.lastMovePickX) || !Number.isFinite(state.lastMovePickY)) {
|
|
1902
1924
|
state.lastMovePickX = x;
|
|
@@ -2065,7 +2087,7 @@ export default class BabylonJSController {
|
|
|
2065
2087
|
* @returns {void}
|
|
2066
2088
|
*/
|
|
2067
2089
|
#onAnimationGroupPlay() {
|
|
2068
|
-
this.#requestRender({ frames: 1, continuousMs:
|
|
2090
|
+
this.#requestRender({ frames: 1, continuousMs: BabylonJSController.config.render.animationMs });
|
|
2069
2091
|
}
|
|
2070
2092
|
|
|
2071
2093
|
/**
|
|
@@ -2074,11 +2096,11 @@ export default class BabylonJSController {
|
|
|
2074
2096
|
* @returns {void}
|
|
2075
2097
|
*/
|
|
2076
2098
|
#onAnimationGroupStop() {
|
|
2077
|
-
if (this.#
|
|
2099
|
+
if (this.#renderSettings.iblEnabled && this.#renderPipelines.iblShadows) {
|
|
2078
2100
|
this.#renderPipelines.iblShadows.updateVoxelization();
|
|
2079
2101
|
this.#scene?.postProcessRenderPipelineManager?.update();
|
|
2080
2102
|
}
|
|
2081
|
-
const frames = this.#
|
|
2103
|
+
const frames = this.#renderSettings.antiAliasingEnabled || this.#renderSettings.ambientOcclusionEnabled || this.#renderSettings.iblEnabled ? BabylonJSController.config.render.burstFramesEnhanced : BabylonJSController.config.render.burstFramesBase;
|
|
2082
2104
|
this.#requestRender({ frames: frames });
|
|
2083
2105
|
}
|
|
2084
2106
|
|
|
@@ -2131,7 +2153,8 @@ export default class BabylonJSController {
|
|
|
2131
2153
|
}
|
|
2132
2154
|
|
|
2133
2155
|
/**
|
|
2134
|
-
*
|
|
2156
|
+
* Wakes rendering for native orbital wheel zoom and handles wheel movement for target cameras.
|
|
2157
|
+
* ArcRotateCamera owns proportional wheel zoom; do not add a second radius offset here.
|
|
2135
2158
|
* OPTIMIZATION: Tracks activity for idle mode.
|
|
2136
2159
|
* @private
|
|
2137
2160
|
* @param {WheelEvent} event - The mouse wheel event.
|
|
@@ -2151,10 +2174,7 @@ export default class BabylonJSController {
|
|
|
2151
2174
|
return false;
|
|
2152
2175
|
}
|
|
2153
2176
|
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) {
|
|
2177
|
+
if (camera instanceof FreeCamera || camera instanceof UniversalCamera) {
|
|
2158
2178
|
camera.wheelPrecision = camera.wheelPrecision || 3.0;
|
|
2159
2179
|
const zoomSpeed = -event.deltaY * camera.wheelPrecision * 0.001;
|
|
2160
2180
|
|
|
@@ -2163,7 +2183,7 @@ export default class BabylonJSController {
|
|
|
2163
2183
|
const movementVector = direction.scale(zoomSpeed);
|
|
2164
2184
|
camera.position = camera.position.add(movementVector);
|
|
2165
2185
|
}
|
|
2166
|
-
this.#requestRender({ frames: 1, continuousMs:
|
|
2186
|
+
this.#requestRender({ frames: 1, continuousMs: BabylonJSController.config.render.interactionMs });
|
|
2167
2187
|
}
|
|
2168
2188
|
}
|
|
2169
2189
|
|
|
@@ -2208,14 +2228,14 @@ export default class BabylonJSController {
|
|
|
2208
2228
|
|
|
2209
2229
|
const camera = this.#scene?.activeCamera;
|
|
2210
2230
|
if (camera && !camera.metadata?.locked) {
|
|
2211
|
-
this.#requestRender({ frames: 1, continuousMs:
|
|
2231
|
+
this.#requestRender({ frames: 1, continuousMs: BabylonJSController.config.render.interactionMs });
|
|
2212
2232
|
}
|
|
2213
2233
|
if (this.#babylonJSAnimationController && pickInfo) {
|
|
2214
2234
|
const pickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
2215
2235
|
if (this.#state.pointerPicking.lastPickedMeshId !== pickedMeshId) {
|
|
2216
2236
|
const highlightResult = this.#babylonJSAnimationController.highlightMeshes(pickInfo);
|
|
2217
2237
|
if (highlightResult.changed) {
|
|
2218
|
-
this.#requestRender({ frames: 1, continuousMs:
|
|
2238
|
+
this.#requestRender({ frames: 1, continuousMs: BabylonJSController.config.render.interactionMs });
|
|
2219
2239
|
}
|
|
2220
2240
|
}
|
|
2221
2241
|
}
|
|
@@ -2272,12 +2292,12 @@ export default class BabylonJSController {
|
|
|
2272
2292
|
console.log(`PrefViewer: Applying resize after ${Math.round(elapsed)}ms`);
|
|
2273
2293
|
this.#applyShowroomCameraFraming();
|
|
2274
2294
|
this.#engine.resize();
|
|
2275
|
-
const needsEnhancedBurst = this.#
|
|
2276
|
-
const frames = needsEnhancedBurst ?
|
|
2277
|
-
this.#requestRender({ frames, continuousMs:
|
|
2295
|
+
const needsEnhancedBurst = this.#renderSettings.antiAliasingEnabled || this.#renderSettings.ambientOcclusionEnabled || this.#renderSettings.iblEnabled || this.#renderSettings.shadowsEnabled;
|
|
2296
|
+
const frames = needsEnhancedBurst ? BabylonJSController.config.render.burstFramesEnhanced : BabylonJSController.config.render.burstFramesBase;
|
|
2297
|
+
this.#requestRender({ frames, continuousMs: BabylonJSController.config.render.interactionMs });
|
|
2278
2298
|
};
|
|
2279
2299
|
|
|
2280
|
-
if (elapsed >=
|
|
2300
|
+
if (elapsed >= BabylonJSController.config.resize.throttleMs) {
|
|
2281
2301
|
applyResize();
|
|
2282
2302
|
return;
|
|
2283
2303
|
}
|
|
@@ -2287,7 +2307,7 @@ export default class BabylonJSController {
|
|
|
2287
2307
|
}
|
|
2288
2308
|
|
|
2289
2309
|
this.#state.resize.isScheduled = true;
|
|
2290
|
-
const waitMs = Math.max(0,
|
|
2310
|
+
const waitMs = Math.max(0, BabylonJSController.config.resize.throttleMs - elapsed);
|
|
2291
2311
|
this.#state.resize.timeoutId = setTimeout(() => {
|
|
2292
2312
|
applyResize();
|
|
2293
2313
|
}, waitMs);
|
|
@@ -2373,11 +2393,14 @@ export default class BabylonJSController {
|
|
|
2373
2393
|
}
|
|
2374
2394
|
|
|
2375
2395
|
/**
|
|
2376
|
-
*
|
|
2396
|
+
* Cancels pending cube navigation and applies configured camera selection and locking.
|
|
2397
|
+
* Configures unlocked orbital cameras for proportional native wheel input, rebinds the model zoom guard
|
|
2398
|
+
* and synchronizes the cube after selecting the active camera.
|
|
2377
2399
|
* @private
|
|
2378
2400
|
* @returns {boolean} True if camera options were set successfully, false otherwise.
|
|
2379
2401
|
*/
|
|
2380
2402
|
#setOptions_Camera() {
|
|
2403
|
+
this.#viewCube?.cancel();
|
|
2381
2404
|
const cameraState = this.#options.camera;
|
|
2382
2405
|
const modelContainer = this.#containers.model;
|
|
2383
2406
|
const environmentContainer = this.#containers.environment;
|
|
@@ -2431,9 +2454,14 @@ export default class BabylonJSController {
|
|
|
2431
2454
|
this.#scene.activeCamera?.detachControl();
|
|
2432
2455
|
camera.detachControl();
|
|
2433
2456
|
if (!cameraState.locked) {
|
|
2457
|
+
if (camera instanceof ArcRotateCamera) {
|
|
2458
|
+
camera.wheelDeltaPercentage = BabylonJSController.config.camera.wheelDeltaPercentage;
|
|
2459
|
+
}
|
|
2434
2460
|
camera.attachControl(this.#canvas, true);
|
|
2435
2461
|
}
|
|
2436
2462
|
this.#scene.activeCamera = camera;
|
|
2463
|
+
this.#cameraZoom?.setCamera(camera);
|
|
2464
|
+
this.#viewCube?.sync();
|
|
2437
2465
|
return true;
|
|
2438
2466
|
}
|
|
2439
2467
|
|
|
@@ -2628,8 +2656,12 @@ export default class BabylonJSController {
|
|
|
2628
2656
|
*/
|
|
2629
2657
|
#assetContainer_simplifyTransmissiveMaterials(assetContainer) {
|
|
2630
2658
|
assetContainer.materials?.forEach((mat) => {
|
|
2631
|
-
if (!(mat instanceof PBRMaterial))
|
|
2632
|
-
|
|
2659
|
+
if (!(mat instanceof PBRMaterial)) {
|
|
2660
|
+
return;
|
|
2661
|
+
}
|
|
2662
|
+
if (!mat.subSurface?.isRefractionEnabled && !mat.subSurface?.isTranslucencyEnabled) {
|
|
2663
|
+
return;
|
|
2664
|
+
}
|
|
2633
2665
|
this.#simplifyTransmissiveMaterial(mat);
|
|
2634
2666
|
});
|
|
2635
2667
|
}
|
|
@@ -2700,11 +2732,13 @@ export default class BabylonJSController {
|
|
|
2700
2732
|
* @returns {Promise<void>}
|
|
2701
2733
|
*/
|
|
2702
2734
|
async #startRender() {
|
|
2703
|
-
if (!this.#scene)
|
|
2735
|
+
if (!this.#scene) {
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2704
2738
|
await this.#loadCameraDependentEffects();
|
|
2705
2739
|
await this.#scene.whenReadyAsync();
|
|
2706
|
-
const frames = this.#
|
|
2707
|
-
this.#requestRender({ frames: frames, continuousMs:
|
|
2740
|
+
const frames = this.#renderSettings.antiAliasingEnabled || this.#renderSettings.ambientOcclusionEnabled || this.#renderSettings.iblEnabled ? BabylonJSController.config.render.burstFramesEnhanced : BabylonJSController.config.render.burstFramesBase;
|
|
2741
|
+
this.#requestRender({ frames: frames, continuousMs: BabylonJSController.config.render.interactionMs });
|
|
2708
2742
|
}
|
|
2709
2743
|
|
|
2710
2744
|
/**
|
|
@@ -2776,15 +2810,18 @@ export default class BabylonJSController {
|
|
|
2776
2810
|
}
|
|
2777
2811
|
|
|
2778
2812
|
/**
|
|
2779
|
-
*
|
|
2813
|
+
* Cancels pending cube navigation, loads asset containers and adds replacements to the scene.
|
|
2814
|
+
* Current model selection is resolved by the zoom guard on subsequent input updates.
|
|
2780
2815
|
* @private
|
|
2816
|
+
* @param {boolean} [force=false] - Whether to bypass existing asset cache metadata.
|
|
2781
2817
|
* @returns {Promise<{success: boolean, error: any}>} Resolves to an object indicating if loading succeeded and any error encountered.
|
|
2782
2818
|
* @description
|
|
2783
2819
|
* Waits for all containers to load in parallel, then replaces or adds them to the scene as needed.
|
|
2784
|
-
* Applies material
|
|
2820
|
+
* Applies material options and wall/floor visibility before camera fitting, then initializes lights and shadows.
|
|
2785
2821
|
* Returns an object with success status and error details.
|
|
2786
2822
|
*/
|
|
2787
2823
|
async #loadContainers(force = false) {
|
|
2824
|
+
this.#viewCube?.cancel();
|
|
2788
2825
|
this.#detachAnimationChangedListener();
|
|
2789
2826
|
await this.#stopRender();
|
|
2790
2827
|
|
|
@@ -2837,9 +2874,10 @@ export default class BabylonJSController {
|
|
|
2837
2874
|
});
|
|
2838
2875
|
|
|
2839
2876
|
this.#setOptions_Materials();
|
|
2877
|
+
// Apply effective geometry visibility before calculating the camera fit.
|
|
2878
|
+
this.#setVisibilityOfWallAndFloorInModel();
|
|
2840
2879
|
this.#setOptions_Camera();
|
|
2841
2880
|
await this.#setOptions_IBL();
|
|
2842
|
-
this.#setVisibilityOfWallAndFloorInModel();
|
|
2843
2881
|
detail.success = true;
|
|
2844
2882
|
})
|
|
2845
2883
|
.catch((error) => {
|
|
@@ -2865,12 +2903,12 @@ export default class BabylonJSController {
|
|
|
2865
2903
|
* @returns {void}
|
|
2866
2904
|
*/
|
|
2867
2905
|
#syncAnimationControllerAfterModelLoad() {
|
|
2868
|
-
if (this.#
|
|
2906
|
+
if (this.#renderSettings.showAnimationMenu && this.#containers.model.assetContainer) {
|
|
2869
2907
|
this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
|
|
2870
2908
|
// Apply stored highlight settings so fresh page loads respect persisted state
|
|
2871
2909
|
this.#setHighlightConfig({
|
|
2872
|
-
enabled: this.#
|
|
2873
|
-
color: this.#
|
|
2910
|
+
enabled: this.#renderSettings.highlightEnabled,
|
|
2911
|
+
color: this.#renderSettings.highlightColor,
|
|
2874
2912
|
});
|
|
2875
2913
|
if (this.#babylonJSAnimationController?.hasAnimations?.()) {
|
|
2876
2914
|
this.#attachAnimationChangedListener();
|
|
@@ -3122,10 +3160,34 @@ export default class BabylonJSController {
|
|
|
3122
3160
|
* ---------------------------
|
|
3123
3161
|
*/
|
|
3124
3162
|
|
|
3163
|
+
/**
|
|
3164
|
+
* Creates the enabled cube once a live scene/camera exists and teardown is not in progress.
|
|
3165
|
+
* Resolves camera/model state through callbacks; does not reload assets or camera options.
|
|
3166
|
+
* @private
|
|
3167
|
+
* @returns {void}
|
|
3168
|
+
*/
|
|
3169
|
+
#initializeViewCube() {
|
|
3170
|
+
if (!this.#viewCubeEnabled || this.#viewCubeSuspended || this.#viewCube || !this.#engine || !this.#scene?.activeCamera) {
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
3173
|
+
this.#viewCube = new BabylonJSViewCubeController({
|
|
3174
|
+
canvas: this.#canvas,
|
|
3175
|
+
getCamera: () => this.#scene?.activeCamera,
|
|
3176
|
+
isLocked: (camera) => !camera || camera.metadata?.locked === true || this.#XRExperience?.baseExperience?.state === WebXRState.IN_XR,
|
|
3177
|
+
getBounds: () => BabylonJSCameraFraming.getModelBounds(this.#containers.model?.assetContainer),
|
|
3178
|
+
getViewport: () => ({ width: this.#canvas.clientWidth, height: this.#canvas.clientHeight }),
|
|
3179
|
+
getCulture: this.#viewCubeConfig.getCulture,
|
|
3180
|
+
onLayout: this.#viewCubeConfig.onLayout,
|
|
3181
|
+
requestRender: () => this.#requestRender({ frames: 2 }),
|
|
3182
|
+
});
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3125
3185
|
/**
|
|
3126
3186
|
* Initializes the Babylon.js engine, scene, camera, lights, and interaction handlers.
|
|
3127
|
-
* Configures Draco compression
|
|
3128
|
-
* Observes canvas resize events
|
|
3187
|
+
* Configures Draco compression and sets up XR; rendering starts through the existing load/request scheduler.
|
|
3188
|
+
* Observes canvas resize events, binds model-only orbital zoom protection and adaptive default-camera near clipping,
|
|
3189
|
+
* and lazily creates the requested cube. Imported camera clipping is preserved by the host policy.
|
|
3190
|
+
* Aborts remaining initialization if the scene was replaced/disposed during texture readiness.
|
|
3129
3191
|
* @public
|
|
3130
3192
|
* @returns {Promise<void>}
|
|
3131
3193
|
*/
|
|
@@ -3161,20 +3223,39 @@ export default class BabylonJSController {
|
|
|
3161
3223
|
this.#scene.skipPointerUpPicking = true;
|
|
3162
3224
|
|
|
3163
3225
|
this.#createCamera();
|
|
3226
|
+
this.#cameraZoom = new BabylonJSCameraZoomController(
|
|
3227
|
+
() => this.#containers.model?.state?.isVisible ? this.#containers.model.assetContainer : null,
|
|
3228
|
+
(camera) => camera.metadata?.locked === true || this.#XRExperience?.baseExperience?.state === WebXRState.IN_XR,
|
|
3229
|
+
() => this.#viewCube?.isAnimating === true,
|
|
3230
|
+
(camera) => camera === this.#camera,
|
|
3231
|
+
);
|
|
3232
|
+
this.#cameraZoom.setCamera(this.#camera);
|
|
3233
|
+
this.#viewCubeSuspended = false;
|
|
3234
|
+
this.#initializeViewCube();
|
|
3235
|
+
const enabledScene = this.#scene;
|
|
3164
3236
|
await this.#ensureEnvironmentBRDFTextureReady();
|
|
3237
|
+
if (!this.#engine || this.#scene !== enabledScene) {
|
|
3238
|
+
return;
|
|
3239
|
+
}
|
|
3165
3240
|
this.#enableInteraction();
|
|
3166
3241
|
await this.#createXRExperience();
|
|
3167
3242
|
}
|
|
3168
3243
|
|
|
3169
3244
|
/**
|
|
3170
3245
|
* Disposes the Babylon.js engine and disconnects the canvas resize observer.
|
|
3171
|
-
*
|
|
3246
|
+
* Releases the zoom observer and cube overlay before scene teardown, restoring the zoom camera
|
|
3247
|
+
* floor and suspending cube creation until the next enable. Cleans up light, XR and resolver resources.
|
|
3172
3248
|
* The teardown is asynchronous: it waits for XR/session-dependent shutdown work
|
|
3173
3249
|
* before disposing the engine, and coalesces concurrent calls into one in-flight promise.
|
|
3174
3250
|
* @public
|
|
3175
3251
|
* @returns {Promise<void>}
|
|
3176
3252
|
*/
|
|
3177
3253
|
async disable() {
|
|
3254
|
+
this.#cameraZoom?.dispose();
|
|
3255
|
+
this.#cameraZoom = null;
|
|
3256
|
+
this.#viewCubeSuspended = true;
|
|
3257
|
+
this.#viewCube?.dispose();
|
|
3258
|
+
this.#viewCube = null;
|
|
3178
3259
|
if (this.#disablingPromises.general) {
|
|
3179
3260
|
return await this.#disablingPromises.general;
|
|
3180
3261
|
}
|
|
@@ -3205,6 +3286,32 @@ export default class BabylonJSController {
|
|
|
3205
3286
|
}
|
|
3206
3287
|
}
|
|
3207
3288
|
|
|
3289
|
+
/**
|
|
3290
|
+
* Lazily creates the cube when enabled and releases all cube resources when disabled.
|
|
3291
|
+
* Stores activation until a live scene is available; never reloads assets or camera options.
|
|
3292
|
+
* @public
|
|
3293
|
+
* @param {boolean} enabled - Whether the visible 3D viewer should own a ViewCube.
|
|
3294
|
+
* @returns {void}
|
|
3295
|
+
*/
|
|
3296
|
+
setViewCubeEnabled(enabled) {
|
|
3297
|
+
this.#viewCubeEnabled = Boolean(enabled);
|
|
3298
|
+
if (this.#viewCubeEnabled) {
|
|
3299
|
+
this.#initializeViewCube();
|
|
3300
|
+
} else {
|
|
3301
|
+
this.#viewCube?.dispose();
|
|
3302
|
+
this.#viewCube = null;
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
/**
|
|
3307
|
+
* Synchronizes the existing cube after culture or camera-state changes without creating an inactive cube.
|
|
3308
|
+
* @public
|
|
3309
|
+
* @returns {void}
|
|
3310
|
+
*/
|
|
3311
|
+
refreshViewCube() {
|
|
3312
|
+
this.#viewCube?.sync();
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3208
3315
|
/**
|
|
3209
3316
|
* Downloads the current scene, model, or environment as a GLB file.
|
|
3210
3317
|
* @public
|
|
@@ -3314,7 +3421,7 @@ export default class BabylonJSController {
|
|
|
3314
3421
|
* @returns {{antiAliasingEnabled:boolean, ambientOcclusionEnabled:boolean, iblEnabled:boolean, shadowsEnabled:boolean}}
|
|
3315
3422
|
*/
|
|
3316
3423
|
getRenderSettings() {
|
|
3317
|
-
return { ...this.#
|
|
3424
|
+
return { ...this.#renderSettings };
|
|
3318
3425
|
}
|
|
3319
3426
|
|
|
3320
3427
|
/**
|
|
@@ -3484,7 +3591,7 @@ export default class BabylonJSController {
|
|
|
3484
3591
|
max: vectorToJSON(modelBounds.max),
|
|
3485
3592
|
} : null,
|
|
3486
3593
|
render: { ...this.#state.render },
|
|
3487
|
-
settings: { ...this.#
|
|
3594
|
+
settings: { ...this.#renderSettings },
|
|
3488
3595
|
pipelines: {
|
|
3489
3596
|
default: Boolean(this.#renderPipelines.default),
|
|
3490
3597
|
ssao: Boolean(this.#renderPipelines.ssao),
|
|
@@ -3548,7 +3655,7 @@ export default class BabylonJSController {
|
|
|
3548
3655
|
* Applies animation-menu, highlight, and time-of-day lighting configuration without forcing a full scene reload.
|
|
3549
3656
|
* Values are merged into the persisted render settings snapshot unless `config.persist === false`.
|
|
3550
3657
|
* When `showLightingTimeOfDay` is set to false and no explicit `lightingTimeOfDay` is supplied,
|
|
3551
|
-
* the active lighting time is reset to `
|
|
3658
|
+
* the active lighting time is reset to `BabylonJSController.config.render.defaultSettings.lightingTimeOfDay` (0.5).
|
|
3552
3659
|
* @public
|
|
3553
3660
|
* @param {object} config - Immediate render/illumination configuration.
|
|
3554
3661
|
* @param {boolean|string} [config.showAnimationMenu] - Enables/disables the animation controls when animations are available.
|
|
@@ -3566,16 +3673,16 @@ export default class BabylonJSController {
|
|
|
3566
3673
|
const showLighting = typeof config.showLightingTimeOfDay === "string"
|
|
3567
3674
|
? config.showLightingTimeOfDay.toLowerCase() !== "false"
|
|
3568
3675
|
: Boolean(config.showLightingTimeOfDay);
|
|
3569
|
-
if (this.#
|
|
3676
|
+
if (this.#renderSettings.showLightingTimeOfDay !== showLighting) {
|
|
3570
3677
|
shouldApplyLighting = true;
|
|
3571
3678
|
}
|
|
3572
|
-
this.#
|
|
3679
|
+
this.#renderSettings.showLightingTimeOfDay = showLighting;
|
|
3573
3680
|
|
|
3574
3681
|
const hasExplicitLightingTime = typeof config.lightingTimeOfDay === "number" && Number.isFinite(config.lightingTimeOfDay);
|
|
3575
3682
|
if (!showLighting && !hasExplicitLightingTime) {
|
|
3576
|
-
const defaultTime = BabylonJSController.
|
|
3577
|
-
if (this.#
|
|
3578
|
-
this.#
|
|
3683
|
+
const defaultTime = BabylonJSController.config.render.defaultSettings.lightingTimeOfDay;
|
|
3684
|
+
if (this.#renderSettings.lightingTimeOfDay !== defaultTime) {
|
|
3685
|
+
this.#renderSettings.lightingTimeOfDay = defaultTime;
|
|
3579
3686
|
shouldApplyLighting = true;
|
|
3580
3687
|
}
|
|
3581
3688
|
}
|
|
@@ -3583,8 +3690,8 @@ export default class BabylonJSController {
|
|
|
3583
3690
|
|
|
3584
3691
|
if (typeof config.lightingTimeOfDay === "number" && Number.isFinite(config.lightingTimeOfDay)) {
|
|
3585
3692
|
const normalizedTime = clamp01(config.lightingTimeOfDay);
|
|
3586
|
-
if (this.#
|
|
3587
|
-
this.#
|
|
3693
|
+
if (this.#renderSettings.lightingTimeOfDay !== normalizedTime) {
|
|
3694
|
+
this.#renderSettings.lightingTimeOfDay = normalizedTime;
|
|
3588
3695
|
shouldApplyLighting = true;
|
|
3589
3696
|
}
|
|
3590
3697
|
}
|
|
@@ -3593,7 +3700,7 @@ export default class BabylonJSController {
|
|
|
3593
3700
|
const showMenu = typeof config.showAnimationMenu === "string"
|
|
3594
3701
|
? config.showAnimationMenu.toLowerCase() !== "false"
|
|
3595
3702
|
: Boolean(config.showAnimationMenu);
|
|
3596
|
-
this.#
|
|
3703
|
+
this.#renderSettings.showAnimationMenu = showMenu;
|
|
3597
3704
|
this.#syncAnimationControllerAfterModelLoad();
|
|
3598
3705
|
}
|
|
3599
3706
|
|
|
@@ -3606,12 +3713,12 @@ export default class BabylonJSController {
|
|
|
3606
3713
|
} else if (enabled === null) {
|
|
3607
3714
|
enabled = undefined;
|
|
3608
3715
|
}
|
|
3609
|
-
// Keep #
|
|
3716
|
+
// Keep #renderSettings in sync so getRenderSettings() always reflects the real state
|
|
3610
3717
|
if (enabled !== undefined) {
|
|
3611
|
-
this.#
|
|
3718
|
+
this.#renderSettings.highlightEnabled = enabled;
|
|
3612
3719
|
}
|
|
3613
3720
|
if (config.highlightColor !== undefined && config.highlightColor !== null) {
|
|
3614
|
-
this.#
|
|
3721
|
+
this.#renderSettings.highlightColor = config.highlightColor;
|
|
3615
3722
|
}
|
|
3616
3723
|
this.#setHighlightConfig({
|
|
3617
3724
|
color: config.highlightColor ?? undefined,
|