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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js CHANGED
@@ -22,7 +22,7 @@
22
22
  * style="width:800px; height:600px;">
23
23
  * </pref-viewer>
24
24
  * ```
25
- *
25
+ *
26
26
  * Load scene a URL:
27
27
  * ```html
28
28
  * <pref-viewer
@@ -39,147 +39,112 @@
39
39
  * </pref-viewer>
40
40
  * ```
41
41
  */
42
- import { Engine, Scene, ArcRotateCamera, Vector3, Color4, HemisphericLight, DirectionalLight, PointLight, ShadowGenerator, LoadAssetContainerAsync, Tools, WebXRSessionManager, WebXRDefaultExperience, MeshBuilder, WebXRFeatureName } from "@babylonjs/core";
42
+ import { Engine, Scene, ArcRotateCamera, Vector3, Color4, HemisphericLight, DirectionalLight, PointLight, ShadowGenerator, LoadAssetContainerAsync, Tools, WebXRSessionManager, WebXRDefaultExperience, MeshBuilder, WebXRFeatureName, HDRCubeTexture, IblShadowsRenderPipeline } from "@babylonjs/core";
43
43
  import "@babylonjs/loaders";
44
44
  import { USDZExportAsync, GLTF2Export } from "@babylonjs/serializers";
45
45
  import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_draco_mesh_compression";
46
46
  import { DracoCompression } from "@babylonjs/core/Meshes/Compression/dracoCompression";
47
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
+ });
48
58
 
49
- class PrefViewer extends HTMLElement {
50
- static LOG_PREFIX = "[PrefViewer]";
51
- static LOG_LEVELS = { none: 0, error: 1, warn: 2, info: 3, debug: 4 };
52
- // Por defecto NO loggear: control únicamente vía atributo `log-level`
53
- static DEFAULT_LOG_LEVEL = "none";
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;
54
72
 
55
- #initialized = false;
56
- #logLevel = PrefViewer.DEFAULT_LOG_LEVEL;
73
+ Object.freeze(this);
74
+ }
75
+ }
76
+
77
+ class PrefViewer extends HTMLElement {
78
+ initialized = false;
79
+ loaded = false;
80
+ loading = false;
81
+ #taskQueue = [];
82
+ #fileStorage = new FileStorage("PrefViewer", "Files");
57
83
 
58
84
  #data = {
59
85
  containers: {
60
86
  model: {
61
87
  name: "model",
62
- container: null,
88
+ assetContainer: null,
63
89
  show: true,
64
90
  storage: null,
65
91
  visible: false,
66
92
  size: null,
67
- timestamp: null,
68
- changed: false,
93
+ timeStamp: null,
94
+ changed: { pending: false, success: false },
69
95
  },
70
96
  environment: {
71
97
  name: "environment",
72
- container: null,
98
+ assetContainer: null,
73
99
  show: true,
74
100
  storage: null,
75
101
  visible: false,
76
102
  size: null,
77
- timestamp: null,
78
- changed: false,
103
+ timeStamp: null,
104
+ changed: { pending: false, success: false },
79
105
  },
80
106
  materials: {
81
107
  name: "materials",
82
- container: null,
108
+ assetContainer: null,
83
109
  storage: null,
84
110
  show: true,
85
111
  visible: false,
86
112
  size: null,
87
- timestamp: null,
88
- changed: false,
113
+ timeStamp: null,
114
+ changed: { pending: false, success: false },
89
115
  },
90
116
  },
91
117
  options: {
92
118
  camera: {
93
119
  value: null,
94
120
  locked: true,
95
- changed: false,
121
+ changed: { pending: false, success: false },
96
122
  },
97
123
  materials: {
98
124
  innerWall: {
99
125
  value: null,
100
126
  prefix: "innerWall",
101
- changed: false,
127
+ changed: { pending: false, success: false },
102
128
  },
103
129
  outerWall: {
104
130
  value: null,
105
131
  prefix: "outerWall",
106
- changed: false,
132
+ changed: { pending: false, success: false },
107
133
  },
108
134
  innerFloor: {
109
135
  value: null,
110
136
  prefix: "innerFloor",
111
- changed: false,
137
+ changed: { pending: false, success: false },
112
138
  },
113
139
  outerFloor: {
114
140
  value: null,
115
141
  prefix: "outerFloor",
116
- changed: false,
142
+ changed: { pending: false, success: false },
117
143
  },
118
144
  },
119
145
  },
120
146
  };
121
147
 
122
- #log(level, message, context) {
123
- const levels = PrefViewer.LOG_LEVELS;
124
- const current = levels[this.#logLevel] ?? levels[PrefViewer.DEFAULT_LOG_LEVEL];
125
- const incoming = levels[level] ?? levels.info;
126
- if (incoming > current || current === levels.none) return;
127
-
128
- const logger = console[level] ?? console.log;
129
- if (context !== undefined) {
130
- logger(`${PrefViewer.LOG_PREFIX}: ${message}`, context);
131
- } else {
132
- logger(`${PrefViewer.LOG_PREFIX}: ${message}`);
133
- }
134
- }
135
-
136
- #setLogLevel(level) {
137
- const value = String(level || "").toLowerCase();
138
- this.#logLevel = (value in PrefViewer.LOG_LEVELS) ? value : PrefViewer.DEFAULT_LOG_LEVEL;
139
- this.#logInfo("Log level set", { level: this.#logLevel });
140
- }
141
-
142
- #logDebug(message, context) {
143
- this.#log("debug", message, context);
144
- }
145
-
146
- #logInfo(message, context) {
147
- this.#log("info", message, context);
148
- }
149
-
150
- #logWarn(message, context) {
151
- this.#log("warn", message, context);
152
- }
153
-
154
- #logError(message, context) {
155
- this.#log("error", message, context);
156
- }
157
-
158
- #summarizeValue(value) {
159
- if (typeof value === "string" && value.length > 150) {
160
- return `${value.slice(0, 150)}… (${value.length} chars)`;
161
- }
162
- return value;
163
- }
164
-
165
- #describeStorage(storage) {
166
- if (!storage) {
167
- return "none";
168
- }
169
- if (storage.db && storage.table && storage.id) {
170
- return `IndexedDB(${storage.db}/${storage.table}#${storage.id})`;
171
- }
172
- if (typeof storage.url === "string") {
173
- return storage.url.startsWith("data:") ? "data-url" : storage.url;
174
- }
175
- return "unknown";
176
- }
177
-
178
- static get observedAttributes() {
179
- // Añadimos "log-level" para controlar logs fuera del objeto config
180
- return ["config", "model", "scene", "show-model", "show-scene", "log-level"];
181
- }
182
-
183
148
  // DOM elements
184
149
  #wrapper = null;
185
150
  #canvas = null;
