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

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,1253 +1,917 @@
1
+ import { PrefViewer2D } from "./pref-viewer-2d.js";
2
+ import { PrefViewer3D } from "./pref-viewer-3d.js";
3
+ import { PrefViewerDialog } from "./pref-viewer-dialog.js";
4
+ import { PrefViewerTask } from "./pref-viewer-task.js";
5
+
1
6
  /**
2
- * =============================================================================
3
- * PrefViewer Web Component (JavaScript)
4
- * =============================================================================
7
+ * PrefViewer - Custom Web Component for advanced 2D and 3D product visualization and configuration.
8
+ *
9
+ * Overview:
10
+ * - Encapsulates both 2D (SVG) and 3D (Babylon.js) viewers, supporting glTF/GLB models, environments, and drawings.
11
+ * - Loads assets from remote URLs, Base64 data URIs, and IndexedDB sources.
12
+ * - Provides a unified API for loading models, scenes, drawings, materials, and configuration via attributes or methods.
13
+ * - Manages an internal task queue for sequential processing of viewer operations.
14
+ * - Emits custom events for loading, errors, and state changes to facilitate integration.
15
+ * - Supports downloading models and scenes in GLB and USDZ formats.
16
+ * - Automatically updates the viewer when reactive attributes change.
17
+ *
18
+ * Usage:
19
+ * - Use as a custom HTML element: <pref-viewer ...>
20
+ * - Configure via attributes (config, model, scene, materials, drawing, options, mode).
21
+ * - Control viewer mode, visibility, and downloads via public methods.
22
+ *
23
+ * Reactive Attributes:
24
+ * - config: URL or Base64 for configuration file.
25
+ * - model: URL or Base64 for 3D model (glTF/GLB).
26
+ * - scene: URL or Base64 for environment/scene (glTF/GLB).
27
+ * - materials: URL or Base64 for materials definition.
28
+ * - drawing: URL or Base64 for SVG drawing.
29
+ * - options: JSON string for viewer options.
30
+ * - mode: Viewer mode ("2d" or "3d").
5
31
  *
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.
32
+ * Public Methods:
33
+ * - loadConfig(config): Loads a configuration object or JSON string.
34
+ * - loadModel(model): Loads a model object or JSON string.
35
+ * - loadScene(scene): Loads a scene/environment object or JSON string.
36
+ * - loadMaterials(materials): Loads materials object or JSON string.
37
+ * - loadDrawing(drawing): Loads a drawing object or JSON string.
38
+ * - setOptions(options): Sets viewer options from an object or JSON string.
39
+ * - setMode(mode): Sets the viewer mode to "2d" or "3d" and updates component visibility.
40
+ * - showModel(): Shows the 3D model.
41
+ * - hideModel(): Hides the 3D model.
42
+ * - showScene(): Shows the 3D environment/scene.
43
+ * - hideScene(): Hides the 3D environment/scene.
44
+ * - zoomCenter(): Centers the 2D drawing view.
45
+ * - zoomExtentsAll(): Zooms the 2D drawing to fit all content.
46
+ * - zoomIn(): Zooms in on the 2D drawing.
47
+ * - zoomOut(): Zooms out of the 2D drawing.
48
+ * - downloadModelGLB(): Downloads the current 3D model as a GLB file.
49
+ * - downloadModelGLTF(): Downloads the current 3D model as a glTF ZIP file.
50
+ * - downloadModelUSDZ(): Downloads the current 3D model as a USDZ file.
51
+ * - downloadModelAndSceneGLB(): Downloads both the model and scene as a GLB file.
52
+ * - downloadModelAndSceneGLTF(): Downloads both the model and scene as a glTF ZIP file.
53
+ * - downloadModelAndSceneUSDZ(): Downloads both the model and scene as a USDZ file.
54
+ * - downloadSceneGLB(): Downloads the environment as a GLB file.
55
+ * - downloadSceneGLTF(): Downloads the environment as a glTF ZIP file.
56
+ * - downloadSceneUSDZ(): Downloads the environment as a USDZ file.
57
+ * - openDialog(title, content, footer): Opens a modal dialog with the specified title, content, and footer.
58
+ * - closeDialog(): Closes the currently open dialog, if any, and removes it from the DOM.
15
59
  *
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
- * ```
60
+ * Public Properties:
61
+ * - isInitialized: Indicates if the viewer is initialized.
62
+ * - isLoaded: Indicates if the viewer has finished loading.
63
+ * - isLoading: Indicates if the viewer is currently loading.
64
+ * - isMode2D: Indicates if the viewer is in 2D mode.
65
+ * - isMode3D: Indicates if the viewer is in 3D mode.
25
66
  *
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
- * ```
67
+ * Events:
68
+ * - "scene-loading": Dispatched when a 3D loading operation starts.
69
+ * - "scene-loaded": Dispatched when a 3D loading operation completes.
70
+ * - "drawing-loading": Dispatched when a 2D drawing loading operation starts.
71
+ * - "drawing-loaded": Dispatched when a 2D drawing loading operation completes.
72
+ * - "drawing-zoom-changed": Dispatched when the 2D drawing zoom/pan state changes.
33
73
  *
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
- * ```
74
+ * Notes:
75
+ * - Automatically creates and manages 2D and 3D viewer components in its shadow DOM.
76
+ * - Processes tasks sequentially to ensure consistent state.
77
+ * - Designed for extensibility and integration in product configurators and visualization tools.
41
78
  */
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
79
  class PrefViewer extends HTMLElement {
78
- initialized = false;
79
- loaded = false;
80
- loading = false;
80
+ #isInitialized = false;
81
+ #isLoaded = false;
82
+ #isLoading = false;
83
+ #mode = "3d";
84
+
81
85
  #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
86
+
149
87
  #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;
88
+ #component2D = null;
89
+ #component3D = null;
90
+ #dialog = null;
161
91
 
92
+ /**
93
+ * Creates a new PrefViewer instance and attaches a shadow DOM.
94
+ * Initializes internal state and component references.
95
+ * @public
96
+ */
162
97
  constructor() {
163
98
  super();
164
99
  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
100
  }
178
101
 
102
+ /**
103
+ * Returns the list of attributes to observe for changes.
104
+ * @public
105
+ * @returns {string[]} Array of attribute names to observe.
106
+ */
179
107
  static get observedAttributes() {
180
- return ["config", "model", "scene", "show-model", "show-scene"];
108
+ return ["config", "drawing", "materials", "mode", "model", "scene", "options", "show-model", "show-scene"];
181
109
  }
182
110
 
111
+ /**
112
+ * Observes changes to specific attributes and triggers corresponding actions.
113
+ * Loads configuration, drawing, model, scene, materials, or options when their attributes change.
114
+ * Toggles model or scene visibility when "show-model" or "show-scene" attributes change.
115
+ * @public
116
+ * @param {string} name - The name of the changed attribute.
117
+ * @param {*} _old - The previous value of the attribute (unused).
118
+ * @param {*} value - The new value of the attribute.
119
+ * @returns {void}
120
+ */
183
121
  attributeChangedCallback(name, _old, value) {
184
- let data = null;
185
122
  switch (name) {
186
123
  case "config":
187
124
  this.loadConfig(value);
188
125
  break;
126
+ case "drawing":
127
+ this.loadDrawing(value);
128
+ break;
129
+ case "materials":
130
+ this.loadMaterials(value);
131
+ break;
132
+ case "mode":
133
+ if (_old === value || value.toLowerCase() === this.#mode) {
134
+ return;
135
+ }
136
+ this.setMode(value.toLowerCase());
137
+ break;
189
138
  case "model":
190
139
  this.loadModel(value);
191
140
  break;
192
141
  case "scene":
193
142
  this.loadScene(value);
194
143
  break;
195
- case "materials":
196
- this.loadMaterials(value);
197
- break;
198
144
  case "options":
199
145
  this.setOptions(value);
200
146
  break;
201
147
  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;
148
+ if (_old === value) {
149
+ return;
207
150
  }
151
+ const showModel = value.toLowerCase() === "true";
152
+ showModel ? this.showModel() : this.hideModel();
208
153
  break;
209
154
  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;
155
+ if (_old === value) {
156
+ return;
215
157
  }
158
+ const showScene = value.toLowerCase() === "true";
159
+ showScene ? this.showScene() : this.hideScene();
216
160
  break;
217
161
  }
218
162
  }
219
163
 
164
+ /**
165
+ * Called when the element is inserted into the DOM.
166
+ * Initializes the 3D and 2D viewer components and starts processing tasks.
167
+ * @public
168
+ * @returns {void|boolean} Returns false if initialization fails; otherwise void.
169
+ */
220
170
  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;
233
- }
171
+ this.#wrapper = document.createElement("div");
172
+ this.#wrapper.classList.add("pref-viewer-wrapper");
173
+ this.shadowRoot.append(this.#wrapper);
234
174
 
235
- this.#initializeBabylon();
236
- this.initialized = true;
237
- this.#processNextTask();
238
- }
175
+ const style = document.createElement("style");
176
+ style.textContent = `@import "/dist/css/pref-viewer.css";`;
177
+ this.shadowRoot.append(style);
239
178
 
