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

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preference-sl/pref-viewer",
3
- "version": "2.10.0-beta.7",
3
+ "version": "2.10.0-beta.9",
4
4
  "description": "Web Component to preview GLTF models with Babylon.js",
5
5
  "author": "Alex Moreno Palacio <amoreno@preference.es>",
6
6
  "scripts": {
@@ -1,4 +1,4 @@
1
- // wwwroot/js/gltf-storage.js
1
+ // wwwroot/js/gltf-storage.js
2
2
 
3
3
  // Public, inspectable namespace
4
4
  const PC = (globalThis.PrefConfigurator ??= {});
package/src/index.js CHANGED
@@ -47,7 +47,13 @@ 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
58
  #data = {
53
59
  containers: {
@@ -113,6 +119,67 @@ class PrefViewer extends HTMLElement {
113
119
  },
114
120
  };
115
121
 
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
+
116
183
  // DOM elements
117
184
  #wrapper = null;
118
185
  #canvas = null;
@@ -129,6 +196,7 @@ class PrefViewer extends HTMLElement {
129
196
 
130
197
  constructor() {
131
198
  super();
199
+ this.#logDebug("Constructing PrefViewer instance");
132
200
  this.attachShadow({ mode: "open" });
133
201
  this.#createCanvas();
134
202
  this.#wrapCanvas();
@@ -144,11 +212,13 @@ class PrefViewer extends HTMLElement {
144
212
  };
145
213
  }
146
214
 
147
- static get observedAttributes() {
148
- return ["config", "model", "scene", "show-model", "show-scene"];
149
- }
150
-
151
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) });
152
222
  let data = null;