@@ -196,7 +161,6 @@ class PrefViewer extends HTMLElement {
196
161
 
197
162
  constructor() {
198
163
  super();
199
- this.#logDebug("Constructing PrefViewer instance");
200
164
  this.attachShadow({ mode: "open" });
201
165
  this.#createCanvas();
202
166
  this.#wrapCanvas();
@@ -212,13 +176,11 @@ class PrefViewer extends HTMLElement {
212
176
  };
213
177
  }
214
178
 
215
- attributeChangedCallback(name, _old, value) {
216
- if (name === "log-level") {
217
- this.#setLogLevel(value);
218
- return;
219
- }
179
+ static get observedAttributes() {
180
+ return ["config", "model", "scene", "show-model", "show-scene"];
181
+ }
220
182
 
221
- this.#logDebug("Attribute change detected", { name, value: this.#summarizeValue(value) });
183
+ attributeChangedCallback(name, _old, value) {
222
184
  let data = null;
223
185
  switch (name) {
224
186
  case "config":
@@ -230,10 +192,15 @@ class PrefViewer extends HTMLElement {
230
192
  case "scene":
231
193
  this.loadScene(value);
232
194
  break;
195
+ case "materials":
196
+ this.loadMaterials(value);
197
+ break;
198
+ case "options":
199
+ this.setOptions(value);
200
+ break;
233
201
  case "show-model":
234
202
  data = value.toLowerCase?.() === "true";
235
- this.#logDebug("Toggling model visibility from attribute", { visible: data, initialized: this.#initialized });
236
- if (this.#initialized) {
203
+ if (this.initialized) {
237
204
  data ? this.showModel() : this.hideModel();
238
205
  } else {
239
206
  this.#data.containers.model.show = data;
@@ -241,8 +208,7 @@ class PrefViewer extends HTMLElement {
241
208
  break;
242
209
  case "show-scene":
243
210
  data = value.toLowerCase?.() === "true";
244
- this.#logDebug("Toggling scene visibility from attribute", { visible: data, initialized: this.#initialized });
245
- if (this.#initialized) {
211
+ if (this.initialized) {
246
212
  data ? this.showScene() : this.hideScene();
247
213
  } else {
248
214
  this.#data.containers.environment.show = data;
@@ -254,33 +220,30 @@ class PrefViewer extends HTMLElement {
254
220
  connectedCallback() {
255
221
  if (!this.hasAttribute("config")) {
256
222
  const error = 'PrefViewer: provide "models" as array of model and environment';
257
- this.#logError("Missing required config attribute", { error });
223
+ console.error(error);
258
224
  this.dispatchEvent(
259
- new CustomEvent("model-error", {
260
- detail: { error: new Error(error) },
225
+ new CustomEvent("scene-error", {
261
226
  bubbles: true,
227
+ cancelable: false,
262
228
  composed: true,
229
+ detail: { error: new Error(error) },
263
230
  })
264
231
  );
265
232
  return false;
266
233
  }
267
234
 
268
- this.#logDebug("Connected to DOM, initializing Babylon");
269
235
  this.#initializeBabylon();
270
- this.#loadContainers(true, true, true);
271
- this.#initialized = true;
272
- this.#logInfo("Initialization completed", { initialized: this.#initialized });
236
+ this.initialized = true;
237
+ this.#processNextTask();
273
238
  }
274
239
 
275
240
  disconnectedCallback() {
276
- this.#logDebug("Disconnected from DOM, disposing resources");
277
241
  this.#disposeEngine();
278
242
  this.#canvasResizeObserver.disconnect();
279
243
  }
280
244
 
281
245
  // Web Component
282
246
  #createCanvas() {
283
- this.#logDebug("Creating rendering canvas");
284
247
  this.#canvas = document.createElement("canvas");
285
248
  Object.assign(this.#canvas.style, {
286
249
  width: "100%",
@@ -288,11 +251,9 @@ class PrefViewer extends HTMLElement {
288
251
  display: "block",
289
252
  outline: "none",
290
253
  });
291
- this.#logDebug("Canvas element created and styled");
292
254
  }
293
255
 
294
256
  #wrapCanvas() {
295
- this.#logDebug("Wrapping canvas inside container div");
296
257
  this.#wrapper = document.createElement("div");
297
258
  Object.assign(this.#wrapper.style, {
298
259
  width: "100%",
@@ -301,94 +262,171 @@ class PrefViewer extends HTMLElement {
301
262
  });
302
263
  this.#wrapper.appendChild(this.#canvas);
303
264
  this.shadowRoot.append(this.#wrapper);
304
- this.#logDebug("Canvas wrapper appended to shadow DOM");
265
+ }
266
+
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);
282
+ }
283
+
284
+ async #setStatusLoaded() {
285
+ const toLoadDetail = {
286
+ container_model: !!this.#data.containers.model.changed.pending,
287
+ container_environment: !!this.#data.containers.environment.changed.pending,
288
+ container_materials: !!this.#data.containers.materials.changed.pending,
289
+ options_camera: !!this.#data.options.camera.changed.pending,
290
+ options_innerWallMaterial: !!this.#data.options.materials.innerWall.changed.pending,
291
+ options_outerWallMaterial: !!this.#data.options.materials.outerWall.changed.pending,
292
+ options_innerFloorMaterial: !!this.#data.options.materials.innerFloor.changed.pending,
293
+ options_outerFloorMaterial: !!this.#data.options.materials.outerFloor.changed.pending,
294
+ };
295
+ const loadedDetail = {
296
+ container_model: !!this.#data.containers.model.changed.success,
297
+ container_environment: !!this.#data.containers.environment.changed.success,
298
+ container_materials: !!this.#data.containers.materials.changed.success,
299
+ options_camera: !!this.#data.options.camera.changed.success,
300
+ options_innerWallMaterial: !!this.#data.options.materials.innerWall.changed.success,
301
+ options_outerWallMaterial: !!this.#data.options.materials.outerWall.changed.success,
302
+ options_innerFloorMaterial: !!this.#data.options.materials.innerFloor.changed.success,
303
+ options_outerFloorMaterial: !!this.#data.options.materials.outerFloor.changed.success,
304
+ };
305
+ const detail = {
306
+ tried: toLoadDetail,
307
+ success: loadedDetail,
308
+ };
309
+
310
+ this.dispatchEvent(
311
+ new CustomEvent("scene-loaded", {
312
+ bubbles: true,
313
+ cancelable: false,
314
+ composed: true,
315
+ detail: detail,
316
+ })
317
+ );
318
+
319
+ await this.#scene.whenReadyAsync();
320
+ this.#engine.runRenderLoop(this.#renderLoop);
321
+
322
+ this.#resetChangedFlags();
323
+
324
+ if (this.hasAttribute("loading")) {
325
+ this.removeAttribute("loading");
326
+ }
327
+ this.setAttribute("loaded", "");
328
+
329
+ this.loaded = true;
330
+ this.loading = false;
331
+
332
+ this.#processNextTask();
305
333
  }
306
334
 
307
335
  // Data
308
336
  #checkCameraChanged(options) {
309
337
  if (!options || !options.camera) {
310
- this.#logDebug("Camera options not provided or unchanged");
311
338
  return false;
312
339
  }
313
- this.#data.options.camera.changed = options.camera && options.camera !== this.#data.options.camera.value ? true : false;
314
- this.#data.options.camera.value = this.#data.options.camera.changed ? options.camera : this.#data.options.camera.value;
315
- this.#logDebug("Camera option processed", { changed: this.#data.options.camera.changed, value: this.#data.options.camera.value });
316
- return this.#data.options.camera.changed;
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;
317
351
  }
318
352
 
319
353
  #checkMaterialsChanged(options) {
320
354
  if (!options) {
321
- this.#logDebug("Material options not provided");
322
355
  return false;
323
356
  }
324
357
  let someChanged = false;
325
358
  Object.keys(this.#data.options.materials).forEach((material) => {
326
359
  const key = `${material}Material`;
327
- this.#data.options.materials[material].changed = options[key] && options[key] !== this.#data.options.materials[material].value ? true : false;
328
- this.#data.options.materials[material].value = this.#data.options.materials[material].changed ? options[key] : this.#data.options.materials[material].value;
329
- someChanged = someChanged || this.#data.options.materials[material].changed;
330
- });
331
- this.#logDebug("Material options processed", {
332
- changed: someChanged,
333
- values: Object.entries(this.#data.options.materials).reduce((acc, [key, entry]) => {
334
- acc[key] = { value: entry.value, changed: entry.changed };
335
- return acc;
336
- }, {}),
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;
337
372
  });
338
373
  return someChanged;
339
374
  }
340
375
 
341
- #storeChangedFlagsForContainer(container) {
342
- container.timestamp = container.changed.timestamp;
343
- container.size = container.changed.size;
344
- this.#logDebug("Stored change flags for container", { name: container.name, timestamp: container.timestamp, size: container.size });
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
+ }
345
384
  }
346
385
 
347
386
  #resetChangedFlags() {
348
- Object.values(this.#data.containers).forEach((container) => (container.changed = false));
349
- Object.values(this.#data.options.materials).forEach((material) => (material.changed = false));
350
- this.#data.options.camera.changed = false;
351
- this.#logDebug("Reset change flags across containers and options");
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);
352
393
  }
353
394
 
354
395
  // Babylon.js
355
396
  async #initializeBabylon() {
356
- this.#logInfo("Initializing Babylon engine and scene");
357
397
  this.#engine = new Engine(this.#canvas, true, { alpha: true });
398
+ this.#engine.disableUniformBuffers = true;
358
399
  this.#scene = new Scene(this.#engine);
359
400
  this.#scene.clearColor = new Color4(1, 1, 1, 1);
360
401
  this.#createCamera();
361
402
  this.#createLights();
362
403
  this.#setupInteraction();
363
-
364
- this.#engine.runRenderLoop(() => this.#scene && this.#scene.render());
365
- this.#canvasResizeObserver.observe(this.#canvas);
366
- this.#logDebug("Engine render loop started and resize observer attached");
367
-
368
404
  await this.#createXRExperience();
369
- this.#logInfo("Babylon initialization finished", { xrEnabled: !!this.#XRExperience });
405
+ this.#engine.runRenderLoop(this.#renderLoop);
406
+ this.#canvasResizeObserver.observe(this.#canvas);
370
407
  }