240
- disconnectedCallback() {
241
- this.#disposeEngine();
242
- this.#canvasResizeObserver.disconnect();
243
- }
179
+ this.#createComponent3D();
180
+ this.#createComponent2D();
244
181
 
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
- }
182
+ if (!this.hasAttribute("mode")) {
183
+ this.setMode();
184
+ }
255
185
 
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);
186
+ this.#isInitialized = true;
187
+ this.#processNextTask();
265
188
  }
266
189
 
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);
190
+ /**
191
+ * Creates and appends the 2D viewer component to the shadow DOM.
192
+ * Sets the "visible" attribute to true by default.
193
+ * @private
194
+ * @returns {void}
195
+ */
196
+ #createComponent2D() {
197
+ this.#component2D = document.createElement("pref-viewer-2d");
198
+ this.#component2D.setAttribute("visible", "false");
199
+ this.#component2D.addEventListener("drawing-zoom-changed", this.#on2DZoomChanged.bind(this));
200
+ this.#wrapper.appendChild(this.#component2D);
282
201
  }
283
202
 
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();
203
+ /**
204
+ * Creates and appends the 3D viewer component to the shadow DOM.
205
+ * Sets the "visible" attribute to true by default.
206
+ * @private
207
+ * @returns {void}
208
+ */
209
+ #createComponent3D() {
210
+ this.#component3D = document.createElement("pref-viewer-3d");
211
+ this.#component3D.setAttribute("visible", "false");
212
+ this.#wrapper.appendChild(this.#component3D);
213
+ }
323
214
 
