@preference-sl/pref-viewer 2.14.0-beta.3 → 2.14.0-beta.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preference-sl/pref-viewer",
3
- "version": "2.14.0-beta.3",
3
+ "version": "2.14.0-beta.5",
4
4
  "description": "Web Component to preview GLTF models with Babylon.js",
5
5
  "author": "Alex Moreno Palacio <amoreno@preference.es>",
6
6
  "scripts": {
@@ -114,6 +114,7 @@ export default class BabylonJSController {
114
114
  #options = {};
115
115
 
116
116
  #gltfResolver = null; // GLTFResolver instance
117
+ #loadGeneration = 0; // incremented per #loadContainers call to discard stale deferred results
117
118
  #babylonJSAnimationController = null; // AnimationController instance
118
119
 
119
120
  #renderPipelines = {
@@ -2400,23 +2401,51 @@ export default class BabylonJSController {
2400
2401
 
2401
2402
  /**
2402
2403
  * Loads all asset containers (model, environment, materials, etc.) and adds them to the scene.
2403
- * @private
2404
- * @returns {Promise<{success: boolean, error: any}>} Resolves to an object indicating if loading succeeded and any error encountered.
2405
- * @description
2406
- * Waits for all containers to load in parallel, then replaces or adds them to the scene as needed.
2407
- * Applies material and camera options, sets wall/floor visibility, and initializes lights and shadows.
2408
- * Returns an object with success status and error details.
2404
+ *
2405
+ * Uses **two-phase progressive loading** when the environment container is pending:
2406
+ * Phase 1 – Load model + materials, apply them, and start the render loop so the user sees
2407
+ * the configured product immediately.
2408
+ * Phase 2 The environment (scene with trees, walls, floor…) was already downloading in
2409
+ * parallel. Once it resolves, briefly stop rendering, splice it in, reapply
2410
+ * camera/IBL/visibility options, and restart.
2411
+ *
2412
+ * When the environment is *not* pending (e.g. only the model changed), the original
2413
+ * single-phase behaviour is preserved — all containers settle immediately and are applied
2414
+ * together.
2415
+ *
2416
+ * A generation counter (`#loadGeneration`) guards Phase 2: if a newer `load()` call starts
2417
+ * while the environment is still downloading, the stale result is disposed instead of applied.
2418
+ *
2419
+ * @private
2420
+ * @param {boolean} [force=false] - Bypass cached size/timestamp checks.
2421
+ * @returns {Promise<{success: boolean, error: any}>}
2409
2422
  */
2410
2423
  async #loadContainers(force = false) {
2424
+ const generation = ++this.#loadGeneration;
2411
2425
  this.#detachAnimationChangedListener();
2412
2426
  await this.#stopRender();
2413
2427
 
2414
2428
  let oldModelMetadata = { ...(this.#containers.model?.state?.metadata ?? {}) };
2415
2429
  let newModelMetadata = {};
2416
2430
 
2417
- const promiseArray = [];
2431
+ // Kick off ALL container loads in parallel — the heavy environment download starts now.
2432
+ const allLoadPromises = new Map();
2418
2433
  Object.values(this.#containers).forEach((container) => {
2419
- promiseArray.push(this.#loadAssetContainer(container, force));
2434
+ allLoadPromises.set(container.state.name, this.#loadAssetContainer(container, force));
2435
+ });
2436
+
2437
+ // When the environment is pending (heavy scene geometry) we defer it to Phase 2 so
2438
+ // the model can render first. Non-pending environments resolve immediately with
2439
+ // [container, false] and are handled in Phase 1 like any other container.
2440
+ const environmentPending = this.#containers.environment?.state?.isPending === true;
2441
+ const priorityPromises = [];
2442
+ let deferredPromise = null;
2443
+ allLoadPromises.forEach((promise, name) => {
2444
+ if (name === "environment" && environmentPending) {
2445
+ deferredPromise = promise;
2446
+ } else {
2447
+ priorityPromises.push(promise);
2448
+ }
2420
2449
  });
2421
2450
 
2422
2451
  let detail = {
@@ -2424,13 +2453,18 @@ export default class BabylonJSController {
2424
2453
  error: null,
2425
2454
  };
2426
2455
 
2427
- await Promise.allSettled(promiseArray)
2456
+ // ── Phase 1: Priority containers (model + materials) ────────────────────
2457
+ await Promise.allSettled(priorityPromises)
2428
2458
  .then(async (values) => {
2429
2459
  // Scene may have been disposed (disconnectedCallback) while async loading was in
2430
2460
  // progress. Abort cleanly: #replaceContainer already guards the GPU calls, but
2431
2461
  // we skip the post-load option/visibility calls too to avoid further null-derefs.
2432
2462
  if (!this.#scene) {
2433
2463
  values.forEach((result) => { result.value?.[1]?.dispose(); });
2464
+ if (deferredPromise) {
2465
+ deferredPromise.then((r) => { r?.[1]?.dispose?.(); }).catch(() => {});
2466
+ deferredPromise = null;
2467
+ }
2434
2468
  return;
2435
2469
  }
2436
2470
  this.#disposeAnimationController();
@@ -2469,7 +2503,10 @@ export default class BabylonJSController {
2469
2503
  detail.error = error;
2470
2504
  })
2471
2505
  .finally(async () => {
2472
- this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
2506
+ if (!deferredPromise) {
2507
+ // No deferred work — single-phase path (original behaviour).
2508
+ this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
2509
+ }
2473
2510
  this.#setMaxSimultaneousLights();
2474
2511
  this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
2475
2512
  // Apply stored highlight settings so fresh page loads respect persisted state
@@ -2482,6 +2519,48 @@ export default class BabylonJSController {
2482
2519
  }
2483
2520
  await this.#startRender();
2484
2521
  });
2522
+
2523
+ // ── Phase 2: Deferred environment (already downloading in parallel) ─────
2524
+ if (deferredPromise) {
2525
+ let deferredResult;
2526
+ try {
2527
+ deferredResult = await deferredPromise;
2528
+ } catch (error) {
2529
+ console.error("PrefViewer: failed to load environment progressively", error);
2530
+ deferredResult = [this.#containers.environment, null];
2531
+ }
2532
+
2533
+ // A newer load() was triggered while we waited — discard stale results.
2534
+ if (this.#loadGeneration !== generation) {
2535
+ deferredResult?.[1]?.dispose?.();
2536
+ return detail;
2537
+ }
2538
+
2539
+ if (this.#scene) {
2540
+ const [container, assetContainer] = deferredResult;
2541
+ if (assetContainer) {
2542
+ this.#detachAnimationChangedListener();
2543
+ await this.#stopRender();
2544
+ this.#assetContainer_retagCameras(assetContainer);
2545
+ this.#replaceContainer(container, assetContainer);
2546
+ container.state.setSuccess(true);
2547
+ this.#setOptions_Camera();
2548
+ await this.#setOptions_IBL();
2549
+ this.#setVisibilityOfWallAndFloorInModel();
2550
+ this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
2551
+ this.#setMaxSimultaneousLights();
2552
+ this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
2553
+ if (this.#babylonJSAnimationController?.hasAnimations?.()) {
2554
+ this.#attachAnimationChangedListener();
2555
+ }
2556
+ await this.#startRender();
2557
+ } else {
2558
+ container.state.setSuccess(false);
2559
+ this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
2560
+ }
2561
+ }
2562
+ }
2563
+
2485
2564
  return detail;
2486
2565
  }
2487
2566
 
@@ -302,14 +302,22 @@ export default class PrefViewer extends HTMLElement {
302
302
  if (this.#menu3D) {
303
303
  this.#menu3D.removeEventListener("pref-viewer-menu-3d-apply", this.#handlers.onMenuApply);
304
304
  this.#menu3D.remove();
305
+ this.#menu3D = null;
305
306
  }
306
-
307
+ if (this.#handlers.onViewerHoverStart) {
308
+ this.#wrapper.removeEventListener("mouseenter", this.#handlers.onViewerHoverStart);
309
+ this.#handlers.onViewerHoverStart = null;
310
+ }
311
+ if (this.#handlers.onViewerHoverEnd) {
312
+ this.#wrapper.removeEventListener("mouseleave", this.#handlers.onViewerHoverEnd);
313
+ this.#handlers.onViewerHoverEnd = null;
314
+ }
315
+
307
316
  // Check if menu should be shown (default: true if not specified)
308
317
  const showMenuAttr = this.getAttribute("show-menu");
309
318
  const showMenu = showMenuAttr === null || showMenuAttr === "true";
310
-
319
+
311
320
  if (!showMenu) {
312
- this.#menu3D = null;
313
321
  return;
314
322
  }
315
323