153
223
  switch (name) {
154
224
  case "config":
@@ -162,6 +232,7 @@ class PrefViewer extends HTMLElement {
162
232
  break;
163
233
  case "show-model":
164
234
  data = value.toLowerCase?.() === "true";
235
+ this.#logDebug("Toggling model visibility from attribute", { visible: data, initialized: this.#initialized });
165
236
  if (this.#initialized) {
166
237
  data ? this.showModel() : this.hideModel();
167
238
  } else {
@@ -170,6 +241,7 @@ class PrefViewer extends HTMLElement {
170
241
  break;
171
242
  case "show-scene":
172
243
  data = value.toLowerCase?.() === "true";
244
+ this.#logDebug("Toggling scene visibility from attribute", { visible: data, initialized: this.#initialized });
173
245
  if (this.#initialized) {
174
246
  data ? this.showScene() : this.hideScene();
175
247
  } else {
@@ -182,7 +254,7 @@ class PrefViewer extends HTMLElement {
182
254
  connectedCallback() {
183
255
  if (!this.hasAttribute("config")) {
184
256
  const error = 'PrefViewer: provide "models" as array of model and environment';
185
- console.error(error);
257
+ this.#logError("Missing required config attribute", { error });
186
258
  this.dispatchEvent(
187
259
  new CustomEvent("model-error", {
188
260
  detail: { error: new Error(error) },
@@ -193,18 +265,22 @@ class PrefViewer extends HTMLElement {
193
265
  return false;
194
266
  }
195
267
 
268
+ this.#logDebug("Connected to DOM, initializing Babylon");
196
269
  this.#initializeBabylon();
197
270
  this.#loadContainers(true, true, true);
198
271
  this.#initialized = true;
272
+ this.#logInfo("Initialization completed", { initialized: this.#initialized });
199
273
  }
200
274
 
201
275
  disconnectedCallback() {
276
+ this.#logDebug("Disconnected from DOM, disposing resources");
202
277
  this.#disposeEngine();
203
278
  this.#canvasResizeObserver.disconnect();
204
279
  }
205
280
 
206
281
  // Web Component
207
282
  #createCanvas() {
283
+ this.#logDebug("Creating rendering canvas");
208
284
  this.#canvas = document.createElement("canvas");
209
285
  Object.assign(this.#canvas.style, {
210
286
  width: "100%",
@@ -212,9 +288,11 @@ class PrefViewer extends HTMLElement {
212
288
  display: "block",
213
289
  outline: "none",
214
290
  });
291
+ this.#logDebug("Canvas element created and styled");
215
292
  }
216
293
 
217
294
  #wrapCanvas() {
295
+ this.#logDebug("Wrapping canvas inside container div");
218
296
  this.#wrapper = document.createElement("div");
219
297
  Object.assign(this.#wrapper.style, {
220
298
  width: "100%",
@@ -223,20 +301,24 @@ class PrefViewer extends HTMLElement {
223
301
  });
224
302
  this.#wrapper.appendChild(this.#canvas);
225
303
  this.shadowRoot.append(this.#wrapper);
304
+ this.#logDebug("Canvas wrapper appended to shadow DOM");
226
305
  }
227
306
 
228
307
  // Data
229
308
  #checkCameraChanged(options) {
230
309
  if (!options || !options.camera) {
310
+ this.#logDebug("Camera options not provided or unchanged");
231
311
  return false;
232
312
  }
233
313
  this.#data.options.camera.changed = options.camera && options.camera !== this.#data.options.camera.value ? true : false;
234
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 });
235
316
  return this.#data.options.camera.changed;
236
317
  }
237
318
 
238
319
  #checkMaterialsChanged(options) {
239
320
  if (!options) {
321
+ this.#logDebug("Material options not provided");
240
322
  return false;
241
323
  }
242
324
  let someChanged = false;
@@ -246,22 +328,32 @@ class PrefViewer extends HTMLElement {
246
328
  this.#data.options.materials[material].value = this.#data.options.materials[material].changed ? options[key] : this.#data.options.materials[material].value;
247
329
  someChanged = someChanged || this.#data.options.materials[material].changed;
248
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
+ });
249
338
  return someChanged;
250
339
  }
251
340
 
252
341
  #storeChangedFlagsForContainer(container) {
253
342
  container.timestamp = container.changed.timestamp;
254
343
  container.size = container.changed.size;
344
+ this.#logDebug("Stored change flags for container", { name: container.name, timestamp: container.timestamp, size: container.size });
255
345
  }
256
346
 
257
347
  #resetChangedFlags() {
258
348
  Object.values(this.#data.containers).forEach((container) => (container.changed = false));
259
349
  Object.values(this.#data.options.materials).forEach((material) => (material.changed = false));
260
350
  this.#data.options.camera.changed = false;
351
+ this.#logDebug("Reset change flags across containers and options");
261
352
  }
262
353
 
263
354
  // Babylon.js
264
355
  async #initializeBabylon() {
356
+ this.#logInfo("Initializing Babylon engine and scene");
265
357
  this.#engine = new Engine(this.#canvas, true, { alpha: true });
266
358
  this.#scene = new Scene(this.#engine);
267
359
  this.#scene.clearColor = new Color4(1, 1, 1, 1);
@@ -271,26 +363,32 @@ class PrefViewer extends HTMLElement {
271
363
 
272
364
  this.#engine.runRenderLoop(() => this.#scene && this.#scene.render());
273
365
  this.#canvasResizeObserver.observe(this.#canvas);
366
+ this.#logDebug("Engine render loop started and resize observer attached");
274
367
 
275
368
  await this.#createXRExperience();
369
+ this.#logInfo("Babylon initialization finished", { xrEnabled: !!this.#XRExperience });
276
370
  }
277
371
 
278
372
  addStylesToARButton() {
373
+ this.#logDebug("Adding styles to AR button");
279
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"}';
280
375
  const style = document.createElement("style");
281
376
  style.appendChild(document.createTextNode(css));
282
377
  this.#wrapper.appendChild(style);
378
+ this.#logDebug("AR button styles applied");
283
379
  }
284
380
 
285
381
  async #createXRExperience() {
286
382
  if (this.#XRExperience) {
383
+ this.#logDebug("XR experience already created, skipping");
287
384
  return true;
288
385
  }
289
386
 
387
+ this.#logDebug("Attempting to create XR experience");
290
388
  const sessionMode = "immersive-ar";
291
389
  const sessionSupported = await WebXRSessionManager.IsSessionSupportedAsync(sessionMode);
292
390
  if (!sessionSupported) {
293
- console.info("PrefViewer: WebXR in mode AR is not supported");
391
+ this.#logInfo("WebXR session mode not supported", { sessionMode });
294
392
  return false;
295
393
  }
296
394
 
@@ -309,6 +407,7 @@ class PrefViewer extends HTMLElement {
309
407
  };
310
408
 
311
409
  this.#XRExperience = await WebXRDefaultExperience.CreateAsync(this.#scene, options);
410
+ this.#logInfo("XR experience created successfully", { sessionMode });
312
411
 
313
412
  const featuresManager = this.#XRExperience.baseExperience.featuresManager;
314
413
  featuresManager.enableFeature(WebXRFeatureName.TELEPORTATION, "stable", {
@@ -316,24 +415,32 @@ class PrefViewer extends HTMLElement {
316
415
  floorMeshes: [ground],
317
416
  timeToTeleport: 1500,
318
417
  });
418
+ this.#logDebug("XR teleportation feature enabled");
319
419
 
320
420
  this.#XRExperience.baseExperience.sessionManager.onXRReady.add(() => {
321
421
  // Set the initial position of xrCamera: use nonVRCamera, which contains a copy of the original this.#scene.activeCamera before entering XR
322
422
  this.#XRExperience.baseExperience.camera.setTransformationFromNonVRCamera(this.#XRExperience.baseExperience._nonVRCamera);
323
423
  this.#XRExperience.baseExperience.camera.setTarget(Vector3.Zero());
324
424
  this.#XRExperience.baseExperience.onInitialXRPoseSetObservable.notifyObservers(this.#XRExperience.baseExperience.camera);
425
+ this.#logDebug("XR session ready and camera transformed");
325
426
  });
326
427
 
327
428
  this.addStylesToARButton();
328
429
  } catch (error) {
329
- console.warn("PrefViewer: failed to create WebXR experience", error);
430
+ this.#logWarn("Failed to create XR experience", { error });
330
431
  this.#XRExperience = null;
331
432
  }
332
433
  }
333
434
 
334
- #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
+ });
335
441
 
