@preference-sl/pref-viewer 2.10.0 → 2.11.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js CHANGED
@@ -1,225 +1,144 @@
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 rendering and managing 2D and 3D product visualizations.
7
+ *
8
+ * Overview:
9
+ * - Encapsulates both 2D and 3D viewers using Babylon.js, supporting glTF/GLB models and environments.
10
+ * - Handles loading 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
+ *
15
+ * Usage:
16
+ * - Use as a custom HTML element: <pref-viewer ...>
17
+ * - Configure via attributes (e.g., config, model, scene, materials, drawing, options).
18
+ * - Control visibility, downloads, and viewer mode via public methods.
5
19
  *
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.
20
+ * Public Methods:
21
+ * - loadConfig(config), loadModel(model), loadScene(scene), loadMaterials(materials), loadDrawing(drawing)
22
+ * - setOptions(options)
23
+ * - setMode(mode): Sets the viewer mode to "2d" or "3d" and updates component visibility.
24
+ * - showModel(), hideModel(), showScene(), hideScene()
25
+ * - downloadModelGLB(), downloadModelUSDZ(), downloadModelAndSceneGLB(), downloadModelAndSceneUSDZ()
15
26
  *
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
- * ```
27
+ * Public Properties:
28
+ * - initialized: Indicates if the viewer is initialized.
29
+ * - loaded: Indicates if the viewer has finished loading.
30
+ * - loading: Indicates if the viewer is currently loading.
25
31
  *
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
- * ```
32
+ * Events:
33
+ * - "scene-loading": Dispatched when a loading operation starts.
34
+ * - "scene-loaded": Dispatched when a loading operation completes.
35
+ * - "scene-error": Dispatched when initialization fails.
33
36
  *
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
- * ```
37
+ * Notes:
38
+ * - Automatically creates and manages 2D and 3D viewer components in its shadow DOM.
39
+ * - Processes tasks sequentially to ensure consistent state.
40
+ * - Designed for extensibility and integration in product configurators and visualization tools.
41
41
  */
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
-
49
- class PrefViewerTask {
50
- static Types = Object.freeze({
51
- Config: "config",
52
- Environment: "environment",
53
- Materials: "materials",
54
- Model: "model",
55
- Options: "options",
56
- });
57
-
58
- /**
59
- * value: any payload for the task
60
- * type: must match one of PrefViewerTask.Types values (case-insensitive)
61
- */
62
- constructor(value, type) {
63
- this.value = value;
64
-
65
- const t = typeof type === "string" ? type.toLowerCase() : String(type).toLowerCase();
66
- const allowed = Object.values(PrefViewerTask.Types);
67
- if (!allowed.includes(t)) {
68
- throw new TypeError(
69
- `PrefViewerTask: invalid type "${type}". Allowed types: ${allowed.join(", ")}`
70
- );
71
- }
72
- this.type = t;
73
-
74
- Object.freeze(this);
75
- }
76
- }
77
-
78
42
  class PrefViewer extends HTMLElement {
79
- initialized = false;
80
- loaded = false;
81
- loading = false;
82
- #taskQueue = [];
43
+ #isInitialized = false;
44
+ #isLoaded = false;
45
+ #isLoading = false;
46
+ #mode = "3d";
83
47
 
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;
48
+ #taskQueue = [];
151
49
 
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;
50
+ #component2D = null;
51
+ #component3D = null;
161
52
 
53
+ /**
54
+ * Creates a new PrefViewer instance and attaches a shadow DOM.
55
+ * Initializes internal state and component references.
56
+ * @public
57
+ */
162
58
  constructor() {
163
59
  super();
164
60
  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
61
  }
178
62
 
63
+ /**
64
+ * Returns the list of attributes to observe for changes.
65
+ * @public
66
+ * @returns {string[]} Array of attribute names to observe.
67
+ */
179
68
  static get observedAttributes() {
180
- return ["config", "model", "scene", "show-model", "show-scene"];
69
+ return ["config", "drawing", "materials", "mode", "model", "scene", "options", "show-model", "show-scene"];
181
70
  }
182
71
 
72
+ /**
73
+ * Observes changes to specific attributes and triggers corresponding actions.
74
+ * Loads configuration, drawing, model, scene, materials, or options when their attributes change.
75
+ * Toggles model or scene visibility when "show-model" or "show-scene" attributes change.
76
+ * @public
77
+ * @param {string} name - The name of the changed attribute.
78
+ * @param {*} _old - The previous value of the attribute (unused).
79
+ * @param {*} value - The new value of the attribute.
80
+ * @returns {void}
81
+ */
183
82
  attributeChangedCallback(name, _old, value) {
184
- let data = null;
185
83
  switch (name) {
186
84
  case "config":
187
85
  this.loadConfig(value);
188
86
  break;
87
+ case "drawing":
88
+ this.loadDrawing(value);
89
+ break;
90
+ case "materials":
91
+ this.loadMaterials(value);
92
+ break;
93
+ case "mode":
94
+ if (_old === value || value.toLowerCase() === this.#mode) {
95
+ return;
96
+ }
97
+ this.setMode(value.toLowerCase());
98
+ break;
189
99
  case "model":
190
100
  this.loadModel(value);
191
101
  break;
192
102
  case "scene":
193
103
  this.loadScene(value);
194
104
  break;
195
- case "materials":
196
- this.loadMaterials(value);
197
- break;
198
105
  case "options":
199
106
  this.setOptions(value);
200
107
  break;
201
108
  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;
109
+ if (_old === value) {
110
+ return;
207
111
  }
112
+ const showModel = value.toLowerCase() === "true";
113
+ showModel ? this.showModel() : this.hideModel();
208
114
  break;
209
115
  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;
116
+ if (_old === value) {
117
+ return;
215
118
  }
119
+ const showScene = value.toLowerCase() === "true";
120
+ showScene ? this.showScene() : this.hideScene();
216
121
  break;
217
122
  }
218
123
  }
219
124
 
125
+ /**
126
+ * Called when the element is inserted into the DOM.
127
+ * Initializes the 3D and 2D viewer components and starts processing tasks.
128
+ * If the "config" attribute is missing, dispatches a "scene-error" event and stops initialization.
129
+ * @public
130
+ * @returns {void|boolean} Returns false if initialization fails; otherwise void.
131
+ */
220
132
  connectedCallback() {
133
+ this.#createComponent3D();
134
+ this.#createComponent2D();
135
+
136
+ if (!this.hasAttribute("mode")) {
137
+ this.setMode();
138
+ }
139
+
221
140
  if (!this.hasAttribute("config")) {
222
- const error = 'PrefViewer: provide "models" as array of model and environment';
141
+ const error = 'PrefViewer: provide "config" as a configuration object to initialize the viewer.';
223
142
  console.error(error);
224
143
  this.dispatchEvent(
225
144
  new CustomEvent("scene-error", {
@@ -232,45 +151,98 @@ class PrefViewer extends HTMLElement {
232
151
  return false;
233
152
  }
234
153
 
235
- this.#initializeBabylon();
236
- this.initialized = true;
154
+ this.#isInitialized = true;
237
155
  this.#processNextTask();
238
156
  }
239
157
 
240
- disconnectedCallback() {
241
- this.#disposeEngine();
242
- this.#canvasResizeObserver.disconnect();
158
+ /**
159
+ * Creates and appends the 2D viewer component to the shadow DOM.
160
+ * Sets the "visible" attribute to true by default.
161
+ * @private
162
+ * @returns {void}
163
+ */
164
+ #createComponent2D() {
165
+ this.#component2D = document.createElement("pref-viewer-2d");
166
+ this.#component2D.setAttribute("visible", "false");
167
+ this.shadowRoot.appendChild(this.#component2D);
243
168
  }
244
169
 
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
- });
170
+ /**
171
+ * Creates and appends the 3D viewer component to the shadow DOM.
172
+ * Sets the "visible" attribute to true by default.
173
+ * @private
174
+ * @returns {void}
175
+ */
176
+ #createComponent3D() {
177
+ this.#component3D = document.createElement("pref-viewer-3d");
178
+ this.#component3D.setAttribute("visible", "false");
179
+ this.shadowRoot.appendChild(this.#component3D);
254
180
  }
255
181
 
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);
182
+ /**
183
+ * Adds a new task to the internal queue for processing.
184
+ * If the viewer is initialized and not currently loading, immediately processes the next task.
185
+ * @private
186
+ * @param {*} value - The payload or data for the task.
187
+ * @param {string} type - The type of task (see PrefViewerTask.Types).
188
+ * @returns {void}
189
+ */
190
+ #addTaskToQueue(value, type) {
191
+ this.#taskQueue.push(new PrefViewerTask(value, type));
192
+ if (this.#isInitialized && !this.#isLoading) {
193
+ this.#processNextTask();
194
+ }
265
195
  }
266
196
 
267
- #setStatusLoading() {
268
- this.loaded = false;
269
- this.loading = true;
270
- if (this.hasAttribute("loaded")) {
271
- this.removeAttribute("loaded");
197
+ /**
198
+ * Processes the next task in the queue, if any.
199
+ * Dispatches the task to the appropriate handler based on its type.
200
+ * @private
201
+ * @returns {boolean|void} Returns false if the queue is empty; otherwise void.
202
+ */
203
+ #processNextTask() {
204
+ if (!this.#taskQueue.length) {
205
+ return false;
272
206
  }
273
- this.setAttribute("loading", "");
207
+ const task = this.#taskQueue[0];
208
+ this.#taskQueue.shift();
209
+ switch (task.type) {
210
+ case PrefViewerTask.Types.Config:
211
+ this.#processConfig(task.value);
212
+ break;
213
+ case PrefViewerTask.Types.Drawing:
214
+ this.#processDrawing(task.value);
215
+ break;
216
+ case PrefViewerTask.Types.Environment:
217
+ this.#processEnvironment(task.value);
218
+ break;
219
+ case PrefViewerTask.Types.Materials:
220
+ this.#processMaterials(task.value);
221
+ break;
222
+ case PrefViewerTask.Types.Model:
223
+ this.#processModel(task.value);
224
+ break;
225
+ case PrefViewerTask.Types.Options:
226
+ this.#processOptions(task.value);
227
+ break;
228
+ case PrefViewerTask.Types.Visibility:
229
+ this.#processVisibility(task.value);
230
+ break;
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Handles the start of a 3D loading operation.
236
+ * Updates loading state, sets attributes, and dispatches a "scene-loading" event.
237
+ * @private
238
+ * @returns {void}
239
+ */
240
+ #on3DLoading() {
241
+ this.#isLoaded = false;
242
+ this.#isLoading = true;
243
+
244
+ this.removeAttribute("loaded-3d");
245
+ this.setAttribute("loading-3d", "");
274
246
  this.dispatchEvent(
275
247
  new CustomEvent("scene-loading", {
276
248
  bubbles: true,
@@ -278,35 +250,16 @@ class PrefViewer extends HTMLElement {
278
250
  composed: true,
279
251
  })
280
252
  );
281
- this.#engine.stopRenderLoop(this.#renderLoop);
282
253
  }
283
254
 
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
-
255
+ /**
256
+ * Handles the completion of a 3D loading operation.
257
+ * Updates loading state, sets attributes, dispatches a "scene-loaded" event, and processes the next task.
258
+ * @private
259
+ * @param {object} [detail={}] - Optional details to include in the event.
260
+ * @returns {void}
261
+ */
262
+ #on3DLoaded(detail = {}) {
310
263
  this.dispatchEvent(
311
264
  new CustomEvent("scene-loaded", {
312
265
  bubbles: true,
@@ -316,755 +269,224 @@ class PrefViewer extends HTMLElement {
316
269
  })
317
270
  );
318
271
 
319
- await this.#scene.whenReadyAsync();
320
- this.#engine.runRenderLoop(this.#renderLoop);
321
-
322
- this.#resetChangedFlags();
272
+ this.removeAttribute("loading-3d");
273
+ this.setAttribute("loaded-3d", "");
323
274
 
324
- if (this.hasAttribute("loading")) {
325
- this.removeAttribute("loading");
326
- }
327
- this.setAttribute("loaded", "");
328
-
329
- this.loaded = true;
330
- this.loading = false;
331
-
332
- this.#processNextTask();
275
+ this.#isLoaded = true;
276
+ this.#isLoading = false;
333
277
  }
334
278
 
335
- // Data
336
- #checkCameraChanged(options) {
337
- if (!options || !options.camera) {
338
- return false;
339
- }
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;
349
- }
350
- return changed;
351
- }
352
-
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
- }
375
-
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
- }
385
-
386
- #resetChangedFlags() {
387
- const reset = (node) => {
388
- node.changed = { pending: false, success: false };
389
- };
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);
407
- }
408
-
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
- };
413
-
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
- }
420
-
421
- async #createXRExperience() {
422
- if (this.#XRExperience) {
423
- return true;
424
- }
425
-
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
- }
432
-
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
- }
469
-
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
- }
482
-
483
- #createLights() {
484
- this.#initEnvironmentTexture();
485
-
486
- if (this.#scene.environmentTexture) {
487
- return true;
488
- }
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;
509
- }
279
+ /**
280
+ * Handles the start of a 2D loading operation.
281
+ * Updates loading state, sets attributes, and dispatches a "drawing-loading" event.
282
+ * @private
283
+ * @returns {void}
284
+ */
285
+ #on2DLoading() {
286
+ this.#isLoaded = false;
287
+ this.#isLoading = true;
510
288
 
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;
289
+ this.removeAttribute("loaded-2d");
290
+ this.setAttribute("loading-2d", "");
291
+ this.dispatchEvent(
292
+ new CustomEvent("drawing-loading", {
293
+ bubbles: true,
294
+ cancelable: false,
295
+ composed: true,
296
+ })
297
+ );
522
298
  }
523
299
 
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;
553
- };
554
-
555
- let iblShadowsPipeline = createIBLShadowPipeline(this.#scene);
300
+ /**
301
+ * Handles the completion of a 2D loading operation.
302
+ * Updates loading state, sets attributes, dispatches a "drawing-loaded" event, and processes the next task.
303
+ * @private
304
+ * @returns {void}
305
+ */
306
+ #on2DLoaded() {
307
+ this.dispatchEvent(
308
+ new CustomEvent("drawing-loaded", {
309
+ bubbles: true,
310
+ cancelable: false,
311
+ composed: true,
312
+ })
313
+ );
556
314
 
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
- });
315
+ this.removeAttribute("loading-2d");
316
+ this.setAttribute("loaded-2d", "");
564
317
 
565
- this.#scene.materials.forEach((material) => {
566
- iblShadowsPipeline.addShadowReceivingMaterial(material);
567
- });
318
+ this.#isLoaded = true;
319
+ this.#isLoading = false;
568
320
  }
569
321
 
570
- #initShadows() {
571
- if (!this.#scene.environmentTexture) {
572
- this.#initIBLShadows();
573
- return true;
322
+ /**
323
+ * Processes a configuration object by loading it into the 3D component.
324
+ * Dispatches loading events and processes the next task when finished.
325
+ * @private
326
+ * @param {object} config - The configuration object to process.
327
+ * @returns {void}
328
+ */
329
+ #processConfig(config) {
330
+ if (!this.#component3D) {
331
+ return;
574
332
  }
575
333
 
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
- }
334
+ this.#on3DLoading();
335
+ this.#component3D.load(config).then((detail) => {
336
+ this.#on3DLoaded(detail);
337
+ this.#processNextTask();
584
338
  });
585
339
  }
586
340
 
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));
341
+ /**
342
+ * Processes a drawing object by loading it into the 2D component.
343
+ * Processes the next task when finished.
344
+ * @private
345
+ * @param {object} drawing - The drawing object to process.
346
+ * @returns {void}
347
+ */
348
+ #processDrawing(drawing) {
349
+ if (!this.#component2D) {
350
+ return;
596
351
  }
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
352
 
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
-
643
- #transformUrl(url) {
644
- return new Promise((resolve) => {
645
- resolve(url.replace(/^blob:[^/]+\//i, "").replace(/\\/g, "/"));
353
+ this.#on2DLoading();
354
+ this.#component2D.load(drawing).then(() => {
355
+ this.#on2DLoaded();
356
+ this.#processNextTask();
646
357
  });
647
358
  }
648
359
 
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));
360
+ /**
361
+ * Processes an environment (scene) object by wrapping it in a config and loading it.
362
+ * @private
363
+ * @param {object} environment - The environment/scene object to process.
364
+ * @returns {void}
365
+ */
366
+ #processEnvironment(environment) {
367
+ const config = {};
368
+ config.scene = environment;
369
+ this.#processConfig(config);
688
370
  }
689
371
 
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;
372
+ /**
373
+ * Processes a materials object by wrapping it in a config and loading it.
374
+ * @private
375
+ * @param {object} materials - The materials object to process.
376
+ * @returns {void}
377
+ */
378
+ #processMaterials(materials) {
379
+ const config = {};
380
+ config.materials = materials;
381
+ this.#processConfig(config);
729
382
  }
730
383
 
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;
384
+ /**
385
+ * Processes a model object by wrapping it in a config and loading it.
386
+ * @private
387
+ * @param {object} model - The model object to process.
388
+ * @returns {void}
389
+ */
390
+ #processModel(model) {
391
+ const config = {};
392
+ config.model = model;
393
+ this.#processConfig(config);
738
394
  }
739
395
 
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;
396
+ /**
397
+ * Processes viewer options by loading them into the 3D component.
398
+ * Dispatches loading events and processes the next task.
399
+ * @private
400
+ * @param {object} options - The options object to process.
401
+ * @returns {void}
402
+ */
403
+ #processOptions(options) {
404
+ if (!this.#component3D) {
405
+ return;
743
406
  }
744
407
 
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;
408
+ this.#on3DLoading();
409
+ const detail = this.#component3D.setOptions(options);
410
+ this.#on3DLoaded(detail);
411
+ this.#processNextTask();
772
412
  }
773
413
 
774
- #addContainer(container) {
775
- if (!container.assetContainer || container.visible || !container.show) {
776
- return false;
414
+ /**
415
+ * Processes visibility configuration for the model and scene.
416
+ * Shows or hides the model and/or scene based on the config, then processes the next task.
417
+ * @private
418
+ * @param {object} config - The visibility configuration object.
419
+ * @returns {void}
420
+ */
421
+ #processVisibility(config) {
422
+ if (!this.#component3D) {
423
+ return;
777
424
  }
778
-
779
- container.assetContainer.addAllToScene();
780
- container.visible = true;
781
- return true;
782
- }
783
-
784
- #removeContainer(container) {
785
- if (!container.assetContainer || !container.visible) {
786
- return false;
425
+ const showModel = config.model?.visible;
426
+ const showScene = config.scene?.visible;
427
+ if (showModel !== undefined) {
428
+ showModel ? this.#component3D.showModel() : this.#component3D.hideModel();
787
429
  }
788
-
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;
430
+ if (showScene !== undefined) {
431
+ showScene ? this.#component3D.showEnvironment() : this.#component3D.hideEnvironment();
799
432
  }
800
- this.#scene.getEngine().releaseEffects();
801
- container.assetContainer = newAssetContainer;
802
- this.#addContainer(container);
803
- return true;
433
+ this.#processNextTask();
804
434
  }
805
435
 
806
- async #loadAssetContainer(container) {
807
- let storage = container?.storage;
808
-
809
- if (!storage) {
810
- return false;
811
- }
812
-
813
- let source = storage.url || null;
814
-
815
- if (storage.db && storage.table && storage.id) {
816
- await this.#initStorage(storage.db, storage.table);
817
- const object = await loadModel(storage.id, storage.table);
818
- source = object.data;
819
- if (object.timeStamp === container.timeStamp) {
820
- container.changed = { pending: false, success: false };
821
- return false;
822
- } else {
823
- container.changed = { pending: true, size: object.size, success: false, timeStamp: object.timeStamp };
824
- }
825
- }
826
-
827
- if (!source) {
828
- return false;
829
- }
830
-
831
- let file = null;
832
-
833
- let { blob, extension, size } = this.#decodeBase64(source);
834
- if (blob && extension) {
835
- file = new File([blob], `${container.name}${extension}`, {
836
- type: blob.type,
837
- });
838
- if (!container.changed.pending) {
839
- if (container.timeStamp === null && container.size === size) {
840
- container.changed = { pending: false, success: false };
841
- return false;
842
- } else {
843
- container.changed = { pending: true, size: size, success: false, timeStamp: null };
844
- }
845
- }
436
+ /**
437
+ * ---------------------------
438
+ * Public methods
439
+ * ---------------------------
440
+ */
441
+
442
+ /**
443
+ * Sets the viewer mode to "2d" or "3d" and updates component visibility accordingly.
444
+ * @public
445
+ * @param {string} [mode=this.#mode] - The mode to set ("2d" or "3d").
446
+ * @returns {void}
447
+ */
448
+ setMode(mode = this.#mode) {
449
+ if (mode !== "2d" && mode !== "3d") {
450
+ console.warn(`PrefViewer: invalid mode "${mode}". Allowed modes are "2d" and "3d".`);
451
+ return;
452
+ }
453
+ this.#mode = mode;
454
+ if (mode === "2d") {
455
+ this.#component3D?.hide();
456
+ this.#component2D?.show();
846
457
  } else {
847
- const extMatch = source.match(/\.(gltf|glb)(\?|#|$)/i);
848
- extension = extMatch ? `.${extMatch[1].toLowerCase()}` : ".gltf";
849
- const [fileSize, fileTimeStamp] = await this.#getServerFileDataHeader(source);
850
- if (container.size === fileSize && container.timeStamp === fileTimeStamp) {
851
- container.changed = { pending: false, success: false };
852
- return false;
853
- } else {
854
- container.changed = { pending: true, size: fileSize, success: false, timeStamp: fileTimeStamp };
855
- }
458
+ this.#component3D?.show();
459
+ this.#component2D?.hide();
856
460
  }
857
-
858
- // https://doc.babylonjs.com/typedoc/interfaces/BABYLON.LoadAssetContainerOptions
859
- let options = {
860
- pluginExtension: extension,
861
- pluginOptions: {
862
- gltf: {
863
- compileMaterials: true,
864
- loadAllMaterials: true,
865
- loadOnlyMaterials: container.name === "materials",
866
- preprocessUrlAsync: this.#transformUrl,
867
- },
868
- },
869
- };
870
-
871
- return LoadAssetContainerAsync(file || source, this.#scene, options);
872
- }
873
-
874
- async #loadContainers(loadModel = true, loadEnvironment = true, loadMaterials = true) {
875
- this.#engine.stopRenderLoop(this.#renderLoop);
876
-
877
- const promiseArray = [];
878
- promiseArray.push(loadModel ? this.#loadAssetContainer(this.#data.containers.model) : false);
879
- promiseArray.push(loadEnvironment ? this.#loadAssetContainer(this.#data.containers.environment) : false);
880
- promiseArray.push(loadMaterials ? this.#loadAssetContainer(this.#data.containers.materials) : false);
881
-
882
- Promise.allSettled(promiseArray)
883
- .then((values) => {
884
- const modelContainer = values[0];
885
- const environmentContainer = values[1];
886
- const materialsContainer = values[2];
887
-
888
- if (modelContainer.status === "fulfilled" && modelContainer.value) {
889
- modelContainer.value.lights = [];
890
- this.#replaceContainer(this.#data.containers.model, modelContainer.value);
891
- this.#storeChangedFlagsForContainer(this.#data.containers.model, true);
892
- } else {
893
- if (this.#data.containers.model.assetContainer && this.#data.containers.model.show !== this.#data.containers.model.visible) {
894
- this.#data.containers.model.show ? this.#addContainer(this.#data.containers.model) : this.#removeContainer(this.#data.containers.model);
895
- }
896
- this.#storeChangedFlagsForContainer(this.#data.containers.model, false);
897
- }
898
-
899
- if (environmentContainer.status === "fulfilled" && environmentContainer.value) {
900
- this.#replaceContainer(this.#data.containers.environment, environmentContainer.value);
901
- this.#storeChangedFlagsForContainer(this.#data.containers.environment, true);
902
- } else {
903
- if (this.#data.containers.environment.assetContainer && this.#data.containers.environment.show !== this.#data.containers.environment.visible) {
904
- this.#data.containers.environment.show ? this.#addContainer(this.#data.containers.environment) : this.#removeContainer(this.#data.containers.environment);
905
- }
906
- this.#storeChangedFlagsForContainer(this.#data.containers.environment, false);
907
- }
908
-
909
- if (materialsContainer.status === "fulfilled" && materialsContainer.value) {
910
- this.#replaceContainer(this.#data.containers.materials, materialsContainer.value);
911
- this.#storeChangedFlagsForContainer(this.#data.containers.materials, true);
912
- } else {
913
- this.#storeChangedFlagsForContainer(this.#data.containers.materials, false);
914
- }
915
-
916
- this.#setOptionsMaterials();
917
- this.#setOptionsCamera();
918
- this.#setVisibilityOfWallAndFloorInModel();
919
- })
920
- .catch((error) => {
921
- this.loaded = true;
922
- console.error("PrefViewer: failed to load model", error);
923
- this.dispatchEvent(
924
- new CustomEvent("scene-error", {
925
- bubbles: true,
926
- cancelable: false,
927
- composed: true,
928
- detail: { error: error },
929
- })
930
- );
931
- })
932
- .finally(async () => {
933
- this.#setMaxSimultaneousLights();
934
- this.#initShadows();
935
- await this.#setStatusLoaded();
936
- });
937
- }
938
-
939
- // Tasks
940
- #addTaskToQueue(value, type) {
941
- this.#taskQueue.push(new PrefViewerTask(value, type));
942
- if (this.initialized && !this.loading) {
943
- this.#processNextTask();
461
+ if (this.getAttribute("mode") !== mode) {
462
+ this.setAttribute("mode", mode);
944
463
  }
945
464
  }
946
465
 
947
- #processNextTask() {
948
- if (!this.#taskQueue.length) {
949
- return false;
950
- }
951
- const task = this.#taskQueue[0];
952
- this.#taskQueue.shift();
953
- switch (task.type) {
954
- case PrefViewerTask.Types.Config:
955
- this.#processConfig(task.value);
956
- break;
957
- case PrefViewerTask.Types.Model:
958
- this.#processModel(task.value);
959
- break;
960
- case PrefViewerTask.Types.Environment:
961
- this.#processEnvironment(task.value);
962
- break;
963
- case PrefViewerTask.Types.Materials:
964
- this.#processMaterials(task.value);
965
- break;
966
- case PrefViewerTask.Types.Options:
967
- this.#processOptions(task.value);
968
- break;
969
- }
970
- }
971
-
972
- #processConfig(config) {
973
- this.#setStatusLoading();
974
-
975
- // Containers
976
- const loadModel = !!config.model?.storage;
977
- this.#data.containers.model.changed.pending = loadModel;
978
- this.#data.containers.model.changed.success = false;
979
- this.#data.containers.model.changed.storage = this.#data.containers.model.storage;
980
- this.#data.containers.model.storage = loadModel ? config.model.storage : this.#data.containers.model.storage;
981
- this.#data.containers.model.show = config.model?.visible !== undefined ? config.model.visible : this.#data.containers.model.show;
982
-
983
- const loadEnvironment = !!config.scene?.storage;
984
- this.#data.containers.environment.changed.pending = loadEnvironment;
985
- this.#data.containers.environment.changed.success = false;
986
- this.#data.containers.environment.changed.storage = this.#data.containers.environment.storage;
987
- this.#data.containers.environment.storage = loadEnvironment ? config.scene.storage : this.#data.containers.environment.storage;
988
- this.#data.containers.environment.show = config.scene?.visible !== undefined ? config.scene.visible : this.#data.containers.environment.show;
989
-
990
- const loadMaterials = !!config.materials?.storage;
991
- this.#data.containers.materials.changed.pending = loadMaterials;
992
- this.#data.containers.materials.changed.success = false;
993
- this.#data.containers.materials.changed.storage = this.#data.containers.materials.storage;
994
- this.#data.containers.materials.storage = loadMaterials ? config.materials.storage : this.#data.containers.materials.storage;
995
-
996
- // Options
997
- if (config.options) {
998
- this.#checkCameraChanged(config.options);
999
- this.#checkMaterialsChanged(config.options);
1000
- }
1001
-
1002
- this.#loadContainers(loadModel, loadEnvironment, loadMaterials);
1003
- }
1004
-
1005
- #processModel(model) {
1006
- this.#setStatusLoading();
1007
-
1008
- const loadModel = !!model.storage;
1009
- this.#data.containers.model.changed.pending = loadModel;
1010
- this.#data.containers.model.changed.success = false;
1011
- this.#data.containers.model.changed.storage = this.#data.containers.model.storage;
1012
- this.#data.containers.model.storage = loadModel ? model.storage : this.#data.containers.model.storage;
1013
- this.#data.containers.model.show = model.visible !== undefined ? model.visible : this.#data.containers.model.show;
1014
-
1015
- this.initialized && this.#loadContainers(loadModel, false, false);
1016
- }
1017
-
1018
- #processEnvironment(environment) {
1019
- this.#setStatusLoading();
1020
-
1021
- const loadEnvironment = !!environment.storage;
1022
- this.#data.containers.environment.changed.pending = loadEnvironment;
1023
- this.#data.containers.environment.changed.success = false;
1024
- this.#data.containers.environment.changed.storage = this.#data.containers.environment.storage;
1025
- this.#data.containers.environment.storage = loadEnvironment ? environment.storage : this.#data.containers.environment.storage;
1026
- this.#data.containers.environment.show = environment.visible !== undefined ? environment.visible : this.#data.containers.environment.show;
1027
-
1028
- this.#loadContainers(false, loadEnvironment, false);
1029
- }
1030
-
1031
- #processMaterials(materials) {
1032
- this.#setStatusLoading();
1033
-
1034
- const loadMaterials = !!materials.storage;
1035
- this.#data.containers.materials.changed.pending = loadMaterials;
1036
- this.#data.containers.materials.changed.success = false;
1037
- this.#data.containers.materials.changed.storage = this.#data.containers.materials.storage;
1038
- this.#data.containers.materials.storage = loadMaterials ? materials.storage : this.#data.containers.materials.storage;
1039
-
1040
- this.#loadContainers(false, false, loadMaterials);
1041
- }
1042
-
1043
- async #processOptions(options) {
1044
- this.#setStatusLoading();
1045
-
1046
- let someSetted = false;
1047
- if (this.#checkCameraChanged(options)) {
1048
- someSetted = someSetted || this.#setOptionsCamera();
1049
- }
1050
- if (this.#checkMaterialsChanged(options)) {
1051
- someSetted = someSetted || this.#setOptionsMaterials();
1052
- }
1053
-
1054
- await this.#setStatusLoaded();
1055
-
1056
- return someSetted;
1057
- }
1058
-
1059
- // Public Methods
466
+ /**
467
+ * Loads a configuration object or JSON string and adds it to the task queue.
468
+ * If the config contains a drawing, adds a drawing task as well.
469
+ * @public
470
+ * @param {object|string} config - Configuration object or JSON string.
471
+ * @returns {boolean|void} Returns false if config is invalid; otherwise void.
472
+ */
1060
473
  loadConfig(config) {
1061
474
  config = typeof config === "string" ? JSON.parse(config) : config;
1062
475
  if (!config) {
1063
476
  return false;
1064
477
  }
478
+ if (config.drawing) {
479
+ this.#addTaskToQueue(config.drawing, "drawing");
480
+ }
1065
481
  this.#addTaskToQueue(config, "config");
1066
482
  }
1067
483
 
484
+ /**
485
+ * Loads a model object or JSON string and adds it to the task queue.
486
+ * @public
487
+ * @param {object|string} model - Model object or JSON string.
488
+ * @returns {boolean|void} Returns false if model is invalid; otherwise void.
489
+ */
1068
490
  loadModel(model) {
1069
491
  model = typeof model === "string" ? JSON.parse(model) : model;
1070
492
  if (!model) {
@@ -1073,6 +495,12 @@ class PrefViewer extends HTMLElement {
1073
495
  this.#addTaskToQueue(model, "model");
1074
496
  }
1075
497
 
498
+ /**
499
+ * Loads a scene/environment object or JSON string and adds it to the task queue.
500
+ * @public
501
+ * @param {object|string} scene - Scene object or JSON string.
502
+ * @returns {boolean|void} Returns false if scene is invalid; otherwise void.
503
+ */
1076
504
  loadScene(scene) {
1077
505
  scene = typeof scene === "string" ? JSON.parse(scene) : scene;
1078
506
  if (!scene) {
@@ -1081,6 +509,12 @@ class PrefViewer extends HTMLElement {
1081
509
  this.#addTaskToQueue(scene, "environment");
1082
510
  }
1083
511
 
512
+ /**
513
+ * Loads materials object or JSON string and adds it to the task queue.
514
+ * @public
515
+ * @param {object|string} materials - Materials object or JSON string.
516
+ * @returns {boolean|void} Returns false if materials are invalid; otherwise void.
517
+ */
1084
518
  loadMaterials(materials) {
1085
519
  materials = typeof materials === "string" ? JSON.parse(materials) : materials;
1086
520
  if (!materials) {
@@ -1089,6 +523,26 @@ class PrefViewer extends HTMLElement {
1089
523
  this.#addTaskToQueue(materials, "materials");
1090
524
  }
1091
525
 
526
+ /**
527
+ * Loads a drawing object or JSON string and adds it to the task queue.
528
+ * @public
529
+ * @param {object|string} drawing - Drawing object or JSON string.
530
+ * @returns {boolean|void} Returns false if drawing is invalid; otherwise void.
531
+ */
532
+ loadDrawing(drawing) {
533
+ drawing = typeof drawing === "string" ? JSON.parse(drawing) : drawing;
534
+ if (!drawing) {
535
+ return false;
536
+ }
537
+ this.#addTaskToQueue(drawing, "drawing");
538
+ }
539
+
540
+ /**
541
+ * Sets viewer options from an object or JSON string and adds them to the task queue.
542
+ * @public
543
+ * @param {object|string} options - Options object or JSON string.
544
+ * @returns {boolean|void} Returns false if options are invalid; otherwise void.
545
+ */
1092
546
  setOptions(options) {
1093
547
  options = typeof options === "string" ? JSON.parse(options) : options;
1094
548
  if (!options) {
@@ -1097,55 +551,131 @@ class PrefViewer extends HTMLElement {
1097
551
  this.#addTaskToQueue(options, "options");
1098
552
  }
1099
553
 
554
+ /**
555
+ * Shows the 3D model by setting its visibility to true.
556
+ * Adds a visibility task to the queue for processing.
557
+ * @public
558
+ * @returns {void}
559
+ */
1100
560
  showModel() {
1101
- this.#data.containers.model.show = true;
1102
- this.#addContainer(this.#data.containers.model);
561
+ const config = { model: { visible: true } };
562
+ this.#addTaskToQueue(config, "visibility");
1103
563
  }
1104
564
 
565
+ /**
566
+ * Hides the 3D model by setting its visibility to false.
567
+ * Adds a visibility task to the queue for processing.
568
+ * @public
569
+ * @returns {void}
570
+ */
1105
571
  hideModel() {
1106
- this.#data.containers.model.show = false;
1107
- this.#removeContainer(this.#data.containers.model);
572
+ const config = { model: { visible: false } };
573
+ this.#addTaskToQueue(config, "visibility");
1108
574
  }
1109
575
 
576
+ /**
577
+ * Shows the scene/environment by setting its visibility to true.
578
+ * Adds a visibility task to the queue for processing.
579
+ * @public
580
+ * @returns {void}
581
+ */
1110
582
  showScene() {
1111
- this.#data.containers.environment.show = true;
1112
- this.#addContainer(this.#data.containers.environment);
1113
- this.#setVisibilityOfWallAndFloorInModel();
583
+ const config = { scene: { visible: true } };
584
+ this.#addTaskToQueue(config, "visibility");
1114
585
  }
1115
586
 
587
+ /**
588
+ * Hides the scene/environment by setting its visibility to false.
589
+ * Adds a visibility task to the queue for processing.
590
+ * @public
591
+ * @returns {void}
592
+ */
1116
593
  hideScene() {
1117
- this.#data.containers.environment.show = false;
1118
- this.#removeContainer(this.#data.containers.environment);
1119
- this.#setVisibilityOfWallAndFloorInModel();
594
+ const config = { scene: { visible: false } };
595
+ this.#addTaskToQueue(config, "visibility");
1120
596
  }
1121
597
 
598
+
599
+ /**
600
+ * Initiates download of the current 3D model in GLB format.
601
+ * @public
602
+ * @returns {void}
603
+ */
1122
604
  downloadModelGLB() {
1123
- const fileName = "model";
1124
- GLTF2Export.GLBAsync(this.#data.containers.model.assetContainer, fileName, { exportWithoutWaitingForScene: true }).then((glb) => glb.downloadFiles());
605
+ if (!this.#component3D) {
606
+ return;
607
+ }
608
+
609
+ this.#component3D.downloadModelGLB();
1125
610
  }
1126
611
 
612
+ /**
613
+ * Initiates download of the current 3D model in USDZ format.
614
+ * @public
615
+ * @returns {void}
616
+ */
1127
617
  downloadModelUSDZ() {
1128
- const fileName = "model";
1129
- USDZExportAsync(this.#data.containers.model.assetContainer).then((response) => {
1130
- if (response) {
1131
- Tools.Download(new Blob([response], { type: "model/vnd.usdz+zip" }), `${fileName}.usdz`);
1132
- }
1133
- });
618
+ if (!this.#component3D) {
619
+ return;
620
+ }
621
+
622
+ this.#component3D.downloadModelUSDZ();
1134
623
  }
1135
624
 
625
+ /**
626
+ * Initiates download of both the 3D model and scene in USDZ format.
627
+ * @public
628
+ * @returns {void}
629
+ */
1136
630
  downloadModelAndSceneUSDZ() {
1137
- const fileName = "scene";
1138
- USDZExportAsync(this.#scene).then((response) => {
1139
- if (response) {
1140
- Tools.Download(new Blob([response], { type: "model/vnd.usdz+zip" }), `${fileName}.usdz`);
1141
- }
1142
- });
631
+ if (!this.#component3D) {
632
+ return;
633
+ }
634
+
635
+ this.#component3D.downloadModelAndSceneUSDZ();
1143
636
  }
1144
637
 
638
+ /**
639
+ * Initiates download of both the 3D model and scene in GLB format.
640
+ * @public
641
+ * @returns {void}
642
+ */
1145
643
  downloadModelAndSceneGLB() {
1146
- const fileName = "scene";
1147
- GLTF2Export.GLBAsync(this.#scene, fileName, { exportWithoutWaitingForScene: true }).then((glb) => glb.downloadFiles());
644
+ if (!this.#component3D) {
645
+ return;
646
+ }
647
+
648
+ this.#component3D.downloadModelAndSceneGLB();
649
+ }
650
+
651
+ /**
652
+ * Indicates whether the viewer has been initialized.
653
+ * @public
654
+ * @returns {boolean} True if initialized; otherwise, false.
655
+ */
656
+ get initialized() {
657
+ return this.#isInitialized;
658
+ }
659
+
660
+ /**
661
+ * Indicates whether the viewer has finished loading.
662
+ * @public
663
+ * @returns {boolean} True if loaded; otherwise, false.
664
+ */
665
+ get loaded() {
666
+ return this.#isLoaded;
667
+ }
668
+
669
+ /**
670
+ * Indicates whether the viewer is currently loading.
671
+ * @public
672
+ * @returns {boolean} True if loading; otherwise, false.
673
+ */
674
+ get loading() {
675
+ return this.#isLoading;
1148
676
  }
1149
677
  }
1150
678
 
679
+ customElements.define("pref-viewer-2d", PrefViewer2D);
680
+ customElements.define("pref-viewer-3d", PrefViewer3D);
1151
681
  customElements.define("pref-viewer", PrefViewer);