@preference-sl/pref-viewer 2.11.0-beta.0 → 2.11.0-beta.2

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/src/index.js CHANGED
@@ -1,1172 +1,515 @@
1
+ import { PrefViewer2D } from "./pref-viewer-2d.js";
2
+ import { PrefViewer3D } from "./pref-viewer-3d.js";
3
+ import { PrefViewerTask } from "./pref-viewer-task.js";
4
+
1
5
  /**
2
- * =============================================================================
3
- * PrefViewer Web Component (JavaScript)
4
- * =============================================================================
6
+ * PrefViewer - Custom Web Component for advanced 2D and 3D product visualization and configuration.
7
+ *
8
+ * Overview:
9
+ * - Encapsulates both 2D (SVG) and 3D (Babylon.js) viewers, supporting glTF/GLB models, environments, and drawings.
10
+ * - Loads assets from remote URLs, Base64 data URIs, and IndexedDB sources.
11
+ * - Provides a unified API for loading models, scenes, drawings, materials, and configuration via attributes or methods.
12
+ * - Manages an internal task queue for sequential processing of viewer operations.
13
+ * - Emits custom events for loading, errors, and state changes to facilitate integration.
14
+ * - Supports downloading models and scenes in GLB and USDZ formats.
15
+ * - Automatically updates the viewer when reactive attributes change.
16
+ *
17
+ * Usage:
18
+ * - Use as a custom HTML element: <pref-viewer ...>
19
+ * - Configure via attributes (config, model, scene, materials, drawing, options, mode).
20
+ * - Control viewer mode, visibility, and downloads via public methods.
21
+ *
22
+ * Reactive Attributes:
23
+ * - config: URL or Base64 for configuration file.
24
+ * - model: URL or Base64 for 3D model (glTF/GLB).
25
+ * - scene: URL or Base64 for environment/scene (glTF/GLB).
26
+ * - materials: URL or Base64 for materials definition.
27
+ * - drawing: URL or Base64 for SVG drawing.
28
+ * - options: JSON string for viewer options.
29
+ * - mode: Viewer mode ("2d" or "3d").
5
30
  *
6
- * Overview
7
- * --------
8
- * `PrefViewer` is a self-contained Web Component built with Babylon.js that:
9
- * Renders the glTF or GLB model of the product ('model') and merges it with the glTF or GLB model of environment ('scene') into a <canvas> inside its shadow DOM.
10
- * Supports loading via remote URL (storage.url), Base64 data URI (storage.url), or from a Base64 stored entry in IndexedDB (storage.db, storage.table, storage.id).
11
- * The data for loading both models is provided via the 'config' (both model and scene), 'model' and 'scene' attributes. The 'config' attribute can carry more initial configurations than just the models.
12
- * Exposes methods for making changes to the scene that replicate what the attribute observables do: 'loadConfig', 'loadModel', 'loadScene'.
13
- * • Automatically handles scene creation (engine, camera, lighting) and resource cleanup.
14
- * • Emits 'model-loaded' and 'model-error' events for integration.
31
+ * Public Methods:
32
+ * - loadConfig(config), loadModel(model), loadScene(scene), loadMaterials(materials), loadDrawing(drawing)
33
+ * - setOptions(options)
34
+ * - setMode(mode): Sets the viewer mode to "2d" or "3d" and updates component visibility.
35
+ * - showModel(), hideModel(), showScene(), hideScene()
36
+ * - downloadModelGLB(), downloadModelUSDZ(), downloadModelAndSceneGLB(), downloadModelAndSceneUSDZ()
37
+ * - zoomCenter(), zoomExtentsAll(), zoomIn(), zoomOut()
15
38
  *
16
- * Usage
17
- * -----
18
- * Load model from IndexedDB:
19
- * ```html
20
- * <pref-viewer
21
- * model='{ "storage": { "db": "PrefConfiguratorDB", "table": "gltfModels", "id": "1234-1234-1234-1234-1234" }, "visible": true" }'
22
- * style="width:800px; height:600px;">
23
- * </pref-viewer>
24
- * ```
39
+ * Public Properties:
40
+ * - isInitialized: Indicates if the viewer is initialized.
41
+ * - isLoaded: Indicates if the viewer has finished loading.
42
+ * - isLoading: Indicates if the viewer is currently loading.
43
+ * - isMode2D: Indicates if the viewer is in 2D mode.
44
+ * - isMode3D: Indicates if the viewer is in 3D mode.
25
45
  *
26
- * Load scene a URL:
27
- * ```html
28
- * <pref-viewer
29
- * scene='{ "storage": { "url" : "https://example.com/scenes/scene1.gltf" }, "visible": true" }'
30
- * style="width:800px; height:600px;">
31
- * </pref-viewer>
32
- * ```
46
+ * Events:
47
+ * - "scene-loading": Dispatched when a 3D loading operation starts.
48
+ * - "scene-loaded": Dispatched when a 3D loading operation completes.
49
+ * - "drawing-loading": Dispatched when a 2D drawing loading operation starts.
50
+ * - "drawing-loaded": Dispatched when a 2D drawing loading operation completes.
51
+ * - "drawing-zoom-changed": Dispatched when the 2D drawing zoom/pan state changes.
33
52
  *
34
- * Load model from Base64 data:
35
- * ```html
36
- * <pref-viewer
37
- * model='{ "storage": { "url" : "data:model/gltf+json;base64,UEsDB..." }, "visible": true" }'
38
- * style="width:800px; height:600px;">
39
- * </pref-viewer>
40
- * ```
53
+ * Notes:
54
+ * - Automatically creates and manages 2D and 3D viewer components in its shadow DOM.
55
+ * - Processes tasks sequentially to ensure consistent state.
56
+ * - Designed for extensibility and integration in product configurators and visualization tools.
41
57
  */
42
- import { Engine, Scene, ArcRotateCamera, Vector3, Color4, HemisphericLight, DirectionalLight, PointLight, ShadowGenerator, LoadAssetContainerAsync, Tools, WebXRSessionManager, WebXRDefaultExperience, MeshBuilder, WebXRFeatureName, HDRCubeTexture, IblShadowsRenderPipeline } from "@babylonjs/core";
43
- import "@babylonjs/loaders";
44
- import { USDZExportAsync, GLTF2Export } from "@babylonjs/serializers";
45
- import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_draco_mesh_compression";
46
- import { DracoCompression } from "@babylonjs/core/Meshes/Compression/dracoCompression";
47
- import { initDb, loadModel } from "./gltf-storage.js";
48
- import { FileStorage } from "./file-storage.js";
49
-
50
- class PrefViewerTask {
51
- static Types = Object.freeze({
52
- Config: "config",
53
- Environment: "environment",
54
- Materials: "materials",
55
- Model: "model",
56
- Options: "options",
57
- });
58
-
59
- /**
60
- * value: any payload for the task
61
- * type: must match one of PrefViewerTask.Types values (case-insensitive)
62
- */
63
- constructor(value, type) {
64
- this.value = value;
65
-
66
- const t = typeof type === "string" ? type.toLowerCase() : String(type).toLowerCase();
67
- const allowed = Object.values(PrefViewerTask.Types);
68
- if (!allowed.includes(t)) {
69
- throw new TypeError(`PrefViewerTask: invalid type "${type}". Allowed types: ${allowed.join(", ")}`);
70
- }
71
- this.type = t;
72
-
73
- Object.freeze(this);
74
- }
75
- }
76
-
77
58
  class PrefViewer extends HTMLElement {
78
- initialized = false;
79
- loaded = false;
80
- loading = false;
59
+ #isInitialized = false;
60
+ #isLoaded = false;
61
+ #isLoading = false;
62
+ #mode = "3d";
63
+
81
64
  #taskQueue = [];
82
- #fileStorage = new FileStorage("PrefViewer", "Files");
83
-
84
- #data = {
85
- containers: {
86
- model: {
87
- name: "model",
88
- assetContainer: null,
89
- show: true,
90
- storage: null,
91
- visible: false,
92
- size: null,
93
- timeStamp: null,
94
- changed: { pending: false, success: false },
95
- },
96
- environment: {
97
- name: "environment",
98
- assetContainer: null,
99
- show: true,
100
- storage: null,
101
- visible: false,
102
- size: null,
103
- timeStamp: null,
104
- changed: { pending: false, success: false },
105
- },
106
- materials: {
107
- name: "materials",
108
- assetContainer: null,
109
- storage: null,
110
- show: true,
111
- visible: false,
112
- size: null,
113
- timeStamp: null,
114
- changed: { pending: false, success: false },
115
- },
116
- },
117
- options: {
118
- camera: {
119
- value: null,
120
- locked: true,
121
- changed: { pending: false, success: false },
122
- },
123
- materials: {
124
- innerWall: {
125
- value: null,
126
- prefix: "innerWall",
127
- changed: { pending: false, success: false },
128
- },
129
- outerWall: {
130
- value: null,
131
- prefix: "outerWall",
132
- changed: { pending: false, success: false },
133
- },
134
- innerFloor: {
135
- value: null,
136
- prefix: "innerFloor",
137
- changed: { pending: false, success: false },
138
- },
139
- outerFloor: {
140
- value: null,
141
- prefix: "outerFloor",
142
- changed: { pending: false, success: false },
143
- },
144
- },
145
- },
146
- };
147
-
148
- // DOM elements
149
- #wrapper = null;
150
- #canvas = null;
151
-
152
- // Babylon.js core objects
153
- #engine = null;
154
- #scene = null;
155
- #camera = null;
156
- #hemiLight = null;
157
- #dirLight = null;
158
- #cameraLight = null;
159
- #shadowGen = null;
160
- #XRExperience = null;
161
65
 