336
442
  #createCamera() {
443
+ this.#logDebug("Creating default camera");
337
444
  this.#camera = new ArcRotateCamera("camera", (3 * Math.PI) / 2, Math.PI * 0.47, 10, Vector3.Zero(), this.#scene);
338
445
  this.#camera.upperBetaLimit = Math.PI * 0.48;
339
446
  this.#camera.lowerBetaLimit = Math.PI * 0.25;
@@ -342,9 +449,16 @@ class PrefViewer extends HTMLElement {
342
449
  this.#camera.metadata = { locked: false }
343
450
  this.#camera = this.#camera;
344
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
+ });
345
458
  }
346
459
 
347
460
  #createLights() {
461
+ this.#logDebug("Creating scene lights");
348
462
  // 1) Stronger ambient fill
349
463
  this.#hemiLight = new HemisphericLight("hemiLight", new Vector3(-10, 10, -10), this.#scene);
350
464
  this.#hemiLight.intensity = 0.6;
@@ -364,32 +478,52 @@ class PrefViewer extends HTMLElement {
364
478
  this.#cameraLight = new PointLight("pl", this.#camera.position, this.#scene);
365
479
  this.#cameraLight.parent = this.#camera;
366
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
+ });
367
487
  }
368
488
 
369
489
  #setupInteraction() {
490
+ this.#logDebug("Setting up canvas interaction listeners");
370
491
  this.#canvas.addEventListener("wheel", (event) => {
371
492
  if (!this.#scene || !this.#camera) {
493
+ this.#logWarn("Wheel interaction ignored; scene or camera missing");
372
494
  return false;
373
495
  }
374
496
  //const pick = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
375
497
  //this.#camera.target = pick.hit ? pick.pickedPoint.clone() : this.#camera.target;
376
498
  if (!this.#scene.activeCamera.metadata?.locked) {
377
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");
378
506
  }
379
507
  event.preventDefault();
380
508
  });
381
509
  }
382
510
 
383
511
  #disposeEngine() {
384
- 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");
385
517
  this.#engine.dispose();
386
518
  this.#engine = this.#scene = this.#camera = null;
387
519
  this.#hemiLight = this.#dirLight = this.#cameraLight = null;
388
520
  this.#shadowGen = null;
521
+ this.#logDebug("Babylon resources disposed");
389
522
  }
390
523
 
391
524
  // Utility methods for loading gltf/glb
392
525
  async #getServerFileDataHeader(uri) {