324
- if (this.hasAttribute("loading")) {
325
- this.removeAttribute("loading");
215
+ /**
216
+ * Adds a new task to the internal queue for processing.
217
+ * If the viewer is initialized and not currently loading, immediately processes the next task.
218
+ * @private
219
+ * @param {*} value - The payload or data for the task.
220
+ * @param {string} type - The type of task (see PrefViewerTask.Types).
221
+ * @returns {void}
222
+ */
223
+ #addTaskToQueue(value, type) {
224
+ this.#taskQueue.push(new PrefViewerTask(value, type));
225
+ if (this.#isInitialized && !this.#isLoading) {
226
+ this.#processNextTask();
326
227
  }
327
- this.setAttribute("loaded", "");
328
-
329
- this.loaded = true;
330
- this.loading = false;
331
-
332
- this.#processNextTask();
333
228
  }
334
229
 
335
- // Data
336
- #checkCameraChanged(options) {
337
- if (!options || !options.camera) {
230
+ /**
231
+ * Processes the next task in the queue, if any.
232
+ * Dispatches the task to the appropriate handler based on its type.
233
+ * @private
234
+ * @returns {boolean|void} Returns false if the queue is empty; otherwise void.
235
+ */
236
+ #processNextTask() {
237
+ if (!this.#taskQueue.length) {
338
238
  return false;
339
239
  }
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;
240
+ const task = this.#taskQueue[0];
241
+ this.#taskQueue.shift();
242
+ switch (task.type) {
243
+ case PrefViewerTask.Types.Config:
244
+ this.#processConfig(task.value);
245
+ break;
246
+ case PrefViewerTask.Types.Drawing:
247
+ this.#processDrawing(task.value);
248
+ break;
249
+ case PrefViewerTask.Types.Environment:
250
+ this.#processEnvironment(task.value);
251
+ break;
252
+ case PrefViewerTask.Types.Materials:
253
+ this.#processMaterials(task.value);
254
+ break;
255
+ case PrefViewerTask.Types.Model:
256
+ this.#processModel(task.value);
257
+ break;
258
+ case PrefViewerTask.Types.Options:
259
+ this.#processOptions(task.value);
260
+ break;
261
+ case PrefViewerTask.Types.Visibility:
262
+ this.#processVisibility(task.value);
263
+ break;
349
264
  }
350
- return changed;
351
265
  }
352
266
 
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
- }
267
+ /**
268
+ * Handles the start of a 3D loading operation.
269
+ * Updates loading state, sets attributes, and dispatches a "scene-loading" event.
270
+ * @private
271
+ * @returns {void}
272
+ */
273
+ #on3DLoading() {
274
+ this.#isLoaded = false;
275
+ this.#isLoading = true;
375
276
 
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
- }
277
+ this.removeAttribute("loaded-3d");
278
+ this.setAttribute("loading-3d", "");
385
279
 
386
- #resetChangedFlags() {
387
- const reset = (node) => {
388
- node.changed = { pending: false, success: false };
280
+ const customEventOptions = {
281
+ bubbles: true,
282
+ cancelable: false,
283
+ composed: true,
389
284
  };
390
- Object.values(this.#data.containers).forEach(reset);
391
- Object.values(this.#data.options.materials).forEach(reset);
392
- reset(this.#data.options.camera);
285
+ this.dispatchEvent(new CustomEvent("scene-loading", customEventOptions));
393
286
  }
394
287
 
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);
407
- }
288
+ /**
289
+ * Handles the completion of a 3D loading operation.
290
+ * Updates loading state, sets attributes, dispatches a "scene-loaded" event, and processes the next task.
291
+ * @private
292
+ * @param {object} [detail] - Optional details to include in the event.
293
+ * @returns {void}
294
+ */
295
+ #on3DLoaded(detail) {
296
+ this.#isLoaded = true;
297
+ this.#isLoading = false;
408
298
 
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
- };
299
+ this.removeAttribute("loading-3d");
300
+ this.setAttribute("loaded-3d", "");
413
301
 
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);
302
+ const customEventOptions = {
303
+ bubbles: true,
304
+ cancelable: true,
305
+ composed: true,
306
+ };
307
+ if (detail) {
308
+ customEventOptions.detail = detail;
309
+ }
310
+ this.dispatchEvent(new CustomEvent("scene-loaded", customEventOptions));
419
311
  }
420
312
 
421
- async #createXRExperience() {
422
- if (this.#XRExperience) {
423
- return true;
424
- }
313
+ /**
314
+ * Handles the start of a 2D loading operation.
315
+ * Updates loading state, sets attributes, and dispatches a "drawing-loading" event.
316
+ * @private
317
+ * @returns {void}
318
+ */
319
+ #on2DLoading() {
320
+ this.#isLoaded = false;
321
+ this.#isLoading = true;
425
322
 
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
- }
323
+ this.removeAttribute("loaded-2d");
324
+ this.setAttribute("loading-2d", "");
432
325
 
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
- }
326
+ const customEventOptions = {
327
+ bubbles: true,
328
+ cancelable: true,
329
+ composed: true,
330
+ };
331
+ this.dispatchEvent(new CustomEvent("drawing-loading", customEventOptions));
468
332
  }