371
408
 
372
- addStylesToARButton() {
373
- this.#logDebug("Adding styles to AR button");
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() {
374
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"}';
375
416
  const style = document.createElement("style");
376
417
  style.appendChild(document.createTextNode(css));
377
418
  this.#wrapper.appendChild(style);
378
- this.#logDebug("AR button styles applied");
379
419
  }
380
420
 
381
421
  async #createXRExperience() {
382
422
  if (this.#XRExperience) {
383
- this.#logDebug("XR experience already created, skipping");
384
423
  return true;
385
424
  }
386
425
 
387
- this.#logDebug("Attempting to create XR experience");
388
426
  const sessionMode = "immersive-ar";
389
427
  const sessionSupported = await WebXRSessionManager.IsSessionSupportedAsync(sessionMode);
390
428
  if (!sessionSupported) {
391
- this.#logInfo("WebXR session mode not supported", { sessionMode });
429
+ console.info("PrefViewer: WebXR in mode AR is not supported");
392
430
  return false;
393
431
  }
394
432
 
@@ -407,7 +445,6 @@ class PrefViewer extends HTMLElement {
407
445
  };
408
446
 
409
447
  this.#XRExperience = await WebXRDefaultExperience.CreateAsync(this.#scene, options);
410
- this.#logInfo("XR experience created successfully", { sessionMode });
411
448
 
412
449
  const featuresManager = this.#XRExperience.baseExperience.featuresManager;
413
450
  featuresManager.enableFeature(WebXRFeatureName.TELEPORTATION, "stable", {
@@ -415,50 +452,41 @@ class PrefViewer extends HTMLElement {
415
452
  floorMeshes: [ground],
416
453
  timeToTeleport: 1500,
417
454
  });
418
- this.#logDebug("XR teleportation feature enabled");
419
455
 
420
456
  this.#XRExperience.baseExperience.sessionManager.onXRReady.add(() => {
421
457
  // Set the initial position of xrCamera: use nonVRCamera, which contains a copy of the original this.#scene.activeCamera before entering XR
422
458
  this.#XRExperience.baseExperience.camera.setTransformationFromNonVRCamera(this.#XRExperience.baseExperience._nonVRCamera);
423
459
  this.#XRExperience.baseExperience.camera.setTarget(Vector3.Zero());
424
460
  this.#XRExperience.baseExperience.onInitialXRPoseSetObservable.notifyObservers(this.#XRExperience.baseExperience.camera);
425
- this.#logDebug("XR session ready and camera transformed");
426
461
  });
427
462
 
428
- this.addStylesToARButton();
463
+ this.#addStylesToARButton();
429
464
  } catch (error) {
430
- this.#logWarn("Failed to create XR experience", { error });
465
+ console.warn("PrefViewer: failed to create WebXR experience", error);
431
466
  this.#XRExperience = null;
432
467
  }
433
468
  }
434
469
 
435
- #canvasResizeObserver = new ResizeObserver(() => {
436
- if (this.#engine) {
437
- this.#logDebug("Resize observer triggered");
438
- this.#engine.resize();
439
- }
440
- });
470
+ #canvasResizeObserver = new ResizeObserver(() => this.#engine && this.#engine.resize());
441
471
 
442
472
  #createCamera() {
443
- this.#logDebug("Creating default camera");
444
473
  this.#camera = new ArcRotateCamera("camera", (3 * Math.PI) / 2, Math.PI * 0.47, 10, Vector3.Zero(), this.#scene);
445
474
  this.#camera.upperBetaLimit = Math.PI * 0.48;
446
475
  this.#camera.lowerBetaLimit = Math.PI * 0.25;
447
476
  this.#camera.lowerRadiusLimit = 5;
448
477
  this.#camera.upperRadiusLimit = 20;
449
- this.#camera.metadata = { locked: false }
450
- this.#camera = this.#camera;
478
+ this.#camera.metadata = { locked: false };
451
479
  this.#camera.attachControl(this.#canvas, true);
452
- this.#logDebug("Camera created", {
453
- upperBetaLimit: this.#camera.upperBetaLimit,
454
- lowerBetaLimit: this.#camera.lowerBetaLimit,
455
- lowerRadiusLimit: this.#camera.lowerRadiusLimit,
456
- upperRadiusLimit: this.#camera.upperRadiusLimit,
457
- });
480
+ this.#scene.activeCamera = this.#camera;
458
481
  }
459
482
 
460
483
  #createLights() {
461
- this.#logDebug("Creating scene lights");
484
+ this.#initEnvironmentTexture();
485
+
486
+ if (this.#scene.environmentTexture) {
487
+ return true;
488
+ }
489
+
462
490
  // 1) Stronger ambient fill
463
491
  this.#hemiLight = new HemisphericLight("hemiLight", new Vector3(-10, 10, -10), this.#scene);
464
492
  this.#hemiLight.intensity = 0.6;
