@preference-sl/pref-viewer 2.13.0-beta.11 → 2.13.0-beta.12

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.13.0-beta.11",
3
+ "version": "2.13.0-beta.12",
4
4
  "description": "Web Component to preview GLTF models with Babylon.js",
5
5
  "author": "Alex Moreno Palacio <amoreno@preference.es>",
6
6
  "scripts": {
@@ -39,7 +39,8 @@ import { translate } from "./localization/i18n.js";
39
39
  * 5. Use `setContainerVisibility`, `setMaterialOptions`, `setCameraOptions`, or `setIBLOptions` for targeted updates; these
40
40
  * helpers stop/restart the render loop while they rebuild camera-dependent resources.
41
41
  * 6. Invoke `disable()` when the element disconnects to tear down scenes, XR sessions, observers, and handlers.
42
- * The controller also disposes the shared GLTF resolver, which closes its internal IndexedDB handle.
42
+ * `disable()` is asynchronous; it waits for XR/session shutdown before engine disposal and also disposes the
43
+ * shared GLTF resolver, which closes its internal IndexedDB handle.
43
44
  *
44
45
  * Public API Highlights
45
46
  * - constructor(canvas, containers, options)
@@ -65,6 +66,8 @@ import { translate } from "./localization/i18n.js";
65
66
  * in SSR/Node contexts (though functionality activates only in browsers).
66
67
  * - Pointer-pick lifecycle: hover/highlight raycasts are sampled on POINTERMOVE (time + distance thresholds), wheel
67
68
  * input avoids picks entirely, and right-click POINTERUP performs an on-demand pick for context-menu targeting.
69
+ * - Teardown lifecycle: concurrent `disable()` calls are coalesced into a single in-flight promise to avoid races
70
+ * during XR exit and engine disposal.
68
71
  */
69
72
  export default class BabylonJSController {
70
73
  #RENDER_SETTINGS_STORAGE_KEY = "pref-viewer/render-settings";
@@ -96,7 +99,7 @@ export default class BabylonJSController {
96
99
  #shadowGen = [];
97
100
  #XRExperience = null;
98
101
  #canvasResizeObserver = null;
99
-
102
+
100
103
  #hdrTexture = null; // reusable in-memory HDR source cloned into scene.environmentTexture across reloads
101
104
  #lastPickedMeshId = null;
102
105
 
@@ -148,6 +151,12 @@ export default class BabylonJSController {
148
151
  movePickMinDistancePx: 2, // skip picks for sub-pixel jitter
149
152
  };
150
153
 
154
+ // Promises to track async disable() lifecycle when XR and general teardown may run concurrently; ensures idempotent disable calls are safe and callers can await full teardown completion.
155
+ #disablingPromises = {
156
+ xr: null,
157
+ general: null,
158
+ };
159
+
151
160
  /**
152
161
  * Constructs a new BabylonJSController instance.
153
162
  * Initializes the canvas, asset containers, and options for the Babylon.js scene.
@@ -1308,30 +1317,43 @@ export default class BabylonJSController {
1308
1317
 
1309
1318
  /**
1310
1319
  * Disposes the Babylon.js WebXR experience if it exists.
1320
+ * If XR is currently active, waits for `exitXRAsync()` before disposing to avoid
1321
+ * tearing down the engine while the XR session is still shutting down.
1322
+ * Concurrent calls share the same in-flight promise so disposal runs only once.
1311
1323
  * @private
1312
- * @returns {void}
1324
+ * @returns {Promise<void>}
1313
1325
  */
1314
- #disposeXRExperience() {
1315
- if (!this.#XRExperience) {
1326
+ async #disposeXRExperience() {
1327
+ if (this.#disablingPromises.xr) {
1328
+ return await this.#disablingPromises.xr;
1329
+ }
1330
+
1331
+ const xrExperience = this.#XRExperience;
1332
+ if (!xrExperience) {
1316
1333
  return;
1317
1334
  }
1318
1335
 
1319
- if (this.#XRExperience.baseExperience.state === WebXRState.IN_XR) {
1320
- this.#XRExperience.baseExperience
1321
- .exitXRAsync()
1322
- .then(() => {
1323
- this.#XRExperience.dispose();
1324
- this.#XRExperience = null;
1325
- })
1326
- .catch((error) => {
1327
- console.warn("Error exiting XR experience:", error);
1328
- this.#XRExperience.dispose();
1336
+ this.#disablingPromises.xr = (async () => {
1337
+ try {
1338
+ if (xrExperience.baseExperience?.state === WebXRState.IN_XR) {
1339
+ await xrExperience.baseExperience.exitXRAsync();
1340
+ }
1341
+ } catch (error) {
1342
+ console.warn("PrefViewer: Error exiting XR experience:", error);
1343
+ } finally {
1344
+ try {
1345
+ xrExperience.dispose();
1346
+ } catch (error) {
1347
+ console.warn("PrefViewer: Error disposing XR experience:", error);
1348
+ }
1349
+ if (this.#XRExperience === xrExperience) {
1329
1350
  this.#XRExperience = null;
1330
- });
1331
- } else {
1332
- this.#XRExperience.dispose();
1333
- this.#XRExperience = null;
1334
- }
1351
+ }
1352
+ this.#disablingPromises.xr = null;
1353
+ }
1354
+ })();
1355
+
1356
+ await this.#disablingPromises.xr;
1335
1357
  }