469
333
 
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;
481
- }
334
+ /**
335
+ * Handles the completion of a 2D loading operation.
336
+ * Updates loading state, sets attributes, dispatches a "drawing-loaded" event, and processes the next task.
337
+ * @private
338
+ * @param {object} [detail] - Optional details to include in the event.
339
+ * @returns {void}
340
+ */
341
+ #on2DLoaded(detail) {
342
+ this.#isLoaded = true;
343
+ this.#isLoading = false;
482
344
 
483
- #createLights() {
484
- this.#initEnvironmentTexture();
345
+ this.removeAttribute("loading-2d");
346
+ this.setAttribute("loaded-2d", "");
485
347
 
486
- if (this.#scene.environmentTexture) {
487
- return true;
348
+ const customEventOptions = {
349
+ bubbles: true,
350
+ cancelable: true,
351
+ composed: true,
352
+ };
353
+ if (detail) {
354
+ customEventOptions.detail = detail;
488
355
  }
489
-
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;
356
+ this.dispatchEvent(new CustomEvent("drawing-loaded", customEventOptions));
509
357
  }
510
358
 
511
- #initEnvironmentTexture() {
512
- return false;
513
- if (this.#scene.environmentTexture) {
514
- return true;
515
- }
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;
359
+ /**
360
+ * Handles the "drawing-zoom-changed" event from the 2D viewer component.
361
+ * Dispatches a custom "drawing-zoom-changed" event from the PrefViewer element, forwarding the event detail to external listeners.
362
+ * @private
363
+ * @param {CustomEvent} event - The original zoom change event from the 2D viewer.
364
+ * @returns {void}
365
+ */
366
+ #on2DZoomChanged(event) {
367
+ event.stopPropagation();
368
+ event.preventDefault();
369
+ const customEventOptions = {
370
+ bubbles: true,
371
+ cancelable: true,
372
+ composed: true,
373
+ detail: event.detail,
374
+ };
375
+ this.dispatchEvent(new CustomEvent("drawing-zoom-changed", customEventOptions));
522
376
  }
523
377
 
524
- #initIBLShadows() {
525
- if (!this.#scene.environmentTexture) {
526
- return false;
378
+ /**
379
+ * Processes a configuration object by loading it into the 3D component.
380
+ * Dispatches loading events and processes the next task when finished.
381
+ * @private
382
+ * @param {object} config - The configuration object to process.
383
+ * @returns {void}
384
+ */
385
+ #processConfig(config) {
386
+ if (!this.#component3D) {
387
+ return;
527
388
  }
528
389
 
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;
553
- };
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);
390
+ this.#on3DLoading();
391
+ this.#component3D.load(config).then((detail) => {
392
+ this.#on3DLoaded(detail);
393
+ this.#processNextTask();
567
394
  });
568
395
  }
569
396
 
570
- #initShadows() {
571
- if (!this.#scene.environmentTexture) {
572
- this.#initIBLShadows();
573
- return true;
397
+ /**
398
+ * Processes a drawing object by loading it into the 2D component.
399
+ * Processes the next task when finished.
400
+ * @private
401
+ * @param {object} drawing - The drawing object to process.
402
+ * @returns {void}
403
+ */
404
+ #processDrawing(drawing) {
405
+ if (!this.#component2D) {
406
+ return;
574
407
  }
575
408
 
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
- }
409
+ this.#on2DLoading();
410
+ this.#component2D.load(drawing).then((detail) => {
411
+ this.#on2DLoaded(detail);
412
+ this.#processNextTask();
584
413
  });
585
414
  }
586
415
 
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));
596
- }
416
+ /**
417
+ * Processes an environment (scene) object by wrapping it in a config and loading it.
418
+ * @private
419
+ * @param {object} environment - The environment/scene object to process.
420
+ * @returns {void}
421
+ */
422
+ #processEnvironment(environment) {
423
+ const config = {};
424
+ config.scene = environment;
425
+ this.#processConfig(config);
597
426
  }
598
427
 
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
- });
428
+ /**
429
+ * Processes a materials object by wrapping it in a config and loading it.
430
+ * @private
431
+ * @param {object} materials - The materials object to process.
432
+ * @returns {void}
433
+ */
434
+ #processMaterials(materials) {
435
+ const config = {};
436
+ config.materials = materials;
437
+ this.#processConfig(config);
611
438
  }
612
439
 
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;
440
+ /**
441
+ * Processes a model object by wrapping it in a config and loading it.
442
+ * @private
443
+ * @param {object} model - The model object to process.
444
+ * @returns {void}
445
+ */
446
+ #processModel(model) {
447
+ const config = {};
448
+ config.model = model;
449
+ this.#processConfig(config);
619
450
  }
620
451
 
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
- }
452
+ /**
453
+ * Processes viewer options by loading them into the 3D component.
454
+ * Dispatches loading events and processes the next task.
455
+ * @private
456
+ * @param {object} options - The options object to process.
457
+ * @returns {void}
458
+ */
459
+ #processOptions(options) {
460
+ if (!this.#component3D) {
461
+ return;
462
+ }
642
463
 