@@ -468,7 +496,7 @@ class PrefViewer extends HTMLElement {
468
496
  this.#dirLight.position = new Vector3(5, 4, 5); // light is IN FRONT + ABOVE + to the RIGHT
469
497
  this.#dirLight.intensity = 0.6;
470
498
 
471
- // 3) Soft shadows
499
+ // // 3) Soft shadows
472
500
  this.#shadowGen = new ShadowGenerator(1024, this.#dirLight);
473
501
  this.#shadowGen.useBlurExponentialShadowMap = true;
474
502
  this.#shadowGen.blurKernel = 16;
@@ -478,52 +506,120 @@ class PrefViewer extends HTMLElement {
478
506
  this.#cameraLight = new PointLight("pl", this.#camera.position, this.#scene);
479
507
  this.#cameraLight.parent = this.#camera;
480
508
  this.#cameraLight.intensity = 0.3;
481
- this.#logDebug("Scene lights configured", {
482
- hemiIntensity: this.#hemiLight.intensity,
483
- dirIntensity: this.#dirLight.intensity,
484
- pointIntensity: this.#cameraLight.intensity,
485
- shadowKernel: this.#shadowGen.blurKernel,
509
+ }
510
+
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;
522
+ }
523
+
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);
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);
486
567
  });
487
568
  }
488
569
 
570
+ #initShadows() {
571
+ if (!this.#scene.environmentTexture) {
572
+ this.#initIBLShadows();
573
+ return true;
574
+ }
575
+
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
+ }
584
+ });
585
+ }
586
+
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
+ }
597
+ }
598
+
489
599
  #setupInteraction() {
490
- this.#logDebug("Setting up canvas interaction listeners");
491
600
  this.#canvas.addEventListener("wheel", (event) => {
492
601
  if (!this.#scene || !this.#camera) {
493
- this.#logWarn("Wheel interaction ignored; scene or camera missing");
494
602
  return false;
495
603
  }
496
604
  //const pick = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
497
605
  //this.#camera.target = pick.hit ? pick.pickedPoint.clone() : this.#camera.target;
498
606
  if (!this.#scene.activeCamera.metadata?.locked) {
499
607
  this.#scene.activeCamera.inertialRadiusOffset -= event.deltaY * this.#scene.activeCamera.wheelPrecision * 0.001;
500
- this.#logDebug("Processed wheel interaction", {
501
- deltaY: event.deltaY,
502
- inertialRadiusOffset: this.#scene.activeCamera.inertialRadiusOffset,
503
- });
504
- } else {
505
- this.#logDebug("Wheel interaction ignored because camera is locked");
506
608
  }
507
609
  event.preventDefault();
508
610
  });
509
611
  }
510
612
 
511
613
  #disposeEngine() {
512
- if (!this.#engine) {
513
- this.#logDebug("Dispose engine called but engine already null");
514
- return;
515
- }
516
- this.#logDebug("Disposing Babylon resources");
614
+ if (!this.#engine) return;
517
615
  this.#engine.dispose();
518
616
  this.#engine = this.#scene = this.#camera = null;
519
617
  this.#hemiLight = this.#dirLight = this.#cameraLight = null;
520
618
  this.#shadowGen = null;
521
- this.#logDebug("Babylon resources disposed");
522
619
  }
523
620
 
524
621
  // Utility methods for loading gltf/glb
525
622
  async #getServerFileDataHeader(uri) {
526
- this.#logDebug("Requesting server file header", { uri });
527
623
  return new Promise((resolve) => {
528
624
  const xhr = new XMLHttpRequest();
529
625
  xhr.open("HEAD", uri, true);
@@ -531,17 +627,14 @@ class PrefViewer extends HTMLElement {
531
627
  xhr.onload = () => {
532
628
  if (xhr.status === 200) {
533
629
  const size = parseInt(xhr.getResponseHeader("Content-Length"));
534
- const timestamp = new Date(xhr.getResponseHeader("Last-Modified")).toISOString();
535
- this.#logDebug("Received server file header", { uri, size, timestamp });
536
- resolve(size, timestamp);
630
+ const timeStamp = new Date(xhr.getResponseHeader("Last-Modified")).toISOString();
631
+ resolve([size, timeStamp]);
537
632
  } else {
538
- this.#logWarn("Failed to retrieve server file header", { uri, status: xhr.status });
539
- resolve(0, null);
633
+ resolve([0, null]);
540
634
  }
541
635
  };
542
636
  xhr.onerror = () => {
543
- this.#logError("Error requesting server file header", { uri });
544
- resolve(0, null);
637
+ resolve([0, null]);
545
638
  };
546
639
  xhr.send();
547
640
  });
@@ -549,14 +642,11 @@ class PrefViewer extends HTMLElement {
549
642
 
550
643
  #transformUrl(url) {
551
644
  return new Promise((resolve) => {
552
- const transformed = url.replace(/^blob:[^/]+\//i, "").replace(/\\/g, "/");
553
- this.#logDebug("Transformed URL", { original: url, transformed });
554
- resolve(transformed);
645
+ resolve(url.replace(/^blob:[^/]+\//i, "").replace(/\\/g, "/"));
555
646
  });
556
647
  }
557
648
 
558
649
  #decodeBase64(base64) {
559
- this.#logDebug("Decoding Base64 payload", { length: base64 ? base64.length : 0 });
560
650
  const [, payload] = base64.split(",");
561
651
  const raw = payload || base64;
562
652
  let decoded = "";
@@ -566,7 +656,6 @@ class PrefViewer extends HTMLElement {
566
656
  try {
567
657
  decoded = atob(raw);
568
658
  } catch {
569
- this.#logWarn("Failed to decode Base64 string");
570
659
  return { blob, extension, size };
571
660
  }
572
661
  let isJson = false;
@@ -578,62 +667,44 @@ class PrefViewer extends HTMLElement {
578
667
  const type = isJson ? "model/gltf+json" : "model/gltf-binary";
579
668
  const array = Uint8Array.from(decoded, (c) => c.charCodeAt(0));
580
669
  blob = new Blob([array], { type });
581
- this.#logDebug("Decoded Base64 payload", { isJson, size, extension });
582
670
  return { blob, extension, size };
583
671
  }
584
672
 
585
673
  async #initStorage(db, table) {
586
- this.#logDebug("Initializing storage access", { db, table });
587
674
  if (window.gltfDB && window.gltfDB.name === db && window.gltfDB.objectStoreNames.contains(table)) {
588
- this.#logDebug("Reusing existing IndexedDB connection", { db, table });
589
675
  return true;
590
676
  }
591
677
  await initDb(db, table);
592
- this.#logDebug("IndexedDB initialized", { db, table });
593
678
  }
594
679
 
595
680
  // Methods for managing Asset Containers
596
681
  #setVisibilityOfWallAndFloorInModel(show) {
597
682
  if (!this.#data.containers.model.assetContainer || !this.#data.containers.model.visible) {
598
- this.#logDebug("Skipping wall/floor visibility update", {
599
- hasModel: !!this.#data.containers.model.assetContainer,
600
- modelVisible: this.#data.containers.model.visible,
601
- });
602
683
  return false;
603
684
  }
604
685
  show = show !== undefined ? show : this.#data.containers.environment.visible;
605
686
  const prefixes = Object.values(this.#data.options.materials).map((material) => material.prefix);
606
687
  this.#data.containers.model.assetContainer.meshes.filter((meshToFilter) => prefixes.some((prefix) => meshToFilter.name.startsWith(prefix))).forEach((mesh) => mesh.setEnabled(show));
607
- this.#logDebug("Updated wall and floor visibility", { show });
608
688
  }
609
689
 
610
690
  #setOptionsMaterial(optionMaterial) {
611
691
  if (!optionMaterial || !optionMaterial.prefix || !optionMaterial.value) {
612
- this.#logWarn("Material option invalid", { optionMaterial });
613
692
  return false;
614
693
  }
615
694
 
616
- this.#logDebug("Applying material option", {
617
- prefix: optionMaterial.prefix,
618
- value: optionMaterial.value,
619
- changed: optionMaterial.changed,
620
- });
621
-
622
695
  const material = this.#data.containers.materials.assetContainer?.materials.find((mat) => mat.name === optionMaterial.value) || null;