66
+ #component2D = null;
67
+ #component3D = null;
68
+
69
+ /**
70
+ * Creates a new PrefViewer instance and attaches a shadow DOM.
71
+ * Initializes internal state and component references.
72
+ * @public
73
+ */
162
74
  constructor() {
163
75
  super();
164
76
  this.attachShadow({ mode: "open" });
165
- this.#createCanvas();
166
- this.#wrapCanvas();
167
- // Point to whichever version you packaged or want to use:
168
- const DRACO_BASE = "https://www.gstatic.com/draco/versioned/decoders/1.5.7";
169
- DracoCompression.Configuration.decoder = {
170
- // loader for the “wrapper” that pulls in the real WASM
171
- wasmUrl: `${DRACO_BASE}/draco_wasm_wrapper_gltf.js`,
172
- // the raw WebAssembly binary
173
- wasmBinaryUrl: `${DRACO_BASE}/draco_decoder_gltf.wasm`,
174
- // JS fallback if WASM isn’t available
175
- fallbackUrl: `${DRACO_BASE}/draco_decoder_gltf.js`,
176
- };
177
77
  }
178
78
 
79
+ /**
80
+ * Returns the list of attributes to observe for changes.
81
+ * @public
82
+ * @returns {string[]} Array of attribute names to observe.
83
+ */
179
84
  static get observedAttributes() {
180
- return ["config", "model", "scene", "show-model", "show-scene"];
85
+ return ["config", "drawing", "materials", "mode", "model", "scene", "options", "show-model", "show-scene"];
181
86
  }
182
87
 
88
+ /**
89
+ * Observes changes to specific attributes and triggers corresponding actions.
90
+ * Loads configuration, drawing, model, scene, materials, or options when their attributes change.
91
+ * Toggles model or scene visibility when "show-model" or "show-scene" attributes change.
92
+ * @public
93
+ * @param {string} name - The name of the changed attribute.
94
+ * @param {*} _old - The previous value of the attribute (unused).
95
+ * @param {*} value - The new value of the attribute.
96
+ * @returns {void}
97
+ */
183
98
  attributeChangedCallback(name, _old, value) {
184
- let data = null;
185
99
  switch (name) {
186
100
  case "config":
187
101
  this.loadConfig(value);
188
102
  break;
103
+ case "drawing":
104
+ this.loadDrawing(value);
105
+ break;
106
+ case "materials":
107
+ this.loadMaterials(value);
108
+ break;
109
+ case "mode":
110
+ if (_old === value || value.toLowerCase() === this.#mode) {
111
+ return;
112
+ }
113
+ this.setMode(value.toLowerCase());
114
+ break;
189
115
  case "model":
190
116
  this.loadModel(value);
191
117
  break;
192
118
  case "scene":
193
119
  this.loadScene(value);
194
120
  break;
195
- case "materials":
196
- this.loadMaterials(value);
197
- break;
198
121
  case "options":
199
122
  this.setOptions(value);
200
123
  break;
201
124
  case "show-model":
202
- data = value.toLowerCase?.() === "true";
203
- if (this.initialized) {
204
- data ? this.showModel() : this.hideModel();
205
- } else {
206
- this.#data.containers.model.show = data;
125
+ if (_old === value) {
126
+ return;
207
127
  }
128
+ const showModel = value.toLowerCase() === "true";
129
+ showModel ? this.showModel() : this.hideModel();
208
130
  break;
209
131
  case "show-scene":
210
- data = value.toLowerCase?.() === "true";
211
- if (this.initialized) {
212
- data ? this.showScene() : this.hideScene();
213
- } else {
214
- this.#data.containers.environment.show = data;
132
+ if (_old === value) {
133
+ return;
215
134
  }
135
+ const showScene = value.toLowerCase() === "true";
136
+ showScene ? this.showScene() : this.hideScene();
216
137
  break;
217
138
  }
218
139
  }
219
140
 
141
+ /**
142
+ * Called when the element is inserted into the DOM.
143
+ * Initializes the 3D and 2D viewer components and starts processing tasks.
144
+ * @public
145
+ * @returns {void|boolean} Returns false if initialization fails; otherwise void.
146
+ */
220
147
  connectedCallback() {
221
- if (!this.hasAttribute("config")) {
222
- const error = 'PrefViewer: provide "models" as array of model and environment';
223
- console.error(error);
224
- this.dispatchEvent(
225
- new CustomEvent("scene-error", {
226
- bubbles: true,
227
- cancelable: false,
228
- composed: true,
229
- detail: { error: new Error(error) },
230
- })
231
- );
232
- return false;
148
+ this.#createComponent3D();
149
+ this.#createComponent2D();
150
+
151
+ if (!this.hasAttribute("mode")) {
152
+ this.setMode();
233
153
  }
234
154
 
235
- this.#initializeBabylon();
236
- this.initialized = true;
155
+ this.#isInitialized = true;
237
156
  this.#processNextTask();
238
157
  }
239
158
 
240
- disconnectedCallback() {
241
- this.#disposeEngine();
242
- this.#canvasResizeObserver.disconnect();
243
- }
244
-
245
- // Web Component
246
- #createCanvas() {
247
- this.#canvas = document.createElement("canvas");
248
- Object.assign(this.#canvas.style, {
249
- width: "100%",
250
- height: "100%",
251
- display: "block",
252
- outline: "none",
253
- });
254
- }
255
-
256
- #wrapCanvas() {
257
- this.#wrapper = document.createElement("div");
258
- Object.assign(this.#wrapper.style, {
259
- width: "100%",
260
- height: "100%",
261
- position: "relative",
262
- });
263
- this.#wrapper.appendChild(this.#canvas);
264
- this.shadowRoot.append(this.#wrapper);
159
+ /**
160
+ * Creates and appends the 2D viewer component to the shadow DOM.
161
+ * Sets the "visible" attribute to true by default.
162
+ * @private
163
+ * @returns {void}
164
+ */
165
+ #createComponent2D() {
166
+ this.#component2D = document.createElement("pref-viewer-2d");
167
+ this.#component2D.setAttribute("visible", "false");
168
+ this.#component2D.addEventListener("drawing-zoom-changed", this.#on2DZoomChanged.bind(this));
169
+ this.shadowRoot.appendChild(this.#component2D);
265
170
  }
266
171
 
267
- #setStatusLoading() {
268
- this.loaded = false;
269
- this.loading = true;
270
- if (this.hasAttribute("loaded")) {
271
- this.removeAttribute("loaded");
272
- }
273
- this.setAttribute("loading", "");
274
- this.dispatchEvent(
275
- new CustomEvent("scene-loading", {
276
- bubbles: true,
277
- cancelable: false,
278
- composed: true,
279
- })
280
- );
281
- this.#engine.stopRenderLoop(this.#renderLoop);
172
+ /**
173
+ * Creates and appends the 3D viewer component to the shadow DOM.
174
+ * Sets the "visible" attribute to true by default.
175
+ * @private
176
+ * @returns {void}
177
+ */
178
+ #createComponent3D() {
179
+ this.#component3D = document.createElement("pref-viewer-3d");
180
+ this.#component3D.setAttribute("visible", "false");
181
+ this.shadowRoot.appendChild(this.#component3D);
282
182
  }
283
183
 
284
- async #setStatusLoaded() {
285
- const toLoadDetail = {
286
- container_model: !!this.#data.containers.model.changed.pending,
287
- container_environment: !!this.#data.containers.environment.changed.pending,
288
- container_materials: !!this.#data.containers.materials.changed.pending,
289
- options_camera: !!this.#data.options.camera.changed.pending,
290
- options_innerWallMaterial: !!this.#data.options.materials.innerWall.changed.pending,
291
- options_outerWallMaterial: !!this.#data.options.materials.outerWall.changed.pending,
292
- options_innerFloorMaterial: !!this.#data.options.materials.innerFloor.changed.pending,
293
- options_outerFloorMaterial: !!this.#data.options.materials.outerFloor.changed.pending,
294
- };
295
- const loadedDetail = {
296
- container_model: !!this.#data.containers.model.changed.success,
297
- container_environment: !!this.#data.containers.environment.changed.success,
298
- container_materials: !!this.#data.containers.materials.changed.success,
299
- options_camera: !!this.#data.options.camera.changed.success,
300
- options_innerWallMaterial: !!this.#data.options.materials.innerWall.changed.success,
301
- options_outerWallMaterial: !!this.#data.options.materials.outerWall.changed.success,
302
- options_innerFloorMaterial: !!this.#data.options.materials.innerFloor.changed.success,
303
- options_outerFloorMaterial: !!this.#data.options.materials.outerFloor.changed.success,
304
- };
305
- const detail = {
306
- tried: toLoadDetail,
307
- success: loadedDetail,
308
- };
309
-
310
- this.dispatchEvent(
311
- new CustomEvent("scene-loaded", {
312
- bubbles: true,
313
- cancelable: false,
314
- composed: true,
315
- detail: detail,
316
- })
317
- );
318
-
319
- await this.#scene.whenReadyAsync();
320
- this.#engine.runRenderLoop(this.#renderLoop);
321
-
322
- this.#resetChangedFlags();
323
-
324
- if (this.hasAttribute("loading")) {
325
- this.removeAttribute("loading");
184
+ /**
185
+ * Adds a new task to the internal queue for processing.
186
+ * If the viewer is initialized and not currently loading, immediately processes the next task.
187
+ * @private
188
+ * @param {*} value - The payload or data for the task.
189
+ * @param {string} type - The type of task (see PrefViewerTask.Types).
190
+ * @returns {void}
191
+ */
192
+ #addTaskToQueue(value, type) {
193
+ this.#taskQueue.push(new PrefViewerTask(value, type));
194
+ if (this.#isInitialized && !this.#isLoading) {
195
+ this.#processNextTask();
326
196
  }
327
- this.setAttribute("loaded", "");
328
-
329
- this.loaded = true;
330
- this.loading = false;
331
-
332
- this.#processNextTask();
333
197
  }
334
198
 
335
- // Data
336
- #checkCameraChanged(options) {
337
- if (!options || !options.camera) {
199
+ /**
200
+ * Processes the next task in the queue, if any.
201
+ * Dispatches the task to the appropriate handler based on its type.
202
+ * @private
203
+ * @returns {boolean|void} Returns false if the queue is empty; otherwise void.
204
+ */
205
+ #processNextTask() {
206
+ if (!this.#taskQueue.length) {
338
207
  return false;
339
208
  }
340
- const prev = this.#data.options.camera.value;
341
- const changed = options.camera !== prev;
342
-
343
- this.#data.options.camera.changed.pending = changed;
344
- this.#data.options.camera.changed.success = false;
345
- if (changed) {
346
- this.#data.options.camera.changed.value = prev;
347
- this.#data.options.camera.changed.locked = this.#data.options.camera.locked;
348
- this.#data.options.camera.value = options.camera;
209
+ const task = this.#taskQueue[0];
210
+ this.#taskQueue.shift();
211
+ switch (task.type) {
212
+ case PrefViewerTask.Types.Config:
213
+ this.#processConfig(task.value);
214
+ break;
215
+ case PrefViewerTask.Types.Drawing:
216
+ this.#processDrawing(task.value);
217
+ break;
218
+ case PrefViewerTask.Types.Environment:
219
+ this.#processEnvironment(task.value);
220
+ break;
221
+ case PrefViewerTask.Types.Materials:
222
+ this.#processMaterials(task.value);
223
+ break;
224
+ case PrefViewerTask.Types.Model:
225
+ this.#processModel(task.value);
226
+ break;
227
+ case PrefViewerTask.Types.Options:
228
+ this.#processOptions(task.value);
229
+ break;
230
+ case PrefViewerTask.Types.Visibility:
231
+ this.#processVisibility(task.value);
232
+ break;
349
233
  }
350
- return changed;
351
234
  }
352
235
 
353
- #checkMaterialsChanged(options) {
354
- if (!options) {
355
- return false;
356
- }
357
- let someChanged = false;
358
- Object.keys(this.#data.options.materials).forEach((material) => {
359
- const key = `${material}Material`;
360
- const state = this.#data.options.materials[material];
361
- const prev = state.value;
362
- const incoming = options[key];
363
- const changed = !!incoming && incoming !== prev;
364
-
365
- state.changed.pending = changed;
366
- state.changed.success = false;
367
- if (changed) {
368
- state.changed.value = prev;
369
- state.value = incoming;
370
- }
371
- someChanged = someChanged || changed;
372
- });
373
- return someChanged;
374
- }
236
+ /**
237
+ * Handles the start of a 3D loading operation.
238
+ * Updates loading state, sets attributes, and dispatches a "scene-loading" event.
239
+ * @private
240
+ * @returns {void}
241
+ */
242
+ #on3DLoading() {
243
+ this.#isLoaded = false;
244
+ this.#isLoading = true;
375
245
 