643
- #transformUrl(url) {
644
- return new Promise((resolve) => {
645
- resolve(url.replace(/^blob:[^/]+\//i, "").replace(/\\/g, "/"));
646
- });
464
+ this.#on3DLoading();
465
+ const detail = this.#component3D.setOptions(options);
466
+ this.#on3DLoaded(detail);
467
+ this.#processNextTask();
647
468
  }
648
469
 
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 };
470
+ /**
471
+ * Processes visibility configuration for the model and scene.
472
+ * Shows or hides the model and/or scene based on the config, then processes the next task.
473
+ * @private
474
+ * @param {object} config - The visibility configuration object.
475
+ * @returns {void}
476
+ */
477
+ #processVisibility(config) {
478
+ if (!this.#component3D) {
479
+ return;
480
+ }
481
+ const showModel = config.model?.visible;
482
+ const showScene = config.scene?.visible;
483
+ if (showModel !== undefined) {
484
+ showModel ? this.#component3D.showModel() : this.#component3D.hideModel();
660
485
  }
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 };
486
+ if (showScene !== undefined) {
487
+ showScene ? this.#component3D.showEnvironment() : this.#component3D.hideEnvironment();
488
+ }
489
+ this.#processNextTask();
671
490
  }
672
491
 
673
- async #initStorage(db, table) {
674
- if (window.gltfDB && window.gltfDB.name === db && window.gltfDB.objectStoreNames.contains(table)) {
675
- return true;
492
+ /**
493
+ * ---------------------------
494
+ * Public methods
495
+ * ---------------------------
496
+ */
497
+
498
+ /**
499
+ * Sets the viewer mode to "2d" or "3d" and updates component visibility accordingly.
500
+ * @public
501
+ * @param {string} [mode=this.#mode] - The mode to set ("2d" or "3d").
502
+ * @returns {void}
503
+ */
504
+ setMode(mode = this.#mode) {
505
+ if (mode !== "2d" && mode !== "3d") {
506
+ console.warn(`PrefViewer: invalid mode "${mode}". Allowed modes are "2d" and "3d".`);
507
+ mode = this.#mode;
508
+ }
509
+ this.#mode = mode;
510
+ if (mode === "2d") {
511
+ this.#component3D?.hide();
512
+ this.#component2D?.show();
513
+ } else {
514
+ this.#component2D?.hide();
515
+ this.#component3D?.show();
516
+ }
517
+ if (this.getAttribute("mode") !== mode) {
518
+ this.setAttribute("mode", mode);
519
+ if (this.#dialog) {
520
+ this.closeDialog();
521
+ }
676
522
  }
677
- await initDb(db, table);
678
523
  }
679
524
 
680
- // Methods for managing Asset Containers
681
- #setVisibilityOfWallAndFloorInModel(show) {
682
- if (!this.#data.containers.model.assetContainer || !this.#data.containers.model.visible) {
525
+ /**
526
+ * Loads a configuration object or JSON string and adds it to the task queue.
527
+ * If the config contains a drawing, adds a drawing task as well.
528
+ * @public
529
+ * @param {object|string} config - Configuration object or JSON string.
530
+ * @returns {boolean|void} Returns false if config is invalid; otherwise void.
531
+ */
532
+ loadConfig(config) {
533
+ config = typeof config === "string" ? JSON.parse(config) : config;
534
+ if (!config) {
683
535
  return false;
684
536
  }
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));
537
+ if (config.drawing) {
538
+ this.#addTaskToQueue(config.drawing, "drawing");
539
+ }
540
+ this.#addTaskToQueue(config, "config");
688
541
  }
689
542
 
