@preference-sl/pref-viewer 2.18.0 → 3.0.0-beta.1
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/README.md +267 -31
- package/index.d.ts +75 -6
- package/package.json +8 -2
- package/src/babylonjs-animation-controller.js +18 -10
- package/src/babylonjs-animation-opening-menu.js +80 -14
- package/src/babylonjs-animation-opening.js +3 -3
- package/src/babylonjs-ar-floor-bounds.js +169 -0
- package/src/babylonjs-ar-glass.js +128 -0
- package/src/babylonjs-ar-placement-feedback.js +449 -0
- package/src/babylonjs-ar-placement.js +1038 -0
- package/src/babylonjs-camera-wheel-motion.js +35 -2
- package/src/babylonjs-controller.js +542 -125
- package/src/babylonjs-orbital-pointers-input.js +186 -0
- package/src/{render-math.js → babylonjs-render-math.js} +11 -11
- package/src/babylonjs-selection-controller.js +4 -4
- package/src/babylonjs-view-cube-canvas-controller.js +1287 -0
- package/src/babylonjs-view-cube-canvas-orbit-math.js +220 -0
- package/src/babylonjs-view-cube-canvas-renderer.js +775 -0
- package/src/babylonjs-view-cube-controller.js +74 -1103
- package/src/babylonjs-view-cube-svg-controller.js +1012 -0
- package/src/file-storage.js +22 -4
- package/src/gltf-resolver.js +83 -13
- package/src/localization/translations.js +12 -0
- package/src/panzoom-controller.js +4 -4
- package/src/pref-viewer-2d.js +2 -2
- package/src/pref-viewer-3d.js +55 -9
- package/src/pref-viewer-controls-presentation.js +292 -0
- package/src/pref-viewer-dialog.js +86 -7
- package/src/pref-viewer-menu-3d.js +134 -17
- package/src/pref-viewer-style-controller.js +320 -0
- package/src/pref-viewer.js +133 -109
- package/src/quick-look-action.js +291 -0
- package/src/styles.js +702 -98
- package/src/babylonjs-view-cube-renderer.js +0 -548
- package/src/view-cube-orbit-math.js +0 -199
package/src/file-storage.js
CHANGED
|
@@ -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)
|
|
@@ -34,6 +35,7 @@ import { openDB } from "idb";
|
|
|
34
35
|
* - getTimeStamp(uri): Retrieves cached or server Last-Modified timestamp.
|
|
35
36
|
* - getSize(uri): Gets cached or server file size in bytes.
|
|
36
37
|
* - getBlob(uri): Retrieves file blob from cache or server.
|
|
38
|
+
* - remove(uri): Deletes a cached record (e.g. an unreadable blob) so it is downloaded again next time.
|
|
37
39
|
* - get(uri): Gets file from cache with automatic server sync and cache versioning.
|
|
38
40
|
* - put(uri): Stores file from server in IndexedDB cache.
|
|
39
41
|
* - dispose(): Closes the active IndexedDB handle held by the instance.
|
|
@@ -423,6 +425,7 @@ export default class FileStorage {
|
|
|
423
425
|
|
|
424
426
|
/**
|
|
425
427
|
* Updates `lastAccess` for a record to keep LRU ordering current.
|
|
428
|
+
* Observes the write and transaction together; failed touches are caught without leaking an abort rejection.
|
|
426
429
|
* @private
|
|
427
430
|
* @param {String} uri Record key.
|
|
428
431
|
* @param {Object} record Existing normalized record.
|
|
@@ -443,8 +446,7 @@ export default class FileStorage {
|
|
|
443
446
|
try {
|
|
444
447
|
const transaction = this.#db.transaction(this.osName, "readwrite");
|
|
445
448
|
const store = transaction.objectStore(this.osName);
|
|
446
|
-
store.put(normalized, uri);
|
|
447
|
-
await transaction.done;
|
|
449
|
+
await Promise.all([store.put(normalized, uri), transaction.done]);
|
|
448
450
|
} catch (error) {}
|
|
449
451
|
}
|
|
450
452
|
|
|
@@ -596,6 +598,7 @@ export default class FileStorage {
|
|
|
596
598
|
* @description
|
|
597
599
|
* Runs maintenance before write (TTL, max entries, and quota-aware cleanup), then retries quota errors
|
|
598
600
|
* by evicting least-recently-used entries.
|
|
601
|
+
* Observes each write request and transaction completion together so simultaneous WebKit failures are handled.
|
|
599
602
|
*/
|
|
600
603
|
async #putFile(file, uri) {
|
|
601
604
|
if (this.#db === undefined) {
|
|
@@ -619,8 +622,9 @@ export default class FileStorage {
|
|
|
619
622
|
try {
|
|
620
623
|
const transaction = this.#db.transaction(this.osName, "readwrite");
|
|
621
624
|
const store = transaction.objectStore(this.osName);
|
|
622
|
-
|
|
623
|
-
|
|
625
|
+
// Observe both promises immediately: WebKit can reject the write and abort
|
|
626
|
+
// its transaction together. Awaiting them sequentially leaks a rejection.
|
|
627
|
+
const [result] = await Promise.all([store.put(record, uri), transaction.done]);
|
|
624
628
|
return result;
|
|
625
629
|
} catch (error) {
|
|
626
630
|
if (!this.#isQuotaExceededError(error) || attempt === this.#maxQuotaEvictionRetries) {
|
|
@@ -695,6 +699,20 @@ export default class FileStorage {
|
|
|
695
699
|
return storedFile ? URL.createObjectURL(storedFile.blob) : (await this.#getServerFileTimeStamp(uri)) ? uri : false;
|
|
696
700
|
}
|
|
697
701
|
|
|
702
|
+
/**
|
|
703
|
+
* Removes a cached record so the next access downloads the file again.
|
|
704
|
+
* Used when a stored blob turns out to be unreadable (WebKit can abort IndexedDB writes and leave a broken record).
|
|
705
|
+
* @public
|
|
706
|
+
* @param {String} uri - File URI (record key).
|
|
707
|
+
* @returns {Promise<number>} Number of requested deletions (0 when IndexedDB is unavailable).
|
|
708
|
+
*/
|
|
709
|
+
async remove(uri) {
|
|
710
|
+
if (!this.#supported || !uri) {
|
|
711
|
+
return 0;
|
|
712
|
+
}
|
|
713
|
+
return this.#deleteKeys([uri]);
|
|
714
|
+
}
|
|
715
|
+
|
|
698
716
|
/**
|
|
699
717
|
* Get the cached or server Last-Modified timestamp for a file.
|
|
700
718
|
* @public
|
package/src/gltf-resolver.js
CHANGED
|
@@ -9,7 +9,9 @@ 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
|
-
*
|
|
12
|
+
* Callers may opt into an explicit unchanged marker to distinguish cache hits from unavailable content.
|
|
13
|
+
* - Tracks generated object URLs (used for .gltf dependencies) so callers can revoke them after Babylon finishes loading.
|
|
14
|
+
* - Hands cached remote .glb files to Babylon as File objects (no blob: URL, which fails on iOS Safari), falling back to the server URL when the cached blob is missing or empty.
|
|
13
15
|
* - Provides methods for initializing storage, decoding assets, and preparing sources for viewer components.
|
|
14
16
|
*
|
|
15
17
|
* Usage:
|
|
@@ -17,7 +19,7 @@ import { closeDb, initDb, loadModel } from "./gltf-storage.js";
|
|
|
17
19
|
* - Prepare asset: await resolver.getSource(storage, currentSize, currentTimeStamp);
|
|
18
20
|
*
|
|
19
21
|
* Public Methods:
|
|
20
|
-
* - getSource(storage, currentSize, currentTimeStamp):
|
|
22
|
+
* - getSource(storage, currentSize, currentTimeStamp, reportUnchanged?): Prepares a source or reports a cache hit.
|
|
21
23
|
* - revokeObjectURLs(objectURLs): Releases temporary blob URLs generated during source resolution.
|
|
22
24
|
* - dispose(): Releases resolver resources and closes the internal FileStorage DB handle.
|
|
23
25
|
*
|
|
@@ -217,10 +219,76 @@ export default class GLTFResolver {
|
|
|
217
219
|
await Promise.all(promisesArray);
|
|
218
220
|
}
|
|
219
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Resolves a remote .glb URL into the source handed to Babylon.
|
|
224
|
+
* @private
|
|
225
|
+
* @param {string} url - Absolute or relative URL of the .glb asset.
|
|
226
|
+
* @returns {Promise<File|string>} A File holding the cached bytes, or the original URL when no readable cached blob exists.
|
|
227
|
+
* @description
|
|
228
|
+
* - The cached blob is read into memory here, so Babylon receives a File backed by plain bytes. This avoids both
|
|
229
|
+
* the XHR over a blob: URL (status 0 on iOS Safari) and FileReader on an IndexedDB blob that iOS cannot read.
|
|
230
|
+
* - A missing or empty cached blob (WebKit can abort IndexedDB writes leaving empty records) falls back to the server URL.
|
|
231
|
+
* - A cached blob whose bytes cannot be read, or whose read does not settle within `CACHE_READ_TIMEOUT_MS`
|
|
232
|
+
* (iOS Safari can hang on a blob left by an aborted IndexedDB write), is purged from the cache without
|
|
233
|
+
* waiting for the purge, and this load falls back to the server URL so the load always completes.
|
|
234
|
+
* - No object URL is created, so there is nothing to revoke.
|
|
235
|
+
*/
|
|
236
|
+
async #getGlbSource(url) {
|
|
237
|
+
let bytes = null;
|
|
238
|
+
let type = "";
|
|
239
|
+
try {
|
|
240
|
+
bytes = await GLTFResolver.#withTimeout((async () => {
|
|
241
|
+
const blob = await this.#fileStorage.getBlob(url);
|
|
242
|
+
if (!blob || !(blob.size > 0)) {
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
type = blob.type || "";
|
|
246
|
+
return blob.arrayBuffer();
|
|
247
|
+
})(), GLTFResolver.CACHE_READ_TIMEOUT_MS);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
bytes = null;
|
|
250
|
+
}
|
|
251
|
+
if (bytes === undefined) {
|
|
252
|
+
return url;
|
|
253
|
+
}
|
|
254
|
+
if (!bytes || bytes.byteLength === 0) {
|
|
255
|
+
// Fire-and-forget: a hung IndexedDB transaction must not block this load either.
|
|
256
|
+
void Promise.resolve(this.#fileStorage.remove(url)).catch(() => {});
|
|
257
|
+
return url;
|
|
258
|
+
}
|
|
259
|
+
const name = url.split(/[?#]/)[0].split("/").pop() || "file.glb";
|
|
260
|
+
return new File([bytes], name, { type: type || "model/gltf-binary" });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Upper bound for reading a cached .glb out of IndexedDB before giving up on the cache.
|
|
265
|
+
* @public
|
|
266
|
+
* @static
|
|
267
|
+
* @type {number}
|
|
268
|
+
*/
|
|
269
|
+
static CACHE_READ_TIMEOUT_MS = 5000;
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Races a promise against a timeout.
|
|
273
|
+
* @private
|
|
274
|
+
* @static
|
|
275
|
+
* @param {Promise<T>} promise - Work to bound.
|
|
276
|
+
* @param {number} ms - Milliseconds before rejecting with a timeout error.
|
|
277
|
+
* @returns {Promise<T>} The promise result, or a rejection when the timeout elapses first.
|
|
278
|
+
* @template T
|
|
279
|
+
*/
|
|
280
|
+
static #withTimeout(promise, ms) {
|
|
281
|
+
let timer = null;
|
|
282
|
+
const timeout = new Promise((resolve, reject) => {
|
|
283
|
+
timer = setTimeout(() => reject(new Error(`Cache read timed out after ${ms}ms`)), ms);
|
|
284
|
+
});
|
|
285
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
286
|
+
}
|
|
287
|
+
|
|
220
288
|
/**
|
|
221
289
|
* Revoke browser object URLs and clear the provided list.
|
|
222
290
|
* @public
|
|
223
|
-
* @param {string[]} objectURLs - URLs previously created via URL.createObjectURL.
|
|
291
|
+
* @param {string[]} [objectURLs=[]] - URLs previously created via URL.createObjectURL.
|
|
224
292
|
* @returns {void}
|
|
225
293
|
*/
|
|
226
294
|
revokeObjectURLs(objectURLs = []) {
|
|
@@ -251,17 +319,22 @@ export default class GLTFResolver {
|
|
|
251
319
|
* @param {object} storage - Storage descriptor containing url, db, table, and id properties.
|
|
252
320
|
* @param {number|null} currentSize - The current cached size of the asset, or null if not cached.
|
|
253
321
|
* @param {string|null} currentTimeStamp - The current cached timestamp of the asset, or null if not cached.
|
|
254
|
-
* @
|
|
255
|
-
*
|
|
322
|
+
* @param {boolean} [reportUnchanged=false] - Return an explicit unchanged marker for cache hits.
|
|
323
|
+
* @returns {Promise<false|{unchanged:true}|{source: File|string, size: number, timeStamp: string|null, extension: string, metadata: object, objectURLs: string[]}>}
|
|
324
|
+
* - Resolves to false for missing data or an unchanged asset when reportUnchanged is false.
|
|
325
|
+
* - Resolves to {unchanged: true} for a cache hit when reportUnchanged is true.
|
|
256
326
|
* - Resolves to an object containing source, cache metadata, and tracked object URLs if an update is required.
|
|
327
|
+
* @throws {Error} Rejects when storage access, source retrieval or glTF JSON decoding fails.
|
|
257
328
|
* @description
|
|
258
329
|
* - If storage specifies IndexedDB (db, table, id), loads the asset from IndexedDB and checks for updates.
|
|
259
330
|
* - If storage specifies a direct URL or base64 data, decodes and validates the asset.
|
|
260
331
|
* - For .gltf files, replaces internal URIs with resolved URLs from FileStorage.
|
|
261
332
|
* - Performs cache validation using size and timestamp to avoid unnecessary reloads.
|
|
333
|
+
* - For remote .glb files, source is a File built from the cached blob, or the server URL string when the cached blob is missing or empty.
|
|
262
334
|
* - Returns the prepared source, size, timestamp, extension, metadata, and objectURLs for further processing.
|
|
335
|
+
* - The caller owns revoking returned objectURLs after the loader finishes consuming them.
|
|
263
336
|
*/
|
|
264
|
-
async getSource(storage, currentSize, currentTimeStamp) {
|
|
337
|
+
async getSource(storage, currentSize, currentTimeStamp, reportUnchanged = false) {
|
|
265
338
|
let source = storage.url || null;
|
|
266
339
|
let newSize, newTimeStamp;
|
|
267
340
|
let pending = false;
|
|
@@ -277,7 +350,7 @@ export default class GLTFResolver {
|
|
|
277
350
|
source = object.data;
|
|
278
351
|
metadata = { ...(object.metadata ?? {}) };
|
|
279
352
|
if (object.timeStamp === currentTimeStamp) {
|
|
280
|
-
return false;
|
|
353
|
+
return reportUnchanged ? { unchanged: true } : false;
|
|
281
354
|
} else {
|
|
282
355
|
pending = true;
|
|
283
356
|
newSize = object.size;
|
|
@@ -304,7 +377,7 @@ export default class GLTFResolver {
|
|
|
304
377
|
}
|
|
305
378
|
if (!pending) {
|
|
306
379
|
if (currentTimeStamp === null && currentSize === size) {
|
|
307
|
-
return false;
|
|
380
|
+
return reportUnchanged ? { unchanged: true } : false;
|
|
308
381
|
} else {
|
|
309
382
|
pending = true;
|
|
310
383
|
newSize = size;
|
|
@@ -316,7 +389,7 @@ export default class GLTFResolver {
|
|
|
316
389
|
extension = extMatch ? `.${extMatch[1].toLowerCase()}` : ".gltf";
|
|
317
390
|
let [fileSize, fileTimeStamp] = [await this.#fileStorage.getSize(source), await this.#fileStorage.getTimeStamp(source)];
|
|
318
391
|
if (currentSize === fileSize && currentTimeStamp === fileTimeStamp) {
|
|
319
|
-
return false;
|
|
392
|
+
return reportUnchanged ? { unchanged: true } : false;
|
|
320
393
|
} else {
|
|
321
394
|
pending = true;
|
|
322
395
|
newSize = fileSize;
|
|
@@ -327,10 +400,7 @@ export default class GLTFResolver {
|
|
|
327
400
|
await this.#replaceSceneURIAsync(assetContainerJSON, source, objectURLs);
|
|
328
401
|
source = `data:${JSON.stringify(assetContainerJSON)}`;
|
|
329
402
|
} else {
|
|
330
|
-
source = await this.#
|
|
331
|
-
if (this.#isBlobURL(source)) {
|
|
332
|
-
objectURLs.push(source);
|
|
333
|
-
}
|
|
403
|
+
source = await this.#getGlbSource(source);
|
|
334
404
|
}
|
|
335
405
|
}
|
|
336
406
|
}
|
|
@@ -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;
|
package/src/pref-viewer-2d.js
CHANGED
|
@@ -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 = {}) {
|
package/src/pref-viewer-3d.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
*/
|