@preference-sl/pref-viewer 2.16.0-beta.1 → 2.16.0-beta.10
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/LICENSE.md +13 -0
- package/README.md +463 -0
- package/package.json +7 -7
- package/src/babylonjs-controller.js +122 -847
- package/src/file-storage.js +5 -126
- package/src/gltf-resolver.js +24 -58
- package/src/localization/translations.js +5 -17
- package/src/material-utils.js +72 -0
- package/src/pref-viewer-3d.js +24 -112
- package/src/pref-viewer-menu-3d.js +133 -260
- package/src/pref-viewer.js +4 -150
- package/src/render-math.js +127 -0
- package/src/styles.js +71 -163
- package/Readme.md +0 -8
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ArcRotateCamera, AssetContainer, 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, 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";
|
|
@@ -10,74 +10,13 @@ import { MaterialData } from "./pref-viewer-3d-data.js";
|
|
|
10
10
|
import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
|
|
11
11
|
import OpeningAnimation from "./babylonjs-animation-opening.js";
|
|
12
12
|
import { translate } from "./localization/i18n.js";
|
|
13
|
+
import { PrefViewerStyleVariables } from "./styles.js";
|
|
14
|
+
import { clamp01, lerp, getAdaptiveHardwareScalingLevel, getLightingCycleProfile } from "./render-math.js";
|
|
15
|
+
import { markPrefViewerSimplifiedTransmissionMaterial, isPrefViewerPhysicalGlassMaterial, isPrefViewerTransparentRenderOrderMaterial } from "./material-utils.js";
|
|
13
16
|
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return safeBaseScaling / safePixelRatio;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function clamp01(value) {
|
|
21
|
-
if (!Number.isFinite(value)) {
|
|
22
|
-
return 0.5;
|
|
23
|
-
}
|
|
24
|
-
return Math.min(1, Math.max(0, value));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function lerp(from, to, amount) {
|
|
28
|
-
const t = clamp01(amount);
|
|
29
|
-
return from + (to - from) * t;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function smoothstep(edge0, edge1, x) {
|
|
33
|
-
if (edge0 === edge1) {
|
|
34
|
-
return x >= edge1 ? 1 : 0;
|
|
35
|
-
}
|
|
36
|
-
const t = clamp01((x - edge0) / (edge1 - edge0));
|
|
37
|
-
return t * t * (3 - 2 * t);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function bellCurve(x) {
|
|
41
|
-
const clamped = clamp01(x);
|
|
42
|
-
return Math.sin(clamped * Math.PI);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function normalizeDirection(x, y, z) {
|
|
46
|
-
const length = Math.hypot(x, y, z) || 1;
|
|
47
|
-
return {
|
|
48
|
-
x: x / length,
|
|
49
|
-
y: y / length,
|
|
50
|
-
z: z / length,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function getLightingCycleProfile(timeOfDay = 0.5) {
|
|
55
|
-
const normalizedTime = clamp01(timeOfDay);
|
|
56
|
-
const timeHours = normalizedTime * 24;
|
|
57
|
-
const sunriseHour = 6.25;
|
|
58
|
-
const sunsetHour = 20.25;
|
|
59
|
-
const daylightProgress = clamp01((timeHours - sunriseHour) / (sunsetHour - sunriseHour));
|
|
60
|
-
const dayFactor = timeHours >= sunriseHour && timeHours <= sunsetHour ? Math.pow(bellCurve(daylightProgress), 0.88) : 0;
|
|
61
|
-
const nightFactor = 1 - dayFactor;
|
|
62
|
-
const solarArc = dayFactor > 0 ? Math.pow(dayFactor, 0.72) : 0;
|
|
63
|
-
const solarAzimuth = lerp(-Math.PI * 0.82, Math.PI * 0.82, daylightProgress);
|
|
64
|
-
const solarElevation = lerp(-0.18, 1.18, solarArc);
|
|
65
|
-
const sunPhase = normalizedTime * Math.PI * 2 - Math.PI / 2;
|
|
66
|
-
const sunDirection = normalizeDirection(-Math.cos(sunPhase), -Math.sin(sunPhase), -0.35);
|
|
67
|
-
const moonDirection = normalizeDirection(-sunDirection.x, -sunDirection.y, -sunDirection.z);
|
|
68
|
-
|
|
69
|
-
return {
|
|
70
|
-
timeOfDay: normalizedTime,
|
|
71
|
-
timeHours,
|
|
72
|
-
sunPhase,
|
|
73
|
-
solarAzimuth,
|
|
74
|
-
solarElevation,
|
|
75
|
-
dayFactor,
|
|
76
|
-
nightFactor,
|
|
77
|
-
sunDirection,
|
|
78
|
-
moonDirection,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
17
|
+
// Re-export so existing consumers (tests, other modules) can keep importing from this file.
|
|
18
|
+
export { getAdaptiveHardwareScalingLevel, getLightingCycleProfile } from "./render-math.js";
|
|
19
|
+
export { markPrefViewerSimplifiedTransmissionMaterial, isPrefViewerPhysicalGlassMaterial, isPrefViewerTransparentRenderOrderMaterial } from "./material-utils.js";
|
|
81
20
|
|
|
82
21
|
/**
|
|
83
22
|
* BabylonJSController coordinates the PrefViewer 3D runtime: it bootstraps Babylon.js, manages asset containers,
|
|
@@ -100,9 +39,10 @@ export function getLightingCycleProfile(timeOfDay = 0.5) {
|
|
|
100
39
|
*
|
|
101
40
|
* Runtime Flow
|
|
102
41
|
* 1. Instantiate with `new BabylonJSController(canvas, containers, options)`.
|
|
103
|
-
* 2. Call `enable()` to configure Draco, spin up the engine/scene, attach interaction + XR hooks
|
|
42
|
+
* 2. Call `enable()` to configure Draco, spin up the engine/scene, and attach interaction + XR hooks. The render loop
|
|
43
|
+
* does not start here; it starts automatically after the first `load()` completes.
|
|
104
44
|
* 3. When PrefViewer marks containers/options pending, invoke `load()` (or `reloadWithCurrentSettings()`) so the controller
|
|
105
|
-
* fetches sources, rebuilds containers, and
|
|
45
|
+
* fetches sources, rebuilds containers, reattaches pipelines, and starts the render loop.
|
|
106
46
|
* 4. Use `scheduleRenderSettingsReload()` to merge/persist toggles; when it reports `changed: true`, call
|
|
107
47
|
* `reloadWithCurrentSettings()` to apply the staged settings.
|
|
108
48
|
* 5. Use `setContainerVisibility`, `setMaterialOptions`, `setCameraOptions`, or `setIBLOptions` for targeted updates; these
|
|
@@ -119,10 +59,13 @@ export function getLightingCycleProfile(timeOfDay = 0.5) {
|
|
|
119
59
|
* - getRenderSettings() / scheduleRenderSettingsReload(settings)
|
|
120
60
|
* - setContainerVisibility(name, show)
|
|
121
61
|
* - setMaterialOptions() / setCameraOptions() / setIBLOptions()
|
|
62
|
+
* - setIlluminationConfig(config)
|
|
63
|
+
* - getAnimations() / playAnimation(name, action) / setAnimationProgress(name, progress) / setAnimationLoop(name, loop)
|
|
122
64
|
*
|
|
123
65
|
* Key Invariants
|
|
124
|
-
* - Asset containers must expose `setPendingWithCurrentStorage
|
|
125
|
-
*
|
|
66
|
+
* - Asset containers must expose `setPendingWithCurrentStorage` before calling load/reload; the controller reads that
|
|
67
|
+
* flag to resolve fresh sources. Option objects (camera, materials, IBL) expose `setPendingWithCurrent` for the same
|
|
68
|
+
* purpose. Neither set of flags touches the DOM — they only gate resolver calls.
|
|
126
69
|
* - Camera-dependent pipelines (DefaultRenderingPipeline, SSAO, IBL shadows, directional shadow generators) are rebuilt
|
|
127
70
|
* only after the active camera and environment textures are ready; render-loop restarts gate those transitions.
|
|
128
71
|
* - `show-model`/`show-scene` DOM attributes reflect container visibility; there are no direct `showModel()/hideModel()` APIs.
|
|
@@ -156,7 +99,7 @@ export default class BabylonJSController {
|
|
|
156
99
|
lightingTimeOfDay: 0.5,
|
|
157
100
|
// Highlight settings
|
|
158
101
|
highlightEnabled: true,
|
|
159
|
-
highlightColor:
|
|
102
|
+
highlightColor: PrefViewerStyleVariables.colorPrimary,
|
|
160
103
|
};
|
|
161
104
|
|
|
162
105
|
// Canvas HTML element
|
|
@@ -165,8 +108,6 @@ export default class BabylonJSController {
|
|
|
165
108
|
// References to parent custom elements
|
|
166
109
|
#prefViewer3D = undefined;
|
|
167
110
|
#prefViewer = undefined;
|
|
168
|
-
#modelFirstPaintedEmitted = false;
|
|
169
|
-
#modelFirstPaintedLoadToken = 0;
|
|
170
111
|
|
|
171
112
|
// Babylon.js core objects
|
|
172
113
|
#engine = null;
|
|
@@ -176,23 +117,17 @@ export default class BabylonJSController {
|
|
|
176
117
|
#dirLight = null;
|
|
177
118
|
#moonLight = null;
|
|
178
119
|
#cameraLight = null;
|
|
179
|
-
#debugCameraFrameInfo = null;
|
|
180
120
|
#shadowGen = [];
|
|
181
121
|
#XRExperience = null;
|
|
182
122
|
#canvasResizeObserver = null;
|
|
183
123
|
|
|
184
|
-
#hdrTexture = null; //
|
|
124
|
+
#hdrTexture = null; // Reusable in-memory HDR source cloned into scene.environmentTexture across reloads
|
|
185
125
|
|
|
186
126
|
#containers = {};
|
|
187
127
|
#options = {};
|
|
188
128
|
|
|
189
129
|
#gltfResolver = null; // GLTFResolver instance
|
|
190
130
|
#babylonJSAnimationController = null; // AnimationController instance
|
|
191
|
-
#loadingInProgress = false; // blocks #requestRender while assets are loading to prevent WebGL errors from rendering with disposed shaders
|
|
192
|
-
#isLoadingModelIndependent = false; // guards against concurrent independent model loads
|
|
193
|
-
#isLoadingEnvironmentIndependent = false; // guards against concurrent independent environment loads
|
|
194
|
-
#isLoadingMaterialsIndependent = false; // guards against concurrent independent materials loads
|
|
195
|
-
#startRenderInProgress = false; // true while #startRender() is running (after #loadingInProgress clears)
|
|
196
131
|
|
|
197
132
|
#renderPipelines = {
|
|
198
133
|
default: null,
|
|
@@ -239,30 +174,30 @@ export default class BabylonJSController {
|
|
|
239
174
|
// Runtime configuration constants (tuning knobs, not per-frame state).
|
|
240
175
|
#config = {
|
|
241
176
|
pointerPicking: {
|
|
242
|
-
movePickIntervalMs: 50, //
|
|
243
|
-
movePickMinDistancePx: 2, //
|
|
177
|
+
movePickIntervalMs: 50, // Cap expensive scene.pick calls to ~20 Hz while moving the pointer
|
|
178
|
+
movePickMinDistancePx: 2, // Skip picks for sub-pixel jitter
|
|
244
179
|
},
|
|
245
180
|
render: {
|
|
246
181
|
burstFramesBase: 2,
|
|
247
|
-
burstFramesEnhanced: 32, //
|
|
182
|
+
burstFramesEnhanced: 32, // When AA/SSAO/IBL is enabled, more frames are needed to reach stable output
|
|
248
183
|
interactionMs: 250,
|
|
249
184
|
animationMs: 200,
|
|
250
185
|
idleThrottleMs: 1000 / 8, // Reduced from 15 FPS to 8 FPS for idle - reduces GPU/CPU load
|
|
251
186
|
idleThresholdMs: 3000, // Time before going to true idle (no renders)
|
|
252
187
|
},
|
|
253
188
|
resize: {
|
|
254
|
-
throttleMs: 50, //
|
|
189
|
+
throttleMs: 50, // Cap resize work to ~20 Hz while dragging/resizing containers
|
|
255
190
|
},
|
|
256
191
|
// Hardware-adaptive quality settings (applied based on GPU capabilities)
|
|
257
192
|
quality: {
|
|
258
193
|
// Settings for different hardware tiers: 'low', 'medium', 'high'
|
|
259
194
|
// Applied automatically based on GPU detected capabilities
|
|
260
|
-
tier: 'medium',
|
|
261
|
-
shadowMapSize: 1024,
|
|
262
|
-
iblShadowResolution: 1,
|
|
263
|
-
iblSampleDirections: 4,
|
|
264
|
-
shadowBlurKernel: 16,
|
|
265
|
-
ssaoEnabled: true,
|
|
195
|
+
tier: 'medium', // Default, will be updated after hardware detection
|
|
196
|
+
shadowMapSize: 1024, // Standard shadow generator resolution
|
|
197
|
+
iblShadowResolution: 1, // IBL shadow resolution exponent (0=1024, 1=2048, 2=4096)
|
|
198
|
+
iblSampleDirections: 4, // IBL shadow quality
|
|
199
|
+
shadowBlurKernel: 16, // Shadow blur amount
|
|
200
|
+
ssaoEnabled: true, // Screen Space Ambient Occlusion
|
|
266
201
|
},
|
|
267
202
|
look: {
|
|
268
203
|
showroom: {
|
|
@@ -386,23 +321,23 @@ export default class BabylonJSController {
|
|
|
386
321
|
// High-end mobile patterns (recent flagship devices)
|
|
387
322
|
const highEndPatterns = [
|
|
388
323
|
/iPhone (1[4-9]|SE)/i, // iPhone 14+
|
|
389
|
-
/iPad Pro/i,
|
|
390
|
-
/SM-S91[0-9]/i,
|
|
391
|
-
/SM-S9[0-9]/i,
|
|
392
|
-
/Pixel [7-9]/i,
|
|
393
|
-
/OnePlus [8-9|10|11]/i,
|
|
324
|
+
/iPad Pro/i, // iPad Pro
|
|
325
|
+
/SM-S91[0-9]/i, // Samsung S21+
|
|
326
|
+
/SM-S9[0-9]/i, // Samsung S20+
|
|
327
|
+
/Pixel [7-9]/i, // Pixel 7+
|
|
328
|
+
/OnePlus [8-9|10|11]/i, // OnePlus 8+
|
|
394
329
|
/Xiaomi (1[2-4]|Mi 1[1-4])/i, // Xiaomi 12+
|
|
395
|
-
/Huawei (P50|Mate 4|)/i,
|
|
330
|
+
/Huawei (P50|Mate 4|)/i, // Huawei P50+
|
|
396
331
|
];
|
|
397
332
|
|
|
398
333
|
// Mid-range patterns
|
|
399
334
|
const midRangePatterns = [
|
|
400
|
-
/iPhone (1[0-3]|SE)/i,
|
|
335
|
+
/iPhone (1[0-3]|SE)/i, // iPhone 10-13
|
|
401
336
|
/iPad Air/i, // iPad Air
|
|
402
|
-
/SM-A[0-9]/i,
|
|
403
|
-
/Redmi Note/i,
|
|
404
|
-
/Pixel [4-6]/i,
|
|
405
|
-
/Moto G/i,
|
|
337
|
+
/SM-A[0-9]/i, // Samsung A series
|
|
338
|
+
/Redmi Note/i, // Redmi Note
|
|
339
|
+
/Pixel [4-6]/i, // Pixel 4-6
|
|
340
|
+
/Moto G/i, // Moto G
|
|
406
341
|
];
|
|
407
342
|
|
|
408
343
|
const isHighEnd = highEndPatterns.some(p => p.test(userAgent));
|
|
@@ -454,13 +389,13 @@ export default class BabylonJSController {
|
|
|
454
389
|
// Mobile low-end (old phones, budget devices)
|
|
455
390
|
low: {
|
|
456
391
|
shadowMapSize: 256, // Very small for speed
|
|
457
|
-
iblShadowResolution: -1,
|
|
392
|
+
iblShadowResolution: -1, // Disable IBL shadows
|
|
458
393
|
iblSampleDirections: 1, // Minimal sampling
|
|
459
394
|
shadowBlurKernel: 0, // No blur
|
|
460
395
|
ssaoEnabled: false,
|
|
461
396
|
antialiasingEnabled: false,
|
|
462
|
-
hardwareScaling: 2.5,
|
|
463
|
-
maxModelResolution: 512,
|
|
397
|
+
hardwareScaling: 2.5, // Aggressive downscaling
|
|
398
|
+
maxModelResolution: 512, // Low-res model loading
|
|
464
399
|
environmentTextureSize: 512,
|
|
465
400
|
},
|
|
466
401
|
// Mobile high-end / Tablet / Desktop low
|
|
@@ -478,7 +413,7 @@ export default class BabylonJSController {
|
|
|
478
413
|
// Desktop mid-range / High-end mobile
|
|
479
414
|
high: {
|
|
480
415
|
shadowMapSize: 1024,
|
|
481
|
-
iblShadowResolution: 0,
|
|
416
|
+
iblShadowResolution: 0, // 1024x1024 - good balance
|
|
482
417
|
iblSampleDirections: 4, // Quality sampling
|
|
483
418
|
shadowBlurKernel: 8, // Sharper shadows for product clarity
|
|
484
419
|
ssaoEnabled: false,
|
|
@@ -490,9 +425,9 @@ export default class BabylonJSController {
|
|
|
490
425
|
// Desktop high-end (RTX, Apple Silicon, etc.)
|
|
491
426
|
ultra: {
|
|
492
427
|
shadowMapSize: 2048,
|
|
493
|
-
iblShadowResolution: 1,
|
|
428
|
+
iblShadowResolution: 1, // 2048x2048
|
|
494
429
|
iblSampleDirections: 8, // Maximum quality
|
|
495
|
-
shadowBlurKernel: 16,
|
|
430
|
+
shadowBlurKernel: 16, // Preserve clarity over softness
|
|
496
431
|
ssaoEnabled: false,
|
|
497
432
|
antialiasingEnabled: true,
|
|
498
433
|
hardwareScaling: 1.0, // Native resolution
|
|
@@ -756,8 +691,8 @@ export default class BabylonJSController {
|
|
|
756
691
|
* @param {number} [options.continuousMs=0] - Milliseconds to keep continuous rendering active.
|
|
757
692
|
* @returns {boolean} True when the request was accepted, false when scene/engine are unavailable.
|
|
758
693
|
*/
|
|
759
|
-
#requestRender({ frames = 1, continuousMs = 0
|
|
760
|
-
if (!this.#scene || !this.#engine
|
|
694
|
+
#requestRender({ frames = 1, continuousMs = 0 } = {}) {
|
|
695
|
+
if (!this.#scene || !this.#engine) {
|
|
761
696
|
return false;
|
|
762
697
|
}
|
|
763
698
|
|
|
@@ -1120,11 +1055,6 @@ export default class BabylonJSController {
|
|
|
1120
1055
|
|
|
1121
1056
|
const pipelineName = "PrefViewerSSAORenderingPipeline";
|
|
1122
1057
|
|
|
1123
|
-
// Skip recreation if already alive — same reason as DefaultRenderingPipeline guard
|
|
1124
|
-
if (this.#renderPipelines.ssao && supportedPipelines.find((p) => p.name === pipelineName)) {
|
|
1125
|
-
return true;
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
1058
|
const ssaoRatio = {
|
|
1129
1059
|
ssaoRatio: 0.5,
|
|
1130
1060
|
combineRatio: 1.0,
|
|
@@ -1226,21 +1156,6 @@ export default class BabylonJSController {
|
|
|
1226
1156
|
|
|
1227
1157
|
const pipelineName = "PrefViewerDefaultRenderingPipeline";
|
|
1228
1158
|
|
|
1229
|
-
// Skip recreation if the pipeline is already alive — creating a new DefaultRenderingPipeline
|
|
1230
|
-
// while one exists disposes the underlying GeometryBufferRenderer, deleting GL programs that
|
|
1231
|
-
// subMeshes still reference, causing useProgram:deleted errors on the next render frame.
|
|
1232
|
-
if (this.#renderPipelines.default && supportedPipelines.find((p) => p.name === pipelineName)) {
|
|
1233
|
-
return true;
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
// Purge stale effect cache before creating a new pipeline. pipeline.dispose() calls
|
|
1237
|
-
// gl.deleteProgram() per post-process but leaves entries in Engine._compiledEffects.
|
|
1238
|
-
// A new DefaultRenderingPipeline with the same shader key gets a cache hit and receives
|
|
1239
|
-
// the deleted program → useProgram:deleted on the first render frame.
|
|
1240
|
-
// Safe here because #startRender always stops the loop before reaching this point.
|
|
1241
|
-
this.#scene.getEngine().releaseEffects?.();
|
|
1242
|
-
this.#scene.getEngine().releaseComputeEffects?.();
|
|
1243
|
-
|
|
1244
1159
|
let defaultPipeline = new DefaultRenderingPipeline(pipelineName, true, this.#scene, [this.#scene.activeCamera], true);
|
|
1245
1160
|
|
|
1246
1161
|
if (!defaultPipeline) {
|
|
@@ -1418,11 +1333,6 @@ export default class BabylonJSController {
|
|
|
1418
1333
|
|
|
1419
1334
|
const pipelineName = "PrefViewerIblShadowsRenderPipeline";
|
|
1420
1335
|
|
|
1421
|
-
// Skip recreation if already alive — same reason as DefaultRenderingPipeline guard
|
|
1422
|
-
if (this.#renderPipelines.iblShadows && supportedPipelines.find((p) => p.name === pipelineName)) {
|
|
1423
|
-
return true;
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
1336
|
// Use hardware-adaptive quality settings
|
|
1427
1337
|
const pipelineOptions = {
|
|
1428
1338
|
resolutionExp: this.#config.quality.iblShadowResolution, // Adaptive: 0=1024, 1=2048, 2=4096
|
|
@@ -1660,7 +1570,7 @@ export default class BabylonJSController {
|
|
|
1660
1570
|
}
|
|
1661
1571
|
});
|
|
1662
1572
|
}
|
|
1663
|
-
|
|
1573
|
+
|
|
1664
1574
|
/**
|
|
1665
1575
|
* Applies the showroom tonemapping curve and lighting bias used by the 3D viewer.
|
|
1666
1576
|
* @private
|
|
@@ -1674,7 +1584,7 @@ export default class BabylonJSController {
|
|
|
1674
1584
|
const showroom = this.#config.look?.showroom || {};
|
|
1675
1585
|
const imageProcessing = this.#scene.imageProcessingConfiguration;
|
|
1676
1586
|
|
|
1677
|
-
this.#scene.clearColor = new Color4(
|
|
1587
|
+
this.#scene.clearColor = new Color4(0.98, 0.98, 0.98, 1).toLinearSpace();
|
|
1678
1588
|
imageProcessing.exposure = showroom.exposure ?? 0.95;
|
|
1679
1589
|
imageProcessing.contrast = showroom.contrast ?? 1.08;
|
|
1680
1590
|
imageProcessing.toneMappingEnabled = showroom.toneMappingEnabled ?? true;
|
|
@@ -1714,7 +1624,12 @@ export default class BabylonJSController {
|
|
|
1714
1624
|
imageProcessing.colorCurvesEnabled = false;
|
|
1715
1625
|
}
|
|
1716
1626
|
|
|
1717
|
-
this.#scene.clearColor = new Color4(
|
|
1627
|
+
this.#scene.clearColor = new Color4(
|
|
1628
|
+
lerp(0.035, 0.98, daylight),
|
|
1629
|
+
lerp(0.045, 0.98, daylight),
|
|
1630
|
+
lerp(0.085, 0.98, daylight),
|
|
1631
|
+
1,
|
|
1632
|
+
).toLinearSpace();
|
|
1718
1633
|
|
|
1719
1634
|
if ("environmentIntensity" in this.#scene) {
|
|
1720
1635
|
this.#scene.environmentIntensity = lerp(0.42, showroom.environmentIntensity ?? 1.08, peakLight);
|
|
@@ -2203,10 +2118,6 @@ export default class BabylonJSController {
|
|
|
2203
2118
|
}
|
|
2204
2119
|
this.#state.resize.lastAppliedAt = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
2205
2120
|
console.log(`PrefViewer: Applying resize after ${Math.round(elapsed)}ms`);
|
|
2206
|
-
// Stop the loop before resize: engine.resize() rebuilds DefaultRenderingPipeline
|
|
2207
|
-
// via onResizeObservable, which disposes GL programs. If the loop is running,
|
|
2208
|
-
// the next frame would call useProgram on a deleted program → INVALID_OPERATION.
|
|
2209
|
-
this.#stopEngineRenderLoop();
|
|
2210
2121
|
this.#applyShowroomCameraFraming();
|
|
2211
2122
|
this.#engine.resize();
|
|
2212
2123
|
const needsEnhancedBurst = this.#settings.antiAliasingEnabled || this.#settings.ambientOcclusionEnabled || this.#settings.iblEnabled || this.#settings.shadowsEnabled;
|
|
@@ -2249,7 +2160,7 @@ export default class BabylonJSController {
|
|
|
2249
2160
|
* @param {MaterialData} optionMaterial - Material option containing value, nodePrefixes, nodeNames, and state.
|
|
2250
2161
|
* @returns {boolean} True if any mesh material was set, false otherwise.
|
|
2251
2162
|
*/
|
|
2252
|
-
#setOptionsMaterial(optionMaterial
|
|
2163
|
+
#setOptionsMaterial(optionMaterial) {
|
|
2253
2164
|
if (!optionMaterial || !(optionMaterial.value || (optionMaterial.isPending && optionMaterial.update.value)) || !(optionMaterial.nodePrefixes.length || optionMaterial.nodeNames.length)) {
|
|
2254
2165
|
return false;
|
|
2255
2166
|
}
|
|
@@ -2267,7 +2178,7 @@ export default class BabylonJSController {
|
|
|
2267
2178
|
if (container.state.name === "materials") {
|
|
2268
2179
|
return;
|
|
2269
2180
|
}
|
|
2270
|
-
if (container.assetContainer && (
|
|
2181
|
+
if (container.assetContainer && (container.state.isPending || materialContainer.state.isPending || optionMaterial.isPending)) {
|
|
2271
2182
|
assetContainersToProcess.push(container.assetContainer);
|
|
2272
2183
|
}
|
|
2273
2184
|
});
|
|
@@ -2278,7 +2189,7 @@ export default class BabylonJSController {
|
|
|
2278
2189
|
let someSetted = false;
|
|
2279
2190
|
assetContainersToProcess.forEach((assetContainer) =>
|
|
2280
2191
|
assetContainer.meshes
|
|
2281
|
-
.filter((meshToFilter) => optionMaterial.nodePrefixes.some((prefix) => meshToFilter.name.startsWith(prefix)) || optionMaterial.nodeNames.
|
|
2192
|
+
.filter((meshToFilter) => optionMaterial.nodePrefixes.some((prefix) => meshToFilter.name.toLowerCase().startsWith(prefix.toLowerCase())) || optionMaterial.nodeNames.some((n) => n.toLowerCase() === meshToFilter.name.toLowerCase()))
|
|
2282
2193
|
.forEach((mesh) => {
|
|
2283
2194
|
mesh.material = material;
|
|
2284
2195
|
someSetted = true;
|
|
@@ -2300,10 +2211,10 @@ export default class BabylonJSController {
|
|
|
2300
2211
|
* @private
|
|
2301
2212
|
* @returns {boolean} True if any material option was set, false otherwise.
|
|
2302
2213
|
*/
|
|
2303
|
-
#setOptions_Materials(
|
|
2214
|
+
#setOptions_Materials() {
|
|
2304
2215
|
let someSetted = false;
|
|
2305
2216
|
Object.values(this.#options.materials).forEach((material) => {
|
|
2306
|
-
let settedMaterial = this.#setOptionsMaterial(material
|
|
2217
|
+
let settedMaterial = this.#setOptionsMaterial(material);
|
|
2307
2218
|
someSetted = someSetted || settedMaterial;
|
|
2308
2219
|
});
|
|
2309
2220
|
return someSetted;
|
|
@@ -2412,9 +2323,7 @@ export default class BabylonJSController {
|
|
|
2412
2323
|
prefViewer3D = host?.nodeName === "PREF-VIEWER-3D" ? host : undefined;
|
|
2413
2324
|
}
|
|
2414
2325
|
|
|
2415
|
-
|
|
2416
|
-
this.#prefViewer3D = prefViewer3D;
|
|
2417
|
-
}
|
|
2326
|
+
this.#prefViewer3D = prefViewer3D;
|
|
2418
2327
|
}
|
|
2419
2328
|
|
|
2420
2329
|
/**
|
|
@@ -2429,6 +2338,7 @@ export default class BabylonJSController {
|
|
|
2429
2338
|
|
|
2430
2339
|
this.#getPrefViewer3DComponent();
|
|
2431
2340
|
if (this.#prefViewer3D === undefined) {
|
|
2341
|
+
this.#prefViewer = undefined;
|
|
2432
2342
|
return;
|
|
2433
2343
|
}
|
|
2434
2344
|
|
|
@@ -2438,9 +2348,7 @@ export default class BabylonJSController {
|
|
|
2438
2348
|
prefViewer = host?.nodeName === "PREF-VIEWER" ? host : undefined;
|
|
2439
2349
|
}
|
|
2440
2350
|
|
|
2441
|
-
|
|
2442
|
-
this.#prefViewer = prefViewer;
|
|
2443
|
-
}
|
|
2351
|
+
this.#prefViewer = prefViewer;
|
|
2444
2352
|
}
|
|
2445
2353
|
|
|
2446
2354
|
/**
|
|
@@ -2454,96 +2362,15 @@ export default class BabylonJSController {
|
|
|
2454
2362
|
// Cache references to parent custom elements
|
|
2455
2363
|
this.#getPrefViewer3DComponent();
|
|
2456
2364
|
this.#getPrefViewerComponent();
|
|
2457
|
-
const syncVisibility = (component, fallbackAttr) => {
|
|
2458
|
-
if (!component) {
|
|
2459
|
-
return;
|
|
2460
|
-
}
|
|
2461
|
-
if (typeof component.syncVisibility === "function") {
|
|
2462
|
-
component.syncVisibility(name, isVisible);
|
|
2463
|
-
return;
|
|
2464
|
-
}
|
|
2465
|
-
component.setAttribute(fallbackAttr, isVisible ? "true" : "false");
|
|
2466
|
-
};
|
|
2467
2365
|
if (name === "model") {
|
|
2468
|
-
|
|
2469
|
-
|
|
2366
|
+
this.#prefViewer3D?.setAttribute("show-model", isVisible ? "true" : "false");
|
|
2367
|
+
this.#prefViewer?.setAttribute("show-model", isVisible ? "true" : "false");
|
|
2470
2368
|
} else if (name === "environment") {
|
|
2471
|
-
|
|
2472
|
-
|
|
2369
|
+
this.#prefViewer3D?.setAttribute("show-scene", isVisible ? "true" : "false");
|
|
2370
|
+
this.#prefViewer?.setAttribute("show-scene", isVisible ? "true" : "false");
|
|
2473
2371
|
}
|
|
2474
2372
|
}
|
|
2475
2373
|
|
|
2476
|
-
/**
|
|
2477
|
-
* Dispatches a "model-first-painted" event once the model-only preview has had a chance to render a frame.
|
|
2478
|
-
* @private
|
|
2479
|
-
* @param {object} [detail] - Optional metadata describing the paint gate.
|
|
2480
|
-
* @returns {void}
|
|
2481
|
-
*/
|
|
2482
|
-
#emitModelFirstPainted(detail = {}) {
|
|
2483
|
-
this.#getPrefViewer3DComponent();
|
|
2484
|
-
this.#getPrefViewerComponent();
|
|
2485
|
-
const target = this.#prefViewer || this.#prefViewer3D || this.#canvas;
|
|
2486
|
-
if (!target) {
|
|
2487
|
-
return;
|
|
2488
|
-
}
|
|
2489
|
-
|
|
2490
|
-
const customEventOptions = {
|
|
2491
|
-
bubbles: true,
|
|
2492
|
-
cancelable: false,
|
|
2493
|
-
composed: true,
|
|
2494
|
-
detail,
|
|
2495
|
-
};
|
|
2496
|
-
target.dispatchEvent(new CustomEvent("model-first-painted", customEventOptions));
|
|
2497
|
-
}
|
|
2498
|
-
|
|
2499
|
-
/**
|
|
2500
|
-
* Dispatches a granular 3D load phase event so the ecommerce trace UI can measure tail phases.
|
|
2501
|
-
* @private
|
|
2502
|
-
* @param {string} phase - Phase event name.
|
|
2503
|
-
* @param {object} [detail] - Optional phase metadata.
|
|
2504
|
-
* @returns {void}
|
|
2505
|
-
*/
|
|
2506
|
-
#emitSceneLoadPhase(phase, detail = {}) {
|
|
2507
|
-
this.#getPrefViewer3DComponent();
|
|
2508
|
-
this.#getPrefViewerComponent();
|
|
2509
|
-
const target = this.#prefViewer || this.#prefViewer3D || this.#canvas;
|
|
2510
|
-
if (!target) {
|
|
2511
|
-
return;
|
|
2512
|
-
}
|
|
2513
|
-
|
|
2514
|
-
const customEventOptions = {
|
|
2515
|
-
bubbles: true,
|
|
2516
|
-
cancelable: false,
|
|
2517
|
-
composed: true,
|
|
2518
|
-
detail,
|
|
2519
|
-
};
|
|
2520
|
-
target.dispatchEvent(new CustomEvent(phase, customEventOptions));
|
|
2521
|
-
}
|
|
2522
|
-
|
|
2523
|
-
/**
|
|
2524
|
-
* Waits for one or more animation frames so the browser can paint the current model-only state.
|
|
2525
|
-
* @private
|
|
2526
|
-
* @param {number} [frameCount=2] - Number of animation frames to wait.
|
|
2527
|
-
* @returns {Promise<void>}
|
|
2528
|
-
*/
|
|
2529
|
-
async #waitForPaint(frameCount = 2) {
|
|
2530
|
-
if (typeof requestAnimationFrame !== "function") {
|
|
2531
|
-
return;
|
|
2532
|
-
}
|
|
2533
|
-
|
|
2534
|
-
const totalFrames = Math.max(1, frameCount);
|
|
2535
|
-
await new Promise((resolve) => {
|
|
2536
|
-
const step = (remaining) => {
|
|
2537
|
-
if (remaining <= 0) {
|
|
2538
|
-
resolve();
|
|
2539
|
-
return;
|
|
2540
|
-
}
|
|
2541
|
-
requestAnimationFrame(() => step(remaining - 1));
|
|
2542
|
-
};
|
|
2543
|
-
requestAnimationFrame(() => step(totalFrames - 1));
|
|
2544
|
-
});
|
|
2545
|
-
}
|
|
2546
|
-
|
|
2547
2374
|
/**
|
|
2548
2375
|
* Adds the asset container to the Babylon.js scene if it should be shown and is not already visible.
|
|
2549
2376
|
* @private
|
|
@@ -2589,7 +2416,7 @@ export default class BabylonJSController {
|
|
|
2589
2416
|
* @param {AssetContainer} newAssetContainer - The new asset container to add to the scene.
|
|
2590
2417
|
* @returns {boolean} True if the container was replaced and added, false otherwise.
|
|
2591
2418
|
*/
|
|
2592
|
-
#replaceContainer(container, newAssetContainer
|
|
2419
|
+
#replaceContainer(container, newAssetContainer) {
|
|
2593
2420
|
if (container.assetContainer) {
|
|
2594
2421
|
this.#removeContainer(container, false);
|
|
2595
2422
|
container.assetContainer.dispose();
|
|
@@ -2601,13 +2428,8 @@ export default class BabylonJSController {
|
|
|
2601
2428
|
newAssetContainer?.dispose();
|
|
2602
2429
|
return false;
|
|
2603
2430
|
}
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
// explicitly passes releaseEffects:false (e.g. loadContainerIndependent).
|
|
2607
|
-
if (releaseEffects && !this.#state.render.isLoopRunning) {
|
|
2608
|
-
this.#scene.getEngine().releaseEffects();
|
|
2609
|
-
this.#scene.getEngine().releaseComputeEffects();
|
|
2610
|
-
}
|
|
2431
|
+
this.#scene.getEngine().releaseEffects();
|
|
2432
|
+
this.#scene.getEngine().releaseComputeEffects();
|
|
2611
2433
|
|
|
2612
2434
|
container.assetContainer = newAssetContainer;
|
|
2613
2435
|
return this.#addContainer(container, false);
|
|
@@ -2643,6 +2465,40 @@ export default class BabylonJSController {
|
|
|
2643
2465
|
assetContainer.lights = [];
|
|
2644
2466
|
}
|
|
2645
2467
|
|
|
2468
|
+
/**
|
|
2469
|
+
* Strips KHR_materials_transmission, KHR_materials_ior and KHR_materials_volume from
|
|
2470
|
+
* environment-container (scene) materials, converting them to simple alpha blending.
|
|
2471
|
+
* Babylon.js cannot render two physical-transmission materials (from separate GLTF
|
|
2472
|
+
* containers) through each other: the scene decorative glass (lamp cups, etc.) is
|
|
2473
|
+
* simplified so the model's physical glass keeps full correct transmission.
|
|
2474
|
+
* @private
|
|
2475
|
+
* @param {AssetContainer} assetContainer
|
|
2476
|
+
* @returns {void}
|
|
2477
|
+
*/
|
|
2478
|
+
#assetContainer_simplifyTransmissiveMaterials(assetContainer) {
|
|
2479
|
+
assetContainer.materials?.forEach((mat) => {
|
|
2480
|
+
if (!(mat instanceof PBRMaterial)) return;
|
|
2481
|
+
if (!mat.subSurface?.isRefractionEnabled && !mat.subSurface?.isTranslucencyEnabled) return;
|
|
2482
|
+
this.#simplifyTransmissiveMaterial(mat);
|
|
2483
|
+
});
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
/**
|
|
2487
|
+
* Converts a single PBRMaterial from physical KHR_materials_transmission to simple alpha
|
|
2488
|
+
* blending. Shared helper used by both the at-load simplification and the post-swap pass.
|
|
2489
|
+
* @private
|
|
2490
|
+
* @param {PBRMaterial} mat
|
|
2491
|
+
* @returns {void}
|
|
2492
|
+
*/
|
|
2493
|
+
#simplifyTransmissiveMaterial(mat) {
|
|
2494
|
+
const intensity = mat.subSurface.refractionIntensity ?? 1.0;
|
|
2495
|
+
mat.subSurface.isRefractionEnabled = false;
|
|
2496
|
+
mat.subSurface.isTranslucencyEnabled = false;
|
|
2497
|
+
mat.alpha = Math.max(0.28, 1.0 - intensity * 0.65);
|
|
2498
|
+
mat.transparencyMode = PBRMaterial.PBRMATERIAL_ALPHABLEND;
|
|
2499
|
+
markPrefViewerSimplifiedTransmissionMaterial(mat);
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2646
2502
|
/**
|
|
2647
2503
|
* Assigns unique ids to every imported camera so Babylon.js does not reuse stale SSAO effects between reloads.
|
|
2648
2504
|
* @private
|
|
@@ -2669,207 +2525,10 @@ export default class BabylonJSController {
|
|
|
2669
2525
|
if (!this.#containers.model.assetContainer || !this.#containers.model.state.isVisible) {
|
|
2670
2526
|
return false;
|
|
2671
2527
|
}
|
|
2672
|
-
|
|
2673
|
-
// Only defer to the environment's visibility after it has fully loaded.
|
|
2674
|
-
show = show !== undefined
|
|
2675
|
-
? show
|
|
2676
|
-
: (this.#containers.environment.state.isSuccess
|
|
2677
|
-
? this.#containers.environment.state.isVisible
|
|
2678
|
-
: true);
|
|
2528
|
+
show = show !== undefined ? show : this.#containers.environment.state.isVisible;
|
|
2679
2529
|
const nodePrefixes = Object.values(this.#options.materials).flatMap((material) => material.nodePrefixes);
|
|
2680
2530
|
const nodeNames = Object.values(this.#options.materials).flatMap((material) => material.nodeNames);
|
|
2681
|
-
this.#containers.model.assetContainer.meshes.filter((meshToFilter) => nodePrefixes.some((prefix) => meshToFilter.name.startsWith(prefix)) || nodeNames.
|
|
2682
|
-
}
|
|
2683
|
-
|
|
2684
|
-
/**
|
|
2685
|
-
* Frames the active ArcRotateCamera around the visible model when no environment scene is loaded.
|
|
2686
|
-
* @private
|
|
2687
|
-
* @returns {boolean} True if the camera was adjusted, otherwise false.
|
|
2688
|
-
*/
|
|
2689
|
-
#frameCameraToModel({ allowWhenEnvironmentVisible = false } = {}) {
|
|
2690
|
-
const camera = this.#scene?.activeCamera instanceof ArcRotateCamera
|
|
2691
|
-
? this.#scene.activeCamera
|
|
2692
|
-
: this.#camera;
|
|
2693
|
-
const modelContainer = this.#containers.model;
|
|
2694
|
-
const inspect = {
|
|
2695
|
-
cameraType: camera?.getClassName?.() ?? camera?.constructor?.name ?? null,
|
|
2696
|
-
hasModelContainer: !!modelContainer.assetContainer,
|
|
2697
|
-
modelVisible: modelContainer.state.isVisible,
|
|
2698
|
-
envHasContainer: !!this.#containers.environment.assetContainer,
|
|
2699
|
-
envVisible: this.#containers.environment.state.isVisible,
|
|
2700
|
-
meshCount: modelContainer.assetContainer?.meshes?.length ?? 0,
|
|
2701
|
-
};
|
|
2702
|
-
this.#debugCameraFrameInfo = { phase: "inspect", ...inspect };
|
|
2703
|
-
console.log("[viewer] frameCameraToModel inspect", inspect);
|
|
2704
|
-
if (!(camera instanceof ArcRotateCamera) || !modelContainer.assetContainer || !modelContainer.state.isVisible) {
|
|
2705
|
-
this.#debugCameraFrameInfo = {
|
|
2706
|
-
phase: "skipped",
|
|
2707
|
-
reason: !(camera instanceof ArcRotateCamera)
|
|
2708
|
-
? "no-arcrotate-camera"
|
|
2709
|
-
: !modelContainer.assetContainer
|
|
2710
|
-
? "no-model"
|
|
2711
|
-
: !modelContainer.state.isVisible
|
|
2712
|
-
? "model-hidden"
|
|
2713
|
-
: "unknown",
|
|
2714
|
-
...inspect,
|
|
2715
|
-
};
|
|
2716
|
-
return false;
|
|
2717
|
-
}
|
|
2718
|
-
if (!allowWhenEnvironmentVisible && this.#containers.environment.assetContainer && this.#containers.environment.state.isVisible) {
|
|
2719
|
-
this.#debugCameraFrameInfo = {
|
|
2720
|
-
phase: "skipped",
|
|
2721
|
-
reason: "environment-visible",
|
|
2722
|
-
...inspect,
|
|
2723
|
-
};
|
|
2724
|
-
return false;
|
|
2725
|
-
}
|
|
2726
|
-
|
|
2727
|
-
let min = null;
|
|
2728
|
-
let max = null;
|
|
2729
|
-
modelContainer.assetContainer.meshes.forEach((mesh) => {
|
|
2730
|
-
if (!mesh?.getTotalVertices?.() || mesh.getTotalVertices() <= 0) {
|
|
2731
|
-
return;
|
|
2732
|
-
}
|
|
2733
|
-
mesh.computeWorldMatrix?.(true);
|
|
2734
|
-
const boundingBox = mesh.getBoundingInfo?.()?.boundingBox;
|
|
2735
|
-
if (!boundingBox) {
|
|
2736
|
-
return;
|
|
2737
|
-
}
|
|
2738
|
-
const minimum = boundingBox.minimumWorld;
|
|
2739
|
-
const maximum = boundingBox.maximumWorld;
|
|
2740
|
-
if (!minimum || !maximum) {
|
|
2741
|
-
return;
|
|
2742
|
-
}
|
|
2743
|
-
min = min
|
|
2744
|
-
? new Vector3(Math.min(min.x, minimum.x), Math.min(min.y, minimum.y), Math.min(min.z, minimum.z))
|
|
2745
|
-
: minimum.clone();
|
|
2746
|
-
max = max
|
|
2747
|
-
? new Vector3(Math.max(max.x, maximum.x), Math.max(max.y, maximum.y), Math.max(max.z, maximum.z))
|
|
2748
|
-
: maximum.clone();
|
|
2749
|
-
});
|
|
2750
|
-
|
|
2751
|
-
if (!min || !max) {
|
|
2752
|
-
const skipped = {
|
|
2753
|
-
hasCamera: camera instanceof ArcRotateCamera,
|
|
2754
|
-
hasModel: !!modelContainer.assetContainer,
|
|
2755
|
-
modelVisible: modelContainer.state.isVisible,
|
|
2756
|
-
environmentVisible: this.#containers.environment.state.isVisible,
|
|
2757
|
-
};
|
|
2758
|
-
this.#debugCameraFrameInfo = {
|
|
2759
|
-
phase: "skipped",
|
|
2760
|
-
reason: "no-bounds",
|
|
2761
|
-
...inspect,
|
|
2762
|
-
...skipped,
|
|
2763
|
-
};
|
|
2764
|
-
console.log("[viewer] frameCameraToModel skipped (no bounds)", skipped);
|
|
2765
|
-
return false;
|
|
2766
|
-
}
|
|
2767
|
-
|
|
2768
|
-
const size = max.subtract(min);
|
|
2769
|
-
const largestDimension = Math.max(size.x, size.y, size.z);
|
|
2770
|
-
if (!Number.isFinite(largestDimension) || largestDimension <= 0) {
|
|
2771
|
-
return false;
|
|
2772
|
-
}
|
|
2773
|
-
|
|
2774
|
-
const center = min.add(max).scale(0.5);
|
|
2775
|
-
camera.setTarget(center);
|
|
2776
|
-
const radiusMultiplier = allowWhenEnvironmentVisible ? 1.7 : 1.4;
|
|
2777
|
-
camera.radius = Math.min(
|
|
2778
|
-
Math.max(largestDimension * radiusMultiplier, camera.lowerRadiusLimit ?? largestDimension * radiusMultiplier),
|
|
2779
|
-
camera.upperRadiusLimit ?? largestDimension * radiusMultiplier,
|
|
2780
|
-
);
|
|
2781
|
-
// Keep the initial model-only view aligned to the showroom's frontal composition.
|
|
2782
|
-
// This preserves the user's camera intent once the scene becomes visible, but gives
|
|
2783
|
-
// the standalone model a consistent front-facing presentation.
|
|
2784
|
-
// Rotate the front-facing composition 180° while keeping the same framing/radius.
|
|
2785
|
-
camera.alpha = Math.PI;
|
|
2786
|
-
camera.beta = Math.PI * 0.47;
|
|
2787
|
-
console.log("[viewer] frameCameraToModel applied", {
|
|
2788
|
-
center,
|
|
2789
|
-
size,
|
|
2790
|
-
radius: camera.radius,
|
|
2791
|
-
});
|
|
2792
|
-
this.#debugCameraFrameInfo = {
|
|
2793
|
-
phase: "applied",
|
|
2794
|
-
...inspect,
|
|
2795
|
-
center: { x: center.x, y: center.y, z: center.z },
|
|
2796
|
-
size: { x: size.x, y: size.y, z: size.z },
|
|
2797
|
-
radius: camera.radius,
|
|
2798
|
-
cameraTarget: camera.target ? { x: camera.target.x, y: camera.target.y, z: camera.target.z } : null,
|
|
2799
|
-
cameraPosition: camera.position ? { x: camera.position.x, y: camera.position.y, z: camera.position.z } : null,
|
|
2800
|
-
cameraLocked: !!camera.metadata?.locked,
|
|
2801
|
-
};
|
|
2802
|
-
return true;
|
|
2803
|
-
}
|
|
2804
|
-
|
|
2805
|
-
/**
|
|
2806
|
-
* Public wrapper to force the active camera to frame the visible model.
|
|
2807
|
-
* @public
|
|
2808
|
-
* @returns {boolean}
|
|
2809
|
-
*/
|
|
2810
|
-
focusModel() {
|
|
2811
|
-
return this.#frameCameraToModel();
|
|
2812
|
-
}
|
|
2813
|
-
|
|
2814
|
-
/**
|
|
2815
|
-
* Returns a compact snapshot of the current debug-relevant 3D state.
|
|
2816
|
-
* @public
|
|
2817
|
-
* @returns {object}
|
|
2818
|
-
*/
|
|
2819
|
-
getDebugState() {
|
|
2820
|
-
const activeCamera = this.#scene?.activeCamera ?? this.#camera ?? null;
|
|
2821
|
-
const modelMeshes = this.#containers.model?.assetContainer?.meshes ?? [];
|
|
2822
|
-
let boundsMin = null;
|
|
2823
|
-
let boundsMax = null;
|
|
2824
|
-
for (const mesh of modelMeshes) {
|
|
2825
|
-
if (!mesh?.getTotalVertices?.() || mesh.getTotalVertices() <= 0) continue;
|
|
2826
|
-
mesh.computeWorldMatrix?.(true);
|
|
2827
|
-
const boundingBox = mesh.getBoundingInfo?.()?.boundingBox;
|
|
2828
|
-
if (!boundingBox?.minimumWorld || !boundingBox?.maximumWorld) continue;
|
|
2829
|
-
const minimum = boundingBox.minimumWorld;
|
|
2830
|
-
const maximum = boundingBox.maximumWorld;
|
|
2831
|
-
boundsMin = boundsMin
|
|
2832
|
-
? { x: Math.min(boundsMin.x, minimum.x), y: Math.min(boundsMin.y, minimum.y), z: Math.min(boundsMin.z, minimum.z) }
|
|
2833
|
-
: { x: minimum.x, y: minimum.y, z: minimum.z };
|
|
2834
|
-
boundsMax = boundsMax
|
|
2835
|
-
? { x: Math.max(boundsMax.x, maximum.x), y: Math.max(boundsMax.y, maximum.y), z: Math.max(boundsMax.z, maximum.z) }
|
|
2836
|
-
: { x: maximum.x, y: maximum.y, z: maximum.z };
|
|
2837
|
-
}
|
|
2838
|
-
const modelBounds = boundsMin && boundsMax ? {
|
|
2839
|
-
min: boundsMin,
|
|
2840
|
-
max: boundsMax,
|
|
2841
|
-
center: {
|
|
2842
|
-
x: (boundsMin.x + boundsMax.x) / 2,
|
|
2843
|
-
y: (boundsMin.y + boundsMax.y) / 2,
|
|
2844
|
-
z: (boundsMin.z + boundsMax.z) / 2,
|
|
2845
|
-
},
|
|
2846
|
-
size: {
|
|
2847
|
-
x: boundsMax.x - boundsMin.x,
|
|
2848
|
-
y: boundsMax.y - boundsMin.y,
|
|
2849
|
-
z: boundsMax.z - boundsMin.z,
|
|
2850
|
-
},
|
|
2851
|
-
} : null;
|
|
2852
|
-
return {
|
|
2853
|
-
camera: activeCamera ? {
|
|
2854
|
-
className: activeCamera.getClassName?.() ?? activeCamera.constructor?.name ?? null,
|
|
2855
|
-
position: activeCamera.position ? { x: activeCamera.position.x, y: activeCamera.position.y, z: activeCamera.position.z } : null,
|
|
2856
|
-
target: activeCamera.target ? { x: activeCamera.target.x, y: activeCamera.target.y, z: activeCamera.target.z } : null,
|
|
2857
|
-
radius: typeof activeCamera.radius === "number" ? activeCamera.radius : null,
|
|
2858
|
-
locked: !!activeCamera.metadata?.locked,
|
|
2859
|
-
} : null,
|
|
2860
|
-
model: {
|
|
2861
|
-
visible: !!this.#containers.model?.state?.isVisible,
|
|
2862
|
-
hasContainer: !!this.#containers.model?.assetContainer,
|
|
2863
|
-
meshCount: this.#containers.model?.assetContainer?.meshes?.length ?? 0,
|
|
2864
|
-
bounds: modelBounds,
|
|
2865
|
-
},
|
|
2866
|
-
environment: {
|
|
2867
|
-
visible: !!this.#containers.environment?.state?.isVisible,
|
|
2868
|
-
hasContainer: !!this.#containers.environment?.assetContainer,
|
|
2869
|
-
},
|
|
2870
|
-
lastCameraFrame: this.#debugCameraFrameInfo,
|
|
2871
|
-
renderLoopRunning: !!this.#state?.render?.isLoopRunning,
|
|
2872
|
-
};
|
|
2531
|
+
this.#containers.model.assetContainer.meshes.filter((meshToFilter) => nodePrefixes.some((prefix) => meshToFilter.name.toLowerCase().startsWith(prefix.toLowerCase())) || nodeNames.some((n) => n.toLowerCase() === meshToFilter.name.toLowerCase())).forEach((mesh) => mesh.setEnabled(show));
|
|
2873
2532
|
}
|
|
2874
2533
|
|
|
2875
2534
|
/**
|
|
@@ -2889,25 +2548,12 @@ export default class BabylonJSController {
|
|
|
2889
2548
|
* @private
|
|
2890
2549
|
* @returns {Promise<void>}
|
|
2891
2550
|
*/
|
|
2892
|
-
async #startRender(
|
|
2551
|
+
async #startRender() {
|
|
2893
2552
|
if (!this.#scene) return;
|
|
2894
|
-
this.#
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
}
|
|
2899
|
-
// skipWhenReady: model materials already compiled via compileMaterials:true.
|
|
2900
|
-
// Skips scene-wide whenReadyAsync so the model appears immediately (~1s).
|
|
2901
|
-
if (!skipWhenReady) {
|
|
2902
|
-
await this.#scene.whenReadyAsync();
|
|
2903
|
-
}
|
|
2904
|
-
const frames = this.#settings.antiAliasingEnabled || this.#settings.ambientOcclusionEnabled || this.#settings.iblEnabled ? this.#config.render.burstFramesEnhanced : this.#config.render.burstFramesBase;
|
|
2905
|
-
// forceRender: bypasses #loadingInProgress gate so loadContainerIndependent
|
|
2906
|
-
// can show the model preview while #loadContainers is still loading scene/materials.
|
|
2907
|
-
this.#requestRender({ frames: frames, continuousMs: this.#config.render.interactionMs, force: forceRender });
|
|
2908
|
-
} finally {
|
|
2909
|
-
this.#startRenderInProgress = false;
|
|
2910
|
-
}
|
|
2553
|
+
await this.#loadCameraDependentEffects();
|
|
2554
|
+
await this.#scene.whenReadyAsync();
|
|
2555
|
+
const frames = this.#settings.antiAliasingEnabled || this.#settings.ambientOcclusionEnabled || this.#settings.iblEnabled ? this.#config.render.burstFramesEnhanced : this.#config.render.burstFramesBase;
|
|
2556
|
+
this.#requestRender({ frames: frames, continuousMs: this.#config.render.interactionMs });
|
|
2911
2557
|
}
|
|
2912
2558
|
|
|
2913
2559
|
/**
|
|
@@ -2926,7 +2572,7 @@ export default class BabylonJSController {
|
|
|
2926
2572
|
* 3. Builds the Babylon plugin options so extras are surfaced as metadata, then imports the container with
|
|
2927
2573
|
* `LoadAssetContainerAsync`, returning the tuple so the caller can decide how to attach it to the scene.
|
|
2928
2574
|
*/
|
|
2929
|
-
async #loadAssetContainer(container, force = false
|
|
2575
|
+
async #loadAssetContainer(container, force = false) {
|
|
2930
2576
|
if (container?.state?.update?.storage === undefined || container?.state?.size === undefined || container?.state?.timeStamp === undefined) {
|
|
2931
2577
|
return [container, false];
|
|
2932
2578
|
}
|
|
@@ -2942,30 +2588,12 @@ export default class BabylonJSController {
|
|
|
2942
2588
|
const currentSize = force ? 0 : container.state.size;
|
|
2943
2589
|
const currentTimeStamp = force ? null : container.state.timeStamp;
|
|
2944
2590
|
|
|
2945
|
-
const storageType = container.state.update.storage?.db ? "idb" : container.state.update.storage?.url ? "url" : "?";
|
|
2946
|
-
const storageId = container.state.update.storage?.id ?? container.state.update.storage?.url ?? "?";
|
|
2947
|
-
const tResolve = performance.now();
|
|
2948
2591
|
let sourceData = await this.#gltfResolver.getSource(container.state.update.storage, currentSize, currentTimeStamp);
|
|
2949
|
-
console.log(`[perf][${container.state.name}] getSource(${storageType}:${storageId}) ${Math.round(performance.now() - tResolve)}ms → ${sourceData ? `${sourceData.extension} ${sourceData.objectURLs?.length ?? 0} assets` : "skipped"}`);
|
|
2950
2592
|
|
|
2951
2593
|
if (!sourceData) {
|
|
2952
2594
|
return [container, false];
|
|
2953
2595
|
}
|
|
2954
2596
|
|
|
2955
|
-
// Abort if loadContainerIndependent claimed this container while getSource was in flight.
|
|
2956
|
-
// isIndependent=true means THIS call IS the independent load — don't abort yourself.
|
|
2957
|
-
// Returns null (not false) so #loadContainers skips setSuccess(false) for this container.
|
|
2958
|
-
const isClaimedIndependently =
|
|
2959
|
-
(container.state.name === "model" && this.#isLoadingModelIndependent) ||
|
|
2960
|
-
(container.state.name === "environment" && this.#isLoadingEnvironmentIndependent) ||
|
|
2961
|
-
(container.state.name === "materials" && this.#isLoadingMaterialsIndependent);
|
|
2962
|
-
if (isClaimedIndependently && !isIndependent) {
|
|
2963
|
-
if (sourceData.objectURLs?.length) {
|
|
2964
|
-
this.#gltfResolver.revokeObjectURLs(sourceData.objectURLs);
|
|
2965
|
-
}
|
|
2966
|
-
return [container, null];
|
|
2967
|
-
}
|
|
2968
|
-
|
|
2969
2597
|
container.state.setPendingCacheData(sourceData.size, sourceData.timeStamp, sourceData.metadata);
|
|
2970
2598
|
|
|
2971
2599
|
// https://doc.babylonjs.com/typedoc/interfaces/BABYLON.LoadAssetContainerOptions
|
|
@@ -2984,14 +2612,12 @@ export default class BabylonJSController {
|
|
|
2984
2612
|
};
|
|
2985
2613
|
|
|
2986
2614
|
let assetContainer = null;
|
|
2987
|
-
const t0 = performance.now();
|
|
2988
2615
|
|
|
2989
2616
|
try {
|
|
2990
2617
|
assetContainer = await LoadAssetContainerAsync(sourceData.source, this.#scene, options);
|
|
2991
|
-
const elapsed = Math.round(performance.now() - t0);
|
|
2992
|
-
console.log(`[perf][${container.state.name}] LoadAssetContainerAsync ${elapsed}ms`);
|
|
2993
2618
|
return [container, assetContainer];
|
|
2994
2619
|
} catch (error) {
|
|
2620
|
+
console.error(`[PrefViewer] LoadAssetContainerAsync failed for [${container.state.name}]`, error);
|
|
2995
2621
|
return [container, assetContainer];
|
|
2996
2622
|
} finally {
|
|
2997
2623
|
this.#gltfResolver.revokeObjectURLs(sourceData.objectURLs);
|
|
@@ -3009,29 +2635,7 @@ export default class BabylonJSController {
|
|
|
3009
2635
|
*/
|
|
3010
2636
|
async #loadContainers(force = false) {
|
|
3011
2637
|
this.#detachAnimationChangedListener();
|
|
3012
|
-
this.#
|
|
3013
|
-
|
|
3014
|
-
// When only the model is pending (scene/environment already rendered), skip
|
|
3015
|
-
// unloading camera-dependent effects (DefaultRenderingPipeline, SSAO, shadows).
|
|
3016
|
-
// Disposing those pipelines while the render loop was recently active deletes GL
|
|
3017
|
-
// programs that are still referenced, causing WebGL INVALID_OPERATION errors.
|
|
3018
|
-
const isModelOnlyLoad =
|
|
3019
|
-
this.#containers.model?.state?.isPending === true &&
|
|
3020
|
-
this.#containers.environment?.state?.isPending !== true &&
|
|
3021
|
-
this.#containers.materials?.state?.isPending !== true;
|
|
3022
|
-
|
|
3023
|
-
if (isModelOnlyLoad) {
|
|
3024
|
-
this.#stopEngineRenderLoop();
|
|
3025
|
-
this.#resetRenderState();
|
|
3026
|
-
// Replacing an already-visible model can leave Babylon post-process chains attached to
|
|
3027
|
-
// a camera that is about to be discarded or re-bound. Tear them down here so the next
|
|
3028
|
-
// startRender() rebuilds a clean pipeline instead of tripping over stale null entries.
|
|
3029
|
-
if (this.#containers.model?.assetContainer) {
|
|
3030
|
-
await this.#unloadCameraDependentEffects();
|
|
3031
|
-
}
|
|
3032
|
-
} else {
|
|
3033
|
-
await this.#stopRender();
|
|
3034
|
-
}
|
|
2638
|
+
await this.#stopRender();
|
|
3035
2639
|
|
|
3036
2640
|
let oldModelMetadata = { ...(this.#containers.model?.state?.metadata ?? {}) };
|
|
3037
2641
|
let newModelMetadata = {};
|
|
@@ -3046,12 +2650,8 @@ export default class BabylonJSController {
|
|
|
3046
2650
|
error: null,
|
|
3047
2651
|
};
|
|
3048
2652
|
|
|
3049
|
-
const tSceneStart = performance.now();
|
|
3050
|
-
let tAfterLoad, tAfterMerge;
|
|
3051
|
-
|
|
3052
2653
|
await Promise.allSettled(promiseArray)
|
|
3053
2654
|
.then(async (values) => {
|
|
3054
|
-
tAfterLoad = performance.now();
|
|
3055
2655
|
// Scene may have been disposed (disconnectedCallback) while async loading was in
|
|
3056
2656
|
// progress. Abort cleanly: #replaceContainer already guards the GPU calls, but
|
|
3057
2657
|
// we skip the post-load option/visibility calls too to avoid further null-derefs.
|
|
@@ -3060,12 +2660,6 @@ export default class BabylonJSController {
|
|
|
3060
2660
|
return;
|
|
3061
2661
|
}
|
|
3062
2662
|
this.#disposeAnimationController();
|
|
3063
|
-
this.#emitSceneLoadPhase("scene-load-assets-complete", {
|
|
3064
|
-
path: "full",
|
|
3065
|
-
containerCount: values.filter((result) => result.status === "fulfilled" && result.value?.[1]).length,
|
|
3066
|
-
tAfterLoad: tAfterLoad != null ? Math.round(tAfterLoad) : null,
|
|
3067
|
-
tSceneStart: Math.round(tSceneStart),
|
|
3068
|
-
});
|
|
3069
2663
|
values.forEach((result) => {
|
|
3070
2664
|
const container = result.value ? result.value[0] : null;
|
|
3071
2665
|
const assetContainer = result.value ? result.value[1] : null;
|
|
@@ -3078,32 +2672,23 @@ export default class BabylonJSController {
|
|
|
3078
2672
|
if (container.state.name === "model" || container.state.name === "environment") {
|
|
3079
2673
|
this.#assetContainer_retagCameras(assetContainer);
|
|
3080
2674
|
}
|
|
2675
|
+
if (container.state.name === "environment") {
|
|
2676
|
+
this.#assetContainer_simplifyTransmissiveMaterials(assetContainer);
|
|
2677
|
+
}
|
|
3081
2678
|
this.#replaceContainer(container, assetContainer);
|
|
3082
2679
|
container.state.setSuccess(true);
|
|
3083
2680
|
} else {
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
if (assetContainer !== null) {
|
|
3087
|
-
if (container.assetContainer && container.state.mustBeShown !== container.state.isVisible && container.state.name === "materials") {
|
|
3088
|
-
container.state.mustBeShown ? this.#addContainer(container) : this.#removeContainer(container);
|
|
3089
|
-
}
|
|
3090
|
-
container.state.setSuccess(false);
|
|
2681
|
+
if (container.assetContainer && container.state.mustBeShown !== container.state.isVisible && container.state.name === "materials") {
|
|
2682
|
+
container.state.mustBeShown ? this.#addContainer(container) : this.#removeContainer(container);
|
|
3091
2683
|
}
|
|
2684
|
+
container.state.setSuccess(false);
|
|
3092
2685
|
}
|
|
3093
2686
|
});
|
|
3094
2687
|
|
|
3095
|
-
this.#setOptions_Materials(
|
|
2688
|
+
this.#setOptions_Materials();
|
|
3096
2689
|
this.#setOptions_Camera();
|
|
3097
2690
|
await this.#setOptions_IBL();
|
|
3098
2691
|
this.#setVisibilityOfWallAndFloorInModel();
|
|
3099
|
-
this.#frameCameraToModel();
|
|
3100
|
-
tAfterMerge = performance.now();
|
|
3101
|
-
this.#emitSceneLoadPhase("scene-load-merge-complete", {
|
|
3102
|
-
path: "full",
|
|
3103
|
-
tAfterMerge: tAfterMerge != null ? Math.round(tAfterMerge) : null,
|
|
3104
|
-
tAfterLoad: tAfterLoad != null ? Math.round(tAfterLoad) : null,
|
|
3105
|
-
tSceneStart: Math.round(tSceneStart),
|
|
3106
|
-
});
|
|
3107
2692
|
detail.success = true;
|
|
3108
2693
|
})
|
|
3109
2694
|
.catch((error) => {
|
|
@@ -3116,9 +2701,7 @@ export default class BabylonJSController {
|
|
|
3116
2701
|
this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
|
|
3117
2702
|
this.#setMaxSimultaneousLights();
|
|
3118
2703
|
this.#enhanceTextureQuality();
|
|
3119
|
-
|
|
3120
|
-
this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
|
|
3121
|
-
}
|
|
2704
|
+
this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
|
|
3122
2705
|
// Apply stored highlight settings so fresh page loads respect persisted state
|
|
3123
2706
|
this.#setHighlightConfig({
|
|
3124
2707
|
enabled: this.#settings.highlightEnabled,
|
|
@@ -3127,24 +2710,7 @@ export default class BabylonJSController {
|
|
|
3127
2710
|
if (this.#babylonJSAnimationController?.hasAnimations?.()) {
|
|
3128
2711
|
this.#attachAnimationChangedListener();
|
|
3129
2712
|
}
|
|
3130
|
-
this.#loadingInProgress = false;
|
|
3131
|
-
// If loadContainerIndependent already merged a container and started the render
|
|
3132
|
-
// loop while this scene load was in flight, #startRender was already called.
|
|
3133
|
-
// Calling it again is safe (pipeline guards skip recreation) but redundant only
|
|
3134
|
-
// when the loop is already running AND scene/shaders are ready; whenReadyAsync
|
|
3135
|
-
// will settle quickly for the newly added containers.
|
|
3136
2713
|
await this.#startRender();
|
|
3137
|
-
this.#frameCameraToModel({ allowWhenEnvironmentVisible: true });
|
|
3138
|
-
this.#emitSceneLoadPhase("scene-load-shaders-complete", {
|
|
3139
|
-
path: "full",
|
|
3140
|
-
tAfterShaders: Math.round(performance.now()),
|
|
3141
|
-
tAfterMerge: tAfterMerge != null ? Math.round(tAfterMerge) : null,
|
|
3142
|
-
});
|
|
3143
|
-
const tLoad = tAfterLoad ? Math.round(tAfterLoad - tSceneStart) : null;
|
|
3144
|
-
const tMerge = tAfterLoad && tAfterMerge ? Math.round(tAfterMerge - tAfterLoad) : null;
|
|
3145
|
-
const tShaders = tAfterMerge ? Math.round(performance.now() - tAfterMerge) : null;
|
|
3146
|
-
const tTotal = Math.round(performance.now() - tSceneStart);
|
|
3147
|
-
console.log(`[perf][scene] load=${tLoad}ms merge=${tMerge}ms shaders=${tShaders}ms total=${tTotal}ms`);
|
|
3148
2714
|
});
|
|
3149
2715
|
return detail;
|
|
3150
2716
|
}
|
|
@@ -3260,7 +2826,7 @@ export default class BabylonJSController {
|
|
|
3260
2826
|
if (!this.#scene) {
|
|
3261
2827
|
return false;
|
|
3262
2828
|
}
|
|
3263
|
-
const node = this.#scene.getNodeByName(name);
|
|
2829
|
+
const node = this.#scene.getNodeByName(name) ?? this.#scene.getNodes().find((n) => n.name.toLowerCase() === name.toLowerCase());
|
|
3264
2830
|
if (!node || !node.position) {
|
|
3265
2831
|
return false;
|
|
3266
2832
|
}
|
|
@@ -3595,296 +3161,6 @@ export default class BabylonJSController {
|
|
|
3595
3161
|
return await this.#loadContainers(force);
|
|
3596
3162
|
}
|
|
3597
3163
|
|
|
3598
|
-
/**
|
|
3599
|
-
* Loads a single named container in the background while the render loop keeps running,
|
|
3600
|
-
* then merges it into the scene atomically (only the merge momentarily pauses the loop).
|
|
3601
|
-
* Camera-dependent effects (post-processing, SSAO, shadows) are preserved across the merge.
|
|
3602
|
-
* If a full #loadContainers is in progress, waits for it to finish before merging.
|
|
3603
|
-
* @public
|
|
3604
|
-
* @param {string} containerName - Name of the container to load ("model", "environment", "materials").
|
|
3605
|
-
* @param {Function} [onReady] - Optional callback invoked after any concurrent scene load finishes
|
|
3606
|
-
* and before the asset is fetched. Use this to mark the container pending (e.g. via
|
|
3607
|
-
* #checkNeedToUpdateContainers) so that the concurrent #loadContainers doesn't consume it first.
|
|
3608
|
-
* @returns {Promise<{success: boolean, error: any, loadedContainers: string[]}>}
|
|
3609
|
-
*/
|
|
3610
|
-
async loadContainerIndependent(containerName, onReady, options = {}) {
|
|
3611
|
-
const loadSessionId = containerName === "model" ? (options?.loadSessionId ?? null) : null;
|
|
3612
|
-
if (containerName === "model" && this.#isLoadingModelIndependent) {
|
|
3613
|
-
return { success: false, error: "Already loading model independently", loadedContainers: [], loadSessionId };
|
|
3614
|
-
}
|
|
3615
|
-
if (containerName === "environment" && this.#isLoadingEnvironmentIndependent) {
|
|
3616
|
-
return { success: false, error: "Already loading environment independently", loadedContainers: [], loadSessionId };
|
|
3617
|
-
}
|
|
3618
|
-
if (containerName === "materials" && this.#isLoadingMaterialsIndependent) {
|
|
3619
|
-
return { success: false, error: "Already loading materials independently", loadedContainers: [], loadSessionId };
|
|
3620
|
-
}
|
|
3621
|
-
|
|
3622
|
-
if (containerName === "model") this.#isLoadingModelIndependent = true;
|
|
3623
|
-
else if (containerName === "environment") this.#isLoadingEnvironmentIndependent = true;
|
|
3624
|
-
else if (containerName === "materials") this.#isLoadingMaterialsIndependent = true;
|
|
3625
|
-
|
|
3626
|
-
// Only reset model-first-painted tracking for model loads — resetting it for
|
|
3627
|
-
// environment/materials would stale any in-flight model-first-painted gate.
|
|
3628
|
-
if (containerName === "model") {
|
|
3629
|
-
this.#modelFirstPaintedEmitted = false;
|
|
3630
|
-
}
|
|
3631
|
-
const modelFirstPaintedLoadToken = containerName === "model"
|
|
3632
|
-
? ++this.#modelFirstPaintedLoadToken
|
|
3633
|
-
: this.#modelFirstPaintedLoadToken;
|
|
3634
|
-
let assetContainer = null;
|
|
3635
|
-
const tIndepStart = performance.now();
|
|
3636
|
-
// Whether the container was already pending inside a running #loadContainers batch.
|
|
3637
|
-
// If so, we fall back to the sequential path (wait → onReady → load) to avoid a
|
|
3638
|
-
// double-load conflict. Otherwise, load the GLTF in parallel with the scene load
|
|
3639
|
-
// and only wait for the atomic merge window.
|
|
3640
|
-
const isAlreadyHandled =
|
|
3641
|
-
this.#containers[containerName]?.state?.isPending === true && this.#loadingInProgress;
|
|
3642
|
-
const path = isAlreadyHandled ? "claimed" : "par";
|
|
3643
|
-
let container;
|
|
3644
|
-
let tAfterLoad, tAfterWait;
|
|
3645
|
-
try {
|
|
3646
|
-
if (!isAlreadyHandled) {
|
|
3647
|
-
// Optimistic path: prepare state and load GLTF NOW, in parallel with any running
|
|
3648
|
-
// #loadContainers. The merge waits for the scene, but the download does not.
|
|
3649
|
-
if (!this.#scene) {
|
|
3650
|
-
return { success: false, error: "No scene available", loadedContainers: [], loadSessionId };
|
|
3651
|
-
}
|
|
3652
|
-
onReady?.();
|
|
3653
|
-
container = this.#containers[containerName];
|
|
3654
|
-
if (!container) {
|
|
3655
|
-
return { success: false, error: `Container "${containerName}" not found`, loadedContainers: [], loadSessionId };
|
|
3656
|
-
}
|
|
3657
|
-
// Phase 1 (parallel): load GLTF while scene may still be loading.
|
|
3658
|
-
// No wait — merge happens immediately after load. #replaceContainer skips
|
|
3659
|
-
// releaseEffects when the loop is running, so no WebGL programs are deleted.
|
|
3660
|
-
const [, loaded] = await this.#loadAssetContainer(container, false, { isIndependent: true });
|
|
3661
|
-
assetContainer = loaded || null;
|
|
3662
|
-
tAfterLoad = performance.now();
|
|
3663
|
-
tAfterWait = tAfterLoad; // no wait phase in optimistic path
|
|
3664
|
-
} else {
|
|
3665
|
-
// Parallel claim path: container was already submitted to a running #loadContainers
|
|
3666
|
-
// batch. #isLoadingModelIndependent (set at the top of this method) signals
|
|
3667
|
-
// #loadAssetContainer to abort the in-flight batch load and return a null sentinel,
|
|
3668
|
-
// so #loadContainers skips merging this container. We load it here in parallel —
|
|
3669
|
-
// no waiting for scene/materials to finish.
|
|
3670
|
-
if (!this.#scene) {
|
|
3671
|
-
return { success: false, error: "No scene available", loadedContainers: [], loadSessionId };
|
|
3672
|
-
}
|
|
3673
|
-
// Call onReady() to ensure the container state carries this load's payload,
|
|
3674
|
-
// which may differ from what #loadContainers was given if the caller swapped the model.
|
|
3675
|
-
onReady?.();
|
|
3676
|
-
container = this.#containers[containerName];
|
|
3677
|
-
if (!container) {
|
|
3678
|
-
return { success: false, error: `Container "${containerName}" not found`, loadedContainers: [], loadSessionId };
|
|
3679
|
-
}
|
|
3680
|
-
const [, loaded] = await this.#loadAssetContainer(container, false, { isIndependent: true });
|
|
3681
|
-
assetContainer = loaded || null;
|
|
3682
|
-
tAfterLoad = performance.now();
|
|
3683
|
-
tAfterWait = tIndepStart; // no wait phase — measure load from start
|
|
3684
|
-
}
|
|
3685
|
-
|
|
3686
|
-
if (!assetContainer) {
|
|
3687
|
-
return { success: false, error: "Failed to load asset container", loadedContainers: [], loadSessionId };
|
|
3688
|
-
}
|
|
3689
|
-
if (!this.#scene) {
|
|
3690
|
-
assetContainer.dispose();
|
|
3691
|
-
return { success: false, error: "Scene was disposed", loadedContainers: [], loadSessionId };
|
|
3692
|
-
}
|
|
3693
|
-
|
|
3694
|
-
this.#emitSceneLoadPhase("scene-load-assets-complete", {
|
|
3695
|
-
containerName,
|
|
3696
|
-
path,
|
|
3697
|
-
tAfterLoad: tAfterLoad != null ? Math.round(tAfterLoad) : null,
|
|
3698
|
-
tIndepStart: Math.round(tIndepStart),
|
|
3699
|
-
});
|
|
3700
|
-
|
|
3701
|
-
// Phase 3: atomic merge — briefly stop the loop for the merge only
|
|
3702
|
-
this.#stopEngineRenderLoop();
|
|
3703
|
-
|
|
3704
|
-
const oldModelMetadata = containerName === "model"
|
|
3705
|
-
? { ...(this.#containers.model?.state?.metadata ?? {}) }
|
|
3706
|
-
: {};
|
|
3707
|
-
|
|
3708
|
-
this.#disposeAnimationController();
|
|
3709
|
-
|
|
3710
|
-
if (containerName === "model") {
|
|
3711
|
-
this.#assetContainer_deleteLights(assetContainer);
|
|
3712
|
-
this.#assetContainer_stopAnimations(assetContainer);
|
|
3713
|
-
}
|
|
3714
|
-
if (containerName === "model" || containerName === "environment") {
|
|
3715
|
-
this.#assetContainer_retagCameras(assetContainer);
|
|
3716
|
-
}
|
|
3717
|
-
|
|
3718
|
-
this.#replaceContainer(container, assetContainer, { releaseEffects: false });
|
|
3719
|
-
container.state.setSuccess(true);
|
|
3720
|
-
|
|
3721
|
-
// Progressive independent loads may bring model/materials/environment in any order.
|
|
3722
|
-
// Force material rebinding after each merge so already-visible model meshes pick up
|
|
3723
|
-
// the latest material definitions instead of waiting for pending flags that may
|
|
3724
|
-
// already have been cleared.
|
|
3725
|
-
this.#setOptions_Materials(true);
|
|
3726
|
-
this.#setOptions_Camera();
|
|
3727
|
-
// For model: IBL not yet loaded (scene still in flight) — create only ambient/directional
|
|
3728
|
-
// lights. #loadContainers will call #createLights() again with full IBL once scene merges.
|
|
3729
|
-
// For environment: IBL state was pre-marked pending by loadSceneIndependent before calling
|
|
3730
|
-
// here, so #setOptions_IBL() picks it up and applies it immediately after lights are ready.
|
|
3731
|
-
await this.#createLights();
|
|
3732
|
-
if (containerName === "environment") {
|
|
3733
|
-
await this.#setOptions_IBL();
|
|
3734
|
-
}
|
|
3735
|
-
|
|
3736
|
-
if (containerName === "model") {
|
|
3737
|
-
const newModelMetadata = { ...(container.state.update?.metadata ?? {}) };
|
|
3738
|
-
this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
|
|
3739
|
-
}
|
|
3740
|
-
|
|
3741
|
-
if (this.#containers.model.assetContainer) {
|
|
3742
|
-
this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
|
|
3743
|
-
}
|
|
3744
|
-
const tAfterMerge = performance.now();
|
|
3745
|
-
this.#emitSceneLoadPhase("scene-load-merge-complete", {
|
|
3746
|
-
containerName,
|
|
3747
|
-
path,
|
|
3748
|
-
tAfterMerge: Math.round(tAfterMerge),
|
|
3749
|
-
tAfterLoad: tAfterLoad != null ? Math.round(tAfterLoad) : null,
|
|
3750
|
-
tIndepStart: Math.round(tIndepStart),
|
|
3751
|
-
});
|
|
3752
|
-
|
|
3753
|
-
// Use #startRender() so the next frame is scheduled after the atomic merge.
|
|
3754
|
-
// In progressive loads, rebuilding camera-dependent effects (DefaultRenderingPipeline,
|
|
3755
|
-
// SSAO, shadows) while the model is already visible can delete GL programs that are
|
|
3756
|
-
// still referenced by the current frame, which leaves the canvas gray until a later
|
|
3757
|
-
// full reload. Keep the existing pipeline alive for independent model/environment/
|
|
3758
|
-
// materials appends; the full load path still rebuilds everything when needed.
|
|
3759
|
-
await this.#startRender({ skipWhenReady: true, forceRender: true, skipCameraDependentEffects: true });
|
|
3760
|
-
const tAfterShaders = performance.now();
|
|
3761
|
-
|
|
3762
|
-
this.#setHighlightConfig({
|
|
3763
|
-
enabled: this.#settings.highlightEnabled,
|
|
3764
|
-
color: this.#settings.highlightColor,
|
|
3765
|
-
});
|
|
3766
|
-
this.#setMaxSimultaneousLights();
|
|
3767
|
-
this.#enhanceTextureQuality();
|
|
3768
|
-
this.#setVisibilityOfWallAndFloorInModel();
|
|
3769
|
-
|
|
3770
|
-
if (this.#babylonJSAnimationController?.hasAnimations?.()) {
|
|
3771
|
-
this.#attachAnimationChangedListener();
|
|
3772
|
-
}
|
|
3773
|
-
|
|
3774
|
-
this.#requestRender({ frames: 3, continuousMs: this.#config.render.interactionMs });
|
|
3775
|
-
if (containerName === "model" && !this.#modelFirstPaintedEmitted) {
|
|
3776
|
-
try {
|
|
3777
|
-
performance.mark(`viewer:model-first-painted:scheduled:${modelFirstPaintedLoadToken}`);
|
|
3778
|
-
} catch {
|
|
3779
|
-
// ignore
|
|
3780
|
-
}
|
|
3781
|
-
console.log("[viewer] model-first-painted gate scheduled", {
|
|
3782
|
-
containerName,
|
|
3783
|
-
path: isAlreadyHandled ? "claimed" : "par",
|
|
3784
|
-
loadToken: modelFirstPaintedLoadToken,
|
|
3785
|
-
renderLoopRunning: this.#state.render.isLoopRunning,
|
|
3786
|
-
loopRequested: true,
|
|
3787
|
-
});
|
|
3788
|
-
void (async () => {
|
|
3789
|
-
await this.#waitForPaint(2);
|
|
3790
|
-
try {
|
|
3791
|
-
performance.mark(`viewer:model-first-painted:after-wait:${modelFirstPaintedLoadToken}`);
|
|
3792
|
-
} catch {
|
|
3793
|
-
// ignore
|
|
3794
|
-
}
|
|
3795
|
-
console.log("[viewer] model-first-painted gate after paint wait", {
|
|
3796
|
-
containerName,
|
|
3797
|
-
path: isAlreadyHandled ? "claimed" : "par",
|
|
3798
|
-
loadToken: modelFirstPaintedLoadToken,
|
|
3799
|
-
currentToken: this.#modelFirstPaintedLoadToken,
|
|
3800
|
-
alreadyEmitted: this.#modelFirstPaintedEmitted,
|
|
3801
|
-
renderLoopRunning: this.#state.render.isLoopRunning,
|
|
3802
|
-
});
|
|
3803
|
-
if (modelFirstPaintedLoadToken !== this.#modelFirstPaintedLoadToken) {
|
|
3804
|
-
try {
|
|
3805
|
-
performance.mark(`viewer:model-first-painted:stale:${modelFirstPaintedLoadToken}`);
|
|
3806
|
-
} catch {
|
|
3807
|
-
// ignore
|
|
3808
|
-
}
|
|
3809
|
-
console.log("[viewer] model-first-painted gate skipped (stale load token)", {
|
|
3810
|
-
containerName,
|
|
3811
|
-
path: isAlreadyHandled ? "claimed" : "par",
|
|
3812
|
-
loadToken: modelFirstPaintedLoadToken,
|
|
3813
|
-
currentToken: this.#modelFirstPaintedLoadToken,
|
|
3814
|
-
});
|
|
3815
|
-
return;
|
|
3816
|
-
}
|
|
3817
|
-
const paintTarget = this.#prefViewer || this.#prefViewer3D || this.#canvas;
|
|
3818
|
-
if (!this.#modelFirstPaintedEmitted && paintTarget) {
|
|
3819
|
-
this.#modelFirstPaintedEmitted = true;
|
|
3820
|
-
try {
|
|
3821
|
-
performance.mark(`viewer:model-first-painted:emitted:${modelFirstPaintedLoadToken}`);
|
|
3822
|
-
} catch {
|
|
3823
|
-
// ignore
|
|
3824
|
-
}
|
|
3825
|
-
console.log("[viewer] model-first-painted ✅ emitted", {
|
|
3826
|
-
containerName,
|
|
3827
|
-
path: isAlreadyHandled ? "claimed" : "par",
|
|
3828
|
-
loadToken: modelFirstPaintedLoadToken,
|
|
3829
|
-
tAfterPaint: Math.round(performance.now()),
|
|
3830
|
-
});
|
|
3831
|
-
this.#emitModelFirstPainted({
|
|
3832
|
-
containerName,
|
|
3833
|
-
path: isAlreadyHandled ? "claimed" : "par",
|
|
3834
|
-
tIndepStart: Math.round(tIndepStart),
|
|
3835
|
-
tAfterLoad: tAfterLoad != null ? Math.round(tAfterLoad) : null,
|
|
3836
|
-
tAfterWait: tAfterWait != null ? Math.round(tAfterWait) : null,
|
|
3837
|
-
tAfterMerge: Math.round(tAfterMerge),
|
|
3838
|
-
tAfterPaint: Math.round(performance.now()),
|
|
3839
|
-
});
|
|
3840
|
-
}
|
|
3841
|
-
})();
|
|
3842
|
-
}
|
|
3843
|
-
|
|
3844
|
-
// In the optimistic path: load runs first, then wait. In the sequential path: wait
|
|
3845
|
-
// runs first, then load. Compute each phase relative to its actual start point.
|
|
3846
|
-
const tLoad = isAlreadyHandled
|
|
3847
|
-
? Math.round(tAfterLoad - tAfterWait)
|
|
3848
|
-
: Math.round(tAfterLoad - tIndepStart);
|
|
3849
|
-
const tWait = isAlreadyHandled
|
|
3850
|
-
? Math.round(tAfterWait - tIndepStart)
|
|
3851
|
-
: Math.round(tAfterWait - tAfterLoad);
|
|
3852
|
-
// Merge starts after the wait in both paths (optimistic: load→wait→merge; sequential: wait→load→merge)
|
|
3853
|
-
const tMerge = Math.round(tAfterMerge - (isAlreadyHandled ? tAfterLoad : tAfterWait));
|
|
3854
|
-
const tShaders = Math.round(tAfterShaders - tAfterMerge);
|
|
3855
|
-
const tTotal = Math.round(tAfterShaders - tIndepStart);
|
|
3856
|
-
this.#emitSceneLoadPhase("scene-load-shaders-complete", {
|
|
3857
|
-
containerName,
|
|
3858
|
-
path,
|
|
3859
|
-
tAfterShaders: Math.round(tAfterShaders),
|
|
3860
|
-
tAfterMerge: Math.round(tAfterMerge),
|
|
3861
|
-
tTotal,
|
|
3862
|
-
});
|
|
3863
|
-
this.#emitSceneLoadPhase("scene-load-container-complete", {
|
|
3864
|
-
containerName,
|
|
3865
|
-
path,
|
|
3866
|
-
tLoad,
|
|
3867
|
-
tWait,
|
|
3868
|
-
tMerge,
|
|
3869
|
-
tShaders,
|
|
3870
|
-
tTotal,
|
|
3871
|
-
});
|
|
3872
|
-
console.log(`[perf][${containerName}:indep:${path}] load=${tLoad}ms wait=${tWait}ms merge=${tMerge}ms shaders=${tShaders}ms total=${tTotal}ms`);
|
|
3873
|
-
|
|
3874
|
-
return { success: true, error: null, loadedContainers: [containerName], loadSessionId };
|
|
3875
|
-
} catch (error) {
|
|
3876
|
-
assetContainer?.dispose();
|
|
3877
|
-
if (!this.#state.render.isLoopRunning && this.#scene && this.#engine) {
|
|
3878
|
-
this.#startEngineRenderLoop();
|
|
3879
|
-
}
|
|
3880
|
-
return { success: false, error, loadedContainers: [], loadSessionId };
|
|
3881
|
-
} finally {
|
|
3882
|
-
if (containerName === "model") this.#isLoadingModelIndependent = false;
|
|
3883
|
-
else if (containerName === "environment") this.#isLoadingEnvironmentIndependent = false;
|
|
3884
|
-
else if (containerName === "materials") this.#isLoadingMaterialsIndependent = false;
|
|
3885
|
-
}
|
|
3886
|
-
}
|
|
3887
|
-
|
|
3888
3164
|
/**
|
|
3889
3165
|
* Applies camera options from the configuration to the active camera.
|
|
3890
3166
|
* Stops and restarts the render loop to apply changes.
|
|
@@ -4089,4 +3365,3 @@ export default class BabylonJSController {
|
|
|
4089
3365
|
}
|
|
4090
3366
|
}
|
|
4091
3367
|
}
|
|
4092
|
-
|