690
- #setOptionsMaterial(optionMaterial) {
691
- if (!optionMaterial || !optionMaterial.prefix || !optionMaterial.value) {
543
+ /**
544
+ * Loads a model object or JSON string and adds it to the task queue.
545
+ * @public
546
+ * @param {object|string} model - Model object or JSON string.
547
+ * @returns {boolean|void} Returns false if model is invalid; otherwise void.
548
+ */
549
+ loadModel(model) {
550
+ model = typeof model === "string" ? JSON.parse(model) : model;
551
+ if (!model) {
692
552
  return false;
693
553
  }
554
+ this.#addTaskToQueue(model, "model");
555
+ }
694
556
 
695
- const material = this.#data.containers.materials.assetContainer?.materials.find((mat) => mat.name === optionMaterial.value) || null;
696
- if (!material) {
557
+ /**
558
+ * Loads a scene/environment object or JSON string and adds it to the task queue.
559
+ * @public
560
+ * @param {object|string} scene - Scene object or JSON string.
561
+ * @returns {boolean|void} Returns false if scene is invalid; otherwise void.
562
+ */
563
+ loadScene(scene) {
564
+ scene = typeof scene === "string" ? JSON.parse(scene) : scene;
565
+ if (!scene) {
697
566
  return false;
698
567
  }
568
+ this.#addTaskToQueue(scene, "environment");
569
+ }
699
570
 
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) {
571
+ /**
572
+ * Loads materials object or JSON string and adds it to the task queue.
573
+ * @public
574
+ * @param {object|string} materials - Materials object or JSON string.
575
+ * @returns {boolean|void} Returns false if materials are invalid; otherwise void.
576
+ */
577
+ loadMaterials(materials) {
578
+ materials = typeof materials === "string" ? JSON.parse(materials) : materials;
579
+ if (!materials) {
708
580
  return false;
709
581
  }
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;
582
+ this.#addTaskToQueue(materials, "materials");
738
583
  }
739
584
 
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) {
585
+ /**
586
+ * Loads a drawing object or JSON string and adds it to the task queue.
587
+ * @public
588
+ * @param {object|string} drawing - Drawing object or JSON string.
589
+ * @returns {boolean|void} Returns false if drawing is invalid; otherwise void.
590
+ */
591
+ loadDrawing(drawing) {
592
+ drawing = typeof drawing === "string" ? JSON.parse(drawing) : drawing;
593
+ if (!drawing) {
742
594
  return false;
743
595
  }
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;
596
+ this.#addTaskToQueue(drawing, "drawing");
772
597
  }
773
598
 
774
- #addContainer(container) {
775
- if (!container.assetContainer || container.visible || !container.show) {
599
+ /**
600
+ * Sets viewer options from an object or JSON string and adds them to the task queue.
601
+ * @public
602
+ * @param {object|string} options - Options object or JSON string.
603
+ * @returns {boolean|void} Returns false if options are invalid; otherwise void.
604
+ */
605
+ setOptions(options) {
606
+ options = typeof options === "string" ? JSON.parse(options) : options;
607
+ if (!options) {
776
608
  return false;
777
609
  }
610
+ this.#addTaskToQueue(options, "options");
611
+ }
778
612
 
779
- container.assetContainer.addAllToScene();
780
- container.visible = true;
781
- return true;
613
+ /**
614
+ * Shows the 3D model by setting its visibility to true.
615
+ * Adds a visibility task to the queue for processing.
616
+ * @public
617
+ * @returns {void}
618
+ */
619
+ showModel() {
620
+ const config = { model: { visible: true } };
621
+ this.#addTaskToQueue(config, "visibility");
782
622
  }
783
623
 
784
- #removeContainer(container) {
785
- if (!container.assetContainer || !container.visible) {
786
- return false;
787
- }
624
+ /**
625
+ * Hides the 3D model by setting its visibility to false.
626
+ * Adds a visibility task to the queue for processing.
627
+ * @public
628
+ * @returns {void}
629
+ */
630
+ hideModel() {
631
+ const config = { model: { visible: false } };
632
+ this.#addTaskToQueue(config, "visibility");
633
+ }
788
634
 
789
- container.assetContainer.removeAllFromScene();
790
- container.visible = false;
791
- return true;
635
+ /**
636
+ * Shows the scene/environment by setting its visibility to true.
637
+ * Adds a visibility task to the queue for processing.
638
+ * @public
639
+ * @returns {void}
640
+ */
641
+ showScene() {
642
+ const config = { scene: { visible: true } };
643
+ this.#addTaskToQueue(config, "visibility");
792
644
  }
793
645
 
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;
646
+ /**
647
+ * Hides the scene/environment by setting its visibility to false.
648
+ * Adds a visibility task to the queue for processing.
649
+ * @public
650
+ * @returns {void}
651
+ */
652
+ hideScene() {
653
+ const config = { scene: { visible: false } };
654
+ this.#addTaskToQueue(config, "visibility");
804
655
  }
805
656
 
806
657
  /**
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.
658
+ * Centers the 2D drawing view in the viewer.
659
+ * @public
660
+ * @returns {void}
811
661
  * @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}
662
+ * Only works when the viewer is in 2D mode. Pending implementation for 3D mode when active camera is not blocked.
821
663
  */
822
- async #replaceSceneURIAsync(assetContainerJSON, assetContainerURL) {
823
- if (!assetContainerJSON) {
664
+ zoomCenter() {
665
+ if (!this.#component2D || this.#mode !== "2d") {
824
666
  return;
825
667
  }
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));
879
- }
880
- if (assetContainerJSON.images) {
881
- assetContainerJSON.images.forEach((asset, index, array) => saveAssetData(asset, index, array));
882
- }
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);
668
+ this.#component2D.zoomCenter();
892
669
  }
893
670
 
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
- }
671
+ /**
672
+ * Zooms the 2D drawing to fit all content within the viewer.
673
+ * @public
674
+ * @returns {void}
675
+ * @description
676
+ * Only works when the viewer is in 2D mode. Pending implementation for 3D mode when active camera is not blocked.
677
+ */
678
+ zoomExtentsAll() {
679
+ if (!this.#component2D || this.#mode !== "2d") {
680
+ return;
913
681
  }
682
+ this.#component2D.zoomExtentsAll();
683
+ }
914
684
 
915
- if (!source) {
916
- return false;
685
+ /**
686
+ * Zooms in on the 2D drawing.
687
+ * @public
688
+ * @returns {void}
689
+ * @description
690
+ * Only works when the viewer is in 2D mode. Pending implementation for 3D mode when active camera is not blocked.
691
+ */
692
+ zoomIn() {
693
+ if (!this.#component2D || this.#mode !== "2d") {
694
+ return;
917
695
  }
696
+ this.#component2D.zoomIn();
697
+ }
918
698
 
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
- }
940
- } 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
- }
699
+ /**
700
+ * Zooms out of the 2D drawing.
701
+ * @public
702
+ * @returns {void}
703
+ * @description
704
+ * Only works when the viewer is in 2D mode. Pending implementation for 3D mode when active camera is not blocked.
705
+ */
706
+ zoomOut() {
707
+ if (!this.#component2D || this.#mode !== "2d") {
708
+ return;
958
709
  }
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);
710
+ this.#component2D.zoomOut();
974
711
  }
