@preference-sl/pref-viewer 2.16.0-beta.5 → 2.16.0-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/babylonjs-controller.js +42 -823
- package/src/file-storage.js +5 -126
- package/src/gltf-resolver.js +24 -58
- package/src/pref-viewer-3d.js +0 -104
- package/src/pref-viewer.js +1 -147
- package/src/styles.js +0 -1
|
@@ -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";
|
|
@@ -165,9 +165,6 @@ export default class BabylonJSController {
|
|
|
165
165
|
// References to parent custom elements
|
|
166
166
|
#prefViewer3D = undefined;
|
|
167
167
|
#prefViewer = undefined;
|
|
168
|
-
#modelFirstPaintedEmitted = false;
|
|
169
|
-
#modelFirstPaintedLoadToken = 0;
|
|
170
|
-
#pendingModelReplacementCleanup = null;
|
|
171
168
|
|
|
172
169
|
// Babylon.js core objects
|
|
173
170
|
#engine = null;
|
|
@@ -177,7 +174,6 @@ export default class BabylonJSController {
|
|
|
177
174
|
#dirLight = null;
|
|
178
175
|
#moonLight = null;
|
|
179
176
|
#cameraLight = null;
|
|
180
|
-
#debugCameraFrameInfo = null;
|
|
181
177
|
#shadowGen = [];
|
|
182
178
|
#XRExperience = null;
|
|
183
179
|
#canvasResizeObserver = null;
|
|
@@ -189,11 +185,6 @@ export default class BabylonJSController {
|
|
|
189
185
|
|
|
190
186
|
#gltfResolver = null; // GLTFResolver instance
|
|
191
187
|
#babylonJSAnimationController = null; // AnimationController instance
|
|
192
|
-
#loadingInProgress = false; // blocks #requestRender while assets are loading to prevent WebGL errors from rendering with disposed shaders
|
|
193
|
-
#isLoadingModelIndependent = false; // guards against concurrent independent model loads
|
|
194
|
-
#isLoadingEnvironmentIndependent = false; // guards against concurrent independent environment loads
|
|
195
|
-
#isLoadingMaterialsIndependent = false; // guards against concurrent independent materials loads
|
|
196
|
-
#startRenderInProgress = false; // true while #startRender() is running (after #loadingInProgress clears)
|
|
197
188
|
|
|
198
189
|
#renderPipelines = {
|
|
199
190
|
default: null,
|
|
@@ -757,8 +748,8 @@ export default class BabylonJSController {
|
|
|
757
748
|
* @param {number} [options.continuousMs=0] - Milliseconds to keep continuous rendering active.
|
|
758
749
|
* @returns {boolean} True when the request was accepted, false when scene/engine are unavailable.
|
|
759
750
|
*/
|
|
760
|
-
#requestRender({ frames = 1, continuousMs = 0
|
|
761
|
-
if (!this.#scene || !this.#engine
|
|
751
|
+
#requestRender({ frames = 1, continuousMs = 0 } = {}) {
|
|
752
|
+
if (!this.#scene || !this.#engine) {
|
|
762
753
|
return false;
|
|
763
754
|
}
|
|
764
755
|
|
|
@@ -1121,11 +1112,6 @@ export default class BabylonJSController {
|
|
|
1121
1112
|
|
|
1122
1113
|
const pipelineName = "PrefViewerSSAORenderingPipeline";
|
|
1123
1114
|
|
|
1124
|
-
// Skip recreation if already alive — same reason as DefaultRenderingPipeline guard
|
|
1125
|
-
if (this.#renderPipelines.ssao && supportedPipelines.find((p) => p.name === pipelineName)) {
|
|
1126
|
-
return true;
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
1115
|
const ssaoRatio = {
|
|
1130
1116
|
ssaoRatio: 0.5,
|
|
1131
1117
|
combineRatio: 1.0,
|
|
@@ -1227,21 +1213,6 @@ export default class BabylonJSController {
|
|
|
1227
1213
|
|
|
1228
1214
|
const pipelineName = "PrefViewerDefaultRenderingPipeline";
|
|
1229
1215
|
|
|
1230
|
-
// Skip recreation if the pipeline is already alive — creating a new DefaultRenderingPipeline
|
|
1231
|
-
// while one exists disposes the underlying GeometryBufferRenderer, deleting GL programs that
|
|
1232
|
-
// subMeshes still reference, causing useProgram:deleted errors on the next render frame.
|
|
1233
|
-
if (this.#renderPipelines.default && supportedPipelines.find((p) => p.name === pipelineName)) {
|
|
1234
|
-
return true;
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
// Purge stale effect cache before creating a new pipeline. pipeline.dispose() calls
|
|
1238
|
-
// gl.deleteProgram() per post-process but leaves entries in Engine._compiledEffects.
|
|
1239
|
-
// A new DefaultRenderingPipeline with the same shader key gets a cache hit and receives
|
|
1240
|
-
// the deleted program → useProgram:deleted on the first render frame.
|
|
1241
|
-
// Safe here because #startRender always stops the loop before reaching this point.
|
|
1242
|
-
this.#scene.getEngine().releaseEffects?.();
|
|
1243
|
-
this.#scene.getEngine().releaseComputeEffects?.();
|
|
1244
|
-
|
|
1245
1216
|
let defaultPipeline = new DefaultRenderingPipeline(pipelineName, true, this.#scene, [this.#scene.activeCamera], true);
|
|
1246
1217
|
|
|
1247
1218
|
if (!defaultPipeline) {
|
|
@@ -1419,11 +1390,6 @@ export default class BabylonJSController {
|
|
|
1419
1390
|
|
|
1420
1391
|
const pipelineName = "PrefViewerIblShadowsRenderPipeline";
|
|
1421
1392
|
|
|
1422
|
-
// Skip recreation if already alive — same reason as DefaultRenderingPipeline guard
|
|
1423
|
-
if (this.#renderPipelines.iblShadows && supportedPipelines.find((p) => p.name === pipelineName)) {
|
|
1424
|
-
return true;
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
1393
|
// Use hardware-adaptive quality settings
|
|
1428
1394
|
const pipelineOptions = {
|
|
1429
1395
|
resolutionExp: this.#config.quality.iblShadowResolution, // Adaptive: 0=1024, 1=2048, 2=4096
|
|
@@ -1675,7 +1641,7 @@ export default class BabylonJSController {
|
|
|
1675
1641
|
const showroom = this.#config.look?.showroom || {};
|
|
1676
1642
|
const imageProcessing = this.#scene.imageProcessingConfiguration;
|
|
1677
1643
|
|
|
1678
|
-
this.#scene.clearColor = new Color4(
|
|
1644
|
+
this.#scene.clearColor = new Color4(0.98, 0.98, 0.98, 1).toLinearSpace();
|
|
1679
1645
|
imageProcessing.exposure = showroom.exposure ?? 0.95;
|
|
1680
1646
|
imageProcessing.contrast = showroom.contrast ?? 1.08;
|
|
1681
1647
|
imageProcessing.toneMappingEnabled = showroom.toneMappingEnabled ?? true;
|
|
@@ -1715,7 +1681,12 @@ export default class BabylonJSController {
|
|
|
1715
1681
|
imageProcessing.colorCurvesEnabled = false;
|
|
1716
1682
|
}
|
|
1717
1683
|
|
|
1718
|
-
this.#scene.clearColor = new Color4(
|
|
1684
|
+
this.#scene.clearColor = new Color4(
|
|
1685
|
+
lerp(0.035, 0.98, daylight),
|
|
1686
|
+
lerp(0.045, 0.98, daylight),
|
|
1687
|
+
lerp(0.085, 0.98, daylight),
|
|
1688
|
+
1,
|
|
1689
|
+
).toLinearSpace();
|
|
1719
1690
|
|
|
1720
1691
|
if ("environmentIntensity" in this.#scene) {
|
|
1721
1692
|
this.#scene.environmentIntensity = lerp(0.42, showroom.environmentIntensity ?? 1.08, peakLight);
|
|
@@ -2204,10 +2175,6 @@ export default class BabylonJSController {
|
|
|
2204
2175
|
}
|
|
2205
2176
|
this.#state.resize.lastAppliedAt = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
2206
2177
|
console.log(`PrefViewer: Applying resize after ${Math.round(elapsed)}ms`);
|
|
2207
|
-
// Stop the loop before resize: engine.resize() rebuilds DefaultRenderingPipeline
|
|
2208
|
-
// via onResizeObservable, which disposes GL programs. If the loop is running,
|
|
2209
|
-
// the next frame would call useProgram on a deleted program → INVALID_OPERATION.
|
|
2210
|
-
this.#stopEngineRenderLoop();
|
|
2211
2178
|
this.#applyShowroomCameraFraming();
|
|
2212
2179
|
this.#engine.resize();
|
|
2213
2180
|
const needsEnhancedBurst = this.#settings.antiAliasingEnabled || this.#settings.ambientOcclusionEnabled || this.#settings.iblEnabled || this.#settings.shadowsEnabled;
|
|
@@ -2250,7 +2217,7 @@ export default class BabylonJSController {
|
|
|
2250
2217
|
* @param {MaterialData} optionMaterial - Material option containing value, nodePrefixes, nodeNames, and state.
|
|
2251
2218
|
* @returns {boolean} True if any mesh material was set, false otherwise.
|
|
2252
2219
|
*/
|
|
2253
|
-
#setOptionsMaterial(optionMaterial
|
|
2220
|
+
#setOptionsMaterial(optionMaterial) {
|
|
2254
2221
|
if (!optionMaterial || !(optionMaterial.value || (optionMaterial.isPending && optionMaterial.update.value)) || !(optionMaterial.nodePrefixes.length || optionMaterial.nodeNames.length)) {
|
|
2255
2222
|
return false;
|
|
2256
2223
|
}
|
|
@@ -2268,7 +2235,7 @@ export default class BabylonJSController {
|
|
|
2268
2235
|
if (container.state.name === "materials") {
|
|
2269
2236
|
return;
|
|
2270
2237
|
}
|
|
2271
|
-
if (container.assetContainer && (
|
|
2238
|
+
if (container.assetContainer && (container.state.isPending || materialContainer.state.isPending || optionMaterial.isPending)) {
|
|
2272
2239
|
assetContainersToProcess.push(container.assetContainer);
|
|
2273
2240
|
}
|
|
2274
2241
|
});
|
|
@@ -2279,7 +2246,7 @@ export default class BabylonJSController {
|
|
|
2279
2246
|
let someSetted = false;
|
|
2280
2247
|
assetContainersToProcess.forEach((assetContainer) =>
|
|
2281
2248
|
assetContainer.meshes
|
|
2282
|
-
.filter((meshToFilter) => optionMaterial.nodePrefixes.some((prefix) => meshToFilter.name.startsWith(prefix)) || optionMaterial.nodeNames.
|
|
2249
|
+
.filter((meshToFilter) => optionMaterial.nodePrefixes.some((prefix) => meshToFilter.name.toLowerCase().startsWith(prefix.toLowerCase())) || optionMaterial.nodeNames.some((n) => n.toLowerCase() === meshToFilter.name.toLowerCase()))
|
|
2283
2250
|
.forEach((mesh) => {
|
|
2284
2251
|
mesh.material = material;
|
|
2285
2252
|
someSetted = true;
|
|
@@ -2301,10 +2268,10 @@ export default class BabylonJSController {
|
|
|
2301
2268
|
* @private
|
|
2302
2269
|
* @returns {boolean} True if any material option was set, false otherwise.
|
|
2303
2270
|
*/
|
|
2304
|
-
#setOptions_Materials(
|
|
2271
|
+
#setOptions_Materials() {
|
|
2305
2272
|
let someSetted = false;
|
|
2306
2273
|
Object.values(this.#options.materials).forEach((material) => {
|
|
2307
|
-
let settedMaterial = this.#setOptionsMaterial(material
|
|
2274
|
+
let settedMaterial = this.#setOptionsMaterial(material);
|
|
2308
2275
|
someSetted = someSetted || settedMaterial;
|
|
2309
2276
|
});
|
|
2310
2277
|
return someSetted;
|
|
@@ -2413,9 +2380,7 @@ export default class BabylonJSController {
|
|
|
2413
2380
|
prefViewer3D = host?.nodeName === "PREF-VIEWER-3D" ? host : undefined;
|
|
2414
2381
|
}
|
|
2415
2382
|
|
|
2416
|
-
|
|
2417
|
-
this.#prefViewer3D = prefViewer3D;
|
|
2418
|
-
}
|
|
2383
|
+
this.#prefViewer3D = prefViewer3D;
|
|
2419
2384
|
}
|
|
2420
2385
|
|
|
2421
2386
|
/**
|
|
@@ -2430,6 +2395,7 @@ export default class BabylonJSController {
|
|
|
2430
2395
|
|
|
2431
2396
|
this.#getPrefViewer3DComponent();
|
|
2432
2397
|
if (this.#prefViewer3D === undefined) {
|
|
2398
|
+
this.#prefViewer = undefined;
|
|
2433
2399
|
return;
|
|
2434
2400
|
}
|
|
2435
2401
|
|
|
@@ -2439,9 +2405,7 @@ export default class BabylonJSController {
|
|
|
2439
2405
|
prefViewer = host?.nodeName === "PREF-VIEWER" ? host : undefined;
|
|
2440
2406
|
}
|
|
2441
2407
|
|
|
2442
|
-
|
|
2443
|
-
this.#prefViewer = prefViewer;
|
|
2444
|
-
}
|
|
2408
|
+
this.#prefViewer = prefViewer;
|
|
2445
2409
|
}
|
|
2446
2410
|
|
|
2447
2411
|
/**
|
|
@@ -2455,96 +2419,15 @@ export default class BabylonJSController {
|
|
|
2455
2419
|
// Cache references to parent custom elements
|
|
2456
2420
|
this.#getPrefViewer3DComponent();
|
|
2457
2421
|
this.#getPrefViewerComponent();
|
|
2458
|
-
const syncVisibility = (component, fallbackAttr) => {
|
|
2459
|
-
if (!component) {
|
|
2460
|
-
return;
|
|
2461
|
-
}
|
|
2462
|
-
if (typeof component.syncVisibility === "function") {
|
|
2463
|
-
component.syncVisibility(name, isVisible);
|
|
2464
|
-
return;
|
|
2465
|
-
}
|
|
2466
|
-
component.setAttribute(fallbackAttr, isVisible ? "true" : "false");
|
|
2467
|
-
};
|
|
2468
2422
|
if (name === "model") {
|
|
2469
|
-
|
|
2470
|
-
|
|
2423
|
+
this.#prefViewer3D?.setAttribute("show-model", isVisible ? "true" : "false");
|
|
2424
|
+
this.#prefViewer?.setAttribute("show-model", isVisible ? "true" : "false");
|
|
2471
2425
|
} else if (name === "environment") {
|
|
2472
|
-
|
|
2473
|
-
|
|
2426
|
+
this.#prefViewer3D?.setAttribute("show-scene", isVisible ? "true" : "false");
|
|
2427
|
+
this.#prefViewer?.setAttribute("show-scene", isVisible ? "true" : "false");
|
|
2474
2428
|
}
|
|
2475
2429
|
}
|
|
2476
2430
|
|
|
2477
|
-
/**
|
|
2478
|
-
* Dispatches a "model-first-painted" event once the model-only preview has had a chance to render a frame.
|
|
2479
|
-
* @private
|
|
2480
|
-
* @param {object} [detail] - Optional metadata describing the paint gate.
|
|
2481
|
-
* @returns {void}
|
|
2482
|
-
*/
|
|
2483
|
-
#emitModelFirstPainted(detail = {}) {
|
|
2484
|
-
this.#getPrefViewer3DComponent();
|
|
2485
|
-
this.#getPrefViewerComponent();
|
|
2486
|
-
const target = this.#prefViewer || this.#prefViewer3D || this.#canvas;
|
|
2487
|
-
if (!target) {
|
|
2488
|
-
return;
|
|
2489
|
-
}
|
|
2490
|
-
|
|
2491
|
-
const customEventOptions = {
|
|
2492
|
-
bubbles: true,
|
|
2493
|
-
cancelable: false,
|
|
2494
|
-
composed: true,
|
|
2495
|
-
detail,
|
|
2496
|
-
};
|
|
2497
|
-
target.dispatchEvent(new CustomEvent("model-first-painted", customEventOptions));
|
|
2498
|
-
}
|
|
2499
|
-
|
|
2500
|
-
/**
|
|
2501
|
-
* Dispatches a granular 3D load phase event so the ecommerce trace UI can measure tail phases.
|
|
2502
|
-
* @private
|
|
2503
|
-
* @param {string} phase - Phase event name.
|
|
2504
|
-
* @param {object} [detail] - Optional phase metadata.
|
|
2505
|
-
* @returns {void}
|
|
2506
|
-
*/
|
|
2507
|
-
#emitSceneLoadPhase(phase, detail = {}) {
|
|
2508
|
-
this.#getPrefViewer3DComponent();
|
|
2509
|
-
this.#getPrefViewerComponent();
|
|
2510
|
-
const target = this.#prefViewer || this.#prefViewer3D || this.#canvas;
|
|
2511
|
-
if (!target) {
|
|
2512
|
-
return;
|
|
2513
|
-
}
|
|
2514
|
-
|
|
2515
|
-
const customEventOptions = {
|
|
2516
|
-
bubbles: true,
|
|
2517
|
-
cancelable: false,
|
|
2518
|
-
composed: true,
|
|
2519
|
-
detail,
|
|
2520
|
-
};
|
|
2521
|
-
target.dispatchEvent(new CustomEvent(phase, customEventOptions));
|
|
2522
|
-
}
|
|
2523
|
-
|
|
2524
|
-
/**
|
|
2525
|
-
* Waits for one or more animation frames so the browser can paint the current model-only state.
|
|
2526
|
-
* @private
|
|
2527
|
-
* @param {number} [frameCount=2] - Number of animation frames to wait.
|
|
2528
|
-
* @returns {Promise<void>}
|
|
2529
|
-
*/
|
|
2530
|
-
async #waitForPaint(frameCount = 2) {
|
|
2531
|
-
if (typeof requestAnimationFrame !== "function") {
|
|
2532
|
-
return;
|
|
2533
|
-
}
|
|
2534
|
-
|
|
2535
|
-
const totalFrames = Math.max(1, frameCount);
|
|
2536
|
-
await new Promise((resolve) => {
|
|
2537
|
-
const step = (remaining) => {
|
|
2538
|
-
if (remaining <= 0) {
|
|
2539
|
-
resolve();
|
|
2540
|
-
return;
|
|
2541
|
-
}
|
|
2542
|
-
requestAnimationFrame(() => step(remaining - 1));
|
|
2543
|
-
};
|
|
2544
|
-
requestAnimationFrame(() => step(totalFrames - 1));
|
|
2545
|
-
});
|
|
2546
|
-
}
|
|
2547
|
-
|
|
2548
2431
|
/**
|
|
2549
2432
|
* Adds the asset container to the Babylon.js scene if it should be shown and is not already visible.
|
|
2550
2433
|
* @private
|
|
@@ -2590,21 +2473,8 @@ export default class BabylonJSController {
|
|
|
2590
2473
|
* @param {AssetContainer} newAssetContainer - The new asset container to add to the scene.
|
|
2591
2474
|
* @returns {boolean} True if the container was replaced and added, false otherwise.
|
|
2592
2475
|
*/
|
|
2593
|
-
#replaceContainer(container, newAssetContainer
|
|
2594
|
-
const keepPreviousVisibleUntilPaint = container.state.name === "model" && container.assetContainer && container.state.isVisible;
|
|
2595
|
-
if (this.#pendingModelReplacementCleanup && container.state.name === "model") {
|
|
2596
|
-
this.#finalizePendingModelReplacement();
|
|
2597
|
-
}
|
|
2476
|
+
#replaceContainer(container, newAssetContainer) {
|
|
2598
2477
|
if (container.assetContainer) {
|
|
2599
|
-
if (keepPreviousVisibleUntilPaint) {
|
|
2600
|
-
if (this.#pendingModelReplacementCleanup) {
|
|
2601
|
-
this.#finalizePendingModelReplacement();
|
|
2602
|
-
}
|
|
2603
|
-
const oldAssetContainer = container.assetContainer;
|
|
2604
|
-
container.assetContainer = newAssetContainer;
|
|
2605
|
-
this.#pendingModelReplacementCleanup = { container, oldAssetContainer, releaseEffects };
|
|
2606
|
-
return true;
|
|
2607
|
-
}
|
|
2608
2478
|
this.#removeContainer(container, false);
|
|
2609
2479
|
container.assetContainer.dispose();
|
|
2610
2480
|
container.assetContainer = null;
|
|
@@ -2615,69 +2485,13 @@ export default class BabylonJSController {
|
|
|
2615
2485
|
newAssetContainer?.dispose();
|
|
2616
2486
|
return false;
|
|
2617
2487
|
}
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
// explicitly passes releaseEffects:false (e.g. loadContainerIndependent).
|
|
2621
|
-
if (releaseEffects && !this.#state.render.isLoopRunning) {
|
|
2622
|
-
this.#scene.getEngine().releaseEffects();
|
|
2623
|
-
this.#scene.getEngine().releaseComputeEffects();
|
|
2624
|
-
}
|
|
2488
|
+
this.#scene.getEngine().releaseEffects();
|
|
2489
|
+
this.#scene.getEngine().releaseComputeEffects();
|
|
2625
2490
|
|
|
2626
2491
|
container.assetContainer = newAssetContainer;
|
|
2627
2492
|
return this.#addContainer(container, false);
|
|
2628
2493
|
}
|
|
2629
2494
|
|
|
2630
|
-
/**
|
|
2631
|
-
* Finalizes a deferred model replacement by disposing the previously visible model container.
|
|
2632
|
-
* @private
|
|
2633
|
-
* @returns {void}
|
|
2634
|
-
*/
|
|
2635
|
-
#finalizePendingModelReplacement() {
|
|
2636
|
-
if (!this.#pendingModelReplacementCleanup) {
|
|
2637
|
-
return;
|
|
2638
|
-
}
|
|
2639
|
-
const { container, oldAssetContainer } = this.#pendingModelReplacementCleanup;
|
|
2640
|
-
this.#pendingModelReplacementCleanup = null;
|
|
2641
|
-
if (!oldAssetContainer) {
|
|
2642
|
-
return;
|
|
2643
|
-
}
|
|
2644
|
-
const stagedAssetContainer = container?.assetContainer ?? null;
|
|
2645
|
-
if (stagedAssetContainer) {
|
|
2646
|
-
try {
|
|
2647
|
-
container.state.visible = false;
|
|
2648
|
-
this.#addContainer(container, false);
|
|
2649
|
-
} catch {
|
|
2650
|
-
// ignore
|
|
2651
|
-
}
|
|
2652
|
-
}
|
|
2653
|
-
try {
|
|
2654
|
-
oldAssetContainer.removeAllFromScene();
|
|
2655
|
-
} catch {
|
|
2656
|
-
// ignore
|
|
2657
|
-
}
|
|
2658
|
-
try {
|
|
2659
|
-
oldAssetContainer.dispose();
|
|
2660
|
-
} catch {
|
|
2661
|
-
// ignore
|
|
2662
|
-
}
|
|
2663
|
-
if (stagedAssetContainer) {
|
|
2664
|
-
try {
|
|
2665
|
-
stagedAssetContainer.meshes?.forEach((mesh) => {
|
|
2666
|
-
mesh.visibility = 1;
|
|
2667
|
-
});
|
|
2668
|
-
this.#setVisibilityOfWallAndFloorInModel();
|
|
2669
|
-
this.#setOptions_Materials(true);
|
|
2670
|
-
} catch {
|
|
2671
|
-
// ignore
|
|
2672
|
-
}
|
|
2673
|
-
}
|
|
2674
|
-
this.#requestRender({
|
|
2675
|
-
frames: 3,
|
|
2676
|
-
continuousMs: this.#config.render.interactionMs,
|
|
2677
|
-
force: true,
|
|
2678
|
-
});
|
|
2679
|
-
}
|
|
2680
|
-
|
|
2681
2495
|
/**
|
|
2682
2496
|
* Stops every animation group on the provided asset container to guarantee new loads start from a clean state.
|
|
2683
2497
|
* @private
|
|
@@ -2734,207 +2548,10 @@ export default class BabylonJSController {
|
|
|
2734
2548
|
if (!this.#containers.model.assetContainer || !this.#containers.model.state.isVisible) {
|
|
2735
2549
|
return false;
|
|
2736
2550
|
}
|
|
2737
|
-
|
|
2738
|
-
// Only defer to the environment's visibility after it has fully loaded.
|
|
2739
|
-
show = show !== undefined
|
|
2740
|
-
? show
|
|
2741
|
-
: (this.#containers.environment.state.isSuccess
|
|
2742
|
-
? this.#containers.environment.state.isVisible
|
|
2743
|
-
: true);
|
|
2551
|
+
show = show !== undefined ? show : this.#containers.environment.state.isVisible;
|
|
2744
2552
|
const nodePrefixes = Object.values(this.#options.materials).flatMap((material) => material.nodePrefixes);
|
|
2745
2553
|
const nodeNames = Object.values(this.#options.materials).flatMap((material) => material.nodeNames);
|
|
2746
|
-
this.#containers.model.assetContainer.meshes.filter((meshToFilter) => nodePrefixes.some((prefix) => meshToFilter.name.startsWith(prefix)) || nodeNames.
|
|
2747
|
-
}
|
|
2748
|
-
|
|
2749
|
-
/**
|
|
2750
|
-
* Frames the active ArcRotateCamera around the visible model when no environment scene is loaded.
|
|
2751
|
-
* @private
|
|
2752
|
-
* @returns {boolean} True if the camera was adjusted, otherwise false.
|
|
2753
|
-
*/
|
|
2754
|
-
#frameCameraToModel({ allowWhenEnvironmentVisible = false } = {}) {
|
|
2755
|
-
const camera = this.#scene?.activeCamera instanceof ArcRotateCamera
|
|
2756
|
-
? this.#scene.activeCamera
|
|
2757
|
-
: this.#camera;
|
|
2758
|
-
const modelContainer = this.#containers.model;
|
|
2759
|
-
const inspect = {
|
|
2760
|
-
cameraType: camera?.getClassName?.() ?? camera?.constructor?.name ?? null,
|
|
2761
|
-
hasModelContainer: !!modelContainer.assetContainer,
|
|
2762
|
-
modelVisible: modelContainer.state.isVisible,
|
|
2763
|
-
envHasContainer: !!this.#containers.environment.assetContainer,
|
|
2764
|
-
envVisible: this.#containers.environment.state.isVisible,
|
|
2765
|
-
meshCount: modelContainer.assetContainer?.meshes?.length ?? 0,
|
|
2766
|
-
};
|
|
2767
|
-
this.#debugCameraFrameInfo = { phase: "inspect", ...inspect };
|
|
2768
|
-
console.log("[viewer] frameCameraToModel inspect", inspect);
|
|
2769
|
-
if (!(camera instanceof ArcRotateCamera) || !modelContainer.assetContainer || !modelContainer.state.isVisible) {
|
|
2770
|
-
this.#debugCameraFrameInfo = {
|
|
2771
|
-
phase: "skipped",
|
|
2772
|
-
reason: !(camera instanceof ArcRotateCamera)
|
|
2773
|
-
? "no-arcrotate-camera"
|
|
2774
|
-
: !modelContainer.assetContainer
|
|
2775
|
-
? "no-model"
|
|
2776
|
-
: !modelContainer.state.isVisible
|
|
2777
|
-
? "model-hidden"
|
|
2778
|
-
: "unknown",
|
|
2779
|
-
...inspect,
|
|
2780
|
-
};
|
|
2781
|
-
return false;
|
|
2782
|
-
}
|
|
2783
|
-
if (!allowWhenEnvironmentVisible && this.#containers.environment.assetContainer && this.#containers.environment.state.isVisible) {
|
|
2784
|
-
this.#debugCameraFrameInfo = {
|
|
2785
|
-
phase: "skipped",
|
|
2786
|
-
reason: "environment-visible",
|
|
2787
|
-
...inspect,
|
|
2788
|
-
};
|
|
2789
|
-
return false;
|
|
2790
|
-
}
|
|
2791
|
-
|
|
2792
|
-
let min = null;
|
|
2793
|
-
let max = null;
|
|
2794
|
-
modelContainer.assetContainer.meshes.forEach((mesh) => {
|
|
2795
|
-
if (!mesh?.getTotalVertices?.() || mesh.getTotalVertices() <= 0) {
|
|
2796
|
-
return;
|
|
2797
|
-
}
|
|
2798
|
-
mesh.computeWorldMatrix?.(true);
|
|
2799
|
-
const boundingBox = mesh.getBoundingInfo?.()?.boundingBox;
|
|
2800
|
-
if (!boundingBox) {
|
|
2801
|
-
return;
|
|
2802
|
-
}
|
|
2803
|
-
const minimum = boundingBox.minimumWorld;
|
|
2804
|
-
const maximum = boundingBox.maximumWorld;
|
|
2805
|
-
if (!minimum || !maximum) {
|
|
2806
|
-
return;
|
|
2807
|
-
}
|
|
2808
|
-
min = min
|
|
2809
|
-
? new Vector3(Math.min(min.x, minimum.x), Math.min(min.y, minimum.y), Math.min(min.z, minimum.z))
|
|
2810
|
-
: minimum.clone();
|
|
2811
|
-
max = max
|
|
2812
|
-
? new Vector3(Math.max(max.x, maximum.x), Math.max(max.y, maximum.y), Math.max(max.z, maximum.z))
|
|
2813
|
-
: maximum.clone();
|
|
2814
|
-
});
|
|
2815
|
-
|
|
2816
|
-
if (!min || !max) {
|
|
2817
|
-
const skipped = {
|
|
2818
|
-
hasCamera: camera instanceof ArcRotateCamera,
|
|
2819
|
-
hasModel: !!modelContainer.assetContainer,
|
|
2820
|
-
modelVisible: modelContainer.state.isVisible,
|
|
2821
|
-
environmentVisible: this.#containers.environment.state.isVisible,
|
|
2822
|
-
};
|
|
2823
|
-
this.#debugCameraFrameInfo = {
|
|
2824
|
-
phase: "skipped",
|
|
2825
|
-
reason: "no-bounds",
|
|
2826
|
-
...inspect,
|
|
2827
|
-
...skipped,
|
|
2828
|
-
};
|
|
2829
|
-
console.log("[viewer] frameCameraToModel skipped (no bounds)", skipped);
|
|
2830
|
-
return false;
|
|
2831
|
-
}
|
|
2832
|
-
|
|
2833
|
-
const size = max.subtract(min);
|
|
2834
|
-
const largestDimension = Math.max(size.x, size.y, size.z);
|
|
2835
|
-
if (!Number.isFinite(largestDimension) || largestDimension <= 0) {
|
|
2836
|
-
return false;
|
|
2837
|
-
}
|
|
2838
|
-
|
|
2839
|
-
const center = min.add(max).scale(0.5);
|
|
2840
|
-
camera.setTarget(center);
|
|
2841
|
-
const radiusMultiplier = allowWhenEnvironmentVisible ? 1.7 : 1.4;
|
|
2842
|
-
camera.radius = Math.min(
|
|
2843
|
-
Math.max(largestDimension * radiusMultiplier, camera.lowerRadiusLimit ?? largestDimension * radiusMultiplier),
|
|
2844
|
-
camera.upperRadiusLimit ?? largestDimension * radiusMultiplier,
|
|
2845
|
-
);
|
|
2846
|
-
// Keep the initial model-only view aligned to the showroom's frontal composition.
|
|
2847
|
-
// This preserves the user's camera intent once the scene becomes visible, but gives
|
|
2848
|
-
// the standalone model a consistent front-facing presentation.
|
|
2849
|
-
// Rotate the front-facing composition 180° while keeping the same framing/radius.
|
|
2850
|
-
camera.alpha = Math.PI;
|
|
2851
|
-
camera.beta = Math.PI * 0.47;
|
|
2852
|
-
console.log("[viewer] frameCameraToModel applied", {
|
|
2853
|
-
center,
|
|
2854
|
-
size,
|
|
2855
|
-
radius: camera.radius,
|
|
2856
|
-
});
|
|
2857
|
-
this.#debugCameraFrameInfo = {
|
|
2858
|
-
phase: "applied",
|
|
2859
|
-
...inspect,
|
|
2860
|
-
center: { x: center.x, y: center.y, z: center.z },
|
|
2861
|
-
size: { x: size.x, y: size.y, z: size.z },
|
|
2862
|
-
radius: camera.radius,
|
|
2863
|
-
cameraTarget: camera.target ? { x: camera.target.x, y: camera.target.y, z: camera.target.z } : null,
|
|
2864
|
-
cameraPosition: camera.position ? { x: camera.position.x, y: camera.position.y, z: camera.position.z } : null,
|
|
2865
|
-
cameraLocked: !!camera.metadata?.locked,
|
|
2866
|
-
};
|
|
2867
|
-
return true;
|
|
2868
|
-
}
|
|
2869
|
-
|
|
2870
|
-
/**
|
|
2871
|
-
* Public wrapper to force the active camera to frame the visible model.
|
|
2872
|
-
* @public
|
|
2873
|
-
* @returns {boolean}
|
|
2874
|
-
*/
|
|
2875
|
-
focusModel() {
|
|
2876
|
-
return this.#frameCameraToModel();
|
|
2877
|
-
}
|
|
2878
|
-
|
|
2879
|
-
/**
|
|
2880
|
-
* Returns a compact snapshot of the current debug-relevant 3D state.
|
|
2881
|
-
* @public
|
|
2882
|
-
* @returns {object}
|
|
2883
|
-
*/
|
|
2884
|
-
getDebugState() {
|
|
2885
|
-
const activeCamera = this.#scene?.activeCamera ?? this.#camera ?? null;
|
|
2886
|
-
const modelMeshes = this.#containers.model?.assetContainer?.meshes ?? [];
|
|
2887
|
-
let boundsMin = null;
|
|
2888
|
-
let boundsMax = null;
|
|
2889
|
-
for (const mesh of modelMeshes) {
|
|
2890
|
-
if (!mesh?.getTotalVertices?.() || mesh.getTotalVertices() <= 0) continue;
|
|
2891
|
-
mesh.computeWorldMatrix?.(true);
|
|
2892
|
-
const boundingBox = mesh.getBoundingInfo?.()?.boundingBox;
|
|
2893
|
-
if (!boundingBox?.minimumWorld || !boundingBox?.maximumWorld) continue;
|
|
2894
|
-
const minimum = boundingBox.minimumWorld;
|
|
2895
|
-
const maximum = boundingBox.maximumWorld;
|
|
2896
|
-
boundsMin = boundsMin
|
|
2897
|
-
? { x: Math.min(boundsMin.x, minimum.x), y: Math.min(boundsMin.y, minimum.y), z: Math.min(boundsMin.z, minimum.z) }
|
|
2898
|
-
: { x: minimum.x, y: minimum.y, z: minimum.z };
|
|
2899
|
-
boundsMax = boundsMax
|
|
2900
|
-
? { x: Math.max(boundsMax.x, maximum.x), y: Math.max(boundsMax.y, maximum.y), z: Math.max(boundsMax.z, maximum.z) }
|
|
2901
|
-
: { x: maximum.x, y: maximum.y, z: maximum.z };
|
|
2902
|
-
}
|
|
2903
|
-
const modelBounds = boundsMin && boundsMax ? {
|
|
2904
|
-
min: boundsMin,
|
|
2905
|
-
max: boundsMax,
|
|
2906
|
-
center: {
|
|
2907
|
-
x: (boundsMin.x + boundsMax.x) / 2,
|
|
2908
|
-
y: (boundsMin.y + boundsMax.y) / 2,
|
|
2909
|
-
z: (boundsMin.z + boundsMax.z) / 2,
|
|
2910
|
-
},
|
|
2911
|
-
size: {
|
|
2912
|
-
x: boundsMax.x - boundsMin.x,
|
|
2913
|
-
y: boundsMax.y - boundsMin.y,
|
|
2914
|
-
z: boundsMax.z - boundsMin.z,
|
|
2915
|
-
},
|
|
2916
|
-
} : null;
|
|
2917
|
-
return {
|
|
2918
|
-
camera: activeCamera ? {
|
|
2919
|
-
className: activeCamera.getClassName?.() ?? activeCamera.constructor?.name ?? null,
|
|
2920
|
-
position: activeCamera.position ? { x: activeCamera.position.x, y: activeCamera.position.y, z: activeCamera.position.z } : null,
|
|
2921
|
-
target: activeCamera.target ? { x: activeCamera.target.x, y: activeCamera.target.y, z: activeCamera.target.z } : null,
|
|
2922
|
-
radius: typeof activeCamera.radius === "number" ? activeCamera.radius : null,
|
|
2923
|
-
locked: !!activeCamera.metadata?.locked,
|
|
2924
|
-
} : null,
|
|
2925
|
-
model: {
|
|
2926
|
-
visible: !!this.#containers.model?.state?.isVisible,
|
|
2927
|
-
hasContainer: !!this.#containers.model?.assetContainer,
|
|
2928
|
-
meshCount: this.#containers.model?.assetContainer?.meshes?.length ?? 0,
|
|
2929
|
-
bounds: modelBounds,
|
|
2930
|
-
},
|
|
2931
|
-
environment: {
|
|
2932
|
-
visible: !!this.#containers.environment?.state?.isVisible,
|
|
2933
|
-
hasContainer: !!this.#containers.environment?.assetContainer,
|
|
2934
|
-
},
|
|
2935
|
-
lastCameraFrame: this.#debugCameraFrameInfo,
|
|
2936
|
-
renderLoopRunning: !!this.#state?.render?.isLoopRunning,
|
|
2937
|
-
};
|
|
2554
|
+
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));
|
|
2938
2555
|
}
|
|
2939
2556
|
|
|
2940
2557
|
/**
|
|
@@ -2954,25 +2571,12 @@ export default class BabylonJSController {
|
|
|
2954
2571
|
* @private
|
|
2955
2572
|
* @returns {Promise<void>}
|
|
2956
2573
|
*/
|
|
2957
|
-
async #startRender(
|
|
2574
|
+
async #startRender() {
|
|
2958
2575
|
if (!this.#scene) return;
|
|
2959
|
-
this.#
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
}
|
|
2964
|
-
// skipWhenReady: model materials already compiled via compileMaterials:true.
|
|
2965
|
-
// Skips scene-wide whenReadyAsync so the model appears immediately (~1s).
|
|
2966
|
-
if (!skipWhenReady) {
|
|
2967
|
-
await this.#scene.whenReadyAsync();
|
|
2968
|
-
}
|
|
2969
|
-
const frames = this.#settings.antiAliasingEnabled || this.#settings.ambientOcclusionEnabled || this.#settings.iblEnabled ? this.#config.render.burstFramesEnhanced : this.#config.render.burstFramesBase;
|
|
2970
|
-
// forceRender: bypasses #loadingInProgress gate so loadContainerIndependent
|
|
2971
|
-
// can show the model preview while #loadContainers is still loading scene/materials.
|
|
2972
|
-
this.#requestRender({ frames: frames, continuousMs: this.#config.render.interactionMs, force: forceRender });
|
|
2973
|
-
} finally {
|
|
2974
|
-
this.#startRenderInProgress = false;
|
|
2975
|
-
}
|
|
2576
|
+
await this.#loadCameraDependentEffects();
|
|
2577
|
+
await this.#scene.whenReadyAsync();
|
|
2578
|
+
const frames = this.#settings.antiAliasingEnabled || this.#settings.ambientOcclusionEnabled || this.#settings.iblEnabled ? this.#config.render.burstFramesEnhanced : this.#config.render.burstFramesBase;
|
|
2579
|
+
this.#requestRender({ frames: frames, continuousMs: this.#config.render.interactionMs });
|
|
2976
2580
|
}
|
|
2977
2581
|
|
|
2978
2582
|
/**
|
|
@@ -2991,7 +2595,7 @@ export default class BabylonJSController {
|
|
|
2991
2595
|
* 3. Builds the Babylon plugin options so extras are surfaced as metadata, then imports the container with
|
|
2992
2596
|
* `LoadAssetContainerAsync`, returning the tuple so the caller can decide how to attach it to the scene.
|
|
2993
2597
|
*/
|
|
2994
|
-
async #loadAssetContainer(container, force = false
|
|
2598
|
+
async #loadAssetContainer(container, force = false) {
|
|
2995
2599
|
if (container?.state?.update?.storage === undefined || container?.state?.size === undefined || container?.state?.timeStamp === undefined) {
|
|
2996
2600
|
return [container, false];
|
|
2997
2601
|
}
|
|
@@ -3007,30 +2611,12 @@ export default class BabylonJSController {
|
|
|
3007
2611
|
const currentSize = force ? 0 : container.state.size;
|
|
3008
2612
|
const currentTimeStamp = force ? null : container.state.timeStamp;
|
|
3009
2613
|
|
|
3010
|
-
const storageType = container.state.update.storage?.db ? "idb" : container.state.update.storage?.url ? "url" : "?";
|
|
3011
|
-
const storageId = container.state.update.storage?.id ?? container.state.update.storage?.url ?? "?";
|
|
3012
|
-
const tResolve = performance.now();
|
|
3013
2614
|
let sourceData = await this.#gltfResolver.getSource(container.state.update.storage, currentSize, currentTimeStamp);
|
|
3014
|
-
console.log(`[perf][${container.state.name}] getSource(${storageType}:${storageId}) ${Math.round(performance.now() - tResolve)}ms → ${sourceData ? `${sourceData.extension} ${sourceData.objectURLs?.length ?? 0} assets` : "skipped"}`);
|
|
3015
2615
|
|
|
3016
2616
|
if (!sourceData) {
|
|
3017
2617
|
return [container, false];
|
|
3018
2618
|
}
|
|
3019
2619
|
|
|
3020
|
-
// Abort if loadContainerIndependent claimed this container while getSource was in flight.
|
|
3021
|
-
// isIndependent=true means THIS call IS the independent load — don't abort yourself.
|
|
3022
|
-
// Returns null (not false) so #loadContainers skips setSuccess(false) for this container.
|
|
3023
|
-
const isClaimedIndependently =
|
|
3024
|
-
(container.state.name === "model" && this.#isLoadingModelIndependent) ||
|
|
3025
|
-
(container.state.name === "environment" && this.#isLoadingEnvironmentIndependent) ||
|
|
3026
|
-
(container.state.name === "materials" && this.#isLoadingMaterialsIndependent);
|
|
3027
|
-
if (isClaimedIndependently && !isIndependent) {
|
|
3028
|
-
if (sourceData.objectURLs?.length) {
|
|
3029
|
-
this.#gltfResolver.revokeObjectURLs(sourceData.objectURLs);
|
|
3030
|
-
}
|
|
3031
|
-
return [container, null];
|
|
3032
|
-
}
|
|
3033
|
-
|
|
3034
2620
|
container.state.setPendingCacheData(sourceData.size, sourceData.timeStamp, sourceData.metadata);
|
|
3035
2621
|
|
|
3036
2622
|
// https://doc.babylonjs.com/typedoc/interfaces/BABYLON.LoadAssetContainerOptions
|
|
@@ -3049,14 +2635,12 @@ export default class BabylonJSController {
|
|
|
3049
2635
|
};
|
|
3050
2636
|
|
|
3051
2637
|
let assetContainer = null;
|
|
3052
|
-
const t0 = performance.now();
|
|
3053
2638
|
|
|
3054
2639
|
try {
|
|
3055
2640
|
assetContainer = await LoadAssetContainerAsync(sourceData.source, this.#scene, options);
|
|
3056
|
-
const elapsed = Math.round(performance.now() - t0);
|
|
3057
|
-
console.log(`[perf][${container.state.name}] LoadAssetContainerAsync ${elapsed}ms`);
|
|
3058
2641
|
return [container, assetContainer];
|
|
3059
2642
|
} catch (error) {
|
|
2643
|
+
console.error(`[PrefViewer] LoadAssetContainerAsync failed for [${container.state.name}]`, error);
|
|
3060
2644
|
return [container, assetContainer];
|
|
3061
2645
|
} finally {
|
|
3062
2646
|
this.#gltfResolver.revokeObjectURLs(sourceData.objectURLs);
|
|
@@ -3074,29 +2658,7 @@ export default class BabylonJSController {
|
|
|
3074
2658
|
*/
|
|
3075
2659
|
async #loadContainers(force = false) {
|
|
3076
2660
|
this.#detachAnimationChangedListener();
|
|
3077
|
-
this.#
|
|
3078
|
-
|
|
3079
|
-
// When only the model is pending (scene/environment already rendered), skip
|
|
3080
|
-
// unloading camera-dependent effects (DefaultRenderingPipeline, SSAO, shadows).
|
|
3081
|
-
// Disposing those pipelines while the render loop was recently active deletes GL
|
|
3082
|
-
// programs that are still referenced, causing WebGL INVALID_OPERATION errors.
|
|
3083
|
-
const isModelOnlyLoad =
|
|
3084
|
-
this.#containers.model?.state?.isPending === true &&
|
|
3085
|
-
this.#containers.environment?.state?.isPending !== true &&
|
|
3086
|
-
this.#containers.materials?.state?.isPending !== true;
|
|
3087
|
-
|
|
3088
|
-
if (isModelOnlyLoad) {
|
|
3089
|
-
this.#stopEngineRenderLoop();
|
|
3090
|
-
this.#resetRenderState();
|
|
3091
|
-
// Replacing an already-visible model can leave Babylon post-process chains attached to
|
|
3092
|
-
// a camera that is about to be discarded or re-bound. Tear them down here so the next
|
|
3093
|
-
// startRender() rebuilds a clean pipeline instead of tripping over stale null entries.
|
|
3094
|
-
if (this.#containers.model?.assetContainer) {
|
|
3095
|
-
await this.#unloadCameraDependentEffects();
|
|
3096
|
-
}
|
|
3097
|
-
} else {
|
|
3098
|
-
await this.#stopRender();
|
|
3099
|
-
}
|
|
2661
|
+
await this.#stopRender();
|
|
3100
2662
|
|
|
3101
2663
|
let oldModelMetadata = { ...(this.#containers.model?.state?.metadata ?? {}) };
|
|
3102
2664
|
let newModelMetadata = {};
|
|
@@ -3111,12 +2673,8 @@ export default class BabylonJSController {
|
|
|
3111
2673
|
error: null,
|
|
3112
2674
|
};
|
|
3113
2675
|
|
|
3114
|
-
const tSceneStart = performance.now();
|
|
3115
|
-
let tAfterLoad, tAfterMerge;
|
|
3116
|
-
|
|
3117
2676
|
await Promise.allSettled(promiseArray)
|
|
3118
2677
|
.then(async (values) => {
|
|
3119
|
-
tAfterLoad = performance.now();
|
|
3120
2678
|
// Scene may have been disposed (disconnectedCallback) while async loading was in
|
|
3121
2679
|
// progress. Abort cleanly: #replaceContainer already guards the GPU calls, but
|
|
3122
2680
|
// we skip the post-load option/visibility calls too to avoid further null-derefs.
|
|
@@ -3125,12 +2683,6 @@ export default class BabylonJSController {
|
|
|
3125
2683
|
return;
|
|
3126
2684
|
}
|
|
3127
2685
|
this.#disposeAnimationController();
|
|
3128
|
-
this.#emitSceneLoadPhase("scene-load-assets-complete", {
|
|
3129
|
-
path: "full",
|
|
3130
|
-
containerCount: values.filter((result) => result.status === "fulfilled" && result.value?.[1]).length,
|
|
3131
|
-
tAfterLoad: tAfterLoad != null ? Math.round(tAfterLoad) : null,
|
|
3132
|
-
tSceneStart: Math.round(tSceneStart),
|
|
3133
|
-
});
|
|
3134
2686
|
values.forEach((result) => {
|
|
3135
2687
|
const container = result.value ? result.value[0] : null;
|
|
3136
2688
|
const assetContainer = result.value ? result.value[1] : null;
|
|
@@ -3146,29 +2698,17 @@ export default class BabylonJSController {
|
|
|
3146
2698
|
this.#replaceContainer(container, assetContainer);
|
|
3147
2699
|
container.state.setSuccess(true);
|
|
3148
2700
|
} else {
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
if (assetContainer !== null) {
|
|
3152
|
-
if (container.assetContainer && container.state.mustBeShown !== container.state.isVisible && container.state.name === "materials") {
|
|
3153
|
-
container.state.mustBeShown ? this.#addContainer(container) : this.#removeContainer(container);
|
|
3154
|
-
}
|
|
3155
|
-
container.state.setSuccess(false);
|
|
2701
|
+
if (container.assetContainer && container.state.mustBeShown !== container.state.isVisible && container.state.name === "materials") {
|
|
2702
|
+
container.state.mustBeShown ? this.#addContainer(container) : this.#removeContainer(container);
|
|
3156
2703
|
}
|
|
2704
|
+
container.state.setSuccess(false);
|
|
3157
2705
|
}
|
|
3158
2706
|
});
|
|
3159
2707
|
|
|
3160
|
-
this.#setOptions_Materials(
|
|
2708
|
+
this.#setOptions_Materials();
|
|
3161
2709
|
this.#setOptions_Camera();
|
|
3162
2710
|
await this.#setOptions_IBL();
|
|
3163
2711
|
this.#setVisibilityOfWallAndFloorInModel();
|
|
3164
|
-
this.#frameCameraToModel();
|
|
3165
|
-
tAfterMerge = performance.now();
|
|
3166
|
-
this.#emitSceneLoadPhase("scene-load-merge-complete", {
|
|
3167
|
-
path: "full",
|
|
3168
|
-
tAfterMerge: tAfterMerge != null ? Math.round(tAfterMerge) : null,
|
|
3169
|
-
tAfterLoad: tAfterLoad != null ? Math.round(tAfterLoad) : null,
|
|
3170
|
-
tSceneStart: Math.round(tSceneStart),
|
|
3171
|
-
});
|
|
3172
2712
|
detail.success = true;
|
|
3173
2713
|
})
|
|
3174
2714
|
.catch((error) => {
|
|
@@ -3181,9 +2721,7 @@ export default class BabylonJSController {
|
|
|
3181
2721
|
this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
|
|
3182
2722
|
this.#setMaxSimultaneousLights();
|
|
3183
2723
|
this.#enhanceTextureQuality();
|
|
3184
|
-
|
|
3185
|
-
this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
|
|
3186
|
-
}
|
|
2724
|
+
this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
|
|
3187
2725
|
// Apply stored highlight settings so fresh page loads respect persisted state
|
|
3188
2726
|
this.#setHighlightConfig({
|
|
3189
2727
|
enabled: this.#settings.highlightEnabled,
|
|
@@ -3192,24 +2730,7 @@ export default class BabylonJSController {
|
|
|
3192
2730
|
if (this.#babylonJSAnimationController?.hasAnimations?.()) {
|
|
3193
2731
|
this.#attachAnimationChangedListener();
|
|
3194
2732
|
}
|
|
3195
|
-
this.#loadingInProgress = false;
|
|
3196
|
-
// If loadContainerIndependent already merged a container and started the render
|
|
3197
|
-
// loop while this scene load was in flight, #startRender was already called.
|
|
3198
|
-
// Calling it again is safe (pipeline guards skip recreation) but redundant only
|
|
3199
|
-
// when the loop is already running AND scene/shaders are ready; whenReadyAsync
|
|
3200
|
-
// will settle quickly for the newly added containers.
|
|
3201
2733
|
await this.#startRender();
|
|
3202
|
-
this.#frameCameraToModel({ allowWhenEnvironmentVisible: true });
|
|
3203
|
-
this.#emitSceneLoadPhase("scene-load-shaders-complete", {
|
|
3204
|
-
path: "full",
|
|
3205
|
-
tAfterShaders: Math.round(performance.now()),
|
|
3206
|
-
tAfterMerge: tAfterMerge != null ? Math.round(tAfterMerge) : null,
|
|
3207
|
-
});
|
|
3208
|
-
const tLoad = tAfterLoad ? Math.round(tAfterLoad - tSceneStart) : null;
|
|
3209
|
-
const tMerge = tAfterLoad && tAfterMerge ? Math.round(tAfterMerge - tAfterLoad) : null;
|
|
3210
|
-
const tShaders = tAfterMerge ? Math.round(performance.now() - tAfterMerge) : null;
|
|
3211
|
-
const tTotal = Math.round(performance.now() - tSceneStart);
|
|
3212
|
-
console.log(`[perf][scene] load=${tLoad}ms merge=${tMerge}ms shaders=${tShaders}ms total=${tTotal}ms`);
|
|
3213
2734
|
});
|
|
3214
2735
|
return detail;
|
|
3215
2736
|
}
|
|
@@ -3325,7 +2846,7 @@ export default class BabylonJSController {
|
|
|
3325
2846
|
if (!this.#scene) {
|
|
3326
2847
|
return false;
|
|
3327
2848
|
}
|
|
3328
|
-
const node = this.#scene.getNodeByName(name);
|
|
2849
|
+
const node = this.#scene.getNodeByName(name) ?? this.#scene.getNodes().find((n) => n.name.toLowerCase() === name.toLowerCase());
|
|
3329
2850
|
if (!node || !node.position) {
|
|
3330
2851
|
return false;
|
|
3331
2852
|
}
|
|
@@ -3515,7 +3036,6 @@ export default class BabylonJSController {
|
|
|
3515
3036
|
this.#disablingPromises.general = (async () => {
|
|
3516
3037
|
this.#disableInteraction();
|
|
3517
3038
|
this.#disposeAnimationController();
|
|
3518
|
-
this.#finalizePendingModelReplacement();
|
|
3519
3039
|
this.#disposeGLTFResolver();
|
|
3520
3040
|
try {
|
|
3521
3041
|
await this.#disposeXRExperience();
|
|
@@ -3661,306 +3181,6 @@ export default class BabylonJSController {
|
|
|
3661
3181
|
return await this.#loadContainers(force);
|
|
3662
3182
|
}
|
|
3663
3183
|
|
|
3664
|
-
/**
|
|
3665
|
-
* Loads a single named container in the background while the render loop keeps running,
|
|
3666
|
-
* then merges it into the scene atomically (only the merge momentarily pauses the loop).
|
|
3667
|
-
* Camera-dependent effects (post-processing, SSAO, shadows) are preserved across the merge.
|
|
3668
|
-
* If a full #loadContainers is in progress, waits for it to finish before merging.
|
|
3669
|
-
* @public
|
|
3670
|
-
* @param {string} containerName - Name of the container to load ("model", "environment", "materials").
|
|
3671
|
-
* @param {Function} [onReady] - Optional callback invoked after any concurrent scene load finishes
|
|
3672
|
-
* and before the asset is fetched. Use this to mark the container pending (e.g. via
|
|
3673
|
-
* #checkNeedToUpdateContainers) so that the concurrent #loadContainers doesn't consume it first.
|
|
3674
|
-
* @returns {Promise<{success: boolean, error: any, loadedContainers: string[]}>}
|
|
3675
|
-
*/
|
|
3676
|
-
async loadContainerIndependent(containerName, onReady, options = {}) {
|
|
3677
|
-
const loadSessionId = containerName === "model" ? (options?.loadSessionId ?? null) : null;
|
|
3678
|
-
if (containerName === "model" && this.#isLoadingModelIndependent) {
|
|
3679
|
-
return { success: false, error: "Already loading model independently", loadedContainers: [], loadSessionId };
|
|
3680
|
-
}
|
|
3681
|
-
if (containerName === "environment" && this.#isLoadingEnvironmentIndependent) {
|
|
3682
|
-
return { success: false, error: "Already loading environment independently", loadedContainers: [], loadSessionId };
|
|
3683
|
-
}
|
|
3684
|
-
if (containerName === "materials" && this.#isLoadingMaterialsIndependent) {
|
|
3685
|
-
return { success: false, error: "Already loading materials independently", loadedContainers: [], loadSessionId };
|
|
3686
|
-
}
|
|
3687
|
-
|
|
3688
|
-
if (containerName === "model") this.#isLoadingModelIndependent = true;
|
|
3689
|
-
else if (containerName === "environment") this.#isLoadingEnvironmentIndependent = true;
|
|
3690
|
-
else if (containerName === "materials") this.#isLoadingMaterialsIndependent = true;
|
|
3691
|
-
|
|
3692
|
-
// Only reset model-first-painted tracking for model loads — resetting it for
|
|
3693
|
-
// environment/materials would stale any in-flight model-first-painted gate.
|
|
3694
|
-
if (containerName === "model") {
|
|
3695
|
-
this.#modelFirstPaintedEmitted = false;
|
|
3696
|
-
}
|
|
3697
|
-
const modelFirstPaintedLoadToken = containerName === "model"
|
|
3698
|
-
? ++this.#modelFirstPaintedLoadToken
|
|
3699
|
-
: this.#modelFirstPaintedLoadToken;
|
|
3700
|
-
let assetContainer = null;
|
|
3701
|
-
const tIndepStart = performance.now();
|
|
3702
|
-
// Whether the container was already pending inside a running #loadContainers batch.
|
|
3703
|
-
// If so, we fall back to the sequential path (wait → onReady → load) to avoid a
|
|
3704
|
-
// double-load conflict. Otherwise, load the GLTF in parallel with the scene load
|
|
3705
|
-
// and only wait for the atomic merge window.
|
|
3706
|
-
const isAlreadyHandled =
|
|
3707
|
-
this.#containers[containerName]?.state?.isPending === true && this.#loadingInProgress;
|
|
3708
|
-
const path = isAlreadyHandled ? "claimed" : "par";
|
|
3709
|
-
let container;
|
|
3710
|
-
let tAfterLoad, tAfterWait;
|
|
3711
|
-
try {
|
|
3712
|
-
if (!isAlreadyHandled) {
|
|
3713
|
-
// Optimistic path: prepare state and load GLTF NOW, in parallel with any running
|
|
3714
|
-
// #loadContainers. The merge waits for the scene, but the download does not.
|
|
3715
|
-
if (!this.#scene) {
|
|
3716
|
-
return { success: false, error: "No scene available", loadedContainers: [], loadSessionId };
|
|
3717
|
-
}
|
|
3718
|
-
onReady?.();
|
|
3719
|
-
container = this.#containers[containerName];
|
|
3720
|
-
if (!container) {
|
|
3721
|
-
return { success: false, error: `Container "${containerName}" not found`, loadedContainers: [], loadSessionId };
|
|
3722
|
-
}
|
|
3723
|
-
// Phase 1 (parallel): load GLTF while scene may still be loading.
|
|
3724
|
-
// No wait — merge happens immediately after load. #replaceContainer skips
|
|
3725
|
-
// releaseEffects when the loop is running, so no WebGL programs are deleted.
|
|
3726
|
-
const [, loaded] = await this.#loadAssetContainer(container, false, { isIndependent: true });
|
|
3727
|
-
assetContainer = loaded || null;
|
|
3728
|
-
tAfterLoad = performance.now();
|
|
3729
|
-
tAfterWait = tAfterLoad; // no wait phase in optimistic path
|
|
3730
|
-
} else {
|
|
3731
|
-
// Parallel claim path: container was already submitted to a running #loadContainers
|
|
3732
|
-
// batch. #isLoadingModelIndependent (set at the top of this method) signals
|
|
3733
|
-
// #loadAssetContainer to abort the in-flight batch load and return a null sentinel,
|
|
3734
|
-
// so #loadContainers skips merging this container. We load it here in parallel —
|
|
3735
|
-
// no waiting for scene/materials to finish.
|
|
3736
|
-
if (!this.#scene) {
|
|
3737
|
-
return { success: false, error: "No scene available", loadedContainers: [], loadSessionId };
|
|
3738
|
-
}
|
|
3739
|
-
// Call onReady() to ensure the container state carries this load's payload,
|
|
3740
|
-
// which may differ from what #loadContainers was given if the caller swapped the model.
|
|
3741
|
-
onReady?.();
|
|
3742
|
-
container = this.#containers[containerName];
|
|
3743
|
-
if (!container) {
|
|
3744
|
-
return { success: false, error: `Container "${containerName}" not found`, loadedContainers: [], loadSessionId };
|
|
3745
|
-
}
|
|
3746
|
-
const [, loaded] = await this.#loadAssetContainer(container, false, { isIndependent: true });
|
|
3747
|
-
assetContainer = loaded || null;
|
|
3748
|
-
tAfterLoad = performance.now();
|
|
3749
|
-
tAfterWait = tIndepStart; // no wait phase — measure load from start
|
|
3750
|
-
}
|
|
3751
|
-
|
|
3752
|
-
if (!assetContainer) {
|
|
3753
|
-
return { success: false, error: "Failed to load asset container", loadedContainers: [], loadSessionId };
|
|
3754
|
-
}
|
|
3755
|
-
if (!this.#scene) {
|
|
3756
|
-
assetContainer.dispose();
|
|
3757
|
-
return { success: false, error: "Scene was disposed", loadedContainers: [], loadSessionId };
|
|
3758
|
-
}
|
|
3759
|
-
|
|
3760
|
-
this.#emitSceneLoadPhase("scene-load-assets-complete", {
|
|
3761
|
-
containerName,
|
|
3762
|
-
path,
|
|
3763
|
-
tAfterLoad: tAfterLoad != null ? Math.round(tAfterLoad) : null,
|
|
3764
|
-
tIndepStart: Math.round(tIndepStart),
|
|
3765
|
-
});
|
|
3766
|
-
|
|
3767
|
-
// Phase 3: atomic merge — briefly stop the loop for the merge only
|
|
3768
|
-
this.#stopEngineRenderLoop();
|
|
3769
|
-
|
|
3770
|
-
const oldModelMetadata = containerName === "model"
|
|
3771
|
-
? { ...(this.#containers.model?.state?.metadata ?? {}) }
|
|
3772
|
-
: {};
|
|
3773
|
-
|
|
3774
|
-
this.#disposeAnimationController();
|
|
3775
|
-
|
|
3776
|
-
if (containerName === "model") {
|
|
3777
|
-
this.#assetContainer_deleteLights(assetContainer);
|
|
3778
|
-
this.#assetContainer_stopAnimations(assetContainer);
|
|
3779
|
-
}
|
|
3780
|
-
if (containerName === "model" || containerName === "environment") {
|
|
3781
|
-
this.#assetContainer_retagCameras(assetContainer);
|
|
3782
|
-
}
|
|
3783
|
-
|
|
3784
|
-
this.#replaceContainer(container, assetContainer, { releaseEffects: false });
|
|
3785
|
-
container.state.setSuccess(true);
|
|
3786
|
-
|
|
3787
|
-
// Progressive independent loads may bring model/materials/environment in any order.
|
|
3788
|
-
// Force material rebinding after each merge so already-visible model meshes pick up
|
|
3789
|
-
// the latest material definitions instead of waiting for pending flags that may
|
|
3790
|
-
// already have been cleared.
|
|
3791
|
-
this.#setOptions_Materials(true);
|
|
3792
|
-
this.#setOptions_Camera();
|
|
3793
|
-
// For model: IBL not yet loaded (scene still in flight) — create only ambient/directional
|
|
3794
|
-
// lights. #loadContainers will call #createLights() again with full IBL once scene merges.
|
|
3795
|
-
// For environment: IBL state was pre-marked pending by loadSceneIndependent before calling
|
|
3796
|
-
// here, so #setOptions_IBL() picks it up and applies it immediately after lights are ready.
|
|
3797
|
-
await this.#createLights();
|
|
3798
|
-
if (containerName === "environment") {
|
|
3799
|
-
await this.#setOptions_IBL();
|
|
3800
|
-
}
|
|
3801
|
-
|
|
3802
|
-
if (containerName === "model") {
|
|
3803
|
-
const newModelMetadata = { ...(container.state.update?.metadata ?? {}) };
|
|
3804
|
-
this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
|
|
3805
|
-
}
|
|
3806
|
-
|
|
3807
|
-
if (this.#containers.model.assetContainer) {
|
|
3808
|
-
this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
|
|
3809
|
-
}
|
|
3810
|
-
const tAfterMerge = performance.now();
|
|
3811
|
-
this.#emitSceneLoadPhase("scene-load-merge-complete", {
|
|
3812
|
-
containerName,
|
|
3813
|
-
path,
|
|
3814
|
-
tAfterMerge: Math.round(tAfterMerge),
|
|
3815
|
-
tAfterLoad: tAfterLoad != null ? Math.round(tAfterLoad) : null,
|
|
3816
|
-
tIndepStart: Math.round(tIndepStart),
|
|
3817
|
-
});
|
|
3818
|
-
|
|
3819
|
-
// Use #startRender() so the next frame is scheduled after the atomic merge.
|
|
3820
|
-
// In progressive loads, rebuilding camera-dependent effects (DefaultRenderingPipeline,
|
|
3821
|
-
// SSAO, shadows) while the model is already visible can delete GL programs that are
|
|
3822
|
-
// still referenced by the current frame, which leaves the canvas gray until a later
|
|
3823
|
-
// full reload. Keep the existing pipeline alive for independent model/environment/
|
|
3824
|
-
// materials appends; the full load path still rebuilds everything when needed.
|
|
3825
|
-
await this.#startRender({ skipWhenReady: true, forceRender: true, skipCameraDependentEffects: true });
|
|
3826
|
-
const tAfterShaders = performance.now();
|
|
3827
|
-
|
|
3828
|
-
this.#setHighlightConfig({
|
|
3829
|
-
enabled: this.#settings.highlightEnabled,
|
|
3830
|
-
color: this.#settings.highlightColor,
|
|
3831
|
-
});
|
|
3832
|
-
this.#setMaxSimultaneousLights();
|
|
3833
|
-
this.#enhanceTextureQuality();
|
|
3834
|
-
this.#setVisibilityOfWallAndFloorInModel();
|
|
3835
|
-
|
|
3836
|
-
if (this.#babylonJSAnimationController?.hasAnimations?.()) {
|
|
3837
|
-
this.#attachAnimationChangedListener();
|
|
3838
|
-
}
|
|
3839
|
-
|
|
3840
|
-
this.#requestRender({ frames: 3, continuousMs: this.#config.render.interactionMs });
|
|
3841
|
-
// Materials: shaders assigned by #setOptions_Materials need compilation. Start
|
|
3842
|
-
// rendering immediately (above) so the loop runs; then when all shaders are ready,
|
|
3843
|
-
// fire additional frames so the compiled textures actually appear without requiring
|
|
3844
|
-
// a camera interaction to trigger them.
|
|
3845
|
-
if (containerName === "materials" && this.#scene) {
|
|
3846
|
-
this.#scene.whenReadyAsync().then(() => {
|
|
3847
|
-
this.#requestRender({ frames: 3, force: true });
|
|
3848
|
-
}).catch(() => { /* scene disposed — ignore */ });
|
|
3849
|
-
}
|
|
3850
|
-
if (containerName === "model" && !this.#modelFirstPaintedEmitted) {
|
|
3851
|
-
try {
|
|
3852
|
-
performance.mark(`viewer:model-first-painted:scheduled:${modelFirstPaintedLoadToken}`);
|
|
3853
|
-
} catch {
|
|
3854
|
-
// ignore
|
|
3855
|
-
}
|
|
3856
|
-
console.log("[viewer] model-first-painted gate scheduled", {
|
|
3857
|
-
containerName,
|
|
3858
|
-
path: isAlreadyHandled ? "claimed" : "par",
|
|
3859
|
-
loadToken: modelFirstPaintedLoadToken,
|
|
3860
|
-
renderLoopRunning: this.#state.render.isLoopRunning,
|
|
3861
|
-
loopRequested: true,
|
|
3862
|
-
});
|
|
3863
|
-
void (async () => {
|
|
3864
|
-
await this.#waitForPaint(2);
|
|
3865
|
-
try {
|
|
3866
|
-
performance.mark(`viewer:model-first-painted:after-wait:${modelFirstPaintedLoadToken}`);
|
|
3867
|
-
} catch {
|
|
3868
|
-
// ignore
|
|
3869
|
-
}
|
|
3870
|
-
console.log("[viewer] model-first-painted gate after paint wait", {
|
|
3871
|
-
containerName,
|
|
3872
|
-
path: isAlreadyHandled ? "claimed" : "par",
|
|
3873
|
-
loadToken: modelFirstPaintedLoadToken,
|
|
3874
|
-
currentToken: this.#modelFirstPaintedLoadToken,
|
|
3875
|
-
alreadyEmitted: this.#modelFirstPaintedEmitted,
|
|
3876
|
-
renderLoopRunning: this.#state.render.isLoopRunning,
|
|
3877
|
-
});
|
|
3878
|
-
if (modelFirstPaintedLoadToken !== this.#modelFirstPaintedLoadToken) {
|
|
3879
|
-
try {
|
|
3880
|
-
performance.mark(`viewer:model-first-painted:stale:${modelFirstPaintedLoadToken}`);
|
|
3881
|
-
} catch {
|
|
3882
|
-
// ignore
|
|
3883
|
-
}
|
|
3884
|
-
console.log("[viewer] model-first-painted gate skipped (stale load token)", {
|
|
3885
|
-
containerName,
|
|
3886
|
-
path: isAlreadyHandled ? "claimed" : "par",
|
|
3887
|
-
loadToken: modelFirstPaintedLoadToken,
|
|
3888
|
-
currentToken: this.#modelFirstPaintedLoadToken,
|
|
3889
|
-
});
|
|
3890
|
-
return;
|
|
3891
|
-
}
|
|
3892
|
-
const paintTarget = this.#prefViewer || this.#prefViewer3D || this.#canvas;
|
|
3893
|
-
if (!this.#modelFirstPaintedEmitted && paintTarget) {
|
|
3894
|
-
this.#modelFirstPaintedEmitted = true;
|
|
3895
|
-
try {
|
|
3896
|
-
performance.mark(`viewer:model-first-painted:emitted:${modelFirstPaintedLoadToken}`);
|
|
3897
|
-
} catch {
|
|
3898
|
-
// ignore
|
|
3899
|
-
}
|
|
3900
|
-
console.log("[viewer] model-first-painted ✅ emitted", {
|
|
3901
|
-
containerName,
|
|
3902
|
-
path: isAlreadyHandled ? "claimed" : "par",
|
|
3903
|
-
loadToken: modelFirstPaintedLoadToken,
|
|
3904
|
-
tAfterPaint: Math.round(performance.now()),
|
|
3905
|
-
});
|
|
3906
|
-
this.#emitModelFirstPainted({
|
|
3907
|
-
containerName,
|
|
3908
|
-
path: isAlreadyHandled ? "claimed" : "par",
|
|
3909
|
-
tIndepStart: Math.round(tIndepStart),
|
|
3910
|
-
tAfterLoad: tAfterLoad != null ? Math.round(tAfterLoad) : null,
|
|
3911
|
-
tAfterWait: tAfterWait != null ? Math.round(tAfterWait) : null,
|
|
3912
|
-
tAfterMerge: Math.round(tAfterMerge),
|
|
3913
|
-
tAfterPaint: Math.round(performance.now()),
|
|
3914
|
-
});
|
|
3915
|
-
this.#finalizePendingModelReplacement();
|
|
3916
|
-
}
|
|
3917
|
-
})();
|
|
3918
|
-
}
|
|
3919
|
-
|
|
3920
|
-
// In the optimistic path: load runs first, then wait. In the sequential path: wait
|
|
3921
|
-
// runs first, then load. Compute each phase relative to its actual start point.
|
|
3922
|
-
const tLoad = isAlreadyHandled
|
|
3923
|
-
? Math.round(tAfterLoad - tAfterWait)
|
|
3924
|
-
: Math.round(tAfterLoad - tIndepStart);
|
|
3925
|
-
const tWait = isAlreadyHandled
|
|
3926
|
-
? Math.round(tAfterWait - tIndepStart)
|
|
3927
|
-
: Math.round(tAfterWait - tAfterLoad);
|
|
3928
|
-
// Merge starts after the wait in both paths (optimistic: load→wait→merge; sequential: wait→load→merge)
|
|
3929
|
-
const tMerge = Math.round(tAfterMerge - (isAlreadyHandled ? tAfterLoad : tAfterWait));
|
|
3930
|
-
const tShaders = Math.round(tAfterShaders - tAfterMerge);
|
|
3931
|
-
const tTotal = Math.round(tAfterShaders - tIndepStart);
|
|
3932
|
-
this.#emitSceneLoadPhase("scene-load-shaders-complete", {
|
|
3933
|
-
containerName,
|
|
3934
|
-
path,
|
|
3935
|
-
tAfterShaders: Math.round(tAfterShaders),
|
|
3936
|
-
tAfterMerge: Math.round(tAfterMerge),
|
|
3937
|
-
tTotal,
|
|
3938
|
-
});
|
|
3939
|
-
this.#emitSceneLoadPhase("scene-load-container-complete", {
|
|
3940
|
-
containerName,
|
|
3941
|
-
path,
|
|
3942
|
-
tLoad,
|
|
3943
|
-
tWait,
|
|
3944
|
-
tMerge,
|
|
3945
|
-
tShaders,
|
|
3946
|
-
tTotal,
|
|
3947
|
-
});
|
|
3948
|
-
console.log(`[perf][${containerName}:indep:${path}] load=${tLoad}ms wait=${tWait}ms merge=${tMerge}ms shaders=${tShaders}ms total=${tTotal}ms`);
|
|
3949
|
-
|
|
3950
|
-
return { success: true, error: null, loadedContainers: [containerName], loadSessionId };
|
|
3951
|
-
} catch (error) {
|
|
3952
|
-
assetContainer?.dispose();
|
|
3953
|
-
if (!this.#state.render.isLoopRunning && this.#scene && this.#engine) {
|
|
3954
|
-
this.#startEngineRenderLoop();
|
|
3955
|
-
}
|
|
3956
|
-
return { success: false, error, loadedContainers: [], loadSessionId };
|
|
3957
|
-
} finally {
|
|
3958
|
-
if (containerName === "model") this.#isLoadingModelIndependent = false;
|
|
3959
|
-
else if (containerName === "environment") this.#isLoadingEnvironmentIndependent = false;
|
|
3960
|
-
else if (containerName === "materials") this.#isLoadingMaterialsIndependent = false;
|
|
3961
|
-
}
|
|
3962
|
-
}
|
|
3963
|
-
|
|
3964
3184
|
/**
|
|
3965
3185
|
* Applies camera options from the configuration to the active camera.
|
|
3966
3186
|
* Stops and restarts the render loop to apply changes.
|
|
@@ -4164,5 +3384,4 @@ export default class BabylonJSController {
|
|
|
4164
3384
|
this.#requestRender({ frames: 2 });
|
|
4165
3385
|
}
|
|
4166
3386
|
}
|
|
4167
|
-
}
|
|
4168
|
-
|
|
3387
|
+
}
|