376
- #storeChangedFlagsForContainer(container, success) {
377
- if (success) {
378
- container.timeStamp = container.changed.timeStamp;
379
- container.size = container.changed.size;
380
- container.changed.success = true;
381
- } else {
382
- container.changed.success = false;
383
- }
384
- }
246
+ this.removeAttribute("loaded-3d");
247
+ this.setAttribute("loading-3d", "");
385
248
 
386
- #resetChangedFlags() {
387
- const reset = (node) => {
388
- node.changed = { pending: false, success: false };
249
+ const customEventOptions = {
250
+ bubbles: true,
251
+ cancelable: false,
252
+ composed: true,
389
253
  };
390
- Object.values(this.#data.containers).forEach(reset);
391
- Object.values(this.#data.options.materials).forEach(reset);
392
- reset(this.#data.options.camera);
393
- }
394
-
395
- // Babylon.js
396
- async #initializeBabylon() {
397
- this.#engine = new Engine(this.#canvas, true, { alpha: true });
398
- this.#engine.disableUniformBuffers = true;
399
- this.#scene = new Scene(this.#engine);
400
- this.#scene.clearColor = new Color4(1, 1, 1, 1);
401
- this.#createCamera();
402
- this.#createLights();
403
- this.#setupInteraction();
404
- await this.#createXRExperience();
405
- this.#engine.runRenderLoop(this.#renderLoop);
406
- this.#canvasResizeObserver.observe(this.#canvas);
254
+ this.dispatchEvent(new CustomEvent("scene-loading", customEventOptions));
407
255
  }
408
256
 
409
- // If this function is defined as '#renderLoop() {}' it is not executed in 'this.#engine.runRenderLoop(this.#renderLoop)'
410
- #renderLoop = () => {
411
- this.#scene && this.#scene.render();
412
- };
257
+ /**
258
+ * Handles the completion of a 3D loading operation.
259
+ * Updates loading state, sets attributes, dispatches a "scene-loaded" event, and processes the next task.
260
+ * @private
261
+ * @param {object} [detail] - Optional details to include in the event.
262
+ * @returns {void}
263
+ */
264
+ #on3DLoaded(detail) {
265
+ this.#isLoaded = true;
266
+ this.#isLoading = false;
413
267
 
414
- #addStylesToARButton() {
415
- const css = '.babylonVRicon { color: #868686; border-color: #868686; border-style: solid; margin-left: 10px; height: 50px; width: 80px; background-color: rgba(51,51,51,0.7); background-image: url(data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%222048%22%20height%3D%221152%22%20viewBox%3D%220%200%202048%201152%22%20version%3D%221.1%22%3E%3Cpath%20transform%3D%22rotate%28180%201024%2C576.0000000000001%29%22%20d%3D%22m1109%2C896q17%2C0%2030%2C-12t13%2C-30t-12.5%2C-30.5t-30.5%2C-12.5l-170%2C0q-18%2C0%20-30.5%2C12.5t-12.5%2C30.5t13%2C30t30%2C12l170%2C0zm-85%2C256q59%2C0%20132.5%2C-1.5t154.5%2C-5.5t164.5%2C-11.5t163%2C-20t150%2C-30t124.5%2C-41.5q23%2C-11%2042%2C-24t38%2C-30q27%2C-25%2041%2C-61.5t14%2C-72.5l0%2C-257q0%2C-123%20-47%2C-232t-128%2C-190t-190%2C-128t-232%2C-47l-81%2C0q-37%2C0%20-68.5%2C14t-60.5%2C34.5t-55.5%2C45t-53%2C45t-53%2C34.5t-55.5%2C14t-55.5%2C-14t-53%2C-34.5t-53%2C-45t-55.5%2C-45t-60.5%2C-34.5t-68.5%2C-14l-81%2C0q-123%2C0%20-232%2C47t-190%2C128t-128%2C190t-47%2C232l0%2C257q0%2C68%2038%2C115t97%2C73q54%2C24%20124.5%2C41.5t150%2C30t163%2C20t164.5%2C11.5t154.5%2C5.5t132.5%2C1.5zm939%2C-298q0%2C39%20-24.5%2C67t-58.5%2C42q-54%2C23%20-122%2C39.5t-143.5%2C28t-155.5%2C19t-157%2C11t-148.5%2C5t-129.5%2C1.5q-59%2C0%20-130%2C-1.5t-148%2C-5t-157%2C-11t-155.5%2C-19t-143.5%2C-28t-122%2C-39.5q-34%2C-14%20-58.5%2C-42t-24.5%2C-67l0%2C-257q0%2C-106%2040.5%2C-199t110%2C-162.5t162.5%2C-109.5t199%2C-40l81%2C0q27%2C0%2052%2C14t50%2C34.5t51%2C44.5t55.5%2C44.5t63.5%2C34.5t74%2C14t74%2C-14t63.5%2C-34.5t55.5%2C-44.5t51%2C-44.5t50%2C-34.5t52%2C-14l14%2C0q37%2C0%2070%2C0.5t64.5%2C4.5t63.5%2C12t68%2C23q71%2C30%20128.5%2C78.5t98.5%2C110t63.5%2C133.5t22.5%2C149l0%2C257z%22%20fill%3D%22white%22%20/%3E%3C/svg%3E%0A); background-size: 80%; background-repeat:no-repeat; background-position: center; border: none; outline: none; transition: transform 0.125s ease-out } .babylonVRicon:hover { transform: scale(1.05) } .babylonVRicon:active {background-color: rgba(51,51,51,1) } .babylonVRicon:focus {background-color: rgba(51,51,51,1) }.babylonVRicon.vrdisplaypresenting { background-image: none;} .vrdisplaypresenting::after { content: "EXIT"} .xr-error::after { content: "ERROR"}';
416
- const style = document.createElement("style");
417
- style.appendChild(document.createTextNode(css));
418
- this.#wrapper.appendChild(style);
419
- }
268
+ this.removeAttribute("loading-3d");
269
+ this.setAttribute("loaded-3d", "");
420
270
 
421
- async #createXRExperience() {
422
- if (this.#XRExperience) {
423
- return true;
271
+ const customEventOptions = {
272
+ bubbles: true,
273
+ cancelable: false,
274
+ composed: true,
275
+ };
276
+ if (detail) {
277
+ customEventOptions.detail = detail;
424
278
  }
279
+ this.dispatchEvent(new CustomEvent("scene-loaded", customEventOptions));
280
+ }
425
281
 
426
- const sessionMode = "immersive-ar";
427
- const sessionSupported = await WebXRSessionManager.IsSessionSupportedAsync(sessionMode);
428
- if (!sessionSupported) {
429
- console.info("PrefViewer: WebXR in mode AR is not supported");
430
- return false;
431
- }
282
+ /**
283
+ * Handles the start of a 2D loading operation.
284
+ * Updates loading state, sets attributes, and dispatches a "drawing-loading" event.
285
+ * @private
286
+ * @returns {void}
287
+ */
288
+ #on2DLoading() {
289
+ this.#isLoaded = false;
290
+ this.#isLoading = true;
432
291
 