623
696
  if (!material) {
624
- this.#logWarn("Requested material not found", { value: optionMaterial.value });
625
697
  return false;
626
698
  }
627
699
 
628
700
  const containers = [];
629
- if (this.#data.containers.model.assetContainer && (this.#data.containers.model.assetContainer.changed || optionMaterial.changed)) {
701
+ if (this.#data.containers.model.assetContainer && (this.#data.containers.model.changed.pending || this.#data.containers.materials.changed.pending || optionMaterial.changed.pending)) {
630
702
  containers.push(this.#data.containers.model.assetContainer);
631
703
  }
632
- if (this.#data.containers.environment.assetContainer && (this.#data.containers.environment.assetContainer.changed || optionMaterial.changed)) {
704
+ if (this.#data.containers.environment.assetContainer && (this.#data.containers.environment.changed.pending || this.#data.containers.materials.changed.pending || optionMaterial.changed.pending)) {
633
705
  containers.push(this.#data.containers.environment.assetContainer);
634
706
  }
635
707
  if (containers.length === 0) {
636
- this.#logDebug("No containers required material update", { prefix: optionMaterial.prefix });
637
708
  return false;
638
709
  }
639
710
 
@@ -644,145 +715,204 @@ class PrefViewer extends HTMLElement {
644
715
  .forEach((mesh) => {
645
716
  mesh.material = material;
646
717
  someSetted = true;
647
- this.#logDebug("Assigned material to mesh", { mesh: mesh.name, material: material.name });
648
718
  })
649
719
  );
650
720
 
651
- this.#logDebug("Material option applied", {
652
- prefix: optionMaterial.prefix,
653
- value: optionMaterial.value,
654
- applied: someSetted,
655
- containers: containers.map((container) => container.name),
656
- });
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
+ }
657
727
 
658
728
  return someSetted;
659
729
  }
660
730
 
661
731
  #setOptionsMaterials() {
662
- if (!this.#data.containers.materials.assetContainer) {
663
- this.#logDebug("Skipping materials update; materials container is missing");
664
- return false;
665
- }
666
-
667
- this.#logDebug("Applying material options batch");
668
732
  let someSetted = false;
669
733
  Object.values(this.#data.options.materials).forEach((material) => {
670
734
  let settedMaterial = this.#setOptionsMaterial(material);
671
735
  someSetted = someSetted || settedMaterial;
672
736
  });
673
- this.#logDebug("Material batch processing finished", { appliedAny: someSetted });
674
737
  return someSetted;
675
738
  }
676
739
 
677
740
  #setOptionsCamera() {
678
- if (!this.#data.options.camera.value || (!this.#data.options.camera.changed && !this.#data.containers.model.assetContainer.changed)) {
679
- this.#logDebug("No camera option update necessary", {
680
- value: this.#data.options.camera.value,
681
- changed: this.#data.options.camera.changed,
682
- modelChanged: this.#data.containers.model.assetContainer?.changed,
683
- });
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) {
684
742
  return false;
685
743
  }
686
744
 
687
- let camera = this.#data.containers.model.assetContainer?.cameras.find((cam) => cam.name === this.#data.options.camera.value) || null;
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;
688
746
  if (!camera) {
689
- this.#logWarn("Requested camera not found", { name: this.#data.options.camera.value });
690
- return false;
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
+ }
691
766
  }
692
-
693
- camera.metadata = { locked: this.#data.options.camera.locked };
694
- if (!this.#data.options.camera.locked) {
767
+ if (!this.#data.options.camera.locked && this.#data.options.camera.value !== null) {
695
768
  camera.attachControl(this.#canvas, true);
696
- this.#logDebug("Attached unlocked camera control", { camera: camera.name });
697
- } else {
698
- this.#logDebug("Using locked camera configuration", { camera: camera.name });
699
769
  }
700
770
  this.#scene.activeCamera = camera;
701
- this.#logDebug("Active camera set", { camera: camera.name });
702
-
703
771
  return true;
704
772
  }
705
773
 
706
774
  #addContainer(container) {
707
- if (container.assetContainer && !container.visible && container.show) {
708
- container.assetContainer.addAllToScene();
709
- container.visible = true;
710
- this.#logDebug("Added container to scene", { name: container.name });
711
- } else {
712
- this.#logDebug("Skipped adding container", {
713
- name: container?.name,
714
- hasAssetContainer: !!container?.assetContainer,
715
- visible: container?.visible,
716
- show: container?.show,
717
- });
775
+ if (!container.assetContainer || container.visible || !container.show) {
776
+ return false;
718
777
  }
778
+
779
+ container.assetContainer.addAllToScene();
780
+ container.visible = true;
781
+ return true;
719
782
  }
720
783
 
721
784
  #removeContainer(container) {
722
- if (container.assetContainer && container.visible) {
723
- container.assetContainer.removeAllFromScene();
724
- container.visible = false;
725
- this.#logDebug("Removed container from scene", { name: container.name });
726
- } else {
727
- this.#logDebug("Skipped removing container", {
728
- name: container?.name,
729
- hasAssetContainer: !!container?.assetContainer,
730
- visible: container?.visible,
731
- });
785
+ if (!container.assetContainer || !container.visible) {
786
+ return false;
732
787
  }
788
+
789
+ container.assetContainer.removeAllFromScene();
790
+ container.visible = false;
791
+ return true;
733
792
  }
734
793
 
735
794
  #replaceContainer(container, newAssetContainer) {
736
- this.#logDebug("Replacing container asset", { name: container.name });
737
- this.#removeContainer(container);
795
+ if (container.assetContainer) {
796
+ this.#removeContainer(container);
797
+ container.assetContainer.dispose();
798
+ container.assetContainer = null;
799
+ }
800
+ this.#scene.getEngine().releaseEffects();
738
801
  container.assetContainer = newAssetContainer;
739
- container.assetContainer.meshes.forEach((mesh) => {
740
- mesh.receiveShadows = true;
741
- this.#shadowGen.addShadowCaster(mesh, true);
742
- this.#logDebug("Configured mesh for shadows", { container: container.name, mesh: mesh.name });
743
- });
744
802
  this.#addContainer(container);
