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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +684 -92
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preference-sl/pref-viewer",
3
- "version": "2.10.0-beta.1",
3
+ "version": "2.10.0-beta.10",
4
4
  "description": "Web Component to preview GLTF models with Babylon.js",
5
5
  "author": "Alex Moreno Palacio <amoreno@preference.es>",
6
6
  "scripts": {
package/src/index.js CHANGED
@@ -39,7 +39,7 @@
39
39
  * </pref-viewer>
40
40
  * ```
41
41
  */
42
- import { Engine, Scene, ArcRotateCamera, Vector3, Color4, HemisphericLight, DirectionalLight, PointLight, ShadowGenerator, LoadAssetContainerAsync, Tools } from "@babylonjs/core";
42
+ import { Engine, Scene, ArcRotateCamera, Vector3, Color4, HemisphericLight, DirectionalLight, PointLight, ShadowGenerator, LoadAssetContainerAsync, Tools, WebXRSessionManager, WebXRDefaultExperience, MeshBuilder, WebXRFeatureName } 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";
@@ -47,22 +47,141 @@ import { DracoCompression } from "@babylonjs/core/Meshes/Compression/dracoCompre
47
47
  import { initDb, loadModel } from "./gltf-storage.js";
48
48
 
49
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";
54
+
50
55
  #initialized = false;
56
+ #logLevel = PrefViewer.DEFAULT_LOG_LEVEL;
51
57
 
52
- #model = {
53
- container: null,
54
- show: true, // Show model by default
55
- storage: null,
56
- visible: false,
58
+ #data = {
59
+ containers: {
60
+ model: {
61
+ name: "model",
62
+ container: null,
63
+ show: true,
64
+ storage: null,
65
+ visible: false,
66
+ size: null,
67
+ timestamp: null,
68
+ changed: false,
69
+ },
70
+ environment: {
71
+ name: "environment",
72
+ container: null,
73
+ show: true,
74
+ storage: null,
75
+ visible: false,
76
+ size: null,
77
+ timestamp: null,
78
+ changed: false,
79
+ },
80
+ materials: {
81
+ name: "materials",
82
+ container: null,
83
+ storage: null,
84
+ show: true,
85
+ visible: false,
86
+ size: null,
87
+ timestamp: null,
88
+ changed: false,
89
+ },
90
+ },
91
+ options: {
92
+ camera: {
93
+ value: null,
94
+ locked: true,
95
+ changed: false,
96
+ },
97
+ materials: {
98
+ innerWall: {
99
+ value: null,
100
+ prefix: "innerWall",
101
+ changed: false,
102
+ },
103
+ outerWall: {
104
+ value: null,
105
+ prefix: "outerWall",
106
+ changed: false,
107
+ },
108
+ innerFloor: {
109
+ value: null,
110
+ prefix: "innerFloor",
111
+ changed: false,
112
+ },
113
+ outerFloor: {
114
+ value: null,
115
+ prefix: "outerFloor",
116
+ changed: false,
117
+ },
118
+ },
119
+ },
57
120
  };
58
121
 
59
- #environment = {
60
- container: null,
61
- show: true, // Show environment by default
62
- storage: null,
63
- visible: false,
64
- };
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;
65
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
+ // DOM elements
184
+ #wrapper = null;
66
185
  #canvas = null;
67
186
 
68
187
  // Babylon.js core objects
@@ -73,9 +192,11 @@ class PrefViewer extends HTMLElement {
73
192
  #dirLight = null;
74
193
  #cameraLight = null;
75
194
  #shadowGen = null;
195
+ #XRExperience = null;
76
196
 
77
197
  constructor() {
78
198
  super();
199
+ this.#logDebug("Constructing PrefViewer instance");
79
200
  this.attachShadow({ mode: "open" });
80
201
  this.#createCanvas();
81
202
  this.#wrapCanvas();
@@ -91,11 +212,13 @@ class PrefViewer extends HTMLElement {
91
212
  };
92
213
  }
93
214
 
94
- static get observedAttributes() {
95
- return ["config", "model", "scene", "show-model", "show-scene"];
96
- }
97
-
98
215
  attributeChangedCallback(name, _old, value) {
216
+ if (name === "log-level") {
217
+ this.#setLogLevel(value);
218
+ return;
219
+ }
220
+
221
+ this.#logDebug("Attribute change detected", { name, value: this.#summarizeValue(value) });
99
222
  let data = null;
100
223
  switch (name) {
101
224
  case "config":
@@ -109,18 +232,20 @@ class PrefViewer extends HTMLElement {
109
232
  break;
110
233
  case "show-model":
111
234
  data = value.toLowerCase?.() === "true";
235
+ this.#logDebug("Toggling model visibility from attribute", { visible: data, initialized: this.#initialized });
112
236
  if (this.#initialized) {
113
237
  data ? this.showModel() : this.hideModel();
114
238
  } else {
115
- this.#model.show = data;
239
+ this.#data.containers.model.show = data;
116
240
  }
117
241
  break;
118
242
  case "show-scene":
119
243
  data = value.toLowerCase?.() === "true";
244
+ this.#logDebug("Toggling scene visibility from attribute", { visible: data, initialized: this.#initialized });
120
245
  if (this.#initialized) {
121
246
  data ? this.showScene() : this.hideScene();
122
247
  } else {
123
- this.#environment.show = data;
248
+ this.#data.containers.environment.show = data;
124
249
  }
125
250
  break;
126
251
  }
@@ -129,7 +254,7 @@ class PrefViewer extends HTMLElement {
129
254
  connectedCallback() {
130
255
  if (!this.hasAttribute("config")) {
131
256
  const error = 'PrefViewer: provide "models" as array of model and environment';
132
- console.error(error);
257
+ this.#logError("Missing required config attribute", { error });
133
258
  this.dispatchEvent(
134
259
  new CustomEvent("model-error", {
135
260
  detail: { error: new Error(error) },
@@ -140,18 +265,22 @@ class PrefViewer extends HTMLElement {
140
265
  return false;
141
266
  }
142
267
 
268
+ this.#logDebug("Connected to DOM, initializing Babylon");
143
269
  this.#initializeBabylon();
144
- this.#loadContainers(true, true);
270
+ this.#loadContainers(true, true, true);
145
271
  this.#initialized = true;
272
+ this.#logInfo("Initialization completed", { initialized: this.#initialized });
146
273
  }
147
274
 
148
275
  disconnectedCallback() {
276
+ this.#logDebug("Disconnected from DOM, disposing resources");
149
277
  this.#disposeEngine();
150
278
  this.#canvasResizeObserver.disconnect();
151
279
  }
152
280
 
153
281
  // Web Component
154
282
  #createCanvas() {
283
+ this.#logDebug("Creating rendering canvas");
155
284
  this.#canvas = document.createElement("canvas");
156
285
  Object.assign(this.#canvas.style, {
157
286
  width: "100%",
@@ -159,45 +288,177 @@ class PrefViewer extends HTMLElement {
159
288
  display: "block",
160
289
  outline: "none",
161
290
  });
291
+ this.#logDebug("Canvas element created and styled");
162
292
  }
163
293
 
164
294
  #wrapCanvas() {
165
- const wrapper = document.createElement("div");
166
- Object.assign(wrapper.style, {
295
+ this.#logDebug("Wrapping canvas inside container div");
296
+ this.#wrapper = document.createElement("div");
297
+ Object.assign(this.#wrapper.style, {
167
298
  width: "100%",
168
299
  height: "100%",
169
300
  position: "relative",
170
301
  });
171
- wrapper.appendChild(this.#canvas);
172
- this.shadowRoot.append(wrapper);
302
+ this.#wrapper.appendChild(this.#canvas);
303
+ this.shadowRoot.append(this.#wrapper);
304
+ this.#logDebug("Canvas wrapper appended to shadow DOM");
173
305
  }
174
306
 
175
-
176
- // Bbylon.js
177
- #initializeBabylon() {
307
+ // Data
308
+ #checkCameraChanged(options) {
309
+ if (!options || !options.camera) {
310
+ this.#logDebug("Camera options not provided or unchanged");
311
+ return false;
312
+ }
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;
317
+ }
318
+
319
+ #checkMaterialsChanged(options) {
320
+ if (!options) {
321
+ this.#logDebug("Material options not provided");
322
+ return false;
323
+ }
324
+ let someChanged = false;
325
+ Object.keys(this.#data.options.materials).forEach((material) => {
326
+ 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
+ }, {}),
337
+ });
338
+ return someChanged;
339
+ }
340
+
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 });
345
+ }
346
+
347
+ #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");
352
+ }
353
+
354
+ // Babylon.js
355
+ async #initializeBabylon() {
356
+ this.#logInfo("Initializing Babylon engine and scene");
178
357
  this.#engine = new Engine(this.#canvas, true, { alpha: true });
179
358
  this.#scene = new Scene(this.#engine);
180
359
  this.#scene.clearColor = new Color4(1, 1, 1, 1);
181
360
  this.#createCamera();
182
361
  this.#createLights();
183
362
  this.#setupInteraction();
184
-
363
+
185
364
  this.#engine.runRenderLoop(() => this.#scene && this.#scene.render());
186
365
  this.#canvasResizeObserver.observe(this.#canvas);
366
+ this.#logDebug("Engine render loop started and resize observer attached");
367
+
368
+ await this.#createXRExperience();
369
+ this.#logInfo("Babylon initialization finished", { xrEnabled: !!this.#XRExperience });
370
+ }
371
+
372
+ addStylesToARButton() {
373
+ this.#logDebug("Adding styles to AR button");
374
+ 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
+ const style = document.createElement("style");
376
+ style.appendChild(document.createTextNode(css));
377
+ this.#wrapper.appendChild(style);
378
+ this.#logDebug("AR button styles applied");
379
+ }
380
+
381
+ async #createXRExperience() {
382
+ if (this.#XRExperience) {
383
+ this.#logDebug("XR experience already created, skipping");
384
+ return true;
385
+ }
386
+
387
+ this.#logDebug("Attempting to create XR experience");
388
+ const sessionMode = "immersive-ar";
389
+ const sessionSupported = await WebXRSessionManager.IsSessionSupportedAsync(sessionMode);
390
+ if (!sessionSupported) {
391
+ this.#logInfo("WebXR session mode not supported", { sessionMode });
392
+ return false;
393
+ }
394
+
395
+ try {
396
+ const ground = MeshBuilder.CreateGround("ground", { width: 1000, height: 1000 }, this.#scene);
397
+ ground.isVisible = false;
398
+
399
+ const options = {
400
+ floorMeshes: [ground],
401
+ uiOptions: {
402
+ sessionMode: sessionMode,
403
+ renderTarget: "xrLayer",
404
+ referenceSpaceType: "local",
405
+ },
406
+ optionalFeatures: true,
407
+ };
408
+
409
+ this.#XRExperience = await WebXRDefaultExperience.CreateAsync(this.#scene, options);
410
+ this.#logInfo("XR experience created successfully", { sessionMode });
411
+
412
+ const featuresManager = this.#XRExperience.baseExperience.featuresManager;
413
+ featuresManager.enableFeature(WebXRFeatureName.TELEPORTATION, "stable", {
414
+ xrInput: this.#XRExperience.input,
415
+ floorMeshes: [ground],
416
+ timeToTeleport: 1500,
417
+ });
418
+ this.#logDebug("XR teleportation feature enabled");
419
+
420
+ this.#XRExperience.baseExperience.sessionManager.onXRReady.add(() => {
421
+ // Set the initial position of xrCamera: use nonVRCamera, which contains a copy of the original this.#scene.activeCamera before entering XR
422
+ this.#XRExperience.baseExperience.camera.setTransformationFromNonVRCamera(this.#XRExperience.baseExperience._nonVRCamera);
423
+ this.#XRExperience.baseExperience.camera.setTarget(Vector3.Zero());
424
+ this.#XRExperience.baseExperience.onInitialXRPoseSetObservable.notifyObservers(this.#XRExperience.baseExperience.camera);
425
+ this.#logDebug("XR session ready and camera transformed");
426
+ });
427
+
428
+ this.addStylesToARButton();
429
+ } catch (error) {
430
+ this.#logWarn("Failed to create XR experience", { error });
431
+ this.#XRExperience = null;
432
+ }
187
433
  }
188
434
 
189
- #canvasResizeObserver = new ResizeObserver(() => this.#engine && this.#engine.resize());
435
+ #canvasResizeObserver = new ResizeObserver(() => {
436
+ if (this.#engine) {
437
+ this.#logDebug("Resize observer triggered");
438
+ this.#engine.resize();
439
+ }
440
+ });
190
441
 
191
442
  #createCamera() {
192
- this.#camera = new ArcRotateCamera("camera", 3 * Math.PI / 2, Math.PI * 0.47, 10, Vector3.Zero(), this.#scene);
443
+ this.#logDebug("Creating default camera");
444
+ this.#camera = new ArcRotateCamera("camera", (3 * Math.PI) / 2, Math.PI * 0.47, 10, Vector3.Zero(), this.#scene);
193
445
  this.#camera.upperBetaLimit = Math.PI * 0.48;
194
446
  this.#camera.lowerBetaLimit = Math.PI * 0.25;
195
447
  this.#camera.lowerRadiusLimit = 5;
196
448
  this.#camera.upperRadiusLimit = 20;
449
+ this.#camera.metadata = { locked: false }
450
+ this.#camera = this.#camera;
197
451
  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
+ });
198
458
  }
199
459
 
200
460
  #createLights() {
461
+ this.#logDebug("Creating scene lights");
201
462
  // 1) Stronger ambient fill
202
463
  this.#hemiLight = new HemisphericLight("hemiLight", new Vector3(-10, 10, -10), this.#scene);
203
464
  this.#hemiLight.intensity = 0.6;
@@ -217,161 +478,408 @@ class PrefViewer extends HTMLElement {
217
478
  this.#cameraLight = new PointLight("pl", this.#camera.position, this.#scene);
218
479
  this.#cameraLight.parent = this.#camera;
219
480
  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,
486
+ });
220
487
  }
221
488
 
222
489
  #setupInteraction() {
490
+ this.#logDebug("Setting up canvas interaction listeners");
223
491
  this.#canvas.addEventListener("wheel", (event) => {
224
- if (!this.#scene || !this.#camera) return;
225
- const pick = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
492
+ if (!this.#scene || !this.#camera) {
493
+ this.#logWarn("Wheel interaction ignored; scene or camera missing");
494
+ return false;
495
+ }
496
+ //const pick = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
226
497
  //this.#camera.target = pick.hit ? pick.pickedPoint.clone() : this.#camera.target;
227
- this.#camera.inertialRadiusOffset -= event.deltaY * this.#camera.wheelPrecision * 0.001;
498
+ if (!this.#scene.activeCamera.metadata?.locked) {
499
+ 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
+ }
228
507
  event.preventDefault();
229
508
  });
230
509
  }
231
510
 
232
511
  #disposeEngine() {
233
- if (!this.#engine) return;
512
+ if (!this.#engine) {
513
+ this.#logDebug("Dispose engine called but engine already null");
514
+ return;
515
+ }
516
+ this.#logDebug("Disposing Babylon resources");
234
517
  this.#engine.dispose();
235
518
  this.#engine = this.#scene = this.#camera = null;
236
519
  this.#hemiLight = this.#dirLight = this.#cameraLight = null;
237
520
  this.#shadowGen = null;
521
+ this.#logDebug("Babylon resources disposed");
238
522
  }
239
523
 
240
524
  // Utility methods for loading gltf/glb
525
+ async #getServerFileDataHeader(uri) {
526
+ this.#logDebug("Requesting server file header", { uri });
527
+ return new Promise((resolve) => {
528
+ const xhr = new XMLHttpRequest();
529
+ xhr.open("HEAD", uri, true);
530
+ xhr.responseType = "blob";
531
+ xhr.onload = () => {
532
+ if (xhr.status === 200) {
533
+ 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);
537
+ } else {
538
+ this.#logWarn("Failed to retrieve server file header", { uri, status: xhr.status });
539
+ resolve(0, null);
540
+ }
541
+ };
542
+ xhr.onerror = () => {
543
+ this.#logError("Error requesting server file header", { uri });
544
+ resolve(0, null);
545
+ };
546
+ xhr.send();
547
+ });
548
+ }
549
+
241
550
  #transformUrl(url) {
242
551
  return new Promise((resolve) => {
243
- resolve(url.replace(/^blob:[^/]+\//i, "").replace(/\\/g, "/"));
552
+ const transformed = url.replace(/^blob:[^/]+\//i, "").replace(/\\/g, "/");
553
+ this.#logDebug("Transformed URL", { original: url, transformed });
554
+ resolve(transformed);
244
555
  });
245
556
  }
246
557
 
247
558
  #decodeBase64(base64) {
559
+ this.#logDebug("Decoding Base64 payload", { length: base64 ? base64.length : 0 });
248
560
  const [, payload] = base64.split(",");
249
561
  const raw = payload || base64;
250
562
  let decoded = "";
251
563
  let blob = null;
252
564
  let extension = null;
565
+ let size = raw.length;
253
566
  try {
254
567
  decoded = atob(raw);
255
568
  } catch {
256
- return { blob, extension };
569
+ this.#logWarn("Failed to decode Base64 string");
570
+ return { blob, extension, size };
257
571
  }
258
572
  let isJson = false;
259
573
  try {
260
574
  JSON.parse(decoded);
261
575
  isJson = true;
262
- } catch {}
576
+ } catch { }
263
577
  extension = isJson ? ".gltf" : ".glb";
264
578
  const type = isJson ? "model/gltf+json" : "model/gltf-binary";
265
579
  const array = Uint8Array.from(decoded, (c) => c.charCodeAt(0));
266
580
  blob = new Blob([array], { type });
267
- return { blob, extension };
581
+ this.#logDebug("Decoded Base64 payload", { isJson, size, extension });
582
+ return { blob, extension, size };
268
583
  }
269
584
 
270
585
  async #initStorage(db, table) {
586
+ this.#logDebug("Initializing storage access", { db, table });
271
587
  if (window.gltfDB && window.gltfDB.name === db && window.gltfDB.objectStoreNames.contains(table)) {
588
+ this.#logDebug("Reusing existing IndexedDB connection", { db, table });
272
589
  return true;
273
590
  }
274
591
  await initDb(db, table);
592
+ this.#logDebug("IndexedDB initialized", { db, table });
275
593
  }
276
594
 
277
595
  // Methods for managing Asset Containers
278
596
  #setVisibilityOfWallAndFloorInModel(show) {
279
- if (this.#model.container && this.#model.visible) {
280
- const names = ["outer_0", "inner_1", "outerFloor", "innerFloor"];
281
- const nodes = this.#model.container.getNodes();
282
- this.#model.container
283
- .getNodes()
284
- .filter((filter) => names.includes(filter.name))
285
- .forEach((node) => node.setEnabled(show !== undefined ? show : this.#environment.show));
597
+ 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
+ return false;
286
603
  }
604
+ show = show !== undefined ? show : this.#data.containers.environment.visible;
605
+ const prefixes = Object.values(this.#data.options.materials).map((material) => material.prefix);
606
+ 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 });
287
608
  }
288
609
 
289
- #addContainer(group) {
290
- if (group.container && !group.visible && group.show) {
291
- group.container.addAllToScene();
292
- group.visible = true;
610
+ #setOptionsMaterial(optionMaterial) {
611
+ if (!optionMaterial || !optionMaterial.prefix || !optionMaterial.value) {
612
+ this.#logWarn("Material option invalid", { optionMaterial });
613
+ return false;
614
+ }
615
+
616
+ this.#logDebug("Applying material option", {
617
+ prefix: optionMaterial.prefix,
618
+ value: optionMaterial.value,
619
+ changed: optionMaterial.changed,
620
+ });
621
+
622
+ const material = this.#data.containers.materials.assetContainer?.materials.find((mat) => mat.name === optionMaterial.value) || null;
623
+ if (!material) {
624
+ this.#logWarn("Requested material not found", { value: optionMaterial.value });
625
+ return false;
626
+ }
627
+
628
+ const containers = [];
629
+ if (this.#data.containers.model.assetContainer && (this.#data.containers.model.assetContainer.changed || optionMaterial.changed)) {
630
+ containers.push(this.#data.containers.model.assetContainer);
631
+ }
632
+ if (this.#data.containers.environment.assetContainer && (this.#data.containers.environment.assetContainer.changed || optionMaterial.changed)) {
633
+ containers.push(this.#data.containers.environment.assetContainer);
293
634
  }
635
+ if (containers.length === 0) {
636
+ this.#logDebug("No containers required material update", { prefix: optionMaterial.prefix });
637
+ return false;
638
+ }
639
+
640
+ let someSetted = false;
641
+ containers.forEach((container) =>
642
+ container.meshes
643
+ .filter((meshToFilter) => meshToFilter.name.startsWith(optionMaterial.prefix))
644
+ .forEach((mesh) => {
645
+ mesh.material = material;
646
+ someSetted = true;
647
+ this.#logDebug("Assigned material to mesh", { mesh: mesh.name, material: material.name });
648
+ })
649
+ );
650
+
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
+ });
657
+
658
+ return someSetted;
294
659
  }
295
660
 
296
- #removeContainer(group) {
297
- if (group.container && group.visible) {
298
- group.container.removeAllFromScene();
299
- group.visible = false;
661
+ #setOptionsMaterials() {
662
+ if (!this.#data.containers.materials.assetContainer) {
663
+ this.#logDebug("Skipping materials update; materials container is missing");
664
+ return false;
300
665
  }
666
+
667
+ this.#logDebug("Applying material options batch");
668
+ let someSetted = false;
669
+ Object.values(this.#data.options.materials).forEach((material) => {
670
+ let settedMaterial = this.#setOptionsMaterial(material);
671
+ someSetted = someSetted || settedMaterial;
672
+ });
673
+ this.#logDebug("Material batch processing finished", { appliedAny: someSetted });
674
+ return someSetted;
301
675
  }
302
676
 
303
- #replaceContainer(group, newContainer) {
304
- this.#removeContainer(group);
305
- group.container = newContainer;
306
- group.container.meshes.forEach((mesh) => {
677
+ #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
+ });
684
+ return false;
685
+ }
686
+
687
+ let camera = this.#data.containers.model.assetContainer?.cameras.find((cam) => cam.name === this.#data.options.camera.value) || null;
688
+ if (!camera) {
689
+ this.#logWarn("Requested camera not found", { name: this.#data.options.camera.value });
690
+ return false;
691
+ }
692
+
693
+ camera.metadata = { locked: this.#data.options.camera.locked };
694
+ if (!this.#data.options.camera.locked) {
695
+ 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
+ }
700
+ this.#scene.activeCamera = camera;
701
+ this.#logDebug("Active camera set", { camera: camera.name });
702
+
703
+ return true;
704
+ }
705
+
706
+ #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
+ });
718
+ }
719
+ }
720
+
721
+ #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
+ });
732
+ }
733
+ }
734
+
735
+ #replaceContainer(container, newAssetContainer) {
736
+ this.#logDebug("Replacing container asset", { name: container.name });
737
+ this.#removeContainer(container);
738
+ container.assetContainer = newAssetContainer;
739
+ container.assetContainer.meshes.forEach((mesh) => {
307
740
  mesh.receiveShadows = true;
308
741
  this.#shadowGen.addShadowCaster(mesh, true);
742
+ this.#logDebug("Configured mesh for shadows", { container: container.name, mesh: mesh.name });
743
+ });
744
+ this.#addContainer(container);
745
+ this.#logDebug("Container replacement complete", {
746
+ name: container.name,
747
+ meshCount: container.assetContainer.meshes.length,
309
748
  });
310
- this.#addContainer(group);
311
749
  }
312
750
 
313
- async #loadAssetContainer(storage) {
751
+ 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
+ });
757
+
314
758
  if (!storage) {
759
+ this.#logWarn("No storage configuration provided for container", { container: container?.name });
315
760
  return false;
316
761
  }
317
762
 
318
763
  let source = storage.url || null;
319
764
 
320
765
  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
+ });
321
772
  await this.#initStorage(storage.db, storage.table);
322
773
  const object = await loadModel(storage.id, storage.table);
323
774
  source = object.data;
775
+ if (object.timestamp === container.timestamp) {
776
+ this.#logDebug("IndexedDB model unchanged; skipping reload", { container: container.name });
777
+ return false;
778
+ } else {
779
+ container.changed = { timestamp: object.timestamp, size: object.size };
780
+ this.#logDebug("IndexedDB model marked as changed", { container: container.name, metadata: container.changed });
781
+ }
324
782
  }
325
783
 
326
784
  if (!source) {
785
+ this.#logWarn("No source resolved for container", { container: container.name });
327
786
  return false;
328
787
  }
329
788
 
330
789
  let file = null;
331
790
 
332
- let { blob, extension } = this.#decodeBase64(source);
791
+ let { blob, extension, size } = this.#decodeBase64(source);
333
792
  if (blob && extension) {
334
- file = new File([blob], `model${extension}`, {
793
+ this.#logDebug("Source detected as Base64", { container: container.name, extension });
794
+ file = new File([blob], `${container.name}${extension}`, {
335
795
  type: blob.type,
336
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 });
800
+ return false;
801
+ } else {
802
+ container.changed = { timestamp: null, size: size };
803
+ this.#logDebug("Base64 model marked as changed", { container: container.name, size });
804
+ }
805
+ }
337
806
  } else {
338
807
  const extMatch = source.match(/\.(gltf|glb)(\?|#|$)/i);
339
808
  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
+ });
816
+ return false;
817
+ } else {
818
+ container.changed = { timestamp: fileTimestamp, size: fileSize };
819
+ this.#logDebug("Remote model marked as changed", { container: container.name, metadata: container.changed });
820
+ }
340
821
  }
341
822
 
342
823
  let options = {
343
824
  pluginExtension: extension,
344
825
  pluginOptions: {
345
826
  gltf: {
827
+ loadAllMaterials: true,
346
828
  preprocessUrlAsync: this.#transformUrl,
347
829
  },
348
830
  },
349
831
  };
350
832
 
833
+ this.#logInfo("Loading asset container", { container: container.name, extension });
351
834
  return LoadAssetContainerAsync(file || source, this.#scene, options);
352
835
  }
353
836
 
354
- async #loadContainers(loadModel = true, loadEnvironment = true) {
837
+ async #loadContainers(loadModel = true, loadEnvironment = true, loadMaterials = true) {
838
+ this.#logInfo("Starting container load", { loadModel, loadEnvironment, loadMaterials });
355
839
  const promiseArray = [];
356
-
357
- promiseArray.push(loadModel ? this.#loadAssetContainer(this.#model.storage) : false);
358
- promiseArray.push(loadEnvironment ? this.#loadAssetContainer(this.#environment.storage) : false);
840
+ promiseArray.push(loadModel ? this.#loadAssetContainer(this.#data.containers.model) : false);
841
+ promiseArray.push(loadEnvironment ? this.#loadAssetContainer(this.#data.containers.environment) : false);
842
+ promiseArray.push(loadMaterials ? this.#loadAssetContainer(this.#data.containers.materials) : false);
359
843
 
360
844
  Promise.allSettled(promiseArray)
361
845
  .then(async (values) => {
362
846
  const modelContainer = values[0];
363
847
  const environmentContainer = values[1];
848
+ const materialsContainer = values[2];
364
849
 
365
850
  if (modelContainer.status === "fulfilled" && modelContainer.value) {
366
- this.#replaceContainer(this.#model, modelContainer.value);
851
+ this.#replaceContainer(this.#data.containers.model, modelContainer.value);
852
+ this.#storeChangedFlagsForContainer(this.#data.containers.model);
853
+ this.#logInfo("Model container loaded successfully");
854
+ } 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 });
367
857
  }
368
858
 
369
859
  if (environmentContainer.status === "fulfilled" && environmentContainer.value) {
370
- this.#replaceContainer(this.#environment, environmentContainer.value);
860
+ this.#replaceContainer(this.#data.containers.environment, environmentContainer.value);
861
+ this.#storeChangedFlagsForContainer(this.#data.containers.environment);
862
+ this.#logInfo("Environment container loaded successfully");
863
+ } 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 });
866
+ }
867
+
868
+ if (materialsContainer.status === "fulfilled" && materialsContainer.value) {
869
+ this.#replaceContainer(this.#data.containers.materials, materialsContainer.value);
870
+ this.#storeChangedFlagsForContainer(this.#data.containers.materials);
871
+ this.#logInfo("Materials container loaded successfully");
872
+ } else {
873
+ this.#logDebug("Materials container load skipped or failed", { status: materialsContainer.status });
371
874
  }
372
875
 
876
+ this.#setOptionsMaterials();
877
+ this.#setOptionsCamera();
373
878
  this.#setVisibilityOfWallAndFloorInModel();
374
879
 
880
+ this.#resetChangedFlags();
881
+
882
+ this.#logInfo("Containers load routine completed");
375
883
  this.dispatchEvent(
376
884
  new CustomEvent("model-loaded", {
377
885
  detail: { success: "" },
@@ -379,9 +887,10 @@ class PrefViewer extends HTMLElement {
379
887
  composed: true,
380
888
  })
381
889
  );
890
+ this.#logDebug("Dispatched model-loaded event");
382
891
  })
383
892
  .catch((error) => {
384
- console.error("PrefViewer: failed to load model", error);
893
+ this.#logError("Failed to load containers", { error });
385
894
  this.dispatchEvent(
386
895
  new CustomEvent("model-error", {
387
896
  detail: { error: error },
@@ -394,87 +903,170 @@ class PrefViewer extends HTMLElement {
394
903
 
395
904
  // Public Methods
396
905
  loadConfig(config) {
397
- config = typeof config === "string" ? JSON.parse(config) : 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
+ }
914
+ }
398
915
  if (!config) {
916
+ this.#logWarn("No config provided");
399
917
  return false;
400
918
  }
401
- this.#model.storage = config.model?.storage || null;
402
- this.#model.show = config.model?.visible !== undefined ? config.model.visible : this.#model.show;
403
- this.#environment.storage = config.scene?.storage || null;
404
- this.#environment.show = config.scene?.visible !== undefined ? config.scene.visible : this.#environment.show;
405
- this.#initialized && this.#loadContainers(true, true);
919
+
920
+ // Containers
921
+ this.#data.containers.model.storage = config.model?.storage || null;
922
+ 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;
924
+ 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;
926
+
927
+ // Options
928
+ if (config.options) {
929
+ this.#checkCameraChanged(config.options);
930
+ this.#checkMaterialsChanged(config.options);
931
+ }
932
+
933
+ this.#logDebug("Config applied", {
934
+ modelStorage: this.#describeStorage(this.#data.containers.model.storage),
935
+ environmentStorage: this.#describeStorage(this.#data.containers.environment.storage),
936
+ });
937
+
938
+ this.#initialized && this.#loadContainers(true, true, true);
939
+ }
940
+
941
+ setOptions(options) {
942
+ this.#logInfo("setOptions called", { optionsProvided: !!options });
943
+ if (!options) {
944
+ this.#logWarn("setOptions called without options");
945
+ return false;
946
+ }
947
+ let someSetted = false;
948
+ if (this.#checkCameraChanged(options)) {
949
+ this.#logDebug("Camera options changed via setOptions");
950
+ someSetted = someSetted || this.#setOptionsCamera();
951
+ }
952
+ if (this.#checkMaterialsChanged(options)) {
953
+ this.#logDebug("Material options changed via setOptions");
954
+ someSetted = someSetted || this.#setOptionsMaterials();
955
+ }
956
+ this.#resetChangedFlags();
957
+ this.#logDebug("setOptions completed", { appliedAny: someSetted });
958
+ debugger;
959
+ return someSetted;
406
960
  }
407
961
 
408
962
  loadModel(model) {
409
- model = typeof model === "string" ? JSON.parse(model) : 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
+ }
971
+ }
410
972
  if (!model) {
973
+ this.#logWarn("No model payload provided");
411
974
  return false;
412
975
  }
413
- this.#model.storage = model.storage || null;
414
- this.#model.show = model.visible !== undefined ? model.visible : this.#model.show;
415
- this.#initialized && this.#loadContainers(true, false);
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);
416
983
  }
417
984
 
418
985
  loadScene(scene) {
419
- scene = typeof scene === "string" ? JSON.parse(scene) : 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
+ }
420
995
  if (!scene) {
996
+ this.#logWarn("No scene payload provided");
421
997
  return false;
422
998
  }
423
- this.#environment.storage = scene.storage || null;
424
- this.#environment.show = scene.visible !== undefined ? scene.visible : this.#environment.show;
425
- this.#initialized && this.#loadContainers(false, true);
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);
426
1006
  }
427
1007
 
428
1008
  showModel() {
429
- this.#model.show = true;
430
- this.#addContainer(this.#model);
1009
+ this.#data.containers.model.show = true;
1010
+ this.#addContainer(this.#data.containers.model);
1011
+ this.#logInfo("Model visibility set to true");
431
1012
  }
432
1013
 
433
1014
  hideModel() {
434
- this.#model.show = false;
435
- this.#removeContainer(this.#model);
1015
+ this.#data.containers.model.show = false;
1016
+ this.#removeContainer(this.#data.containers.model);
1017
+ this.#logInfo("Model visibility set to false");
436
1018
  }
437
1019
 
438
1020
  showScene() {
439
- this.#environment.show = true;
440
- this.#addContainer(this.#environment);
1021
+ this.#data.containers.environment.show = true;
1022
+ this.#addContainer(this.#data.containers.environment);
441
1023
  this.#setVisibilityOfWallAndFloorInModel();
1024
+ this.#logInfo("Scene visibility set to true");
442
1025
  }
443
1026
 
444
1027
  hideScene() {
445
- this.#environment.show = false;
446
- this.#removeContainer(this.#environment);
1028
+ this.#data.containers.environment.show = false;
1029
+ this.#removeContainer(this.#data.containers.environment);
447
1030
  this.#setVisibilityOfWallAndFloorInModel();
1031
+ this.#logInfo("Scene visibility set to false");
448
1032
  }
449
1033
 
450
1034
  downloadModelGLB() {
451
1035
  const fileName = "model";
452
- GLTF2Export.GLBAsync(this.#model.container, fileName, { exportWithoutWaitingForScene: true }).then((glb) => {
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 });
453
1039
  glb.downloadFiles();
454
1040
  });
455
1041
  }
456
1042
 
457
1043
  downloadModelUSDZ() {
458
1044
  const fileName = "model";
459
- USDZExportAsync(this.#model.container).then((response) => {
1045
+ this.#logInfo("Initiating USDZ download for model", { fileName });
1046
+ USDZExportAsync(this.#data.containers.model.assetContainer).then((response) => {
460
1047
  if (response) {
461
1048
  Tools.Download(new Blob([response], { type: "model/vnd.usdz+zip" }), `${fileName}.usdz`);
1049
+ this.#logDebug("Model USDZ export ready", { fileName });
462
1050
  }
463
1051
  });
464
1052
  }
465
1053
 
466
1054
  downloadModelAndSceneUSDZ() {
467
1055
  const fileName = "scene";
1056
+ this.#logInfo("Initiating USDZ download for scene", { fileName });
468
1057
  USDZExportAsync(this.#scene).then((response) => {
469
1058
  if (response) {
470
1059
  Tools.Download(new Blob([response], { type: "model/vnd.usdz+zip" }), `${fileName}.usdz`);
1060
+ this.#logDebug("Scene USDZ export ready", { fileName });
471
1061
  }
472
1062
  });
473
1063
  }
474
1064
 
475
1065
  downloadModelAndSceneGLB() {
476
1066
  const fileName = "scene";
1067
+ this.#logInfo("Initiating GLB download for scene", { fileName });
477
1068
  GLTF2Export.GLBAsync(this.#scene, fileName, { exportWithoutWaitingForScene: true }).then((glb) => {
1069
+ this.#logDebug("Scene GLB export ready", { fileName });
478
1070
  glb.downloadFiles();
479
1071
  });
480
1072
  }