@preference-sl/pref-viewer 2.16.0 → 2.16.2-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 +129 -102
- package/index.d.ts +373 -12
- package/package.json +2 -1
- package/src/babylonjs-animation-opening.js +11 -5
- package/src/babylonjs-controller.js +442 -65
- package/src/panzoom-controller.js +36 -17
- package/src/pref-viewer-2d.js +395 -21
- package/src/pref-viewer-3d.js +140 -85
- package/src/pref-viewer-menu-3d.js +84 -40
- package/src/pref-viewer-task.js +7 -2
- package/src/pref-viewer.js +615 -185
- package/src/styles.js +111 -29
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
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";
|
|
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";
|
|
@@ -54,9 +54,10 @@ export { markPrefViewerSimplifiedTransmissionMaterial, isPrefViewerPhysicalGlass
|
|
|
54
54
|
* Public API Highlights
|
|
55
55
|
* - constructor(canvas, containers, options)
|
|
56
56
|
* - enable() / disable()
|
|
57
|
-
* - load() / reloadWithCurrentSettings()
|
|
57
|
+
* - load(force?) / reloadWithCurrentSettings()
|
|
58
58
|
* - downloadGLB(content) / downloadGLTF(content) / downloadUSDZ(content)
|
|
59
59
|
* - getRenderSettings() / scheduleRenderSettingsReload(settings)
|
|
60
|
+
* - getDebugInfo()
|
|
60
61
|
* - setContainerVisibility(name, show)
|
|
61
62
|
* - setMaterialOptions() / setCameraOptions() / setIBLOptions()
|
|
62
63
|
* - setIlluminationConfig(config)
|
|
@@ -96,10 +97,13 @@ export default class BabylonJSController {
|
|
|
96
97
|
ambientOcclusionEnabled: true,
|
|
97
98
|
iblEnabled: true,
|
|
98
99
|
shadowsEnabled: false,
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
// Animation menu and highlight settings
|
|
101
|
+
showAnimationMenu: true,
|
|
101
102
|
highlightEnabled: true,
|
|
102
103
|
highlightColor: PrefViewerStyleVariables.colorPrimary,
|
|
104
|
+
// Lighting time of day (0=midnight, 0.5=noon, 1=midnight)
|
|
105
|
+
showLightingTimeOfDay: true,
|
|
106
|
+
lightingTimeOfDay: 0.5,
|
|
103
107
|
};
|
|
104
108
|
|
|
105
109
|
// Canvas HTML element
|
|
@@ -120,7 +124,7 @@ export default class BabylonJSController {
|
|
|
120
124
|
#shadowGen = [];
|
|
121
125
|
#XRExperience = null;
|
|
122
126
|
#canvasResizeObserver = null;
|
|
123
|
-
|
|
127
|
+
|
|
124
128
|
#hdrTexture = null; // Reusable in-memory HDR source cloned into scene.environmentTexture across reloads
|
|
125
129
|
|
|
126
130
|
#containers = {};
|
|
@@ -188,6 +192,11 @@ export default class BabylonJSController {
|
|
|
188
192
|
resize: {
|
|
189
193
|
throttleMs: 50, // Cap resize work to ~20 Hz while dragging/resizing containers
|
|
190
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
|
|
198
|
+
maxRadiusFactor: 4, // Pull back past the whole model
|
|
199
|
+
},
|
|
191
200
|
// Hardware-adaptive quality settings (applied based on GPU capabilities)
|
|
192
201
|
quality: {
|
|
193
202
|
// Settings for different hardware tiers: 'low', 'medium', 'high'
|
|
@@ -209,6 +218,24 @@ export default class BabylonJSController {
|
|
|
209
218
|
hemiLightIntensity: 0.52,
|
|
210
219
|
dirLightIntensity: 0.56,
|
|
211
220
|
cameraLightIntensity: 0.16,
|
|
221
|
+
// Daylight background, in the LINEAR domain the tonemapper consumes — NOT sRGB.
|
|
222
|
+
//
|
|
223
|
+
// The clear colour goes through the same exposure + ACES + gamma chain as the
|
|
224
|
+
// render, so the value that reads correctly here is not the value that reaches the
|
|
225
|
+
// screen. The previous literal was an sRGB 0.98 ("almost white") and it arrived as
|
|
226
|
+
// #C0C0C0, a mid grey.
|
|
227
|
+
//
|
|
228
|
+
// This one is solved against the ACES fit at exposure 0.92 and verified on screen:
|
|
229
|
+
// a light studio grey, #D9D9D9. Other targets, if this needs revisiting —
|
|
230
|
+
// #F4F3F0 warm off-white (matches the host app's 2D/PR surface) -> 4.289 3.991 3.304
|
|
231
|
+
// #D9D9D9 light studio (current) -> 1.424
|
|
232
|
+
// #CCCCCC neutral studio -> 1.112
|
|
233
|
+
// #B3B3B3 darker -> 0.688
|
|
234
|
+
// Note how steeply the required input climbs near white: that is the ACES shoulder,
|
|
235
|
+
// and it is why a near-white background needs a value well above 1.
|
|
236
|
+
//
|
|
237
|
+
// Re-solve these if exposure, contrast or the tonemapping curve change.
|
|
238
|
+
backgroundDay: [1.424, 1.424, 1.424],
|
|
212
239
|
},
|
|
213
240
|
},
|
|
214
241
|
};
|
|
@@ -222,18 +249,18 @@ export default class BabylonJSController {
|
|
|
222
249
|
#detectHardwareTier() {
|
|
223
250
|
// First, try to get WebGL info to help identify the device
|
|
224
251
|
const gl = this.#engine?.getRenderingCanvas()?.getContext?.('webgl2') || this.#engine?.getRenderingCanvas()?.getContext?.('webgl');
|
|
225
|
-
|
|
252
|
+
|
|
226
253
|
const debugInfo = gl?.getExtension('WEBGL_debug_renderer_info');
|
|
227
254
|
const rendererName = debugInfo ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : '';
|
|
228
255
|
const isAngleRenderer = /^ANGLE\s*\(/i.test(rendererName);
|
|
229
|
-
|
|
256
|
+
|
|
230
257
|
// If we have WebGL renderer info and it looks like a desktop GPU, skip mobile detection
|
|
231
258
|
// Desktop GPUs typically have "NVIDIA", "AMD", "Intel" in the name
|
|
232
259
|
const isDesktopGPU = /NVIDIA|AMD|Intel|Radeon|GeForce|Quadro/i.test(rendererName);
|
|
233
|
-
|
|
260
|
+
|
|
234
261
|
// Also check if it's explicitly a mobile GPU
|
|
235
262
|
const isMobileGPU = /Adreno|Mali|Apple GPU/i.test(rendererName);
|
|
236
|
-
|
|
263
|
+
|
|
237
264
|
// If we detect a desktop GPU, go directly to desktop detection
|
|
238
265
|
if (isDesktopGPU && !isMobileGPU) {
|
|
239
266
|
if (isAngleRenderer) {
|
|
@@ -252,7 +279,7 @@ export default class BabylonJSController {
|
|
|
252
279
|
|
|
253
280
|
// If no WebGL info or unclear, fall back to user agent detection
|
|
254
281
|
const mobileInfo = this.#detectMobileInfo();
|
|
255
|
-
|
|
282
|
+
|
|
256
283
|
// Only treat as mobile if we have strong mobile indicators
|
|
257
284
|
if (mobileInfo.isMobile && !mobileInfo.isDesktopLike) {
|
|
258
285
|
if (mobileInfo.isHighEnd) {
|
|
@@ -279,15 +306,15 @@ export default class BabylonJSController {
|
|
|
279
306
|
#detectDesktopTier(gl, rendererName, conservative = false) {
|
|
280
307
|
// Check WebGL capabilities
|
|
281
308
|
const maxTextureSize = gl?.getParameter(gl.MAX_TEXTURE_SIZE) || 8192;
|
|
282
|
-
|
|
309
|
+
|
|
283
310
|
// Detect high-end GPUs
|
|
284
311
|
const isAppleSilicon = /Apple|M1|M2|M3|M4/i.test(rendererName);
|
|
285
312
|
const isNvidiaHighEnd = /RTX|GeForce (3|4|5|6|7|8|9|40|50|60|70|80|90)|RTX A[5-9]|RTX Pro|Quadro/i.test(rendererName);
|
|
286
313
|
const isAmdHighEnd = /RX (6|7|8|9)[0-9]{2,}|RX Vega|Radeon Pro|WX [0-9]/i.test(rendererName);
|
|
287
314
|
const isIntelIntegrated = /Intel.*UHD|Intel.*Iris|Mesa|llvmpipe/i.test(rendererName);
|
|
288
|
-
|
|
315
|
+
|
|
289
316
|
let tier = conservative ? 'medium' : 'high'; // Default to conservative desktop when ANGLE is in the path
|
|
290
|
-
|
|
317
|
+
|
|
291
318
|
if (isIntelIntegrated || maxTextureSize < 8192) {
|
|
292
319
|
tier = 'medium';
|
|
293
320
|
} else if (!conservative && (isAppleSilicon || isNvidiaHighEnd || isAmdHighEnd || maxTextureSize >= 16384)) {
|
|
@@ -295,7 +322,7 @@ export default class BabylonJSController {
|
|
|
295
322
|
} else if (conservative && maxTextureSize >= 16384) {
|
|
296
323
|
tier = 'high';
|
|
297
324
|
}
|
|
298
|
-
|
|
325
|
+
|
|
299
326
|
console.log(`[PrefViewer] Desktop tier: ${tier} (GPU: ${rendererName || 'unknown'}, MaxTexture: ${maxTextureSize})`);
|
|
300
327
|
return tier;
|
|
301
328
|
}
|
|
@@ -311,13 +338,13 @@ export default class BabylonJSController {
|
|
|
311
338
|
const screenWidth = window.screen?.width || 0;
|
|
312
339
|
const screenHeight = window.screen?.height || 0;
|
|
313
340
|
const pixelRatio = window.devicePixelRatio || 1;
|
|
314
|
-
|
|
341
|
+
|
|
315
342
|
// Detect device type
|
|
316
343
|
const isIPhone = /iPhone/i.test(userAgent);
|
|
317
344
|
const isIPad = /iPad/i.test(userAgent);
|
|
318
345
|
const isAndroid = /Android/i.test(userAgent);
|
|
319
346
|
const isMobile = /Mobile/i.test(userAgent);
|
|
320
|
-
|
|
347
|
+
|
|
321
348
|
// High-end mobile patterns (recent flagship devices)
|
|
322
349
|
const highEndPatterns = [
|
|
323
350
|
/iPhone (1[4-9]|SE)/i, // iPhone 14+
|
|
@@ -329,7 +356,7 @@ export default class BabylonJSController {
|
|
|
329
356
|
/Xiaomi (1[2-4]|Mi 1[1-4])/i, // Xiaomi 12+
|
|
330
357
|
/Huawei (P50|Mate 4|)/i, // Huawei P50+
|
|
331
358
|
];
|
|
332
|
-
|
|
359
|
+
|
|
333
360
|
// Mid-range patterns
|
|
334
361
|
const midRangePatterns = [
|
|
335
362
|
/iPhone (1[0-3]|SE)/i, // iPhone 10-13
|
|
@@ -339,20 +366,20 @@ export default class BabylonJSController {
|
|
|
339
366
|
/Pixel [4-6]/i, // Pixel 4-6
|
|
340
367
|
/Moto G/i, // Moto G
|
|
341
368
|
];
|
|
342
|
-
|
|
369
|
+
|
|
343
370
|
const isHighEnd = highEndPatterns.some(p => p.test(userAgent));
|
|
344
371
|
const isMidRange = midRangePatterns.some(p => p.test(userAgent));
|
|
345
|
-
|
|
372
|
+
|
|
346
373
|
// Detect tablet ONLY if explicitly in user agent (not just screen size)
|
|
347
374
|
// Many desktops have large screens and would incorrectly be detected as tablets
|
|
348
375
|
const isTablet = /iPad|Android.*Tablet|Nexus 9|Nexus 10/i.test(userAgent);
|
|
349
|
-
|
|
376
|
+
|
|
350
377
|
// Detect if mobile (phone form factor) - must have explicit mobile indicators
|
|
351
378
|
const isMobilePhone = /Android.*Mobile|iPhone|iPod/i.test(userAgent);
|
|
352
|
-
|
|
379
|
+
|
|
353
380
|
// Check if it looks like a desktop/laptop (has "Windows", "Mac", "Linux" but no mobile indicators)
|
|
354
381
|
const isDesktopLike = /Windows NT|Macintosh|Linux|X11/i.test(userAgent) && !isMobilePhone && !isTablet;
|
|
355
|
-
|
|
382
|
+
|
|
356
383
|
// Device name for logging
|
|
357
384
|
let deviceName = 'Unknown';
|
|
358
385
|
if (isIPhone) {
|
|
@@ -364,7 +391,7 @@ export default class BabylonJSController {
|
|
|
364
391
|
const match = userAgent.match(/Android.*;.*(.+?)\)/);
|
|
365
392
|
deviceName = match ? match[1].trim() : 'Android';
|
|
366
393
|
}
|
|
367
|
-
|
|
394
|
+
|
|
368
395
|
return {
|
|
369
396
|
isMobile: isMobilePhone && !isDesktopLike,
|
|
370
397
|
isTablet: isTablet && !isDesktopLike,
|
|
@@ -445,10 +472,10 @@ export default class BabylonJSController {
|
|
|
445
472
|
};
|
|
446
473
|
|
|
447
474
|
const preset = presetMap[tier] || qualityPresets.medium;
|
|
448
|
-
|
|
475
|
+
|
|
449
476
|
// Apply preset to config
|
|
450
|
-
this.#config.quality = {
|
|
451
|
-
tier,
|
|
477
|
+
this.#config.quality = {
|
|
478
|
+
tier,
|
|
452
479
|
...preset,
|
|
453
480
|
// Legacy compatibility
|
|
454
481
|
shadowMapSize: preset.shadowMapSize,
|
|
@@ -642,6 +669,55 @@ export default class BabylonJSController {
|
|
|
642
669
|
}
|
|
643
670
|
}
|
|
644
671
|
|
|
672
|
+
/**
|
|
673
|
+
* Preloads Babylon's internal BRDF lookup texture required by PBR materials.
|
|
674
|
+
* In render-on-demand hosts the RGBD expansion can stall unless a few renders are kept alive.
|
|
675
|
+
* @private
|
|
676
|
+
* @returns {Promise<boolean>} True when the texture is ready, false when it timed out or is unavailable.
|
|
677
|
+
*/
|
|
678
|
+
async #ensureEnvironmentBRDFTextureReady() {
|
|
679
|
+
if (!this.#scene || !this.#engine) {
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const brdfTexture = BRDFTextureTools.GetEnvironmentBRDFTexture(this.#scene);
|
|
684
|
+
if (!brdfTexture || brdfTexture.isReady?.()) {
|
|
685
|
+
return true;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const timeoutMs = 2000;
|
|
689
|
+
const startedAt = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
690
|
+
this.#requestRender({ frames: this.#config.render.burstFramesEnhanced, continuousMs: timeoutMs });
|
|
691
|
+
|
|
692
|
+
try {
|
|
693
|
+
await new Promise((resolve) => {
|
|
694
|
+
const tick = () => {
|
|
695
|
+
if (!this.#scene || !this.#engine || brdfTexture.isDisposed?.()) {
|
|
696
|
+
resolve(false);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
if (brdfTexture.isReady?.()) {
|
|
700
|
+
resolve(true);
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
704
|
+
if (now - startedAt >= timeoutMs) {
|
|
705
|
+
resolve(false);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
this.#requestRender({ frames: 1, continuousMs: 100 });
|
|
709
|
+
setTimeout(tick, 16);
|
|
710
|
+
};
|
|
711
|
+
tick();
|
|
712
|
+
});
|
|
713
|
+
} catch (error) {
|
|
714
|
+
console.warn("PrefViewer: Error preparing Babylon BRDF texture:", error);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
this.#requestRender({ frames: 2 });
|
|
718
|
+
return brdfTexture.isReady?.() ?? false;
|
|
719
|
+
}
|
|
720
|
+
|
|
645
721
|
/**
|
|
646
722
|
* Starts Babylon's engine render loop if it is not already running.
|
|
647
723
|
* @private
|
|
@@ -669,7 +745,7 @@ export default class BabylonJSController {
|
|
|
669
745
|
this.#state.render.isLoopRunning = false;
|
|
670
746
|
return true;
|
|
671
747
|
}
|
|
672
|
-
|
|
748
|
+
|
|
673
749
|
/**
|
|
674
750
|
* Resets transient render-loop bookkeeping so the next render request starts from a clean baseline.
|
|
675
751
|
* Clears queued burst frames, ends any active continuous-render window, and drops the idle-throttle timestamp.
|
|
@@ -804,12 +880,12 @@ export default class BabylonJSController {
|
|
|
804
880
|
}
|
|
805
881
|
|
|
806
882
|
const shouldRender = animationRunning || cameraInMotion || this.#state.render.continuousUntil > now;
|
|
807
|
-
|
|
883
|
+
|
|
808
884
|
// Reset idle flag if we need to render
|
|
809
885
|
if (shouldRender) {
|
|
810
886
|
this.#state.render.isIdle = false;
|
|
811
887
|
}
|
|
812
|
-
|
|
888
|
+
|
|
813
889
|
return shouldRender;
|
|
814
890
|
}
|
|
815
891
|
|
|
@@ -845,7 +921,7 @@ export default class BabylonJSController {
|
|
|
845
921
|
if (!continuous && this.#state.render.lastRenderAt > 0 && now - this.#state.render.lastRenderAt < this.#config.render.idleThrottleMs) {
|
|
846
922
|
return;
|
|
847
923
|
}
|
|
848
|
-
|
|
924
|
+
|
|
849
925
|
this.#scene.render();
|
|
850
926
|
this.#state.render.lastRenderAt = now;
|
|
851
927
|
|
|
@@ -1570,7 +1646,7 @@ export default class BabylonJSController {
|
|
|
1570
1646
|
}
|
|
1571
1647
|
});
|
|
1572
1648
|
}
|
|
1573
|
-
|
|
1649
|
+
|
|
1574
1650
|
/**
|
|
1575
1651
|
* Applies the showroom tonemapping curve and lighting bias used by the 3D viewer.
|
|
1576
1652
|
* @private
|
|
@@ -1584,7 +1660,8 @@ export default class BabylonJSController {
|
|
|
1584
1660
|
const showroom = this.#config.look?.showroom || {};
|
|
1585
1661
|
const imageProcessing = this.#scene.imageProcessingConfiguration;
|
|
1586
1662
|
|
|
1587
|
-
|
|
1663
|
+
const bg = showroom.backgroundDay ?? this.#config.look.showroom.backgroundDay;
|
|
1664
|
+
this.#scene.clearColor = new Color4(bg[0], bg[1], bg[2], 1);
|
|
1588
1665
|
imageProcessing.exposure = showroom.exposure ?? 0.95;
|
|
1589
1666
|
imageProcessing.contrast = showroom.contrast ?? 1.08;
|
|
1590
1667
|
imageProcessing.toneMappingEnabled = showroom.toneMappingEnabled ?? true;
|
|
@@ -1608,7 +1685,7 @@ export default class BabylonJSController {
|
|
|
1608
1685
|
}
|
|
1609
1686
|
|
|
1610
1687
|
const showroom = this.#config.look?.showroom || {};
|
|
1611
|
-
const timeOfDay = clamp01(this.#settings.lightingTimeOfDay ?? 0.5);
|
|
1688
|
+
const timeOfDay = clamp01(this.#settings.lightingTimeOfDay ? this.#settings.lightingTimeOfDay ?? 0.5 : 0.5);
|
|
1612
1689
|
const profile = getLightingCycleProfile(timeOfDay);
|
|
1613
1690
|
const daylight = profile.dayFactor;
|
|
1614
1691
|
const peakLight = daylight > 0 ? Math.pow(daylight, 0.72) : 0;
|
|
@@ -1624,12 +1701,15 @@ export default class BabylonJSController {
|
|
|
1624
1701
|
imageProcessing.colorCurvesEnabled = false;
|
|
1625
1702
|
}
|
|
1626
1703
|
|
|
1704
|
+
// Night end stays in low linear values (a dark blue sky); the day end is the calibrated
|
|
1705
|
+
// HDR off-white. Both are linear already, so no toLinearSpace() on either.
|
|
1706
|
+
const bgDay = showroom.backgroundDay ?? this.#config.look.showroom.backgroundDay;
|
|
1627
1707
|
this.#scene.clearColor = new Color4(
|
|
1628
|
-
lerp(0.
|
|
1629
|
-
lerp(0.
|
|
1630
|
-
lerp(0.
|
|
1708
|
+
lerp(0.000627, bgDay[0], daylight),
|
|
1709
|
+
lerp(0.001089, bgDay[1], daylight),
|
|
1710
|
+
lerp(0.004413, bgDay[2], daylight),
|
|
1631
1711
|
1,
|
|
1632
|
-
)
|
|
1712
|
+
);
|
|
1633
1713
|
|
|
1634
1714
|
if ("environmentIntensity" in this.#scene) {
|
|
1635
1715
|
this.#scene.environmentIntensity = lerp(0.42, showroom.environmentIntensity ?? 1.08, peakLight);
|
|
@@ -1701,6 +1781,78 @@ export default class BabylonJSController {
|
|
|
1701
1781
|
this.#engine.setHardwareScalingLevel(scalingLevel);
|
|
1702
1782
|
}
|
|
1703
1783
|
|
|
1784
|
+
/**
|
|
1785
|
+
* World-space bounds of the loaded model's renderable geometry.
|
|
1786
|
+
* @private
|
|
1787
|
+
* @returns {{center: Vector3, radius: number}|null} Centre and bounding-sphere radius, or null when nothing renderable is loaded.
|
|
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.
|
|
1828
|
+
* @returns {void}
|
|
1829
|
+
*/
|
|
1830
|
+
#fitDefaultCameraToModel(camera) {
|
|
1831
|
+
if (camera !== this.#camera || !(camera instanceof ArcRotateCamera)) {
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
const bounds = this.#getModelBounds();
|
|
1836
|
+
if (!bounds) {
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
// fovMode is FOVMODE_HORIZONTAL_FIXED (see #applyShowroomCameraFraming), so camera.fov is
|
|
1841
|
+
// the horizontal angle and the vertical one follows the aspect ratio. Fitting to the
|
|
1842
|
+
// horizontal angle alone clips the model top and bottom on any viewport wider than tall.
|
|
1843
|
+
const aspect = this.#engine?.getAspectRatio?.(camera) || 1;
|
|
1844
|
+
const horizontalFov = camera.fov;
|
|
1845
|
+
const verticalFov = 2 * Math.atan(Math.tan(horizontalFov / 2) / aspect);
|
|
1846
|
+
const limitingFov = Math.min(horizontalFov, verticalFov);
|
|
1847
|
+
const distance = (bounds.radius / Math.sin(limitingFov / 2)) * this.#config.defaultCameraFraming.margin;
|
|
1848
|
+
|
|
1849
|
+
camera.setTarget(bounds.center);
|
|
1850
|
+
camera.radius = distance;
|
|
1851
|
+
// The 5-20 m defaults were sized for a room; left in place they clamp the fit itself.
|
|
1852
|
+
camera.lowerRadiusLimit = distance * this.#config.defaultCameraFraming.minRadiusFactor;
|
|
1853
|
+
camera.upperRadiusLimit = distance * this.#config.defaultCameraFraming.maxRadiusFactor;
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1704
1856
|
/**
|
|
1705
1857
|
* Keeps the showroom camera framing stable when the viewport aspect ratio changes.
|
|
1706
1858
|
* The 3D showroom is meant to feel compositionally stable, so we prefer a horizontal-fixed
|
|
@@ -1931,7 +2083,7 @@ export default class BabylonJSController {
|
|
|
1931
2083
|
}
|
|
1932
2084
|
|
|
1933
2085
|
/**
|
|
1934
|
-
* Attaches the `
|
|
2086
|
+
* Attaches the `pref-viewer-3d-animation-changed` listener to the nearest `pref-viewer-3d` host.
|
|
1935
2087
|
* Removes any previous registration first to avoid duplicate callbacks across reload cycles.
|
|
1936
2088
|
* @private
|
|
1937
2089
|
* @returns {boolean} True when the listener is attached, false when no host is available.
|
|
@@ -1942,12 +2094,12 @@ export default class BabylonJSController {
|
|
|
1942
2094
|
return false;
|
|
1943
2095
|
}
|
|
1944
2096
|
this.#detachAnimationChangedListener();
|
|
1945
|
-
this.#prefViewer3D.addEventListener("
|
|
2097
|
+
this.#prefViewer3D.addEventListener("pref-viewer-3d-animation-changed", this.#handlers.onAnimationGroupChanged);
|
|
1946
2098
|
return true;
|
|
1947
2099
|
}
|
|
1948
2100
|
|
|
1949
2101
|
/**
|
|
1950
|
-
* Detaches the `
|
|
2102
|
+
* Detaches the `pref-viewer-3d-animation-changed` listener from the cached `pref-viewer-3d` host.
|
|
1951
2103
|
* @private
|
|
1952
2104
|
* @returns {boolean} True when a host exists and the listener removal was attempted, false otherwise.
|
|
1953
2105
|
*/
|
|
@@ -1955,7 +2107,7 @@ export default class BabylonJSController {
|
|
|
1955
2107
|
if (!this.#prefViewer3D) {
|
|
1956
2108
|
return false;
|
|
1957
2109
|
}
|
|
1958
|
-
this.#prefViewer3D.removeEventListener("
|
|
2110
|
+
this.#prefViewer3D.removeEventListener("pref-viewer-3d-animation-changed", this.#handlers.onAnimationGroupChanged);
|
|
1959
2111
|
return true;
|
|
1960
2112
|
}
|
|
1961
2113
|
|
|
@@ -1988,12 +2140,12 @@ export default class BabylonJSController {
|
|
|
1988
2140
|
*/
|
|
1989
2141
|
#onMouseWheel(event, pickInfo) {
|
|
1990
2142
|
event.preventDefault();
|
|
1991
|
-
|
|
2143
|
+
|
|
1992
2144
|
// OPTIMIZATION: Mark activity for idle mode
|
|
1993
2145
|
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1994
2146
|
this.#state.render.lastActivityAt = now;
|
|
1995
2147
|
this.#state.render.isIdle = false;
|
|
1996
|
-
|
|
2148
|
+
|
|
1997
2149
|
const camera = this.#scene?.activeCamera;
|
|
1998
2150
|
if (!camera) {
|
|
1999
2151
|
return false;
|
|
@@ -2028,7 +2180,7 @@ export default class BabylonJSController {
|
|
|
2028
2180
|
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
2029
2181
|
this.#state.render.lastActivityAt = now;
|
|
2030
2182
|
this.#state.render.isIdle = false;
|
|
2031
|
-
|
|
2183
|
+
|
|
2032
2184
|
if (this.#babylonJSAnimationController) {
|
|
2033
2185
|
if (event.button !== 2 || !pickInfo || !pickInfo.pickedMesh) {
|
|
2034
2186
|
this.#babylonJSAnimationController.hideMenu();
|
|
@@ -2053,7 +2205,7 @@ export default class BabylonJSController {
|
|
|
2053
2205
|
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
2054
2206
|
this.#state.render.lastActivityAt = now;
|
|
2055
2207
|
this.#state.render.isIdle = false;
|
|
2056
|
-
|
|
2208
|
+
|
|
2057
2209
|
const camera = this.#scene?.activeCamera;
|
|
2058
2210
|
if (camera && !camera.metadata?.locked) {
|
|
2059
2211
|
this.#requestRender({ frames: 1, continuousMs: this.#config.render.interactionMs });
|
|
@@ -2275,6 +2427,7 @@ export default class BabylonJSController {
|
|
|
2275
2427
|
}
|
|
2276
2428
|
}
|
|
2277
2429
|
this.#applyShowroomCameraFraming(camera);
|
|
2430
|
+
this.#fitDefaultCameraToModel(camera);
|
|
2278
2431
|
this.#scene.activeCamera?.detachControl();
|
|
2279
2432
|
camera.detachControl();
|
|
2280
2433
|
if (!cameraState.locked) {
|
|
@@ -2701,20 +2854,34 @@ export default class BabylonJSController {
|
|
|
2701
2854
|
this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
|
|
2702
2855
|
this.#setMaxSimultaneousLights();
|
|
2703
2856
|
this.#enhanceTextureQuality();
|
|
2704
|
-
this.#
|
|
2705
|
-
|
|
2706
|
-
this.#setHighlightConfig({
|
|
2707
|
-
enabled: this.#settings.highlightEnabled,
|
|
2708
|
-
color: this.#settings.highlightColor,
|
|
2709
|
-
});
|
|
2710
|
-
if (this.#babylonJSAnimationController?.hasAnimations?.()) {
|
|
2711
|
-
this.#attachAnimationChangedListener();
|
|
2712
|
-
}
|
|
2857
|
+
this.#syncAnimationControllerAfterModelLoad();
|
|
2858
|
+
|
|
2713
2859
|
await this.#startRender();
|
|
2714
2860
|
});
|
|
2715
2861
|
return detail;
|
|
2716
2862
|
}
|
|
2717
2863
|
|
|
2864
|
+
/**
|
|
2865
|
+
* Creates or disposes the animation controller after a model load according to current settings.
|
|
2866
|
+
* @private
|
|
2867
|
+
* @returns {void}
|
|
2868
|
+
*/
|
|
2869
|
+
#syncAnimationControllerAfterModelLoad() {
|
|
2870
|
+
if (this.#settings.showAnimationMenu && this.#containers.model.assetContainer) {
|
|
2871
|
+
this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
|
|
2872
|
+
// Apply stored highlight settings so fresh page loads respect persisted state
|
|
2873
|
+
this.#setHighlightConfig({
|
|
2874
|
+
enabled: this.#settings.highlightEnabled,
|
|
2875
|
+
color: this.#settings.highlightColor,
|
|
2876
|
+
});
|
|
2877
|
+
if (this.#babylonJSAnimationController?.hasAnimations?.()) {
|
|
2878
|
+
this.#attachAnimationChangedListener();
|
|
2879
|
+
}
|
|
2880
|
+
} else {
|
|
2881
|
+
this.#disposeAnimationController();
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
|
|
2718
2885
|
/**
|
|
2719
2886
|
* Reinstalls every camera-sensitive post-process (default pipeline, SSAO, shadows) after loads or option changes.
|
|
2720
2887
|
* Ensures the active camera always owns fresh pipelines so render quality remains consistent across reload cycles.
|
|
@@ -2968,14 +3135,14 @@ export default class BabylonJSController {
|
|
|
2968
3135
|
this.#configureDracoCompression();
|
|
2969
3136
|
this.#engine = new Engine(this.#canvas, true, { alpha: true, stencil: true, preserveDrawingBuffer: false }, true);
|
|
2970
3137
|
this.#engine.disableUniformBuffers = true;
|
|
2971
|
-
|
|
3138
|
+
|
|
2972
3139
|
// OPTIMIZATION: Detect hardware tier and apply quality settings
|
|
2973
3140
|
const detectedTier = this.#detectHardwareTier();
|
|
2974
3141
|
this.#applyQualitySettings(detectedTier);
|
|
2975
|
-
|
|
3142
|
+
|
|
2976
3143
|
// OPTIMIZATION: Initialize idle tracking
|
|
2977
3144
|
this.#state.render.lastActivityAt = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
2978
|
-
|
|
3145
|
+
|
|
2979
3146
|
this.#scene = new Scene(this.#engine);
|
|
2980
3147
|
|
|
2981
3148
|
// Activate the rendering of geometry data into a G-buffer, essential for advanced effects like deferred shading,
|
|
@@ -2996,6 +3163,7 @@ export default class BabylonJSController {
|
|
|
2996
3163
|
this.#scene.skipPointerUpPicking = true;
|
|
2997
3164
|
|
|
2998
3165
|
this.#createCamera();
|
|
3166
|
+
await this.#ensureEnvironmentBRDFTextureReady();
|
|
2999
3167
|
this.#enableInteraction();
|
|
3000
3168
|
await this.#createXRExperience();
|
|
3001
3169
|
}
|
|
@@ -3151,6 +3319,182 @@ export default class BabylonJSController {
|
|
|
3151
3319
|
return { ...this.#settings };
|
|
3152
3320
|
}
|
|
3153
3321
|
|
|
3322
|
+
/**
|
|
3323
|
+
* Returns a diagnostic snapshot of the current Babylon.js runtime state.
|
|
3324
|
+
* Intended for integration troubleshooting and should not mutate scene state.
|
|
3325
|
+
* @public
|
|
3326
|
+
* @returns {object} Debug information for canvas, engine, scene, containers, render loop and settings.
|
|
3327
|
+
*/
|
|
3328
|
+
getDebugInfo() {
|
|
3329
|
+
const vectorToJSON = (value) => value ? { x: value.x, y: value.y, z: value.z } : null;
|
|
3330
|
+
const rect = this.#canvas?.getBoundingClientRect?.();
|
|
3331
|
+
const camera = this.#scene?.activeCamera ?? null;
|
|
3332
|
+
const sceneMeshes = this.#scene?.meshes ?? [];
|
|
3333
|
+
const sceneMaterials = this.#scene?.materials ?? [];
|
|
3334
|
+
const sceneTextures = this.#scene?.textures ?? [];
|
|
3335
|
+
const activeMeshes = this.#scene?.getActiveMeshes?.();
|
|
3336
|
+
const modelAssetContainer = this.#containers?.model?.assetContainer ?? null;
|
|
3337
|
+
const modelMeshes = modelAssetContainer?.meshes ?? [];
|
|
3338
|
+
const enabledModelMeshes = modelMeshes.filter((mesh) => mesh.isEnabled?.() !== false && mesh.isVisible !== false);
|
|
3339
|
+
const textureToJSON = (texture) => ({
|
|
3340
|
+
name: texture?.name ?? null,
|
|
3341
|
+
url: texture?.url ?? null,
|
|
3342
|
+
isReady: texture?.isReady?.() ?? null,
|
|
3343
|
+
hasTexture: Boolean(texture),
|
|
3344
|
+
className: texture?.getClassName?.() ?? texture?.constructor?.name ?? null,
|
|
3345
|
+
});
|
|
3346
|
+
const summarizeMaterial = (material) => {
|
|
3347
|
+
const relatedMeshes = sceneMeshes.filter((mesh) => mesh.material === material);
|
|
3348
|
+
const sampleMesh = relatedMeshes[0] ?? null;
|
|
3349
|
+
const activeTextures = material?.getActiveTextures?.() ?? [];
|
|
3350
|
+
let isReadyWithoutMesh = null;
|
|
3351
|
+
let isReadyWithMesh = null;
|
|
3352
|
+
try {
|
|
3353
|
+
isReadyWithoutMesh = typeof material?.isReady === "function" ? material.isReady() : null;
|
|
3354
|
+
} catch (error) {
|
|
3355
|
+
isReadyWithoutMesh = `error: ${error?.message ?? error}`;
|
|
3356
|
+
}
|
|
3357
|
+
try {
|
|
3358
|
+
isReadyWithMesh = typeof material?.isReady === "function" && sampleMesh ? material.isReady(sampleMesh) : null;
|
|
3359
|
+
} catch (error) {
|
|
3360
|
+
isReadyWithMesh = `error: ${error?.message ?? error}`;
|
|
3361
|
+
}
|
|
3362
|
+
|
|
3363
|
+
return {
|
|
3364
|
+
name: material?.name ?? null,
|
|
3365
|
+
id: material?.id ?? null,
|
|
3366
|
+
className: material?.getClassName?.() ?? material?.constructor?.name ?? null,
|
|
3367
|
+
isReadyWithoutMesh,
|
|
3368
|
+
isReadyWithMesh,
|
|
3369
|
+
relatedMeshes: relatedMeshes.length,
|
|
3370
|
+
sampleMesh: sampleMesh ? {
|
|
3371
|
+
name: sampleMesh.name,
|
|
3372
|
+
id: sampleMesh.id,
|
|
3373
|
+
enabled: sampleMesh.isEnabled?.() ?? null,
|
|
3374
|
+
isVisible: sampleMesh.isVisible ?? null,
|
|
3375
|
+
hasVertices: sampleMesh.getTotalVertices?.() ?? null,
|
|
3376
|
+
hasIndices: sampleMesh.getTotalIndices?.() ?? null,
|
|
3377
|
+
} : null,
|
|
3378
|
+
textures: activeTextures.map(textureToJSON),
|
|
3379
|
+
notReadyTextures: activeTextures.filter((texture) => texture?.isReady?.() === false).map(textureToJSON),
|
|
3380
|
+
forceDepthWrite: material?.forceDepthWrite ?? null,
|
|
3381
|
+
transparencyMode: material?.transparencyMode ?? null,
|
|
3382
|
+
alpha: material?.alpha ?? null,
|
|
3383
|
+
backFaceCulling: material?.backFaceCulling ?? null,
|
|
3384
|
+
disableLighting: material?.disableLighting ?? null,
|
|
3385
|
+
};
|
|
3386
|
+
};
|
|
3387
|
+
const materialDiagnostics = sceneMaterials.slice(0, 25).map(summarizeMaterial);
|
|
3388
|
+
const notReadyMaterials = materialDiagnostics
|
|
3389
|
+
.filter((material) => material.isReadyWithoutMesh === false || material.isReadyWithMesh === false || material.notReadyTextures.length > 0)
|
|
3390
|
+
.slice(0, 25);
|
|
3391
|
+
const notReadyTextures = sceneTextures
|
|
3392
|
+
.filter((texture) => texture?.isReady?.() === false)
|
|
3393
|
+
.slice(0, 25)
|
|
3394
|
+
.map(textureToJSON);
|
|
3395
|
+
const notReadyMeshes = sceneMeshes
|
|
3396
|
+
.filter((mesh) => typeof mesh.isReady === "function" && !mesh.isReady())
|
|
3397
|
+
.slice(0, 25)
|
|
3398
|
+
.map((mesh) => ({
|
|
3399
|
+
name: mesh.name,
|
|
3400
|
+
id: mesh.id,
|
|
3401
|
+
enabled: mesh.isEnabled?.() ?? null,
|
|
3402
|
+
isVisible: mesh.isVisible ?? null,
|
|
3403
|
+
material: mesh.material?.name ?? null,
|
|
3404
|
+
}));
|
|
3405
|
+
const modelBounds = enabledModelMeshes.length && this.#scene?.getWorldExtends
|
|
3406
|
+
? this.#scene.getWorldExtends((mesh) => enabledModelMeshes.includes(mesh))
|
|
3407
|
+
: null;
|
|
3408
|
+
const summarizeContainer = (container) => ({
|
|
3409
|
+
show: container?.state?.show ?? null,
|
|
3410
|
+
visible: container?.state?.visible ?? null,
|
|
3411
|
+
mustBeShown: container?.state?.mustBeShown ?? null,
|
|
3412
|
+
isPending: container?.state?.isPending ?? null,
|
|
3413
|
+
isSuccess: container?.state?.isSuccess ?? null,
|
|
3414
|
+
hasAssetContainer: Boolean(container?.assetContainer),
|
|
3415
|
+
meshes: container?.assetContainer?.meshes?.length ?? 0,
|
|
3416
|
+
enabledMeshes: (container?.assetContainer?.meshes ?? []).filter((mesh) => mesh.isEnabled?.() !== false && mesh.isVisible !== false).length,
|
|
3417
|
+
materials: container?.assetContainer?.materials?.length ?? 0,
|
|
3418
|
+
cameras: container?.assetContainer?.cameras?.map((item) => ({
|
|
3419
|
+
name: item.name,
|
|
3420
|
+
id: item.id,
|
|
3421
|
+
className: item.getClassName?.() ?? item.constructor?.name ?? null,
|
|
3422
|
+
position: vectorToJSON(item.position),
|
|
3423
|
+
target: vectorToJSON(item.target),
|
|
3424
|
+
})) ?? [],
|
|
3425
|
+
animationGroups: container?.assetContainer?.animationGroups?.length ?? 0,
|
|
3426
|
+
});
|
|
3427
|
+
|
|
3428
|
+
return {
|
|
3429
|
+
canvas: {
|
|
3430
|
+
clientWidth: this.#canvas?.clientWidth ?? null,
|
|
3431
|
+
clientHeight: this.#canvas?.clientHeight ?? null,
|
|
3432
|
+
width: this.#canvas?.width ?? null,
|
|
3433
|
+
height: this.#canvas?.height ?? null,
|
|
3434
|
+
boundingWidth: rect?.width ?? null,
|
|
3435
|
+
boundingHeight: rect?.height ?? null,
|
|
3436
|
+
},
|
|
3437
|
+
engine: {
|
|
3438
|
+
renderWidth: this.#engine?.getRenderWidth?.() ?? null,
|
|
3439
|
+
renderHeight: this.#engine?.getRenderHeight?.() ?? null,
|
|
3440
|
+
hardwareScalingLevel: this.#engine?.getHardwareScalingLevel?.() ?? null,
|
|
3441
|
+
isPointerLock: this.#engine?.isPointerLock ?? null,
|
|
3442
|
+
isDisposed: this.#engine?.isDisposed ?? null,
|
|
3443
|
+
contextLost: this.#engine?.isContextLost?.() ?? null,
|
|
3444
|
+
},
|
|
3445
|
+
scene: {
|
|
3446
|
+
uid: this.#scene?.uid ?? null,
|
|
3447
|
+
isReady: this.#scene?.isReady?.() ?? null,
|
|
3448
|
+
meshes: sceneMeshes.length,
|
|
3449
|
+
visibleMeshes: activeMeshes?.length ?? activeMeshes?.data?.length ?? null,
|
|
3450
|
+
materials: sceneMaterials.length,
|
|
3451
|
+
textures: sceneTextures.length,
|
|
3452
|
+
lights: this.#scene?.lights?.length ?? 0,
|
|
3453
|
+
animationGroups: this.#scene?.animationGroups?.length ?? 0,
|
|
3454
|
+
activeCamera: camera ? {
|
|
3455
|
+
name: camera.name,
|
|
3456
|
+
id: camera.id,
|
|
3457
|
+
className: camera.getClassName?.() ?? camera.constructor?.name ?? null,
|
|
3458
|
+
position: vectorToJSON(camera.position),
|
|
3459
|
+
target: vectorToJSON(camera.target),
|
|
3460
|
+
radius: camera.radius ?? null,
|
|
3461
|
+
alpha: camera.alpha ?? null,
|
|
3462
|
+
beta: camera.beta ?? null,
|
|
3463
|
+
} : null,
|
|
3464
|
+
clearColor: this.#scene?.clearColor ? {
|
|
3465
|
+
r: this.#scene.clearColor.r,
|
|
3466
|
+
g: this.#scene.clearColor.g,
|
|
3467
|
+
b: this.#scene.clearColor.b,
|
|
3468
|
+
a: this.#scene.clearColor.a,
|
|
3469
|
+
} : null,
|
|
3470
|
+
environmentTexture: this.#scene?.environmentTexture ? {
|
|
3471
|
+
name: this.#scene.environmentTexture.name,
|
|
3472
|
+
isReady: this.#scene.environmentTexture.isReady?.() ?? null,
|
|
3473
|
+
} : null,
|
|
3474
|
+
notReadyMaterials,
|
|
3475
|
+
notReadyTextures,
|
|
3476
|
+
materialDiagnostics,
|
|
3477
|
+
notReadyMeshes,
|
|
3478
|
+
},
|
|
3479
|
+
containers: {
|
|
3480
|
+
model: summarizeContainer(this.#containers.model),
|
|
3481
|
+
environment: summarizeContainer(this.#containers.environment),
|
|
3482
|
+
materials: summarizeContainer(this.#containers.materials),
|
|
3483
|
+
},
|
|
3484
|
+
modelBounds: modelBounds ? {
|
|
3485
|
+
min: vectorToJSON(modelBounds.min),
|
|
3486
|
+
max: vectorToJSON(modelBounds.max),
|
|
3487
|
+
} : null,
|
|
3488
|
+
render: { ...this.#state.render },
|
|
3489
|
+
settings: { ...this.#settings },
|
|
3490
|
+
pipelines: {
|
|
3491
|
+
default: Boolean(this.#renderPipelines.default),
|
|
3492
|
+
ssao: Boolean(this.#renderPipelines.ssao),
|
|
3493
|
+
iblShadows: Boolean(this.#renderPipelines.iblShadows),
|
|
3494
|
+
},
|
|
3495
|
+
};
|
|
3496
|
+
}
|
|
3497
|
+
|
|
3154
3498
|
/**
|
|
3155
3499
|
* Loads all asset containers (model, environment, materials, etc.) and adds them to the scene.
|
|
3156
3500
|
* Applies material and camera options, sets visibility, and initializes lights and shadows.
|
|
@@ -3203,14 +3547,42 @@ export default class BabylonJSController {
|
|
|
3203
3547
|
}
|
|
3204
3548
|
|
|
3205
3549
|
/**
|
|
3206
|
-
* Applies
|
|
3550
|
+
* Applies animation-menu, highlight, and time-of-day lighting configuration without forcing a full scene reload.
|
|
3551
|
+
* Values are merged into the persisted render settings snapshot unless `config.persist === false`.
|
|
3552
|
+
* When `showLightingTimeOfDay` is set to false and no explicit `lightingTimeOfDay` is supplied,
|
|
3553
|
+
* the active lighting time is reset to `DEFAULT_RENDER_SETTINGS.lightingTimeOfDay` (0.5).
|
|
3207
3554
|
* @public
|
|
3208
|
-
* @param {
|
|
3555
|
+
* @param {object} config - Immediate render/illumination configuration.
|
|
3556
|
+
* @param {boolean|string} [config.showAnimationMenu] - Enables/disables the animation controls when animations are available.
|
|
3557
|
+
* @param {boolean|string|null} [config.highlightEnabled] - Enables/disables hover highlight; string "false" is normalized to false, null is ignored.
|
|
3558
|
+
* @param {string|null} [config.highlightColor] - Hex color used by hover highlight; null is ignored.
|
|
3559
|
+
* @param {boolean|string} [config.showLightingTimeOfDay] - Shows/hides the time-of-day control and can reset time to the default when hidden.
|
|
3560
|
+
* @param {number} [config.lightingTimeOfDay] - Normalized time of day in the 0..1 range.
|
|
3561
|
+
* @param {boolean} [config.persist=true] - False for transient updates such as lighting playback frames.
|
|
3209
3562
|
* @returns {void}
|
|
3210
3563
|
*/
|
|
3211
3564
|
setIlluminationConfig(config = {}) {
|
|
3212
3565
|
let shouldApplyLighting = false;
|
|
3213
3566
|
|
|
3567
|
+
if (config.showLightingTimeOfDay !== undefined) {
|
|
3568
|
+
const showLighting = typeof config.showLightingTimeOfDay === "string"
|
|
3569
|
+
? config.showLightingTimeOfDay.toLowerCase() !== "false"
|
|
3570
|
+
: Boolean(config.showLightingTimeOfDay);
|
|
3571
|
+
if (this.#settings.showLightingTimeOfDay !== showLighting) {
|
|
3572
|
+
shouldApplyLighting = true;
|
|
3573
|
+
}
|
|
3574
|
+
this.#settings.showLightingTimeOfDay = showLighting;
|
|
3575
|
+
|
|
3576
|
+
const hasExplicitLightingTime = typeof config.lightingTimeOfDay === "number" && Number.isFinite(config.lightingTimeOfDay);
|
|
3577
|
+
if (!showLighting && !hasExplicitLightingTime) {
|
|
3578
|
+
const defaultTime = BabylonJSController.DEFAULT_RENDER_SETTINGS.lightingTimeOfDay;
|
|
3579
|
+
if (this.#settings.lightingTimeOfDay !== defaultTime) {
|
|
3580
|
+
this.#settings.lightingTimeOfDay = defaultTime;
|
|
3581
|
+
shouldApplyLighting = true;
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3214
3586
|
if (typeof config.lightingTimeOfDay === "number" && Number.isFinite(config.lightingTimeOfDay)) {
|
|
3215
3587
|
const normalizedTime = clamp01(config.lightingTimeOfDay);
|
|
3216
3588
|
if (this.#settings.lightingTimeOfDay !== normalizedTime) {
|
|
@@ -3220,8 +3592,11 @@ export default class BabylonJSController {
|
|
|
3220
3592
|
}
|
|
3221
3593
|
|
|
3222
3594
|
if (config.showAnimationMenu !== undefined) {
|
|
3223
|
-
|
|
3224
|
-
|
|
3595
|
+
const showMenu = typeof config.showAnimationMenu === "string"
|
|
3596
|
+
? config.showAnimationMenu.toLowerCase() !== "false"
|
|
3597
|
+
: Boolean(config.showAnimationMenu);
|
|
3598
|
+
this.#settings.showAnimationMenu = showMenu;
|
|
3599
|
+
this.#syncAnimationControllerAfterModelLoad();
|
|
3225
3600
|
}
|
|
3226
3601
|
|
|
3227
3602
|
// Handle highlight color for animation hover
|
|
@@ -3242,7 +3617,7 @@ export default class BabylonJSController {
|
|
|
3242
3617
|
}
|
|
3243
3618
|
this.#setHighlightConfig({
|
|
3244
3619
|
color: config.highlightColor ?? undefined,
|
|
3245
|
-
enabled,
|
|
3620
|
+
enabled: enabled,
|
|
3246
3621
|
});
|
|
3247
3622
|
}
|
|
3248
3623
|
|
|
@@ -3281,11 +3656,13 @@ export default class BabylonJSController {
|
|
|
3281
3656
|
}
|
|
3282
3657
|
|
|
3283
3658
|
/**
|
|
3284
|
-
* Merges
|
|
3659
|
+
* Merges reload-oriented render flags with the current configuration, persists them, and marks
|
|
3285
3660
|
* all dependent loaders/options as pending when something actually changed.
|
|
3661
|
+
* Immediate settings such as highlight, animation menu, and time-of-day lighting are handled by
|
|
3662
|
+
* `setIlluminationConfig()` before this method receives the remaining reload settings.
|
|
3286
3663
|
* @public
|
|
3287
|
-
* @param {{antiAliasingEnabled?:boolean, ambientOcclusionEnabled?:boolean, iblEnabled?:boolean, shadowsEnabled?:boolean}} settings Partial set of render toggles to apply.
|
|
3288
|
-
* @returns {{changed:boolean, settings:
|
|
3664
|
+
* @param {{antiAliasingEnabled?:boolean, ambientOcclusionEnabled?:boolean, iblEnabled?:boolean, shadowsEnabled?:boolean}} settings Partial set of reload render toggles to apply.
|
|
3665
|
+
* @returns {{changed:boolean, settings:object}} Current full render settings snapshot and whether a reload is required.
|
|
3289
3666
|
* @description
|
|
3290
3667
|
* Callers can inspect the `changed` flag to decide whether to trigger a reload with
|
|
3291
3668
|
* `reloadWithCurrentSettings()` or simply reuse the returned snapshot.
|