975
712
 
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
- }
713
+ /**
714
+ * Initiates download of the current 3D model in GLB format.
715
+ * @public
716
+ * @returns {void}
717
+ */
718
+ downloadModelGLB() {
719
+ if (!this.#component3D) {
720
+ return;
721
+ }
1017
722
 
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
- });
723
+ this.#component3D.downloadModelGLB();
1039
724
  }
1040
725
 
1041
- // Tasks
1042
- #addTaskToQueue(value, type) {
1043
- this.#taskQueue.push(new PrefViewerTask(value, type));
1044
- if (this.initialized && !this.loading) {
1045
- this.#processNextTask();
726
+ /**
727
+ * Initiates download of the current 3D model in GLTF format.
728
+ * @public
729
+ * @returns {void}
730
+ */
731
+ downloadModelGLTF() {
732
+ if (!this.#component3D) {
733
+ return;
1046
734
  }
1047
- }
1048
735
 
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
- }
736
+ this.#component3D.downloadModelGLTF();
1072
737
  }
1073
738
 
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);
739
+ /**
740
+ * Initiates download of the current 3D model in USDZ format.
741
+ * @public
742
+ * @returns {void}
743
+ */
744
+ downloadModelUSDZ() {
745
+ if (!this.#component3D) {
746
+ return;
1102
747
  }
1103
748
 
1104
- this.#loadContainers(loadModel, loadEnvironment, loadMaterials);
749
+ this.#component3D.downloadModelUSDZ();
1105
750
  }
1106
751
 
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;
752
+ /**
753
+ * Initiates download of the current complete scene (3D model and environment) in GLB format.
754
+ * @public
755
+ * @returns {void}
756
+ */
757
+ downloadModelAndSceneGLB() {
758
+ if (!this.#component3D) {
759
+ return;
760
+ }
1116
761
 
1117
- this.initialized && this.#loadContainers(loadModel, false, false);
762
+ this.#component3D.downloadModelAndSceneGLB();
1118
763
  }
1119
764
 
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;
765
+ /**
766
+ * Initiates download of the current complete scene (3D model and environment) in GLTF format.
767
+ * @public
768
+ * @returns {void}
769
+ */
770
+ downloadModelAndSceneGLTF() {
771
+ if (!this.#component3D) {
772
+ return;
773
+ }
1129
774
 
1130
- this.#loadContainers(false, loadEnvironment, false);
775
+ this.#component3D.downloadModelAndSceneGLTF();
1131
776
  }
1132
777
 
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;
778
+ /**
779
+ * Initiates download of the current complete scene (3D model and environment) in USDZ format.
780
+ * @public
781
+ * @returns {void}
782
+ */
783
+ downloadModelAndSceneUSDZ() {
784
+ if (!this.#component3D) {
785
+ return;
786
+ }
1141
787
 
1142
- this.#loadContainers(false, false, loadMaterials);
788
+ this.#component3D.downloadModelAndSceneUSDZ();
1143
789
  }
1144
790
 
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();
791
+ /**
792
+ * Initiates download of the current 3D environment in GLB format.
793
+ * @public
794
+ * @returns {void}
795
+ */
796
+ downloadSceneGLB() {
797
+ if (!this.#component3D) {
798
+ return;
1154
799
  }
1155
800
 
1156
- await this.#setStatusLoaded();
1157
-
1158
- return someSetted;
801
+ this.#component3D.downloadSceneGLB();
1159
802
  }
1160
803
 