433
- try {
434
- const ground = MeshBuilder.CreateGround("ground", { width: 1000, height: 1000 }, this.#scene);
435
- ground.isVisible = false;
436
-
437
- const options = {
438
- floorMeshes: [ground],
439
- uiOptions: {
440
- sessionMode: sessionMode,
441
- renderTarget: "xrLayer",
442
- referenceSpaceType: "local",
443
- },
444
- optionalFeatures: true,
445
- };
446
-
447
- this.#XRExperience = await WebXRDefaultExperience.CreateAsync(this.#scene, options);
448
-
449
- const featuresManager = this.#XRExperience.baseExperience.featuresManager;
450
- featuresManager.enableFeature(WebXRFeatureName.TELEPORTATION, "stable", {
451
- xrInput: this.#XRExperience.input,
452
- floorMeshes: [ground],
453
- timeToTeleport: 1500,
454
- });
455
-
456
- this.#XRExperience.baseExperience.sessionManager.onXRReady.add(() => {
457
- // Set the initial position of xrCamera: use nonVRCamera, which contains a copy of the original this.#scene.activeCamera before entering XR
458
- this.#XRExperience.baseExperience.camera.setTransformationFromNonVRCamera(this.#XRExperience.baseExperience._nonVRCamera);
459
- this.#XRExperience.baseExperience.camera.setTarget(Vector3.Zero());
460
- this.#XRExperience.baseExperience.onInitialXRPoseSetObservable.notifyObservers(this.#XRExperience.baseExperience.camera);
461
- });
462
-
463
- this.#addStylesToARButton();
464
- } catch (error) {
465
- console.warn("PrefViewer: failed to create WebXR experience", error);
466
- this.#XRExperience = null;
467
- }
468
- }
292
+ this.removeAttribute("loaded-2d");
293
+ this.setAttribute("loading-2d", "");
469
294
 
470
- #canvasResizeObserver = new ResizeObserver(() => this.#engine && this.#engine.resize());
471
-
472
- #createCamera() {
473
- this.#camera = new ArcRotateCamera("camera", (3 * Math.PI) / 2, Math.PI * 0.47, 10, Vector3.Zero(), this.#scene);
474
- this.#camera.upperBetaLimit = Math.PI * 0.48;
475
- this.#camera.lowerBetaLimit = Math.PI * 0.25;
476
- this.#camera.lowerRadiusLimit = 5;
477
- this.#camera.upperRadiusLimit = 20;
478
- this.#camera.metadata = { locked: false };
479
- this.#camera.attachControl(this.#canvas, true);
480
- this.#scene.activeCamera = this.#camera;
295
+ const customEventOptions = {
296
+ bubbles: true,
297
+ cancelable: false,
298
+ composed: true,
299
+ };
300
+ this.dispatchEvent(new CustomEvent("drawing-loading", customEventOptions));
481
301
  }
482
302
 
483
- #createLights() {
484
- this.#initEnvironmentTexture();
485
-
486
- if (this.#scene.environmentTexture) {
487
- return true;
488
- }
303
+ /**
304
+ * Handles the completion of a 2D loading operation.
305
+ * Updates loading state, sets attributes, dispatches a "drawing-loaded" event, and processes the next task.
306
+ * @private
307
+ * @param {object} [detail] - Optional details to include in the event.
308
+ * @returns {void}
309
+ */
310
+ #on2DLoaded(detail) {
311
+ this.#isLoaded = true;
312
+ this.#isLoading = false;
489
313
 
490
- // 1) Stronger ambient fill
491
- this.#hemiLight = new HemisphericLight("hemiLight", new Vector3(-10, 10, -10), this.#scene);
492
- this.#hemiLight.intensity = 0.6;
493
-
494
- // 2) Directional light from the front-right, angled slightly down
495
- this.#dirLight = new DirectionalLight("dirLight", new Vector3(-10, 10, -10), this.#scene);
496
- this.#dirLight.position = new Vector3(5, 4, 5); // light is IN FRONT + ABOVE + to the RIGHT
497
- this.#dirLight.intensity = 0.6;
498
-
499
- // // 3) Soft shadows
500
- this.#shadowGen = new ShadowGenerator(1024, this.#dirLight);
501
- this.#shadowGen.useBlurExponentialShadowMap = true;
502
- this.#shadowGen.blurKernel = 16;
503
- this.#shadowGen.darkness = 0.5;
504
-
505
- // 4) Camera‐attached headlight
506
- this.#cameraLight = new PointLight("pl", this.#camera.position, this.#scene);
507
- this.#cameraLight.parent = this.#camera;
508
- this.#cameraLight.intensity = 0.3;
509
- }
314
+ this.removeAttribute("loading-2d");
315
+ this.setAttribute("loaded-2d", "");
510
316
 
511
- #initEnvironmentTexture() {
512
- return false;
513
- if (this.#scene.environmentTexture) {
514
- return true;
317
+ const customEventOptions = {
318
+ bubbles: true,
319
+ cancelable: false,
320
+ composed: true,
321
+ };
322
+ if (detail) {
323
+ customEventOptions.detail = detail;
515
324
  }
516
- const hdrTextureURI = "../src/environments/noon_grass.hdr";
517
- const hdrTexture = new HDRCubeTexture(hdrTextureURI, this.#scene, 128);
518
- hdrTexture.gammaSpace = true;
519
- hdrTexture._noMipmap = false;
520
- hdrTexture.level = 2.0;
521
- this.#scene.environmentTexture = hdrTexture;
325
+ this.dispatchEvent(new CustomEvent("drawing-loaded", customEventOptions));
522
326
  }
523
327
 
524
- #initIBLShadows() {
525
- if (!this.#scene.environmentTexture) {
526
- return false;
527
- }
528
-
529
- let createIBLShadowPipeline = function (scene) {
530
- const pipeline = new IblShadowsRenderPipeline(
531
- "iblShadowsPipeline",
532
- scene,
533
- {
534
- resolutionExp: 7,
535
- sampleDirections: 2,
536
- ssShadowsEnabled: true,
537
- shadowRemanence: 0.8,
538
- triPlanarVoxelization: true,
539
- shadowOpacity: 0.8,
540
- },
541
- [scene.activeCamera]
542
- );
543
- pipeline.allowDebugPasses = false;
544
- pipeline.gbufferDebugEnabled = true;
545
- pipeline.importanceSamplingDebugEnabled = false;
546
- pipeline.voxelDebugEnabled = false;
547
- pipeline.voxelDebugDisplayMip = 1;
548
- pipeline.voxelDebugAxis = 2;
549
- pipeline.voxelTracingDebugEnabled = false;
550
- pipeline.spatialBlurPassDebugEnabled = false;
551
- pipeline.accumulationPassDebugEnabled = false;
552
- return pipeline;
328
+ /**
329
+ * Handles the "drawing-zoom-changed" event from the 2D viewer component.
330
+ * Dispatches a custom "drawing-zoom-changed" event from the PrefViewer element, forwarding the event detail to external listeners.
331
+ * @private
332
+ * @param {CustomEvent} event - The original zoom change event from the 2D viewer.
333
+ * @returns {void}
334
+ */
335
+ #on2DZoomChanged(event) {
336
+ const customEventOptions = {
337
+ bubbles: true,
338
+ cancelable: false,
339
+ composed: true,
340
+ detail: event.detail,
553
341
  };
554
-
555
- let iblShadowsPipeline = createIBLShadowPipeline(this.#scene);
556
-
557
- this.#scene.meshes.forEach((mesh) => {
558
- if (mesh.id.startsWith("__root__") || mesh.name === "hdri") {
559
- return false;
560
- }
561
- iblShadowsPipeline.addShadowCastingMesh(mesh);
562
- iblShadowsPipeline.updateSceneBounds();
563
- });
564
-
565
- this.#scene.materials.forEach((material) => {
566
- iblShadowsPipeline.addShadowReceivingMaterial(material);
567
- });
342
+ this.dispatchEvent(new CustomEvent("drawing-zoom-changed", customEventOptions));
568
343
  }
569
344
 
570
- #initShadows() {
571
- if (!this.#scene.environmentTexture) {
572
- this.#initIBLShadows();
573
- return true;
345
+ /**
346
+ * Processes a configuration object by loading it into the 3D component.
347
+ * Dispatches loading events and processes the next task when finished.
348
+ * @private
349
+ * @param {object} config - The configuration object to process.
350
+ * @returns {void}
351
+ */
352
+ #processConfig(config) {
353
+ if (!this.#component3D) {
354
+ return;
574
355
  }
575
356
 
576
- this.#scene.meshes.forEach((mesh) => {
577
- if (mesh.id.startsWith("__root__")) {
578
- return false;
579
- }
580
- mesh.receiveShadows = true;
581
- if (!mesh.name === "hdri") {
582
- this.#shadowGen.addShadowCaster(mesh, true);
583
- }
357
+ this.#on3DLoading();
358
+ this.#component3D.load(config).then((detail) => {
359
+ this.#on3DLoaded(detail);
360
+ this.#processNextTask();
584
361
  });
585
362
  }
586
363
 