745
- this.#logDebug("Container replacement complete", {
746
- name: container.name,
747
- meshCount: container.assetContainer.meshes.length,
803
+ return true;
804
+ }
805
+
806
+ /**
807
+ * Replace internal URIs in a glTF AssetContainer JSON with URLs pointing to files stored in IndexedDB.
808
+ * @param {JSON} assetContainerJSON AssetContainer in glTF (JSON) (modified in-place).
809
+ * @param {URL} [assetContainerURL] Optional URL of the AssetContainer. Used as the base path to resolve relative URIs.
810
+ * @returns {Promise<void>} Resolves when all applicable URIs have been resolved/replaced.
811
+ * @description
812
+ * - When provided, assetContainerURL is used as the base path for other scene files (binary buffers and all images).
813
+ * If not provided (null/undefined), it is because it is the assetContainer of the model or materials whose URIs are absolute.
814
+ * - According to the glTF 2.0 spec, only items inside the "buffers" and "images" arrays may have a "uri" property.
815
+ * - Data URIs (embedded base64) are ignored and left unchanged.
816
+ * - Matching asset URIs are normalized (backslashes converted to forward slashes) and passed to the FileStorage layer
817
+ * to obtain a usable URL (object URL or cached URL).
818
+ * - The function performs replacements in parallel and waits for all lookups to complete.
819
+ * - The JSON is updated in-place with the resolved URLs.
820
+ * @see {@link https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#uris|glTF™ 2.0 Specification - URIs}
821
+ */
822
+ async #replaceSceneURIAsync(assetContainerJSON, assetContainerURL) {
823
+ if (!assetContainerJSON) {
824
+ return;
825
+ }
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
+ }
748
890
  });
891
+ await Promise.all(promisesArray);
749
892
  }
750
893
 
751
894
  async #loadAssetContainer(container) {
752
- const storage = container?.storage;
753
- this.#logDebug("Requested asset container load", {
754
- container: container?.name,
755
- storage: this.#describeStorage(storage),
756
- });
895
+ let storage = container?.storage;
757
896
 
758
897
  if (!storage) {
759
- this.#logWarn("No storage configuration provided for container", { container: container?.name });
760
898
  return false;
761
899
  }
762
900
 
763
901
  let source = storage.url || null;
764
902
 
765
903
  if (storage.db && storage.table && storage.id) {
766
- this.#logDebug("Loading container from IndexedDB", {
767
- container: container.name,
768
- db: storage.db,
769
- table: storage.table,
770
- id: storage.id,
771
- });
772
904
  await this.#initStorage(storage.db, storage.table);
773
905
  const object = await loadModel(storage.id, storage.table);
774
906
  source = object.data;
775
- if (object.timestamp === container.timestamp) {
776
- this.#logDebug("IndexedDB model unchanged; skipping reload", { container: container.name });
907
+ if (object.timeStamp === container.timeStamp) {
908
+ container.changed = { pending: false, success: false };
777
909
  return false;
778
910
  } else {
779
- container.changed = { timestamp: object.timestamp, size: object.size };
780
- this.#logDebug("IndexedDB model marked as changed", { container: container.name, metadata: container.changed });
911
+ container.changed = { pending: true, size: object.size, success: false, timeStamp: object.timeStamp };
781
912
  }
782
913
  }
783
914
 
784
915
  if (!source) {
785
- this.#logWarn("No source resolved for container", { container: container.name });
786
916
  return false;
787
917
  }
788
918
 
@@ -790,139 +920,180 @@ class PrefViewer extends HTMLElement {
790
920
 
791
921
  let { blob, extension, size } = this.#decodeBase64(source);
792
922
  if (blob && extension) {
793
- this.#logDebug("Source detected as Base64", { container: container.name, extension });
794
- file = new File([blob], `${container.name}${extension}`, {
795
- type: blob.type,
796
- });
797
- if (!container.changed) {
798
- if (container.timestamp === null && container.size === size) {
799
- this.#logDebug("Base64 model unchanged; skipping reload", { container: container.name, size });
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 };
800
935
  return false;
801
936
  } else {
802
- container.changed = { timestamp: null, size: size };
803
- this.#logDebug("Base64 model marked as changed", { container: container.name, size });
937
+ container.changed = { pending: true, size: size, success: false, timeStamp: null };
804
938
  }
805
939
  }
806
940
  } else {
807
941
  const extMatch = source.match(/\.(gltf|glb)(\?|#|$)/i);
808
942
  extension = extMatch ? `.${extMatch[1].toLowerCase()}` : ".gltf";
809
- const { fileSize, fileTimestamp } = await this.#getServerFileDataHeader(source);
810
- if (container.timestamp === fileTimestamp && container.size === fileSize) {
811
- this.#logDebug("Remote model unchanged; skipping reload", {
812
- container: container.name,
813
- fileTimestamp,
814
- fileSize,
815
- });
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 };
816
946
  return false;
817
947
  } else {
818
- container.changed = { timestamp: fileTimestamp, size: fileSize };
819
- this.#logDebug("Remote model marked as changed", { container: container.name, metadata: container.changed });
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
+ }
820
957
  }
821
958
  }
822
959
 
960
+ // https://doc.babylonjs.com/typedoc/interfaces/BABYLON.LoadAssetContainerOptions
823
961
  let options = {
824
962
  pluginExtension: extension,
825
963
  pluginOptions: {
826
964
  gltf: {
965
+ compileMaterials: true,
827
966
  loadAllMaterials: true,
828
- preprocessUrlAsync: this.#transformUrl,
967
+ loadOnlyMaterials: container.name === "materials",
968
+ //preprocessUrlAsync: this.#transformUrl,
829
969
  },
830
970
  },
831
971
  };
832
972
 
833
- this.#logInfo("Loading asset container", { container: container.name, extension });
834
973
  return LoadAssetContainerAsync(file || source, this.#scene, options);
835
974
  }
836
975
 
837
976
  async #loadContainers(loadModel = true, loadEnvironment = true, loadMaterials = true) {
838
- this.#logInfo("Starting container load", { loadModel, loadEnvironment, loadMaterials });
977
+ this.#engine.stopRenderLoop(this.#renderLoop);
978
+
839
979
  const promiseArray = [];