526
+ this.#logDebug("Requesting server file header", { uri });
393
527
  return new Promise((resolve) => {
394
528
  const xhr = new XMLHttpRequest();
395
529
  xhr.open("HEAD", uri, true);
@@ -398,12 +532,15 @@ class PrefViewer extends HTMLElement {
398
532
  if (xhr.status === 200) {
399
533
  const size = parseInt(xhr.getResponseHeader("Content-Length"));
400
534
  const timestamp = new Date(xhr.getResponseHeader("Last-Modified")).toISOString();
535
+ this.#logDebug("Received server file header", { uri, size, timestamp });
401
536
  resolve(size, timestamp);
402
537
  } else {
538
+ this.#logWarn("Failed to retrieve server file header", { uri, status: xhr.status });
403
539
  resolve(0, null);
404
540
  }
405
541
  };
406
542
  xhr.onerror = () => {
543
+ this.#logError("Error requesting server file header", { uri });
407
544
  resolve(0, null);
408
545
  };
409
546
  xhr.send();
@@ -412,11 +549,14 @@ class PrefViewer extends HTMLElement {
412
549
 
413
550
  #transformUrl(url) {
414
551
  return new Promise((resolve) => {
415
- 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);
416
555
  });
417
556
  }
418
557
 
419
558
  #decodeBase64(base64) {
559
+ this.#logDebug("Decoding Base64 payload", { length: base64 ? base64.length : 0 });
420
560
  const [, payload] = base64.split(",");
421
561
  const raw = payload || base64;
422
562
  let decoded = "";
@@ -426,44 +566,62 @@ class PrefViewer extends HTMLElement {
426
566
  try {
427
567
  decoded = atob(raw);
428
568
  } catch {
569
+ this.#logWarn("Failed to decode Base64 string");
429
570
  return { blob, extension, size };
430
571
  }
431
572
  let isJson = false;
432
573
  try {
433
574
  JSON.parse(decoded);
434
575
  isJson = true;
435
- } catch {}
576
+ } catch { }
436
577
  extension = isJson ? ".gltf" : ".glb";
437
578
  const type = isJson ? "model/gltf+json" : "model/gltf-binary";
438
579
  const array = Uint8Array.from(decoded, (c) => c.charCodeAt(0));
439
580
  blob = new Blob([array], { type });
581
+ this.#logDebug("Decoded Base64 payload", { isJson, size, extension });
440
582
  return { blob, extension, size };
441
583
  }
442
584
 
443
585
  async #initStorage(db, table) {
586
+ this.#logDebug("Initializing storage access", { db, table });
444
587
  if (window.gltfDB && window.gltfDB.name === db && window.gltfDB.objectStoreNames.contains(table)) {
588
+ this.#logDebug("Reusing existing IndexedDB connection", { db, table });
445
589
  return true;
446
590
  }
447
591
  await initDb(db, table);
592
+ this.#logDebug("IndexedDB initialized", { db, table });
448
593
  }
449
594
 
450
595
  // Methods for managing Asset Containers
451
596
  #setVisibilityOfWallAndFloorInModel(show) {
452
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
+ });
453
602
  return false;
454
603
  }
455
604
  show = show !== undefined ? show : this.#data.containers.environment.visible;
456
605
  const prefixes = Object.values(this.#data.options.materials).map((material) => material.prefix);
457
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 });
458
608
  }
459
609
 
460
610
  #setOptionsMaterial(optionMaterial) {
461
611
  if (!optionMaterial || !optionMaterial.prefix || !optionMaterial.value) {
612
+ this.#logWarn("Material option invalid", { optionMaterial });
462
613
  return false;
463
614
  }
464
615
 
616
+ this.#logDebug("Applying material option", {
617
+ prefix: optionMaterial.prefix,
618
+ value: optionMaterial.value,
619
+ changed: optionMaterial.changed,
620
+ });
621
+
465
622
  const material = this.#data.containers.materials.assetContainer?.materials.find((mat) => mat.name === optionMaterial.value) || null;
466
623
  if (!material) {
624
+ this.#logWarn("Requested material not found", { value: optionMaterial.value });
467
625
  return false;
468
626
  }
469
627
 
@@ -475,6 +633,7 @@ class PrefViewer extends HTMLElement {
475
633
  containers.push(this.#data.containers.environment.assetContainer);
476
634
  }
477
635
  if (containers.length === 0) {
636
+ this.#logDebug("No containers required material update", { prefix: optionMaterial.prefix });
478
637
  return false;
479
638
  }
480
639
 
@@ -485,36 +644,61 @@ class PrefViewer extends HTMLElement {
485
644
  .forEach((mesh) => {
486
645
  mesh.material = material;
487
646
  someSetted = true;
647
+ this.#logDebug("Assigned material to mesh", { mesh: mesh.name, material: material.name });
488
648
  })
489
649
  );
490
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
+
491
658
  return someSetted;
492
659
  }