587
- #setMaxSimultaneousLights() {
588
- let lightsNumber = 1; // Como mínimo una luz correspondiente a la textura de environmentTexture
589
- this.#scene.lights.forEach((light) => {
590
- if (light.isEnabled()) {
591
- ++lightsNumber;
592
- }
593
- });
594
- if (this.#scene.materials) {
595
- this.#scene.materials.forEach((material) => (material.maxSimultaneousLights = lightsNumber));
364
+ /**
365
+ * Processes a drawing object by loading it into the 2D component.
366
+ * Processes the next task when finished.
367
+ * @private
368
+ * @param {object} drawing - The drawing object to process.
369
+ * @returns {void}
370
+ */
371
+ #processDrawing(drawing) {
372
+ if (!this.#component2D) {
373
+ return;
596
374
  }
597
- }
598
-
599
- #setupInteraction() {
600
- this.#canvas.addEventListener("wheel", (event) => {
601
- if (!this.#scene || !this.#camera) {
602
- return false;
603
- }
604
- //const pick = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
605
- //this.#camera.target = pick.hit ? pick.pickedPoint.clone() : this.#camera.target;
606
- if (!this.#scene.activeCamera.metadata?.locked) {
607
- this.#scene.activeCamera.inertialRadiusOffset -= event.deltaY * this.#scene.activeCamera.wheelPrecision * 0.001;
608
- }
609
- event.preventDefault();
610
- });
611
- }
612
-
613
- #disposeEngine() {
614
- if (!this.#engine) return;
615
- this.#engine.dispose();
616
- this.#engine = this.#scene = this.#camera = null;
617
- this.#hemiLight = this.#dirLight = this.#cameraLight = null;
618
- this.#shadowGen = null;
619
- }
620
-
621
- // Utility methods for loading gltf/glb
622
- async #getServerFileDataHeader(uri) {
623
- return new Promise((resolve) => {
624
- const xhr = new XMLHttpRequest();
625
- xhr.open("HEAD", uri, true);
626
- xhr.responseType = "blob";
627
- xhr.onload = () => {
628
- if (xhr.status === 200) {
629
- const size = parseInt(xhr.getResponseHeader("Content-Length"));
630
- const timeStamp = new Date(xhr.getResponseHeader("Last-Modified")).toISOString();
631
- resolve([size, timeStamp]);
632
- } else {
633
- resolve([0, null]);
634
- }
635
- };
636
- xhr.onerror = () => {
637
- resolve([0, null]);
638
- };
639
- xhr.send();
640
- });
641
- }
642
375
 
643
- #transformUrl(url) {
644
- return new Promise((resolve) => {
645
- resolve(url.replace(/^blob:[^/]+\//i, "").replace(/\\/g, "/"));
376
+ this.#on2DLoading();
377
+ this.#component2D.load(drawing).then((detail) => {
378
+ this.#on2DLoaded(detail);
379
+ this.#processNextTask();
646
380
  });
647
381
  }
648
382
 
649
- #decodeBase64(base64) {
650
- const [, payload] = base64.split(",");
651
- const raw = payload || base64;
652
- let decoded = "";
653
- let blob = null;
654
- let extension = null;
655
- let size = raw.length;
656
- try {
657
- decoded = atob(raw);
658
- } catch {
659
- return { blob, extension, size };
660
- }
661
- let isJson = false;
662
- try {
663
- JSON.parse(decoded);
664
- isJson = true;
665
- } catch { }
666
- extension = isJson ? ".gltf" : ".glb";
667
- const type = isJson ? "model/gltf+json" : "model/gltf-binary";
668
- const array = Uint8Array.from(decoded, (c) => c.charCodeAt(0));
669
- blob = new Blob([array], { type });
670
- return { blob, extension, size };
671
- }
672
-
673
- async #initStorage(db, table) {
674
- if (window.gltfDB && window.gltfDB.name === db && window.gltfDB.objectStoreNames.contains(table)) {
675
- return true;
676
- }
677
- await initDb(db, table);
678
- }
679
-
680
- // Methods for managing Asset Containers
681
- #setVisibilityOfWallAndFloorInModel(show) {
682
- if (!this.#data.containers.model.assetContainer || !this.#data.containers.model.visible) {
683
- return false;
684
- }
685
- show = show !== undefined ? show : this.#data.containers.environment.visible;
686
- const prefixes = Object.values(this.#data.options.materials).map((material) => material.prefix);
687
- this.#data.containers.model.assetContainer.meshes.filter((meshToFilter) => prefixes.some((prefix) => meshToFilter.name.startsWith(prefix))).forEach((mesh) => mesh.setEnabled(show));
688
- }
689
-
690
- #setOptionsMaterial(optionMaterial) {
691
- if (!optionMaterial || !optionMaterial.prefix || !optionMaterial.value) {
692
- return false;
693
- }
694
-
695
- const material = this.#data.containers.materials.assetContainer?.materials.find((mat) => mat.name === optionMaterial.value) || null;
696
- if (!material) {
697
- return false;
698
- }
699
-
700
- const containers = [];
701
- if (this.#data.containers.model.assetContainer && (this.#data.containers.model.changed.pending || this.#data.containers.materials.changed.pending || optionMaterial.changed.pending)) {
702
- containers.push(this.#data.containers.model.assetContainer);
703
- }
704
- if (this.#data.containers.environment.assetContainer && (this.#data.containers.environment.changed.pending || this.#data.containers.materials.changed.pending || optionMaterial.changed.pending)) {
705
- containers.push(this.#data.containers.environment.assetContainer);
706
- }
707
- if (containers.length === 0) {
708
- return false;
709
- }
710
-
711
- let someSetted = false;
712
- containers.forEach((container) =>
713
- container.meshes
714
- .filter((meshToFilter) => meshToFilter.name.startsWith(optionMaterial.prefix))
715
- .forEach((mesh) => {
716
- mesh.material = material;
717
- someSetted = true;
718
- })
719
- );
720
-
721
- if (someSetted) {
722
- optionMaterial.changed.success = true;
723
- } else if (optionMaterial.changed.pending) {
724
- optionMaterial.value = optionMaterial.changed.value;
725
- optionMaterial.changed.success = false;
726
- }
727
-
728
- return someSetted;
729
- }
730
-
731
- #setOptionsMaterials() {
732
- let someSetted = false;
733
- Object.values(this.#data.options.materials).forEach((material) => {
734
- let settedMaterial = this.#setOptionsMaterial(material);
735
- someSetted = someSetted || settedMaterial;
736
- });
737
- return someSetted;
383
+ /**
384
+ * Processes an environment (scene) object by wrapping it in a config and loading it.
385
+ * @private
386
+ * @param {object} environment - The environment/scene object to process.
387
+ * @returns {void}
388
+ */
389
+ #processEnvironment(environment) {
390
+ const config = {};
391
+ config.scene = environment;
392
+ this.#processConfig(config);
738
393
  }
739
394
 
740
- #setOptionsCamera() {
741
- if (!this.#data.options.camera.value && !this.#data.options.camera.changed.pending && !this.#data.containers.model.changed.pending && !this.#data.containers.environment.changed.pending) {
742
- return false;
743
- }
744
-
745
- let camera = this.#data.containers.model.assetContainer?.cameras.find((thisCamera) => thisCamera.name === this.#data.options.camera.value) || this.#data.containers.environment.assetContainer?.cameras.find((thisCamera) => thisCamera.name === this.#data.options.camera.value) || null;
746
- if (!camera) {
747
- if (this.#data.options.camera.changed.value && this.#data.options.camera.changed.value !== this.#data.options.camera.value) {
748
- camera = this.#data.containers.model.assetContainer?.cameras.find((thisCamera) => thisCamera.name === this.#data.options.camera.changed.value) || this.#data.containers.environment.assetContainer?.cameras.find((thisCamera) => thisCamera.name === this.#data.options.camera.changed.value) || null;
749
- }
750
- if (camera) {
751
- camera.metadata = { locked: this.#data.options.camera.changed.locked };
752
- this.#data.options.camera.value = this.#data.options.camera.changed.value;
753
- this.#data.options.camera.locked = this.#data.options.camera.changed.locked;
754
- this.#data.options.camera.changed.success = false;
755
- } else {
756
- camera = this.#camera;
757
- this.#data.options.camera.value = null;
758
- this.#data.options.camera.locked = this.#camera.metadata.locked;
759
- this.#data.options.camera.changed.success = false;
760
- }
761
- } else {
762
- camera.metadata = { locked: this.#data.options.camera.locked };
763
- if (this.#data.options.camera.changed.pending) {
764
- this.#data.options.camera.changed.success = true;
765
- }
766
- }
767
- if (!this.#data.options.camera.locked && this.#data.options.camera.value !== null) {
768
- camera.attachControl(this.#canvas, true);
769
- }
770
- this.#scene.activeCamera = camera;
771
- return true;
395
+ /**
396
+ * Processes a materials object by wrapping it in a config and loading it.
397
+ * @private
398
+ * @param {object} materials - The materials object to process.
399
+ * @returns {void}
400
+ */
401
+ #processMaterials(materials) {
402
+ const config = {};
403
+ config.materials = materials;
404
+ this.#processConfig(config);
772
405
  }
773
406
 
774
- #addContainer(container) {
775
- if (!container.assetContainer || container.visible || !container.show) {
776
- return false;
777
- }
778
-
779
- container.assetContainer.addAllToScene();
780
- container.visible = true;
781
- return true;
407
+ /**
408
+ * Processes a model object by wrapping it in a config and loading it.
409
+ * @private
410
+ * @param {object} model - The model object to process.
411
+ * @returns {void}
412
+ */
413
+ #processModel(model) {
414
+ const config = {};
415
+ config.model = model;
416
+ this.#processConfig(config);
782
417
  }
783
418
 
784
- #removeContainer(container) {
785
- if (!container.assetContainer || !container.visible) {
786
- return false;
419
+ /**
420
+ * Processes viewer options by loading them into the 3D component.
421
+ * Dispatches loading events and processes the next task.
422
+ * @private
423
+ * @param {object} options - The options object to process.
424
+ * @returns {void}
425
+ */
426
+ #processOptions(options) {
427
+ if (!this.#component3D) {
428
+ return;
787
429
  }
788
430
 
789
- container.assetContainer.removeAllFromScene();
790
- container.visible = false;
791
- return true;
792
- }
793
-
794
- #replaceContainer(container, newAssetContainer) {
795
- if (container.assetContainer) {
796
- this.#removeContainer(container);
797
- container.assetContainer.dispose();
798
- container.assetContainer = null;
799
- }
800
- this.#scene.getEngine().releaseEffects();
801
- container.assetContainer = newAssetContainer;
802
- this.#addContainer(container);
803
- return true;
431
+ this.#on3DLoading();
432
+ const detail = this.#component3D.setOptions(options);
433
+ this.#on3DLoaded(detail);
434
+ this.#processNextTask();
804
435
  }
805
436
 