1336
1358
 
1337
1359
  /**
@@ -2345,17 +2367,40 @@ export default class BabylonJSController {
2345
2367
  /**
2346
2368
  * Disposes the Babylon.js engine and disconnects the canvas resize observer.
2347
2369
  * Cleans up all scene, camera, light, XR, and GLTF resolver resources.
2370
+ * The teardown is asynchronous: it waits for XR/session-dependent shutdown work
2371
+ * before disposing the engine, and coalesces concurrent calls into one in-flight promise.
2348
2372
  * @public
2349
- * @returns {void}
2373
+ * @returns {Promise<void>}
2350
2374
  */
2351
- disable() {
2352
- this.#disableInteraction();
2353
- this.#disposeAnimationController();
2354
- this.#disposeGLTFResolver();
2355
- this.#disposeXRExperience();
2356
- this.#unloadCameraDependentEffects();
2357
- this.#stopEngineRenderLoop();
2358
- this.#disposeEngine();
2375
+ async disable() {
2376
+ if (this.#disablingPromises.general) {
2377
+ return await this.#disablingPromises.general;
2378
+ }
2379
+
2380
+ this.#disablingPromises.general = (async () => {
2381
+ this.#disableInteraction();
2382
+ this.#disposeAnimationController();
2383
+ this.#disposeGLTFResolver();
2384
+ try {
2385
+ await this.#disposeXRExperience();
2386
+ } catch (error) {
2387
+ console.warn("PrefViewer: Error while disposing XR experience:", error);
2388
+ }
2389
+ try {
2390
+ await this.#unloadCameraDependentEffects();
2391
+ } catch (error) {
2392
+ console.warn("PrefViewer: Error while unloading camera-dependent effects:", error);
2393
+ } finally {
2394
+ this.#stopEngineRenderLoop();
2395
+ this.#disposeEngine();
2396
+ }
2397
+ })();
2398
+
2399
+ try {
2400
+ await this.#disablingPromises.general;
2401
+ } finally {
2402
+ this.#disablingPromises.general = null;
2403
+ }
2359
2404
  }
2360
2405
 
2361
2406
  /**
@@ -40,7 +40,7 @@ export const setLocale = (localeId) => {
40
40
  try {
41
41
  listener(currentLocale);
42
42
  } catch (error) {
43
- console.warn("PrefViewer i18n listener failed", error);
43
+ console.warn("PrefViewer: i18n listener failed", error);
44
44
  }
45
45
  });
46
46
  return currentLocale;
@@ -162,7 +162,7 @@ export default class PrefViewer3D extends HTMLElement {
162
162
  */
163
163
  disconnectedCallback() {
164
164
  if (this.#babylonJSController) {
165
- this.#babylonJSController.disable();
165
+ void this.#babylonJSController.disable();
166
166
  }
167
167
  }
168
168