@preference-sl/pref-viewer 2.18.0-beta.1 → 3.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +267 -31
  2. package/index.d.ts +75 -6
  3. package/package.json +8 -2
  4. package/src/babylonjs-animation-controller.js +18 -10
  5. package/src/babylonjs-animation-opening-menu.js +80 -14
  6. package/src/babylonjs-animation-opening.js +3 -3
  7. package/src/babylonjs-ar-floor-bounds.js +169 -0
  8. package/src/babylonjs-ar-glass.js +128 -0
  9. package/src/babylonjs-ar-placement-feedback.js +449 -0
  10. package/src/babylonjs-ar-placement.js +1038 -0
  11. package/src/babylonjs-camera-wheel-motion.js +35 -2
  12. package/src/babylonjs-controller.js +507 -125
  13. package/src/babylonjs-orbital-pointers-input.js +186 -0
  14. package/src/{render-math.js → babylonjs-render-math.js} +11 -11
  15. package/src/babylonjs-selection-controller.js +4 -4
  16. package/src/babylonjs-view-cube-canvas-controller.js +1287 -0
  17. package/src/babylonjs-view-cube-canvas-orbit-math.js +220 -0
  18. package/src/babylonjs-view-cube-canvas-renderer.js +775 -0
  19. package/src/babylonjs-view-cube-controller.js +74 -1103
  20. package/src/babylonjs-view-cube-svg-controller.js +1012 -0
  21. package/src/file-storage.js +7 -4
  22. package/src/gltf-resolver.js +13 -8
  23. package/src/localization/translations.js +12 -0
  24. package/src/panzoom-controller.js +4 -4
  25. package/src/pref-viewer-2d.js +2 -2
  26. package/src/pref-viewer-3d.js +55 -9
  27. package/src/pref-viewer-controls-presentation.js +292 -0
  28. package/src/pref-viewer-dialog.js +86 -7
  29. package/src/pref-viewer-menu-3d.js +134 -17
  30. package/src/pref-viewer-style-controller.js +320 -0
  31. package/src/pref-viewer.js +133 -109
  32. package/src/quick-look-action.js +280 -0
  33. package/src/styles.js +702 -98
  34. package/src/babylonjs-view-cube-renderer.js +0 -548
  35. package/src/view-cube-orbit-math.js +0 -199
@@ -9,6 +9,7 @@ import { openDB } from "idb";
9
9
  * - Falls back to direct server access when IndexedDB is unavailable.
10
10
  * - Supports file blob retrieval, timestamp checking, and size queries.
11
11
  * - Uses the idb library for simplified IndexedDB operations.
12
+ * - Observes cache-write request and transaction completion together so simultaneous failures are handled.
12
13
  *
13
14
  * Dependencies:
14
15
  * - idb (IndexedDB wrapper library)