806
437
  /**
807
- * Replace internal URIs in a glTF AssetContainer JSON with URLs pointing to files stored in IndexedDB.
808
- * @param {JSON} assetContainerJSON AssetContainer in glTF (JSON) (modified in-place).
809
- * @param {URL} [assetContainerURL] Optional URL of the AssetContainer. Used as the base path to resolve relative URIs.
810
- * @returns {Promise<void>} Resolves when all applicable URIs have been resolved/replaced.
811
- * @description
812
- * - When provided, assetContainerURL is used as the base path for other scene files (binary buffers and all images).
813
- * If not provided (null/undefined), it is because it is the assetContainer of the model or materials whose URIs are absolute.
814
- * - According to the glTF 2.0 spec, only items inside the "buffers" and "images" arrays may have a "uri" property.
815
- * - Data URIs (embedded base64) are ignored and left unchanged.
816
- * - Matching asset URIs are normalized (backslashes converted to forward slashes) and passed to the FileStorage layer
817
- * to obtain a usable URL (object URL or cached URL).
818
- * - The function performs replacements in parallel and waits for all lookups to complete.
819
- * - The JSON is updated in-place with the resolved URLs.
820
- * @see {@link https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#uris|glTF™ 2.0 Specification - URIs}
438
+ * Processes visibility configuration for the model and scene.
439
+ * Shows or hides the model and/or scene based on the config, then processes the next task.
440
+ * @private
441
+ * @param {object} config - The visibility configuration object.
442
+ * @returns {void}
821
443
  */