493
660
 
494
661
  #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");
495
668
  let someSetted = false;
496
669
  Object.values(this.#data.options.materials).forEach((material) => {
497
670
  let settedMaterial = this.#setOptionsMaterial(material);
498
671
  someSetted = someSetted || settedMaterial;
499
672
  });
673
+ this.#logDebug("Material batch processing finished", { appliedAny: someSetted });
500
674
  return someSetted;
501
675
  }
502
676
 
503
677
  #setOptionsCamera() {
504
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
+ });
505
684
  return false;
506
685
  }
507
686
 
508
687
  let camera = this.#data.containers.model.assetContainer?.cameras.find((cam) => cam.name === this.#data.options.camera.value) || null;
509
688
  if (!camera) {
689
+ this.#logWarn("Requested camera not found", { name: this.#data.options.camera.value });
510
690
  return false;
511
691
  }
512
692
 
513
693
  camera.metadata = { locked: this.#data.options.camera.locked };
514
694
  if (!this.#data.options.camera.locked) {
515
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 });
516
699
  }
517
700
  this.#scene.activeCamera = camera;
701
+ this.#logDebug("Active camera set", { camera: camera.name });
518
702
 
519
703
  return true;
520
704
  }
@@ -523,6 +707,14 @@ class PrefViewer extends HTMLElement {
523
707
  if (container.assetContainer && !container.visible && container.show) {
524
708
  container.assetContainer.addAllToScene();
525
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
+ });
526
718
  }
527
719
  }
528
720
 
@@ -530,40 +722,67 @@ class PrefViewer extends HTMLElement {
530
722
  if (container.assetContainer && container.visible) {
531
723
  container.assetContainer.removeAllFromScene();
532
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
+ });
533
732
  }
534
733
  }
535
734
 
536
735
  #replaceContainer(container, newAssetContainer) {
736
+ this.#logDebug("Replacing container asset", { name: container.name });
537
737
  this.#removeContainer(container);
538
738
  container.assetContainer = newAssetContainer;
539
739
  container.assetContainer.meshes.forEach((mesh) => {
540
740
  mesh.receiveShadows = true;
541
741
  this.#shadowGen.addShadowCaster(mesh, true);
742
+ this.#logDebug("Configured mesh for shadows", { container: container.name, mesh: mesh.name });
542
743
  });
543
744
  this.#addContainer(container);
745
+ this.#logDebug("Container replacement complete", {
746
+ name: container.name,
747
+ meshCount: container.assetContainer.meshes.length,
748
+ });
544
749
  }
545
750
 
546
751
  async #loadAssetContainer(container) {
547
- let storage = container?.storage;
752
+ const storage = container?.storage;
753
+ this.#logDebug("Requested asset container load", {
754
+ container: container?.name,
755
+ storage: this.#describeStorage(storage),
756
+ });
548
757
 
549
758
  if (!storage) {
759
+ this.#logWarn("No storage configuration provided for container", { container: container?.name });
550
760
  return false;
551
761
  }
552
762
 
553
763
  let source = storage.url || null;
554
764
 
555
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
+ });
556
772
  await this.#initStorage(storage.db, storage.table);
557
773
  const object = await loadModel(storage.id, storage.table);
558
774
  source = object.data;
559
775
  if (object.timestamp === container.timestamp) {
776
+ this.#logDebug("IndexedDB model unchanged; skipping reload", { container: container.name });
560
777
  return false;
561
778
  } else {
562
779
  container.changed = { timestamp: object.timestamp, size: object.size };
780
+ this.#logDebug("IndexedDB model marked as changed", { container: container.name, metadata: container.changed });
563
781
  }
564
782
  }
565
783
 
566
784
  if (!source) {
785
+ this.#logWarn("No source resolved for container", { container: container.name });
567
786
  return false;
568
787
  }
569
788
 