840
980
  promiseArray.push(loadModel ? this.#loadAssetContainer(this.#data.containers.model) : false);
841
981
  promiseArray.push(loadEnvironment ? this.#loadAssetContainer(this.#data.containers.environment) : false);
842
982
  promiseArray.push(loadMaterials ? this.#loadAssetContainer(this.#data.containers.materials) : false);
843
983
 
844
984
  Promise.allSettled(promiseArray)
845
- .then(async (values) => {
985
+ .then((values) => {
846
986
  const modelContainer = values[0];
847
987
  const environmentContainer = values[1];
848
988
  const materialsContainer = values[2];
849
989
 
850
990
  if (modelContainer.status === "fulfilled" && modelContainer.value) {
991
+ modelContainer.value.lights = [];
851
992
  this.#replaceContainer(this.#data.containers.model, modelContainer.value);
852
- this.#storeChangedFlagsForContainer(this.#data.containers.model);
853
- this.#logInfo("Model container loaded successfully");
993
+ this.#storeChangedFlagsForContainer(this.#data.containers.model, true);
854
994
  } else {
855
- this.#data.containers.model.show ? this.#addContainer(this.#data.containers.model) : this.#removeContainer(this.#data.containers.model);
856
- this.#logDebug("Model container load skipped or failed", { status: modelContainer.status });
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);
857
999
  }
858
1000
 
859
1001
  if (environmentContainer.status === "fulfilled" && environmentContainer.value) {
860
1002
  this.#replaceContainer(this.#data.containers.environment, environmentContainer.value);
861
- this.#storeChangedFlagsForContainer(this.#data.containers.environment);
862
- this.#logInfo("Environment container loaded successfully");
1003
+ this.#storeChangedFlagsForContainer(this.#data.containers.environment, true);
863
1004
  } else {
864
- this.#data.containers.environment.show ? this.#addContainer(this.#data.containers.environment) : this.#removeContainer(this.#data.containers.environment);
865
- this.#logDebug("Environment container load skipped or failed", { status: environmentContainer.status });
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);
866
1009
  }
867
1010
 
868
1011
  if (materialsContainer.status === "fulfilled" && materialsContainer.value) {
869
1012
  this.#replaceContainer(this.#data.containers.materials, materialsContainer.value);
870
- this.#storeChangedFlagsForContainer(this.#data.containers.materials);
871
- this.#logInfo("Materials container loaded successfully");
1013
+ this.#storeChangedFlagsForContainer(this.#data.containers.materials, true);
872
1014
  } else {
873
- this.#logDebug("Materials container load skipped or failed", { status: materialsContainer.status });
1015
+ this.#storeChangedFlagsForContainer(this.#data.containers.materials, false);
874
1016
  }
875
1017
 
876
1018
  this.#setOptionsMaterials();
877
1019
  this.#setOptionsCamera();
878
1020
  this.#setVisibilityOfWallAndFloorInModel();
879
-
880
- this.#resetChangedFlags();
881
-
882
- this.#logInfo("Containers load routine completed");
883
- this.dispatchEvent(
884
- new CustomEvent("model-loaded", {
885
- detail: { success: "" },
886
- bubbles: true,
887
- composed: true,
888
- })
889
- );
890
- this.#logDebug("Dispatched model-loaded event");
891
1021
  })
892
1022
  .catch((error) => {
893
- this.#logError("Failed to load containers", { error });
1023
+ this.loaded = true;
1024
+ console.error("PrefViewer: failed to load model", error);
894
1025
  this.dispatchEvent(
895
- new CustomEvent("model-error", {
896
- detail: { error: error },
1026
+ new CustomEvent("scene-error", {
897
1027
  bubbles: true,
1028
+ cancelable: false,
898
1029
  composed: true,
1030
+ detail: { error: error },
899
1031
  })
900
1032
  );
1033
+ })
1034
+ .finally(async () => {
1035
+ this.#setMaxSimultaneousLights();
1036
+ this.#initShadows();
1037
+ await this.#setStatusLoaded();
901
1038
  });
902
1039
  }
903
1040
 
904
- // Public Methods
905
- loadConfig(config) {
906
- this.#logInfo("loadConfig called", { initialized: this.#initialized, inputType: typeof config });
907
- if (typeof config === "string") {
908
- try {
909
- config = JSON.parse(config);
910
- } catch (error) {
911
- this.#logError("Failed to parse config JSON", { error });
912
- throw error;
913
- }
1041
+ // Tasks
1042
+ #addTaskToQueue(value, type) {
1043
+ this.#taskQueue.push(new PrefViewerTask(value, type));
1044
+ if (this.initialized && !this.loading) {
1045
+ this.#processNextTask();
914
1046
  }
915
- if (!config) {
916
- this.#logWarn("No config provided");
1047
+ }
1048
+
1049
+ #processNextTask() {
1050
+ if (!this.#taskQueue.length) {
917
1051
  return false;
918
1052
  }
1053
+ const task = this.#taskQueue[0];
1054
+ this.#taskQueue.shift();
1055
+ switch (task.type) {
1056
+ case PrefViewerTask.Types.Config:
1057
+ this.#processConfig(task.value);
1058
+ break;
1059
+ case PrefViewerTask.Types.Model:
1060
+ this.#processModel(task.value);
1061
+ break;
1062
+ case PrefViewerTask.Types.Environment:
1063
+ this.#processEnvironment(task.value);
1064
+ break;
1065
+ case PrefViewerTask.Types.Materials:
1066
+ this.#processMaterials(task.value);
1067
+ break;
1068
+ case PrefViewerTask.Types.Options:
1069
+ this.#processOptions(task.value);
1070
+ break;
1071
+ }
1072
+ }
1073
+
1074
+ #processConfig(config) {
1075
+ this.#setStatusLoading();
919
1076
 
920
1077
  // Containers
921
- this.#data.containers.model.storage = config.model?.storage || null;
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;
922
1083
  this.#data.containers.model.show = config.model?.visible !== undefined ? config.model.visible : this.#data.containers.model.show;
923
- this.#data.containers.environment.storage = config.scene?.storage || null;
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;
924
1090
  this.#data.containers.environment.show = config.scene?.visible !== undefined ? config.scene.visible : this.#data.containers.environment.show;
925
- this.#data.containers.materials.storage = config.materials?.storage || null;
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;
926
1097
 
927
1098
  // Options
928
1099
  if (config.options) {
@@ -930,145 +1101,152 @@ class PrefViewer extends HTMLElement {
930
1101
  this.#checkMaterialsChanged(config.options);
931
1102
  }
932
1103
 
933
- this.#logDebug("Config applied", {
934
- modelStorage: this.#describeStorage(this.#data.containers.model.storage),
935
- environmentStorage: this.#describeStorage(this.#data.containers.environment.storage),
936
- });
1104
+ this.#loadContainers(loadModel, loadEnvironment, loadMaterials);
1105
+ }
1106
+
1107
+ #processModel(model) {
1108
+ this.#setStatusLoading();
1109
+
1110
+ const loadModel = !!model.storage;
1111
+ this.#data.containers.model.changed.pending = loadModel;
1112
+ this.#data.containers.model.changed.success = false;
1113
+ this.#data.containers.model.changed.storage = this.#data.containers.model.storage;
1114
+ this.#data.containers.model.storage = loadModel ? model.storage : this.#data.containers.model.storage;
1115
+ this.#data.containers.model.show = model.visible !== undefined ? model.visible : this.#data.containers.model.show;
937
1116
 
938
- this.#initialized && this.#loadContainers(true, true, true);
1117
+ this.initialized && this.#loadContainers(loadModel, false, false);
939
1118
  }
940
1119
 
941
- setOptions(options) {
942
- this.#logInfo("setOptions called", { optionsProvided: !!options });
943
- if (!options) {
944
- this.#logWarn("setOptions called without options");
945
- return false;
946
- }
1120
+ #processEnvironment(environment) {
1121
+ this.#setStatusLoading();
1122
+
1123
+ const loadEnvironment = !!environment.storage;
1124
+ this.#data.containers.environment.changed.pending = loadEnvironment;
1125
+ this.#data.containers.environment.changed.success = false;
1126
+ this.#data.containers.environment.changed.storage = this.#data.containers.environment.storage;
1127
+ this.#data.containers.environment.storage = loadEnvironment ? environment.storage : this.#data.containers.environment.storage;
1128
+ this.#data.containers.environment.show = environment.visible !== undefined ? environment.visible : this.#data.containers.environment.show;
1129
+
1130
+ this.#loadContainers(false, loadEnvironment, false);
1131
+ }
1132
+
1133
+ #processMaterials(materials) {
1134
+ this.#setStatusLoading();
1135
+
1136
+ const loadMaterials = !!materials.storage;
1137
+ this.#data.containers.materials.changed.pending = loadMaterials;
1138
+ this.#data.containers.materials.changed.success = false;
1139
+ this.#data.containers.materials.changed.storage = this.#data.containers.materials.storage;
1140
+ this.#data.containers.materials.storage = loadMaterials ? materials.storage : this.#data.containers.materials.storage;
1141
+
1142
+ this.#loadContainers(false, false, loadMaterials);
1143
+ }
1144
+
1145
+ async #processOptions(options) {
1146
+ this.#setStatusLoading();
1147
+
947
1148
  let someSetted = false;
948
1149
  if (this.#checkCameraChanged(options)) {
949
- this.#logDebug("Camera options changed via setOptions");
950
1150
  someSetted = someSetted || this.#setOptionsCamera();
951
1151
  }
952
1152
  if (this.#checkMaterialsChanged(options)) {
953
- this.#logDebug("Material options changed via setOptions");
954
1153
  someSetted = someSetted || this.#setOptionsMaterials();
955
1154
  }
956
- this.#resetChangedFlags();
957
- this.#logDebug("setOptions completed", { appliedAny: someSetted });
958
- debugger;
1155
+
1156
+ await this.#setStatusLoaded();
1157
+
959
1158
  return someSetted;
960
1159
  }