822
- async #replaceSceneURIAsync(assetContainerJSON, assetContainerURL) {
823
- if (!assetContainerJSON) {
444
+ #processVisibility(config) {
445
+ if (!this.#component3D) {
824
446
  return;
825
447
  }
826
-
827
- let sceneURLBase = assetContainerURL;
828
-
829
- if (typeof assetContainerURL === "string") {
830
- const lastIndexOfSlash = assetContainerURL.lastIndexOf("/");
831
- if (lastIndexOfSlash !== -1) {
832
- sceneURLBase = assetContainerURL.substring(0, lastIndexOfSlash + 1);
833
- }
834
- }
835
-
836
- const arrayOfAssetsWithURI = [];
837
-
838
- /**
839
- * Check whether a value is a syntactically absolute URL.
840
- * @param {string} url Value to test.
841
- * @returns {boolean} True when `url` is a string that can be parsed by the global URL constructor (i.e. a syntactically absolute URL); false otherwise.
842
- * @description
843
- * - Returns false for non-string inputs.
844
- * - Uses the browser's URL parser, so protocol-relative URLs ("//host/...") and relative paths are considered non-absolute.
845
- * - This is a syntactic check only — it does not perform network requests or validate reachability/CORS.
846
- */
847
- var isURLAbsolute = function (url) {
848
- if (typeof url !== "string") {
849
- return false;
850
- }
851
- try {
852
- new URL(url);
853
- return true;
854
- } catch {
855
- return false;
856
- }
857
- };
858
-
859
- /**
860
- * Collect asset entries that have an external URI (non-data URI) and store a normalized absolute/relative-resolved URI for later replacement.
861
- * @param {Object} asset glTF asset entry (an element of buffers[] or images[]).
862
- * @param {number} index Index of the asset within its parent array.
863
- * @param {Array} array Reference to the parent array (buffers or images).
864
- * @returns {void} Side-effect: pushes a record into arrayOfAssetsWithURI when applicable { parent: <array>, index: <number>, uri: <string> }.
865
- */
866
- var saveAssetData = function (asset, index, array) {
867
- if (asset.uri && !asset.uri.startsWith("data:")) {
868
- const assetData = {
869
- parent: array,
870
- index: index,
871
- uri: `${!isURLAbsolute(asset.uri) && sceneURLBase ? sceneURLBase : ""}${asset.uri}`.replace(/\\\\|\\|\/\\/g, "/"),
872
- };
873
- arrayOfAssetsWithURI.push(assetData);
874
- }
875
- };
876
-
877
- if (assetContainerJSON.buffers) {
878
- assetContainerJSON.buffers.forEach((asset, index, array) => saveAssetData(asset, index, array));
448
+ const showModel = config.model?.visible;
449
+ const showScene = config.scene?.visible;
450
+ if (showModel !== undefined) {
451
+ showModel ? this.#component3D.showModel() : this.#component3D.hideModel();
879
452
  }
880
- if (assetContainerJSON.images) {
881
- assetContainerJSON.images.forEach((asset, index, array) => saveAssetData(asset, index, array));
453
+ if (showScene !== undefined) {
454
+ showScene ? this.#component3D.showEnvironment() : this.#component3D.hideEnvironment();
882
455
  }
883
-
884
- // Replace parallel URIs so that if files are not stored yet, they are downloaded in parallel
885
- const promisesArray = arrayOfAssetsWithURI.map(async (asset) => {
886
- const uri = await this.#fileStorage.getURL(asset.uri);
887
- if (uri) {
888
- asset.parent[asset.index].uri = uri;
889
- }
890
- });
891
- await Promise.all(promisesArray);
456
+ this.#processNextTask();
892
457
  }
893
458
 
894
- async #loadAssetContainer(container) {
895
- let storage = container?.storage;
896
-
897
- if (!storage) {
898
- return false;
899
- }
900
-
901
- let source = storage.url || null;
902
-
903
- if (storage.db && storage.table && storage.id) {
904
- await this.#initStorage(storage.db, storage.table);
905
- const object = await loadModel(storage.id, storage.table);
906
- source = object.data;
907
- if (object.timeStamp === container.timeStamp) {
908
- container.changed = { pending: false, success: false };
909
- return false;
910
- } else {
911
- container.changed = { pending: true, size: object.size, success: false, timeStamp: object.timeStamp };
912
- }
913
- }
914
-
915
- if (!source) {
916
- return false;
917
- }
459
+ /**
460
+ * ---------------------------
461
+ * Public methods
462
+ * ---------------------------
463
+ */
918
464
 
919
- let file = null;
920
-
921
- let { blob, extension, size } = this.#decodeBase64(source);
922
- if (blob && extension) {
923
- if ((container.name === "model" || container.name === "materials") && extension === ".gltf") {
924
- const assetContainerJSON = JSON.parse(await blob.text());
925
- await this.#replaceSceneURIAsync(assetContainerJSON, source);
926
- source = `data:${JSON.stringify(assetContainerJSON)}`;
927
- } else {
928
- file = new File([blob], `${container.name}${extension}`, {
929
- type: blob.type,
930
- });
931
- }
932
- if (!container.changed.pending) {
933
- if (container.timeStamp === null && container.size === size) {
934
- container.changed = { pending: false, success: false };
935
- return false;
936
- } else {
937
- container.changed = { pending: true, size: size, success: false, timeStamp: null };
938
- }
939
- }
465
+ /**
466
+ * Sets the viewer mode to "2d" or "3d" and updates component visibility accordingly.
467
+ * @public
468
+ * @param {string} [mode=this.#mode] - The mode to set ("2d" or "3d").
469
+ * @returns {void}
470
+ */
471
+ setMode(mode = this.#mode) {
472
+ if (mode !== "2d" && mode !== "3d") {
473
+ console.warn(`PrefViewer: invalid mode "${mode}". Allowed modes are "2d" and "3d".`);
474
+ mode = this.#mode;
475
+ }
476
+ this.#mode = mode;
477
+ if (mode === "2d") {
478
+ this.#component3D?.hide();
479
+ this.#component2D?.show();
940
480
  } else {
941
- const extMatch = source.match(/\.(gltf|glb)(\?|#|$)/i);
942
- extension = extMatch ? `.${extMatch[1].toLowerCase()}` : ".gltf";
943
- let [fileSize, fileTimeStamp] = [await this.#fileStorage.getSize(source), await this.#fileStorage.getTimeStamp(source)];
944
- if (container.size === fileSize && container.timeStamp === fileTimeStamp) {
945
- container.changed = { pending: false, success: false };
946
- return false;
947
- } else {
948
- container.changed = { pending: true, size: fileSize, success: false, timeStamp: fileTimeStamp };
949
- if (extension === ".gltf") {
950
- const assetContainerBlob = await this.#fileStorage.getBlob(source);
951
- const assetContainerJSON = JSON.parse(await assetContainerBlob.text());
952
- await this.#replaceSceneURIAsync(assetContainerJSON, source);
953
- source = `data:${JSON.stringify(assetContainerJSON)}`;
954
- } else {
955
- source = await this.#fileStorage.getURL(source);
956
- }
957
- }
481
+ this.#component3D?.show();
482
+ this.#component2D?.hide();
958
483
  }
959
-
960
- // https://doc.babylonjs.com/typedoc/interfaces/BABYLON.LoadAssetContainerOptions
961
- let options = {
962
- pluginExtension: extension,
963
- pluginOptions: {
964
- gltf: {
965
- compileMaterials: true,
966
- loadAllMaterials: true,
967
- loadOnlyMaterials: container.name === "materials",
968
- //preprocessUrlAsync: this.#transformUrl,
969
- },
970
- },
971
- };
972
-
973
- return LoadAssetContainerAsync(file || source, this.#scene, options);
974
- }
975
-
976
- async #loadContainers(loadModel = true, loadEnvironment = true, loadMaterials = true) {
977
- this.#engine.stopRenderLoop(this.#renderLoop);
978
-
979
- const promiseArray = [];
980
- promiseArray.push(loadModel ? this.#loadAssetContainer(this.#data.containers.model) : false);
981
- promiseArray.push(loadEnvironment ? this.#loadAssetContainer(this.#data.containers.environment) : false);
982
- promiseArray.push(loadMaterials ? this.#loadAssetContainer(this.#data.containers.materials) : false);
983
-
984
- Promise.allSettled(promiseArray)
985
- .then((values) => {
986
- const modelContainer = values[0];
987
- const environmentContainer = values[1];
988
- const materialsContainer = values[2];
989
-
990
- if (modelContainer.status === "fulfilled" && modelContainer.value) {
991
- modelContainer.value.lights = [];
992
- this.#replaceContainer(this.#data.containers.model, modelContainer.value);
993
- this.#storeChangedFlagsForContainer(this.#data.containers.model, true);
994
- } else {
995
- if (this.#data.containers.model.assetContainer && this.#data.containers.model.show !== this.#data.containers.model.visible) {
996
- this.#data.containers.model.show ? this.#addContainer(this.#data.containers.model) : this.#removeContainer(this.#data.containers.model);
997
- }
998
- this.#storeChangedFlagsForContainer(this.#data.containers.model, false);
999
- }
1000
-
1001
- if (environmentContainer.status === "fulfilled" && environmentContainer.value) {
1002
- this.#replaceContainer(this.#data.containers.environment, environmentContainer.value);
1003
- this.#storeChangedFlagsForContainer(this.#data.containers.environment, true);
1004
- } else {
1005
- if (this.#data.containers.environment.assetContainer && this.#data.containers.environment.show !== this.#data.containers.environment.visible) {
1006
- this.#data.containers.environment.show ? this.#addContainer(this.#data.containers.environment) : this.#removeContainer(this.#data.containers.environment);
1007
- }
1008
- this.#storeChangedFlagsForContainer(this.#data.containers.environment, false);
1009
- }
1010
-
1011
- if (materialsContainer.status === "fulfilled" && materialsContainer.value) {
1012
- this.#replaceContainer(this.#data.containers.materials, materialsContainer.value);
1013
- this.#storeChangedFlagsForContainer(this.#data.containers.materials, true);
1014
- } else {
1015
- this.#storeChangedFlagsForContainer(this.#data.containers.materials, false);
1016
- }
1017
-
1018
- this.#setOptionsMaterials();
1019
- this.#setOptionsCamera();
1020
- this.#setVisibilityOfWallAndFloorInModel();
1021
- })
1022
- .catch((error) => {
1023
- this.loaded = true;
1024
- console.error("PrefViewer: failed to load model", error);
1025
- this.dispatchEvent(
1026
- new CustomEvent("scene-error", {
1027
- bubbles: true,
1028
- cancelable: false,
1029
- composed: true,
1030
- detail: { error: error },
1031
- })
1032
- );
1033
- })
1034
- .finally(async () => {
1035
- this.#setMaxSimultaneousLights();
1036
- this.#initShadows();
1037
- await this.#setStatusLoaded();
1038
- });
1039
- }
1040
-
1041
- // Tasks
1042
- #addTaskToQueue(value, type) {
1043
- this.#taskQueue.push(new PrefViewerTask(value, type));
1044
- if (this.initialized && !this.loading) {
1045
- this.#processNextTask();
484
+ if (this.getAttribute("mode") !== mode) {
485
+ this.setAttribute("mode", mode);
1046
486
  }
1047
487
  }
1048
488
 
1049
- #processNextTask() {
1050
- if (!this.#taskQueue.length) {
1051
- return false;
1052
- }
1053
- const task = this.#taskQueue[0];
1054
- this.#taskQueue.shift();
1055
- switch (task.type) {
1056
- case PrefViewerTask.Types.Config:
1057
- this.#processConfig(task.value);
1058
- break;
1059
- case PrefViewerTask.Types.Model:
1060
- this.#processModel(task.value);
1061
- break;
1062
- case PrefViewerTask.Types.Environment:
1063
- this.#processEnvironment(task.value);
1064
- break;
1065
- case PrefViewerTask.Types.Materials:
1066
- this.#processMaterials(task.value);
1067
- break;
1068
- case PrefViewerTask.Types.Options:
1069
- this.#processOptions(task.value);
1070
- break;
1071
- }
1072
- }
1073
-
1074
- #processConfig(config) {
1075
- this.#setStatusLoading();
1076
-
1077
- // Containers
1078
- const loadModel = !!config.model?.storage;
1079
- this.#data.containers.model.changed.pending = loadModel;
1080
- this.#data.containers.model.changed.success = false;
1081
- this.#data.containers.model.changed.storage = this.#data.containers.model.storage;
1082
- this.#data.containers.model.storage = loadModel ? config.model.storage : this.#data.containers.model.storage;
1083
- this.#data.containers.model.show = config.model?.visible !== undefined ? config.model.visible : this.#data.containers.model.show;
1084
-
1085
- const loadEnvironment = !!config.scene?.storage;
1086
- this.#data.containers.environment.changed.pending = loadEnvironment;
1087
- this.#data.containers.environment.changed.success = false;
1088
- this.#data.containers.environment.changed.storage = this.#data.containers.environment.storage;
1089
- this.#data.containers.environment.storage = loadEnvironment ? config.scene.storage : this.#data.containers.environment.storage;
1090
- this.#data.containers.environment.show = config.scene?.visible !== undefined ? config.scene.visible : this.#data.containers.environment.show;
1091
-
1092
- const loadMaterials = !!config.materials?.storage;
1093
- this.#data.containers.materials.changed.pending = loadMaterials;
1094
- this.#data.containers.materials.changed.success = false;
1095
- this.#data.containers.materials.changed.storage = this.#data.containers.materials.storage;
1096
- this.#data.containers.materials.storage = loadMaterials ? config.materials.storage : this.#data.containers.materials.storage;
1097
-
1098
- // Options
1099
- if (config.options) {
1100
- this.#checkCameraChanged(config.options);
1101
- this.#checkMaterialsChanged(config.options);
1102
- }
1103
-
1104
- this.#loadContainers(loadModel, loadEnvironment, loadMaterials);
1105
- }
1106
-
1107
- #processModel(model) {
1108
- this.#setStatusLoading();
1109
-
1110
- const loadModel = !!model.storage;
1111
- this.#data.containers.model.changed.pending = loadModel;
1112
- this.#data.containers.model.changed.success = false;
1113
- this.#data.containers.model.changed.storage = this.#data.containers.model.storage;
1114
- this.#data.containers.model.storage = loadModel ? model.storage : this.#data.containers.model.storage;
1115
- this.#data.containers.model.show = model.visible !== undefined ? model.visible : this.#data.containers.model.show;
1116
-
1117
- this.initialized && this.#loadContainers(loadModel, false, false);
1118
- }
1119
-
1120
- #processEnvironment(environment) {
1121
- this.#setStatusLoading();
1122
-
1123
- const loadEnvironment = !!environment.storage;
1124
- this.#data.containers.environment.changed.pending = loadEnvironment;
1125
- this.#data.containers.environment.changed.success = false;
1126
- this.#data.containers.environment.changed.storage = this.#data.containers.environment.storage;
1127
- this.#data.containers.environment.storage = loadEnvironment ? environment.storage : this.#data.containers.environment.storage;
1128
- this.#data.containers.environment.show = environment.visible !== undefined ? environment.visible : this.#data.containers.environment.show;
1129
-
1130
- this.#loadContainers(false, loadEnvironment, false);
1131
- }
1132
-
1133
- #processMaterials(materials) {
1134
- this.#setStatusLoading();
1135
-
1136
- const loadMaterials = !!materials.storage;
1137
- this.#data.containers.materials.changed.pending = loadMaterials;
1138
- this.#data.containers.materials.changed.success = false;
1139
- this.#data.containers.materials.changed.storage = this.#data.containers.materials.storage;
1140
- this.#data.containers.materials.storage = loadMaterials ? materials.storage : this.#data.containers.materials.storage;
1141
-
1142
- this.#loadContainers(false, false, loadMaterials);
1143
- }
1144
-
1145
- async #processOptions(options) {
1146
- this.#setStatusLoading();
1147
-
1148
- let someSetted = false;
1149
- if (this.#checkCameraChanged(options)) {
1150
- someSetted = someSetted || this.#setOptionsCamera();
1151
- }
1152
- if (this.#checkMaterialsChanged(options)) {
1153
- someSetted = someSetted || this.#setOptionsMaterials();
1154
- }
1155
-
1156
- await this.#setStatusLoaded();
1157
-
1158
- return someSetted;
1159
- }
1160
-
1161
- // Public Methods
489
+ /**
490
+ * Loads a configuration object or JSON string and adds it to the task queue.
491
+ * If the config contains a drawing, adds a drawing task as well.
492
+ * @public
493
+ * @param {object|string} config - Configuration object or JSON string.
494
+ * @returns {boolean|void} Returns false if config is invalid; otherwise void.
495
+ */
1162
496
  loadConfig(config) {
1163
497
  config = typeof config === "string" ? JSON.parse(config) : config;
1164
498
  if (!config) {
1165
499
  return false;
1166
500
  }
501
+ if (config.drawing) {
502
+ this.#addTaskToQueue(config.drawing, "drawing");
503
+ }
1167
504
  this.#addTaskToQueue(config, "config");
1168
505
  }
1169
506
 
507
+ /**
508
+ * Loads a model object or JSON string and adds it to the task queue.
509
+ * @public
510
+ * @param {object|string} model - Model object or JSON string.
511
+ * @returns {boolean|void} Returns false if model is invalid; otherwise void.
512
+ */
1170
513
  loadModel(model) {
1171
514
  model = typeof model === "string" ? JSON.parse(model) : model;
1172
515
  if (!model) {
@@ -1175,6 +518,12 @@ class PrefViewer extends HTMLElement {
1175
518
  this.#addTaskToQueue(model, "model");
1176
519
  }
1177
520
 
521
+ /**
522
+ * Loads a scene/environment object or JSON string and adds it to the task queue.
523
+ * @public
524
+ * @param {object|string} scene - Scene object or JSON string.
525
+ * @returns {boolean|void} Returns false if scene is invalid; otherwise void.
526
+ */
1178
527
  loadScene(scene) {
1179
528
  scene = typeof scene === "string" ? JSON.parse(scene) : scene;
1180
529
  if (!scene) {
@@ -1183,6 +532,12 @@ class PrefViewer extends HTMLElement {
1183
532
  this.#addTaskToQueue(scene, "environment");
1184
533
  }
1185
534
 
535
+ /**
536
+ * Loads materials object or JSON string and adds it to the task queue.
537
+ * @public
538
+ * @param {object|string} materials - Materials object or JSON string.
539
+ * @returns {boolean|void} Returns false if materials are invalid; otherwise void.
540
+ */
1186
541
  loadMaterials(materials) {
1187
542
  materials = typeof materials === "string" ? JSON.parse(materials) : materials;
1188
543
  if (!materials) {
@@ -1191,6 +546,26 @@ class PrefViewer extends HTMLElement {
1191
546
  this.#addTaskToQueue(materials, "materials");
1192
547
  }
1193
548
 
549
+ /**
550
+ * Loads a drawing object or JSON string and adds it to the task queue.
551
+ * @public
552
+ * @param {object|string} drawing - Drawing object or JSON string.
553
+ * @returns {boolean|void} Returns false if drawing is invalid; otherwise void.
554
+ */
555
+ loadDrawing(drawing) {
556
+ drawing = typeof drawing === "string" ? JSON.parse(drawing) : drawing;
557
+ if (!drawing) {
558
+ return false;
559
+ }
560
+ this.#addTaskToQueue(drawing, "drawing");
561
+ }
562
+
563
+ /**
564
+ * Sets viewer options from an object or JSON string and adds them to the task queue.
565
+ * @public
566
+ * @param {object|string} options - Options object or JSON string.
567
+ * @returns {boolean|void} Returns false if options are invalid; otherwise void.
568
+ */
1194
569
  setOptions(options) {
1195
570
  options = typeof options === "string" ? JSON.parse(options) : options;
1196
571
  if (!options) {
@@ -1199,55 +574,204 @@ class PrefViewer extends HTMLElement {
1199
574
  this.#addTaskToQueue(options, "options");
1200
575
  }
1201
576
 
577
+ /**
578
+ * Shows the 3D model by setting its visibility to true.
579
+ * Adds a visibility task to the queue for processing.
580
+ * @public
581
+ * @returns {void}
582
+ */
1202
583
  showModel() {
1203
- this.#data.containers.model.show = true;
1204
- this.#addContainer(this.#data.containers.model);
584
+ const config = { model: { visible: true } };
585
+ this.#addTaskToQueue(config, "visibility");
1205
586
  }
1206
587
 
588
+ /**
589
+ * Hides the 3D model by setting its visibility to false.
590
+ * Adds a visibility task to the queue for processing.
591
+ * @public
592
+ * @returns {void}
593
+ */
1207
594
  hideModel() {
1208
- this.#data.containers.model.show = false;
1209
- this.#removeContainer(this.#data.containers.model);
595
+ const config = { model: { visible: false } };
596
+ this.#addTaskToQueue(config, "visibility");
1210
597
  }
1211
598
 
599
+ /**
600
+ * Shows the scene/environment by setting its visibility to true.
601
+ * Adds a visibility task to the queue for processing.
602
+ * @public
603
+ * @returns {void}
604
+ */
1212
605
  showScene() {
1213
- this.#data.containers.environment.show = true;
1214
- this.#addContainer(this.#data.containers.environment);
1215
- this.#setVisibilityOfWallAndFloorInModel();
606
+ const config = { scene: { visible: true } };
607
+ this.#addTaskToQueue(config, "visibility");
1216
608
  }
1217
609
 
610
+ /**
611
+ * Hides the scene/environment by setting its visibility to false.
612
+ * Adds a visibility task to the queue for processing.
613
+ * @public
614
+ * @returns {void}
615
+ */
1218
616
  hideScene() {
1219
- this.#data.containers.environment.show = false;
1220
- this.#removeContainer(this.#data.containers.environment);
1221
- this.#setVisibilityOfWallAndFloorInModel();
617
+ const config = { scene: { visible: false } };
618
+ this.#addTaskToQueue(config, "visibility");
619
+ }
620
+
621
+ /**
622
+ * Centers the 2D drawing view in the viewer.
623
+ * @public
624
+ * @returns {void}
625
+ * @description
626
+ * Only works when the viewer is in 2D mode. Pending implementation for 3D mode when active camera is not blocked.
627
+ */
628
+ zoomCenter() {
629
+ if (!this.#component2D || this.#mode !== "2d") {
630
+ return;
631
+ }
632
+ this.#component2D.zoomCenter();
633
+ }
634
+
635
+ /**
636
+ * Zooms the 2D drawing to fit all content within the viewer.
637
+ * @public
638
+ * @returns {void}
639
+ * @description
640
+ * Only works when the viewer is in 2D mode. Pending implementation for 3D mode when active camera is not blocked.
641
+ */
642
+ zoomExtentsAll() {
643
+ if (!this.#component2D || this.#mode !== "2d") {
644
+ return;
645
+ }
646
+ this.#component2D.zoomExtentsAll();
647
+ }
648
+
649
+ /**
650
+ * Zooms in on the 2D drawing.
651
+ * @public
652
+ * @returns {void}
653
+ * @description
654
+ * Only works when the viewer is in 2D mode. Pending implementation for 3D mode when active camera is not blocked.
655
+ */
656
+ zoomIn() {
657
+ if (!this.#component2D || this.#mode !== "2d") {
658
+ return;
659
+ }
660
+ this.#component2D.zoomIn();
661
+ }
662
+
663
+ /**
664
+ * Zooms out of the 2D drawing.
665
+ * @public
666
+ * @returns {void}
667
+ * @description
668
+ * Only works when the viewer is in 2D mode. Pending implementation for 3D mode when active camera is not blocked.
669
+ */
670
+ zoomOut() {
671
+ if (!this.#component2D || this.#mode !== "2d") {
672
+ return;
673
+ }
674
+ this.#component2D.zoomOut();
1222
675
  }
1223
676
 
677
+ /**
678
+ * Initiates download of the current 3D model in GLB format.
679
+ * @public
680
+ * @returns {void}
681
+ */
1224
682
  downloadModelGLB() {
1225
- const fileName = "model";
1226
- GLTF2Export.GLBAsync(this.#data.containers.model.assetContainer, fileName, { exportWithoutWaitingForScene: true }).then((glb) => glb.downloadFiles());
683
+ if (!this.#component3D) {
684
+ return;
685
+ }
686
+
687
+ this.#component3D.downloadModelGLB();
1227
688
  }
1228
689
 
690
+ /**
691
+ * Initiates download of the current 3D model in USDZ format.
692
+ * @public
693
+ * @returns {void}
694
+ */
1229
695
  downloadModelUSDZ() {
1230
- const fileName = "model";
1231
- USDZExportAsync(this.#data.containers.model.assetContainer).then((response) => {
1232
- if (response) {
1233
- Tools.Download(new Blob([response], { type: "model/vnd.usdz+zip" }), `${fileName}.usdz`);
1234
- }
1235
- });
696
+ if (!this.#component3D) {
697
+ return;
698
+ }
699
+
700
+ this.#component3D.downloadModelUSDZ();
1236
701
  }
1237
702
 
703
+ /**
704
+ * Initiates download of both the 3D model and scene in USDZ format.
705
+ * @public
706
+ * @returns {void}
707
+ */
1238
708
  downloadModelAndSceneUSDZ() {
1239
- const fileName = "scene";
1240
- USDZExportAsync(this.#scene).then((response) => {
1241
- if (response) {
1242
- Tools.Download(new Blob([response], { type: "model/vnd.usdz+zip" }), `${fileName}.usdz`);
1243
- }
1244
- });
709
+ if (!this.#component3D) {
710
+ return;
711
+ }
712
+
713
+ this.#component3D.downloadModelAndSceneUSDZ();
1245
714
  }
1246
715
 
716
+ /**
717
+ * Initiates download of both the 3D model and scene in GLB format.
718
+ * @public
719
+ * @returns {void}
720
+ */
1247
721
  downloadModelAndSceneGLB() {
1248
- const fileName = "scene";
1249
- GLTF2Export.GLBAsync(this.#scene, fileName, { exportWithoutWaitingForScene: true }).then((glb) => glb.downloadFiles());
722
+ if (!this.#component3D) {
723
+ return;
724
+ }
725
+
726
+ this.#component3D.downloadModelAndSceneGLB();
727
+ }
728
+
729
+ /**
730
+ * Indicates whether the viewer has been initialized.
731
+ * @public
732
+ * @returns {boolean} True if initialized; otherwise, false.
733
+ */
734
+ get isInitialized() {
735
+ return this.#isInitialized;
736
+ }
737
+
738
+ /**
739
+ * Indicates whether the viewer has finished loading.
740
+ * @public
741
+ * @returns {boolean} True if loaded; otherwise, false.
742
+ */
743
+ get isLoaded() {
744
+ return this.#isLoaded;
745
+ }
746
+
747
+ /**
748
+ * Indicates whether the viewer is currently loading.
749
+ * @public
750
+ * @returns {boolean} True if loading; otherwise, false.
751
+ */
752
+ get isLoading() {
753
+ return this.#isLoading;
754
+ }
755
+
756
+ /**
757
+ * Indicates whether the viewer is currently in 2D mode.
758
+ * @public
759
+ * @returns {boolean} True if the viewer is in 2D mode; otherwise, false.
760
+ */
761
+ get isMode2D() {
762
+ return this.#mode === "2d";
763
+ }
764
+
765
+ /**
766
+ * Indicates whether the viewer is currently in 3D mode.
767
+ * @public
768
+ * @returns {boolean} True if the viewer is in 3D mode; otherwise, false.
769
+ */
770
+ get isMode3D() {
771
+ return this.#mode === "3d";
1250
772
  }
1251
773
  }
1252
774
 
775
+ customElements.define("pref-viewer-2d", PrefViewer2D);
776
+ customElements.define("pref-viewer-3d", PrefViewer3D);
1253
777
  customElements.define("pref-viewer", PrefViewer);