@@ -571,14 +790,17 @@ class PrefViewer extends HTMLElement {
571
790
 
572
791
  let { blob, extension, size } = this.#decodeBase64(source);
573
792
  if (blob && extension) {
793
+ this.#logDebug("Source detected as Base64", { container: container.name, extension });
574
794
  file = new File([blob], `${container.name}${extension}`, {
575
795
  type: blob.type,
576
796
  });
577
797
  if (!container.changed) {
578
798
  if (container.timestamp === null && container.size === size) {
799
+ this.#logDebug("Base64 model unchanged; skipping reload", { container: container.name, size });
579
800
  return false;
580
801
  } else {
581
802
  container.changed = { timestamp: null, size: size };
803
+ this.#logDebug("Base64 model marked as changed", { container: container.name, size });
582
804
  }
583
805
  }
584
806
  } else {
@@ -586,9 +808,15 @@ class PrefViewer extends HTMLElement {
586
808
  extension = extMatch ? `.${extMatch[1].toLowerCase()}` : ".gltf";
587
809
  const { fileSize, fileTimestamp } = await this.#getServerFileDataHeader(source);
588
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
+ });
589
816
  return false;
590
817
  } else {
591
818
  container.changed = { timestamp: fileTimestamp, size: fileSize };
819
+ this.#logDebug("Remote model marked as changed", { container: container.name, metadata: container.changed });
592
820
  }
593
821
  }
594
822
 
@@ -602,10 +830,12 @@ class PrefViewer extends HTMLElement {
602
830
  },
603
831
  };
604
832
 
833
+ this.#logInfo("Loading asset container", { container: container.name, extension });
605
834
  return LoadAssetContainerAsync(file || source, this.#scene, options);
606
835
  }
607
836
 
608
837
  async #loadContainers(loadModel = true, loadEnvironment = true, loadMaterials = true) {
838
+ this.#logInfo("Starting container load", { loadModel, loadEnvironment, loadMaterials });
609
839
  const promiseArray = [];
610
840
  promiseArray.push(loadModel ? this.#loadAssetContainer(this.#data.containers.model) : false);
611
841
  promiseArray.push(loadEnvironment ? this.#loadAssetContainer(this.#data.containers.environment) : false);
@@ -620,20 +850,27 @@ class PrefViewer extends HTMLElement {
620
850
  if (modelContainer.status === "fulfilled" && modelContainer.value) {
621
851
  this.#replaceContainer(this.#data.containers.model, modelContainer.value);
622
852
  this.#storeChangedFlagsForContainer(this.#data.containers.model);
853
+ this.#logInfo("Model container loaded successfully");
623
854
  } else {
624
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 });
625
857
  }
626
858
 
627
859
  if (environmentContainer.status === "fulfilled" && environmentContainer.value) {
628
860
  this.#replaceContainer(this.#data.containers.environment, environmentContainer.value);
629
861
  this.#storeChangedFlagsForContainer(this.#data.containers.environment);
862
+ this.#logInfo("Environment container loaded successfully");
630
863
  } else {
631
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 });
632
866
  }
633
867
 
634
868
  if (materialsContainer.status === "fulfilled" && materialsContainer.value) {
635
869
  this.#replaceContainer(this.#data.containers.materials, materialsContainer.value);
636
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 });
637
874
  }
638
875
 
639
876
  this.#setOptionsMaterials();
@@ -642,6 +879,7 @@ class PrefViewer extends HTMLElement {
642
879
 
643
880
  this.#resetChangedFlags();
644
881
 
882
+ this.#logInfo("Containers load routine completed");
645
883
  this.dispatchEvent(
646
884
  new CustomEvent("model-loaded", {
647
885
  detail: { success: "" },
@@ -649,9 +887,10 @@ class PrefViewer extends HTMLElement {
649
887
  composed: true,
650
888
  })
651
889
  );
890
+ this.#logDebug("Dispatched model-loaded event");
652
891
  })