1161
- // Public Methods
1162
- loadConfig(config) {
1163
- config = typeof config === "string" ? JSON.parse(config) : config;
1164
- if (!config) {
1165
- return false;
804
+ /**
805
+ * Initiates download of the current 3D environment in GLTF format.
806
+ * @public
807
+ * @returns {void}
808
+ */
809
+ downloadSceneGLTF() {
810
+ if (!this.#component3D) {
811
+ return;
1166
812
  }
1167
- this.#addTaskToQueue(config, "config");
1168
- }
1169
813
 
1170
- loadModel(model) {
1171
- model = typeof model === "string" ? JSON.parse(model) : model;
1172
- if (!model) {
1173
- return false;
1174
- }
1175
- this.#addTaskToQueue(model, "model");
814
+ this.#component3D.downloadSceneGLTF();
1176
815
  }
1177
816
 
1178
- loadScene(scene) {
1179
- scene = typeof scene === "string" ? JSON.parse(scene) : scene;
1180
- if (!scene) {
1181
- return false;
817
+ /**
818
+ * Initiates download of the current 3D environment in USDZ format.
819
+ * @public
820
+ * @returns {void}
821
+ */
822
+ downloadSceneUSDZ() {
823
+ if (!this.#component3D) {
824
+ return;
1182
825
  }
1183
- this.#addTaskToQueue(scene, "environment");
826
+ this.#component3D.downloadSceneUSDZ();
1184
827
  }
1185
828
 
1186
- loadMaterials(materials) {
1187
- materials = typeof materials === "string" ? JSON.parse(materials) : materials;
1188
- if (!materials) {
1189
- return false;
829
+ /**
830
+ * Opens a modal dialog with the specified title, content, and footer.
831
+ * @public
832
+ * @param {string} title - The dialog title to display in the header.
833
+ * @param {string} content - The HTML content to display in the dialog body.
834
+ * @param {string} footer - The HTML content to display in the dialog footer (e.g., action buttons).
835
+ * @returns {HTMLElement} The created dialog element.
836
+ * @description
837
+ * If a dialog is already open, it is closed before opening the new one.
838
+ * The dialog is appended to the viewer's shadow DOM and returned for further manipulation.
839
+ */
840
+ openDialog(title, content, footer) {
841
+ if (this.#dialog && this.#dialog.hasAttribute("open")) {
842
+ this.#dialog.close();
1190
843
  }
1191
- this.#addTaskToQueue(materials, "materials");
844
+ this.#dialog = document.createElement("pref-viewer-dialog");
845
+ this.shadowRoot.querySelector(".pref-viewer-wrapper").appendChild(this.#dialog);
846
+ const opened = this.#dialog.open(title, content, footer);
847
+ return opened ? this.#dialog : null;
1192
848
  }
1193
849
 
1194
- setOptions(options) {
1195
- options = typeof options === "string" ? JSON.parse(options) : options;
1196
- if (!options) {
1197
- return false;
850
+ /**
851
+ * Closes the currently open dialog, if any, and removes it from the DOM.
852
+ * @public
853
+ * @returns {void}
854
+ */
855
+ closeDialog() {
856
+ if (this.#dialog) {
857
+ this.#dialog.close();
858
+ this.#dialog = null;
1198
859
  }
1199
- this.#addTaskToQueue(options, "options");
1200
- }
1201
-
1202
- showModel() {
1203
- this.#data.containers.model.show = true;
1204
- this.#addContainer(this.#data.containers.model);
1205
- }
1206
-
1207
- hideModel() {
1208
- this.#data.containers.model.show = false;
1209
- this.#removeContainer(this.#data.containers.model);
1210
860
  }
1211
861
 
1212
- showScene() {
1213
- this.#data.containers.environment.show = true;
1214
- this.#addContainer(this.#data.containers.environment);
1215
- this.#setVisibilityOfWallAndFloorInModel();
1216
- }
862
+ /**
863
+ * ---------------------------
864
+ * Public properties
865
+ * ---------------------------
866
+ */
1217
867
 
1218
- hideScene() {
1219
- this.#data.containers.environment.show = false;
1220
- this.#removeContainer(this.#data.containers.environment);
1221
- this.#setVisibilityOfWallAndFloorInModel();
868
+ /**
869
+ * Indicates whether the viewer has been initialized.
870
+ * @public
871
+ * @returns {boolean} True if initialized; otherwise, false.
872
+ */
873
+ get isInitialized() {
874
+ return this.#isInitialized;
1222
875
  }
1223
876
 
1224
- downloadModelGLB() {
1225
- const fileName = "model";
1226
- GLTF2Export.GLBAsync(this.#data.containers.model.assetContainer, fileName, { exportWithoutWaitingForScene: true }).then((glb) => glb.downloadFiles());
877
+ /**
878
+ * Indicates whether the viewer has finished loading.
879
+ * @public
880
+ * @returns {boolean} True if loaded; otherwise, false.
881
+ */
882
+ get isLoaded() {
883
+ return this.#isLoaded;
1227
884
  }
1228
885
 
1229
- 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
- });
886
+ /**
887
+ * Indicates whether the viewer is currently loading.
888
+ * @public
889
+ * @returns {boolean} True if loading; otherwise, false.
890
+ */
891
+ get isLoading() {
892
+ return this.#isLoading;
1236
893
  }
1237
894
 
1238
- 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
- });
895
+ /**
896
+ * Indicates whether the viewer is currently in 2D mode.
897
+ * @public
898
+ * @returns {boolean} True if the viewer is in 2D mode; otherwise, false.
899
+ */
900
+ get isMode2D() {
901
+ return this.#mode === "2d";
1245
902
  }
1246
903
 
1247
- downloadModelAndSceneGLB() {
1248
- const fileName = "scene";
1249
- GLTF2Export.GLBAsync(this.#scene, fileName, { exportWithoutWaitingForScene: true }).then((glb) => glb.downloadFiles());
904
+ /**
905
+ * Indicates whether the viewer is currently in 3D mode.
906
+ * @public
907
+ * @returns {boolean} True if the viewer is in 3D mode; otherwise, false.
908
+ */
909
+ get isMode3D() {
910
+ return this.#mode === "3d";
1250
911
  }
1251
912
  }
1252
913
 
914
+ customElements.define("pref-viewer-2d", PrefViewer2D);
915
+ customElements.define("pref-viewer-3d", PrefViewer3D);
916
+ customElements.define("pref-viewer-dialog", PrefViewerDialog);
1253
917
  customElements.define("pref-viewer", PrefViewer);