961
1160
 
962
- loadModel(model) {
963
- this.#logInfo("loadModel called", { initialized: this.#initialized, inputType: typeof model });
964
- if (typeof model === "string") {
965
- try {
966
- model = JSON.parse(model);
967
- } catch (error) {
968
- this.#logError("Failed to parse model JSON", { error });
969
- throw error;
970
- }
1161
+ // Public Methods
1162
+ loadConfig(config) {
1163
+ config = typeof config === "string" ? JSON.parse(config) : config;
1164
+ if (!config) {
1165
+ return false;
971
1166
  }
1167
+ this.#addTaskToQueue(config, "config");
1168
+ }
1169
+
1170
+ loadModel(model) {
1171
+ model = typeof model === "string" ? JSON.parse(model) : model;
972
1172
  if (!model) {
973
- this.#logWarn("No model payload provided");
974
1173
  return false;
975
1174
  }
976
- this.#data.containers.model.storage = model.storage || null;
977
- this.#data.containers.model.show = model.visible !== undefined ? model.visible : this.#data.containers.model.show;
978
- this.#logDebug("Model configuration updated", {
979
- storage: this.#describeStorage(this.#data.containers.model.storage),
980
- show: this.#data.containers.model.show,
981
- });
982
- this.#initialized && this.#loadContainers(true, false, false);
1175
+ this.#addTaskToQueue(model, "model");
983
1176
  }
984
1177
 
985
1178
  loadScene(scene) {
986
- this.#logInfo("loadScene called", { initialized: this.#initialized, inputType: typeof scene });
987
- if (typeof scene === "string") {
988
- try {
989
- scene = JSON.parse(scene);
990
- } catch (error) {
991
- this.#logError("Failed to parse scene JSON", { error });
992
- throw error;
993
- }
994
- }
1179
+ scene = typeof scene === "string" ? JSON.parse(scene) : scene;
995
1180
  if (!scene) {
996
- this.#logWarn("No scene payload provided");
997
1181
  return false;
998
1182
  }
999
- this.#data.containers.environment.storage = scene.storage || null;
1000
- this.#data.containers.environment.show = scene.visible !== undefined ? scene.visible : this.#data.containers.environment.show;
1001
- this.#logDebug("Scene configuration updated", {
1002
- storage: this.#describeStorage(this.#data.containers.environment.storage),
1003
- show: this.#data.containers.environment.show,
1004
- });
1005
- this.#initialized && this.#loadContainers(false, true, false);
1183
+ this.#addTaskToQueue(scene, "environment");
1184
+ }
1185
+
1186
+ loadMaterials(materials) {
1187
+ materials = typeof materials === "string" ? JSON.parse(materials) : materials;
1188
+ if (!materials) {
1189
+ return false;
1190
+ }
1191
+ this.#addTaskToQueue(materials, "materials");
1192
+ }
1193
+
1194
+ setOptions(options) {
1195
+ options = typeof options === "string" ? JSON.parse(options) : options;
1196
+ if (!options) {
1197
+ return false;
1198
+ }
1199
+ this.#addTaskToQueue(options, "options");
1006
1200
  }
1007
1201
 
1008
1202
  showModel() {
1009
1203
  this.#data.containers.model.show = true;
1010
1204
  this.#addContainer(this.#data.containers.model);
1011
- this.#logInfo("Model visibility set to true");
1012
1205
  }
1013
1206
 
1014
1207
  hideModel() {
1015
1208
  this.#data.containers.model.show = false;
1016
1209
  this.#removeContainer(this.#data.containers.model);
1017
- this.#logInfo("Model visibility set to false");
1018
1210
  }
1019
1211
 
1020
1212
  showScene() {
1021
1213
  this.#data.containers.environment.show = true;
1022
1214
  this.#addContainer(this.#data.containers.environment);
1023
1215
  this.#setVisibilityOfWallAndFloorInModel();
1024
- this.#logInfo("Scene visibility set to true");
1025
1216
  }
1026
1217
 
1027
1218
  hideScene() {
1028
1219
  this.#data.containers.environment.show = false;
1029
1220
  this.#removeContainer(this.#data.containers.environment);
1030
1221
  this.#setVisibilityOfWallAndFloorInModel();
1031
- this.#logInfo("Scene visibility set to false");
1032
1222
  }
1033
1223
 
1034
1224
  downloadModelGLB() {
1035
1225
  const fileName = "model";
1036
- this.#logInfo("Initiating GLB download for model", { fileName });
1037
- GLTF2Export.GLBAsync(this.#data.containers.model.assetContainer, fileName, { exportWithoutWaitingForScene: true }).then((glb) => {
1038
- this.#logDebug("Model GLB export ready", { fileName });
1039
- glb.downloadFiles();
1040
- });
1226
+ GLTF2Export.GLBAsync(this.#data.containers.model.assetContainer, fileName, { exportWithoutWaitingForScene: true }).then((glb) => glb.downloadFiles());
1041
1227
  }
1042
1228
 
1043
1229
  downloadModelUSDZ() {
1044
1230
  const fileName = "model";
1045
- this.#logInfo("Initiating USDZ download for model", { fileName });
1046
1231
  USDZExportAsync(this.#data.containers.model.assetContainer).then((response) => {
1047
1232
  if (response) {
1048
1233
  Tools.Download(new Blob([response], { type: "model/vnd.usdz+zip" }), `${fileName}.usdz`);
1049
- this.#logDebug("Model USDZ export ready", { fileName });
1050
1234
  }
1051
1235
  });
1052
1236
  }
1053
1237
 
1054
1238
  downloadModelAndSceneUSDZ() {
1055
1239
  const fileName = "scene";
1056
- this.#logInfo("Initiating USDZ download for scene", { fileName });
1057
1240
  USDZExportAsync(this.#scene).then((response) => {
1058
1241
  if (response) {
1059
1242
  Tools.Download(new Blob([response], { type: "model/vnd.usdz+zip" }), `${fileName}.usdz`);
1060
- this.#logDebug("Scene USDZ export ready", { fileName });
1061
1243
  }
1062
1244
  });
1063
1245
  }
1064
1246
 
1065
1247
  downloadModelAndSceneGLB() {
1066
1248
  const fileName = "scene";
1067
- this.#logInfo("Initiating GLB download for scene", { fileName });
1068
- GLTF2Export.GLBAsync(this.#scene, fileName, { exportWithoutWaitingForScene: true }).then((glb) => {
1069
- this.#logDebug("Scene GLB export ready", { fileName });
1070
- glb.downloadFiles();
1071
- });
1249
+ GLTF2Export.GLBAsync(this.#scene, fileName, { exportWithoutWaitingForScene: true }).then((glb) => glb.downloadFiles());
1072
1250
  }
1073
1251
  }
1074
1252