653
892
  .catch((error) => {
654
- console.error("PrefViewer: failed to load model", error);
893
+ this.#logError("Failed to load containers", { error });
655
894
  this.dispatchEvent(
656
895
  new CustomEvent("model-error", {
657
896
  detail: { error: error },
@@ -664,8 +903,17 @@ class PrefViewer extends HTMLElement {
664
903
 
665
904
  // Public Methods
666
905
  loadConfig(config) {
667
- 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
+ }
668
915
  if (!config) {
916
+ this.#logWarn("No config provided");
669
917
  return false;
670
918
  }
671
919
 
@@ -682,93 +930,145 @@ class PrefViewer extends HTMLElement {
682
930
  this.#checkMaterialsChanged(config.options);
683
931
  }
684
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
+
685
938
  this.#initialized && this.#loadContainers(true, true, true);
686
939
  }
687
940
 
688
941
  setOptions(options) {
942
+ this.#logInfo("setOptions called", { optionsProvided: !!options });
689
943
  if (!options) {
944
+ this.#logWarn("setOptions called without options");
690
945
  return false;
691
946
  }
692
947
  let someSetted = false;
693
948
  if (this.#checkCameraChanged(options)) {
949
+ this.#logDebug("Camera options changed via setOptions");
694
950
  someSetted = someSetted || this.#setOptionsCamera();
695
951
  }
696
952
  if (this.#checkMaterialsChanged(options)) {
953
+ this.#logDebug("Material options changed via setOptions");
697
954
  someSetted = someSetted || this.#setOptionsMaterials();
698
955
  }
699
956
  this.#resetChangedFlags();
957
+ this.#logDebug("setOptions completed", { appliedAny: someSetted });
700
958
  debugger;
701
959
  return someSetted;
702
960
  }
703
961
 
704
962
  loadModel(model) {
705
- 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
+ }
706
972
  if (!model) {
973
+ this.#logWarn("No model payload provided");
707
974
  return false;
708
975
  }
709
976
  this.#data.containers.model.storage = model.storage || null;
710
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
+ });
711
982
  this.#initialized && this.#loadContainers(true, false, false);
712
983
  }
713
984
 
714
985
  loadScene(scene) {
715
- 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
+ }
716
995
  if (!scene) {
996
+ this.#logWarn("No scene payload provided");
717
997
  return false;
718
998
  }
719
999
  this.#data.containers.environment.storage = scene.storage || null;
720
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
+ });
721
1005
  this.#initialized && this.#loadContainers(false, true, false);
722
1006
  }
723
1007
 
724
1008
  showModel() {
725
1009
  this.#data.containers.model.show = true;
726
1010
  this.#addContainer(this.#data.containers.model);
1011
+ this.#logInfo("Model visibility set to true");
727
1012
  }
728
1013
 
729
1014
  hideModel() {
730
1015
  this.#data.containers.model.show = false;
731
1016
  this.#removeContainer(this.#data.containers.model);
1017
+ this.#logInfo("Model visibility set to false");
732
1018
  }
733
1019
 
734
1020
  showScene() {
735
1021
  this.#data.containers.environment.show = true;
736
1022
  this.#addContainer(this.#data.containers.environment);
737
1023
  this.#setVisibilityOfWallAndFloorInModel();
1024
+ this.#logInfo("Scene visibility set to true");
738
1025
  }
739
1026
 
740
1027
  hideScene() {
741
1028
  this.#data.containers.environment.show = false;
742
1029
  this.#removeContainer(this.#data.containers.environment);
743
1030
  this.#setVisibilityOfWallAndFloorInModel();
1031
+ this.#logInfo("Scene visibility set to false");
744
1032
  }
745
1033
 
746
1034
  downloadModelGLB() {
747
1035
  const fileName = "model";
748
- GLTF2Export.GLBAsync(this.#data.containers.model.assetContainer, fileName, { exportWithoutWaitingForScene: true }).then((glb) => glb.downloadFiles());
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
+ });
749
1041
  }
750
1042
 
751
1043
  downloadModelUSDZ() {
752
1044
  const fileName = "model";
1045
+ this.#logInfo("Initiating USDZ download for model", { fileName });
753
1046
  USDZExportAsync(this.#data.containers.model.assetContainer).then((response) => {
754
1047
  if (response) {
755
1048
  Tools.Download(new Blob([response], { type: "model/vnd.usdz+zip" }), `${fileName}.usdz`);
1049
+ this.#logDebug("Model USDZ export ready", { fileName });
756
1050
  }
757
1051
  });
758
1052
  }
759
1053
 
760
1054
  downloadModelAndSceneUSDZ() {
761
1055
  const fileName = "scene";
1056
+ this.#logInfo("Initiating USDZ download for scene", { fileName });
762
1057
  USDZExportAsync(this.#scene).then((response) => {
763
1058
  if (response) {
764
1059
  Tools.Download(new Blob([response], { type: "model/vnd.usdz+zip" }), `${fileName}.usdz`);
1060
+ this.#logDebug("Scene USDZ export ready", { fileName });
765
1061
  }
766
1062
  });
767
1063
  }
768
1064
 
769
1065
  downloadModelAndSceneGLB() {
770
1066
  const fileName = "scene";
771
- GLTF2Export.GLBAsync(this.#scene, fileName, { exportWithoutWaitingForScene: true }).then((glb) => glb.downloadFiles());
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
+ });
772
1072
  }
773
1073
  }
774
1074