@@ -423,6 +424,7 @@ export default class FileStorage {
423
424
 
424
425
  /**
425
426
  * Updates `lastAccess` for a record to keep LRU ordering current.
427
+ * Observes the write and transaction together; failed touches are caught without leaking an abort rejection.
426
428
  * @private
427
429
  * @param {String} uri Record key.
428
430
  * @param {Object} record Existing normalized record.
@@ -443,8 +445,7 @@ export default class FileStorage {
443
445
  try {
444
446
  const transaction = this.#db.transaction(this.osName, "readwrite");
445
447
  const store = transaction.objectStore(this.osName);
446
- store.put(normalized, uri);
447
- await transaction.done;
448
+ await Promise.all([store.put(normalized, uri), transaction.done]);
448
449
  } catch (error) {}
449
450
  }
450
451
 
@@ -596,6 +597,7 @@ export default class FileStorage {
596
597
  * @description
597
598
  * Runs maintenance before write (TTL, max entries, and quota-aware cleanup), then retries quota errors
598
599
  * by evicting least-recently-used entries.
600
+ * Observes each write request and transaction completion together so simultaneous WebKit failures are handled.
599
601
  */
600
602
  async #putFile(file, uri) {
601
603
  if (this.#db === undefined) {
@@ -619,8 +621,9 @@ export default class FileStorage {
619
621
  try {
620
622
  const transaction = this.#db.transaction(this.osName, "readwrite");
621
623
  const store = transaction.objectStore(this.osName);
622
- const result = await store.put(record, uri);
623
- await transaction.done;
624
+ // Observe both promises immediately: WebKit can reject the write and abort
625
+ // its transaction together. Awaiting them sequentially leaks a rejection.
626
+ const [result] = await Promise.all([store.put(record, uri), transaction.done]);
624
627
  return result;
625
628
  } catch (error) {
626
629
  if (!this.#isQuotaExceededError(error) || attempt === this.#maxQuotaEvictionRetries) {
@@ -9,6 +9,7 @@ import { closeDb, initDb, loadModel } from "./gltf-storage.js";
9
9
  * - Decodes base64 data and determines file type (.gltf or .glb).
10
10
  * - Normalizes and replaces asset URIs with resolved URLs from FileStorage.
11
11
  * - Handles cache validation using asset size and timestamp to avoid unnecessary reloads.
12
+ * Callers may opt into an explicit unchanged marker to distinguish cache hits from unavailable content.
12
13
  * - Tracks generated object URLs so callers can revoke them after Babylon finishes loading.
13
14
  * - Provides methods for initializing storage, decoding assets, and preparing sources for viewer components.
14
15
  *
@@ -17,7 +18,7 @@ import { closeDb, initDb, loadModel } from "./gltf-storage.js";
17
18
  * - Prepare asset: await resolver.getSource(storage, currentSize, currentTimeStamp);
18
19
  *
19
20
  * Public Methods:
20
- * - getSource(storage, currentSize, currentTimeStamp): Resolves and prepares a glTF/GLB source for loading.
21
+ * - getSource(storage, currentSize, currentTimeStamp, reportUnchanged?): Prepares a source or reports a cache hit.
21
22
  * - revokeObjectURLs(objectURLs): Releases temporary blob URLs generated during source resolution.
22
23
  * - dispose(): Releases resolver resources and closes the internal FileStorage DB handle.
23
24
  *
@@ -220,7 +221,7 @@ export default class GLTFResolver {
220
221
  /**
221
222
  * Revoke browser object URLs and clear the provided list.
222
223
  * @public
223
- * @param {string[]} objectURLs - URLs previously created via URL.createObjectURL.
224
+ * @param {string[]} [objectURLs=[]] - URLs previously created via URL.createObjectURL.
224
225
  * @returns {void}
225
226
  */
226
227
  revokeObjectURLs(objectURLs = []) {
@@ -251,17 +252,21 @@ export default class GLTFResolver {
251
252
  * @param {object} storage - Storage descriptor containing url, db, table, and id properties.
252
253
  * @param {number|null} currentSize - The current cached size of the asset, or null if not cached.
253
254
  * @param {string|null} currentTimeStamp - The current cached timestamp of the asset, or null if not cached.
254
- * @returns {Promise<false|{source: File|string, size: number, timeStamp: string|null, extension: string, metadata: object, objectURLs: string[]}>}
255
- * - Resolves to false if no update is needed or on error.
255
+ * @param {boolean} [reportUnchanged=false] - Return an explicit unchanged marker for cache hits.
256
+ * @returns {Promise<false|{unchanged:true}|{source: File|string, size: number, timeStamp: string|null, extension: string, metadata: object, objectURLs: string[]}>}
257
+ * - Resolves to false for missing data or an unchanged asset when reportUnchanged is false.
258
+ * - Resolves to {unchanged: true} for a cache hit when reportUnchanged is true.
256
259
  * - Resolves to an object containing source, cache metadata, and tracked object URLs if an update is required.
260
+ * @throws {Error} Rejects when storage access, source retrieval or glTF JSON decoding fails.
257
261
  * @description
258
262
  * - If storage specifies IndexedDB (db, table, id), loads the asset from IndexedDB and checks for updates.
259
263
  * - If storage specifies a direct URL or base64 data, decodes and validates the asset.
260
264
  * - For .gltf files, replaces internal URIs with resolved URLs from FileStorage.
261
265
  * - Performs cache validation using size and timestamp to avoid unnecessary reloads.
262
266
  * - Returns the prepared source, size, timestamp, extension, metadata, and objectURLs for further processing.
267
+ * - The caller owns revoking returned objectURLs after the loader finishes consuming them.
263
268
  */
264
- async getSource(storage, currentSize, currentTimeStamp) {
269
+ async getSource(storage, currentSize, currentTimeStamp, reportUnchanged = false) {
265
270
  let source = storage.url || null;
266
271
  let newSize, newTimeStamp;
267
272
  let pending = false;
@@ -277,7 +282,7 @@ export default class GLTFResolver {
277
282
  source = object.data;
278
283
  metadata = { ...(object.metadata ?? {}) };
279
284
  if (object.timeStamp === currentTimeStamp) {
280
- return false;
285
+ return reportUnchanged ? { unchanged: true } : false;
281
286
  } else {
282
287
  pending = true;
283
288
  newSize = object.size;
@@ -304,7 +309,7 @@ export default class GLTFResolver {
304
309
  }
305
310
  if (!pending) {
306
311
  if (currentTimeStamp === null && currentSize === size) {
307
- return false;
312
+ return reportUnchanged ? { unchanged: true } : false;
308
313
  } else {
309
314
  pending = true;
310
315
  newSize = size;
@@ -316,7 +321,7 @@ export default class GLTFResolver {
316
321
  extension = extMatch ? `.${extMatch[1].toLowerCase()}` : ".gltf";
317
322
  let [fileSize, fileTimeStamp] = [await this.#fileStorage.getSize(source), await this.#fileStorage.getTimeStamp(source)];
318
323
  if (currentSize === fileSize && currentTimeStamp === fileTimeStamp) {
319
- return false;
324
+ return reportUnchanged ? { unchanged: true } : false;
320
325
  } else {
321
326
  pending = true;
322
327
  newSize = fileSize;
@@ -1,6 +1,11 @@
1
1
  export const translations = {
2
2
  "en-EN": {
3
3
  prefViewer: {
4
+ ar: {
5
+ open: "View in AR",
6
+ preparing: "Preparing AR…",
7
+ retry: "Retry preparing AR",
8
+ },
4
9
  viewCube: {
5
10
  title: "View orientation",
6
11
  front: "Inner",
@@ -14,6 +19,7 @@ export const translations = {
14
19
  title: "3D render settings",
15
20
  subtitle: "Modify the visual effects.",
16
21
  actions: {
22
+ close: "Close",
17
23
  play: "Play",
18
24
  pause: "Pause",
19
25
  },
@@ -71,6 +77,11 @@ export const translations = {
71
77
  },
72
78
  "es-ES": {
73
79
  prefViewer: {
80
+ ar: {
81
+ open: "Ver en RA",
82
+ preparing: "Preparando RA…",
83
+ retry: "Reintentar preparar RA",
84
+ },
74
85
  viewCube: {
75
86
  title: "Orientación de vista",
76
87
  front: "Interior",
@@ -84,6 +95,7 @@ export const translations = {
84
95
  title: "Ajustes de render 3D",
85
96
  subtitle: "Modifica los efectos visuales.",
86
97
  actions: {
98
+ close: "Cerrar",
87
99
  play: "Reproducir",
88
100
  pause: "Pausar",
89
101
  },
@@ -108,10 +108,10 @@ export default class PanzoomController {
108
108
  /**
109
109
  * Stores the SVG wrapper, Panzoom options and callbacks, and binds reusable input handlers.
110
110
  * @public
111
- * @param {HTMLElement|SVGElement} wrapper - Element transformed by Panzoom.
112
- * @param {object} [options] - Panzoom configuration.
113
- * @param {function(object): void} [changedCallback] - Receives the current viewport state.
114
- * @param {function(Event): void} [clickCallback] - Receives the original event for a recognized click.
111
+ * @param {HTMLElement|SVGElement|null} [wrapper=null] - Element transformed by Panzoom.
112
+ * @param {object} [options={}] - Panzoom configuration.
113
+ * @param {?(function(object): void)} [changedCallback=null] - Receives the current viewport state.
114
+ * @param {?(function(Event): void)} [clickCallback=null] - Receives the original event for a recognized click.
115
115
  */
116
116
  constructor(wrapper = null, options = {}, changedCallback = null, clickCallback = null) {
117
117
  this.#wrapper = wrapper;
@@ -492,7 +492,7 @@ export default class PrefViewer2D extends HTMLElement {
492
492
  * @param {Element} node - Clone to append.
493
493
  * @param {string} semanticId - Business id represented by the clone.
494
494
  * @param {"selectable"|"selected"|"definition"} layer - Layer type.
495
- * @param {Element|null} parent - Optional parent; defaults to drawing root.
495
+ * @param {Element|null} [parent=null] - Optional parent; defaults to drawing root.
496
496
  * @returns {Element|null}
497
497
  */
498
498
  #appendSelectionNode(node, semanticId, layer, parent = null) {
@@ -697,7 +697,7 @@ export default class PrefViewer2D extends HTMLElement {
697
697
  /**
698
698
  * Apply a complete semantic selection snapshot to the current drawing.
699
699
  * @public
700
- * @param {{selectableIds?: string[], selectedItems?: Array<{id: string, highlights?: string[]}>}} state - Selection snapshot.
700
+ * @param {{selectableIds?: string[], selectedItems?: Array<{id: string, highlights?: string[]}>}} [state={}] - Selection snapshot.
701
701
  * @returns {void}
702
702
  */
703
703
  applySelectionState(state = {}) {
@@ -1,6 +1,7 @@
1
1
  import { CameraData, ContainerData, MaterialData, IBLData } from "./pref-viewer-3d-data.js";
2
2
  import BabylonJSController from "./babylonjs-controller.js";
3
3
  import { PrefViewer3DStyles } from "./styles.js";
4
+ import PrefViewerControlsPresentation from "./pref-viewer-controls-presentation.js";
4
5
  import FileStorage from "./file-storage.js";
5
6
 
6
7
  /**
@@ -25,6 +26,7 @@ import FileStorage from "./file-storage.js";
25
26
  * - show-model: Show or hide the 3D model ("true"/"false").
26
27
  * - show-scene: Show or hide the 3D environment ("true"/"false").
27
28
  * - visible: Show or hide the entire viewer ("true"/"false").
29
+ * - view-cube-renderer: Selects independent Babylon Canvas (default) or SVG via reflected viewCubeRenderer.
28
30
  * - show-view-cube: Enable the orientation overlay while visible; only "false" disables it (default: enabled).
29
31
  *
30
32
  * Public Methods:
@@ -80,11 +82,14 @@ import FileStorage from "./file-storage.js";
80
82
  * - "pref-viewer-3d-element-click": Internal event carrying distance-ordered semantic model mesh ids.
81
83
  *
82
84
  * Notes:
85
+ * - Owns responsive controls only when standalone; nested 3D inherits its root viewer presentation.
83
86
  * - Internal state and heavy initialization are handled in connectedCallback.
84
87
  * - Designed for extensibility and integration in product configurators, PrefViewer host elements, and visualization tools.
85
88
  * - All resource management and rendering operations are performed asynchronously for performance.
86
89
  */
87
90
  export default class PrefViewer3D extends HTMLElement {
91
+ #controlsPresentation = null;
92
+ #componentStyle = null;
88
93
  // State flags
89
94
  #isInitialized = false;
90
95
  #isLoaded = false;
@@ -98,6 +103,20 @@ export default class PrefViewer3D extends HTMLElement {
98
103
 
99
104
  #babylonJSController = null; // BabylonJSController instance
100
105
 
106
+ /** @returns {"svg"|"canvas"} Per-instance normalized renderer; missing/invalid attributes use Canvas. */
107
+ get viewCubeRenderer() {
108
+ return this.getAttribute("view-cube-renderer") === "svg" ? "svg" : "canvas";
109
+ }
110
+
111
+ /**
112
+ * Reflects the renderer preference immediately without reloading assets or changing visibility.
113
+ * @param {"svg"|"canvas"} value - SVG selects the vector widget; all other values select the dedicated Canvas 3D scene.
114
+ * @returns {void}
115
+ */
116
+ set viewCubeRenderer(value) {
117
+ this.setAttribute("view-cube-renderer", value === "svg" ? "svg" : "canvas");
118
+ }
119
+
101
120
  /**
102
121
  * Create a new instance of the 3D viewer component.
103
122
  * The constructor only calls super(); heavy initialization happens in connectedCallback.
@@ -107,11 +126,11 @@ export default class PrefViewer3D extends HTMLElement {
107
126
  }
108
127
 
109
128
  /**
110
- * Attributes observed by the custom element.
129
+ * Attributes observed by the custom element, including renderer selection and standalone controls-density.
111
130
  * @returns {string[]} Array of attribute names that trigger attributeChangedCallback.
112
131
  */
113
132
  static get observedAttributes() {
114
- return ["show-model", "show-scene", "visible", "show-view-cube"];
133
+ return ["show-model", "show-scene", "visible", "show-view-cube", "view-cube-renderer", "controls-density"];
115
134
  }
116
135
 
117
136
  /**
@@ -124,10 +143,18 @@ export default class PrefViewer3D extends HTMLElement {
124
143
  * - "show-model": shows or hides the model according to the attribute value ("true"/"false").
125
144
  * - "show-scene": shows or hides the scene according to the attribute value ("true"/"false").
126
145
  * - "visible": shows or hides the component according to the attribute value ("true"/"false").
146
+ * - "view-cube-renderer": replaces only the cube overlay, preserving camera and assets.
147
+ * - "controls-density": refreshes standalone presentation without reloading the scene.
127
148
  * - "show-view-cube": activates the cube only while visible and the attribute is not "false".
128
149
  */
129
150
  attributeChangedCallback(name, _old, value) {
130
151
  switch (name) {
152
+ case "view-cube-renderer":
153
+ this.#babylonJSController?.setViewCubeRenderer(this.viewCubeRenderer);
154
+ break;
155
+ case "controls-density":
156
+ this.#controlsPresentation?.refresh();
157
+ break;
131
158
  case "show-view-cube":
132
159
  this.#babylonJSController?.setViewCubeEnabled(this.#isVisible && value !== "false");
133
160
  break;
@@ -161,12 +188,16 @@ export default class PrefViewer3D extends HTMLElement {
161
188
  /**
162
189
  * Called when the element is added to the DOM.
163
190
  * Creates the DOM structure, sets initial visibility, initializes component data, and sets up the BabylonJSController.
164
- * Performs heavy initialization tasks required for the 3D viewer.
191
+ * Creates a presentation owner only when standalone, then performs heavy 3D initialization.
165
192
  * @public
166
193
  * @returns {void}
167
194
  */
168
195
  connectedCallback() {
169
196
  this.#createDOMElements();
197
+ if (!this.getRootNode().host?.matches?.("pref-viewer")) {
198
+ this.#controlsPresentation = new PrefViewerControlsPresentation(this,
199
+ () => this.getAttribute("controls-density"), () => this.refreshViewCube());
200
+ }
170
201
  this.#setInitialVisibility();
171
202
  this.#initializeData();
172
203
  this.#initializeBabylonJS();
@@ -175,29 +206,33 @@ export default class PrefViewer3D extends HTMLElement {
175
206
 
176
207
  /**
177
208
  * Called when the element is removed from the DOM.
178
- * Disables the BabylonJSController to clean up resources and event listeners associated with the 3D viewer.
209
+ * Releases standalone presentation observers and disables Babylon resources/listeners.
179
210
  * @public
180
211
  * @returns {void}
181
212
  */
182
213
  disconnectedCallback() {
214
+ this.#controlsPresentation?.dispose();
215
+ this.#controlsPresentation = null;
183
216
  if (this.#babylonJSController) {
184
217
  void this.#babylonJSController.disable();
185
218
  }
186
219
  }
187
220
 
188
221
  /**
189
- * Create DOM structure and basic styles used by the component.
222
+ * Replaces the owned canvas wrapper/styles on reconnect, retaining any consumer-added child content.
190
223
  * @private
191
224
  * @returns {void}
192
225
  */
193
226
  #createDOMElements() {
227
+ this.#wrapper?.remove();
228
+ this.#componentStyle?.remove();
194
229
  this.#wrapper = document.createElement("div");
195
230
  this.append(this.#wrapper);
196
231
 
197
232
  this.#canvas = document.createElement("canvas");
198
233
  this.#wrapper.appendChild(this.#canvas);
199
234
 
200
- const style = document.createElement("style");
235
+ const style = this.#componentStyle = document.createElement("style");
201
236
  style.textContent = PrefViewer3DStyles;
202
237
  this.append(style);
203
238
  }
@@ -245,7 +280,7 @@ export default class PrefViewer3D extends HTMLElement {
245
280
  }
246
281
 
247
282
  /**
248
- * Initializes the Babylon controller with asset/option state and the visibility-aware cube contract.
283
+ * Initializes the Babylon controller with asset/option state and the selected visibility-aware cube contract.
249
284
  * Supplies current host culture and dispatches measured view-cube-layout events for render-menu positioning.
250
285
  * Starts asynchronous engine initialization without waiting for asset loading.
251
286
  * @private
@@ -253,8 +288,19 @@ export default class PrefViewer3D extends HTMLElement {
253
288
  */
254
289
  #initializeBabylonJS() {
255
290
  this.#babylonJSController = new BabylonJSController(this.#canvas, this.#data.containers, this.#data.options, {
291
+ renderer: this.viewCubeRenderer,
256
292
  enabled: this.#isVisible && this.getAttribute("show-view-cube") !== "false",
293
+ /**
294
+ * Resolves the containing viewer culture for localized ViewCube labels.
295
+ * @returns {string|undefined} Host culture, or undefined for standalone default handling.
296
+ */
257
297
  getCulture: () => this.getRootNode().host?.culture,
298
+ /**
299
+ * Publishes current ViewCube layout measurements for the containing viewer controls.
300
+ * @param {number} offset - Active cube height in CSS pixels; zero clears the offset.
301
+ * @param {number} height - Scene viewport height in CSS pixels.
302
+ * @returns {void}
303
+ */
258
304
  onLayout: (offset, height) => this.dispatchEvent(new CustomEvent("view-cube-layout", { detail: { offset, height } })),
259
305
  });
260
306
  this.#babylonJSController.enable();
@@ -798,7 +844,7 @@ export default class PrefViewer3D extends HTMLElement {
798
844
  /**
799
845
  * Applies the host-owned semantic selection snapshot to the 3D model.
800
846
  * @public
801
- * @param {object} state - Canonical PrefViewer selection state.
847
+ * @param {object} [state={}] - Canonical PrefViewer selection state.
802
848
  * @returns {void}
803
849
  */
804
850
  applySelectionState(state = {}) {
@@ -927,7 +973,7 @@ export default class PrefViewer3D extends HTMLElement {
927
973
  }
928
974
 
929
975
  /**
930
- * Forwards an internal culture/camera refresh to the existing overlay without activating a disabled cube.
976
+ * Forwards an internal culture/camera/presentation resize refresh to the existing overlay without activating a disabled cube.
931
977
  * @public
932
978
  * @returns {void}
933
979
  */