@preference-sl/pref-viewer 2.10.0-beta.20 → 2.10.0-beta.21

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 +252 -82
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preference-sl/pref-viewer",
3
- "version": "2.10.0-beta.20",
3
+ "version": "2.10.0-beta.21",
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,23 @@
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 {
43
+ Engine,
44
+ Scene,
45
+ ArcRotateCamera,
46
+ Vector3,
47
+ Color4,
48
+ HemisphericLight,
49
+ DirectionalLight,
50
+ PointLight,
51
+ ShadowGenerator,
52
+ LoadAssetContainerAsync,
53
+ Tools,
54
+ WebXRSessionManager,
55
+ WebXRDefaultExperience,
56
+ MeshBuilder,
57
+ WebXRFeatureName,
58
+ } from "@babylonjs/core";
43
59
  import "@babylonjs/loaders";
44
60
  import { USDZExportAsync, GLTF2Export } from "@babylonjs/serializers";
45
61
  import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_draco_mesh_compression";
@@ -129,6 +145,38 @@ class PrefViewer extends HTMLElement {
129
145
  #shadowGen = null;
130
146
  #XRExperience = null;
131
147
 
148
+ // -------------------------
149
+ // Logger util + timers
150
+ // -------------------------
151
+ #LOG_LEVEL = (window?.PREFV_LOG_LEVEL ?? "debug"); // 'debug' | 'info' | 'warn' | 'error'
152
+ #timeMarks = new Map();
153
+
154
+ #log(level, msg, extra) {
155
+ const order = { debug: 0, info: 1, warn: 2, error: 3 };
156
+ const cur = order[this.#LOG_LEVEL] ?? 0;
157
+ const now = new Date().toISOString();
158
+ if ((order[level] ?? 0) < cur) return;
159
+ const line = `[PrefViewer][${level.toUpperCase()}][${now}] ${msg}`;
160
+ try {
161
+ if (extra !== undefined) console[level](line, extra);
162
+ else console[level](line);
163
+ } catch {
164
+ console.log(line, extra);
165
+ }
166
+ }
167
+ #timeStart(label) {
168
+ this.#timeMarks.set(label, performance.now());
169
+ this.#log("debug", `⏱️ start ${label}`);
170
+ }
171
+ #timeEnd(label) {
172
+ const t0 = this.#timeMarks.get(label);
173
+ if (t0 !== undefined) {
174
+ const dt = (performance.now() - t0).toFixed(1);
175
+ this.#log("debug", `⏱️ end ${label} +${dt}ms`);
176
+ this.#timeMarks.delete(label);
177
+ }
178
+ }
179
+
132
180
  constructor() {
133
181
  super();
134
182
  this.attachShadow({ mode: "open" });
@@ -151,6 +199,7 @@ class PrefViewer extends HTMLElement {
151
199
  }
152
200
 
153
201
  attributeChangedCallback(name, _old, value) {
202
+ this.#log("debug", `attributeChangedCallback: ${name}`, value);
154
203
  let data = null;
155
204
  switch (name) {
156
205
  case "config":
@@ -182,8 +231,10 @@ class PrefViewer extends HTMLElement {
182
231
  }
183
232
 
184
233
  connectedCallback() {
234
+ this.#log("info", "connectedCallback");
185
235
  if (!this.hasAttribute("config")) {
186
236
  const error = 'PrefViewer: provide "models" as array of model and environment';
237
+ this.#log("error", error);
187
238
  console.error(error);
188
239
  this.dispatchEvent(
189
240
  new CustomEvent("scene-error", {
@@ -202,6 +253,7 @@ class PrefViewer extends HTMLElement {
202
253
  }
203
254
 
204
255
  disconnectedCallback() {
256
+ this.#log("info", "disconnectedCallback → dispose engine");
205
257
  this.#disposeEngine();
206
258
  this.#canvasResizeObserver.disconnect();
207
259
  }
@@ -235,6 +287,7 @@ class PrefViewer extends HTMLElement {
235
287
  this.removeAttribute("loaded");
236
288
  }
237
289
  this.setAttribute("loading", "");
290
+ this.#log("info", "Escena → loading");
238
291
  this.dispatchEvent(
239
292
  new CustomEvent("scene-loading", {
240
293
  bubbles: true,
@@ -278,6 +331,7 @@ class PrefViewer extends HTMLElement {
278
331
  this.removeAttribute("loading");
279
332
  }
280
333
  this.setAttribute("loaded", "");
334
+ this.#log("info", "Escena → loaded", detail);
281
335
  this.dispatchEvent(
282
336
  new CustomEvent("scene-loaded", {
283
337
  bubbles: true,
@@ -289,6 +343,7 @@ class PrefViewer extends HTMLElement {
289
343
  }
290
344
 
291
345
  #setStatusOptionsLoading() {
346
+ this.#log("info", "Opciones → loading");
292
347
  this.dispatchEvent(
293
348
  new CustomEvent("options-loading", {
294
349
  bubbles: true,
@@ -317,6 +372,7 @@ class PrefViewer extends HTMLElement {
317
372
  success: loadedDetail,
318
373
  };
319
374
 
375
+ this.#log("info", "Opciones → loaded", detail);
320
376
  this.dispatchEvent(
321
377
  new CustomEvent("options-loaded", {
322
378
  bubbles: true,
@@ -332,26 +388,31 @@ class PrefViewer extends HTMLElement {
332
388
  if (!options || !options.camera) {
333
389
  return false;
334
390
  }
335
-
336
391
  const changed = options.camera !== this.#data.options.camera.value;
337
- this.#data.options.camera.changed = changed ? { oldValue: this.#data.options.camera.value, oldLocked: this.#data.options.camera.locked, success: false } : false;
392
+ this.#data.options.camera.changed = changed
393
+ ? { oldValue: this.#data.options.camera.value, oldLocked: this.#data.options.camera.locked, success: false }
394
+ : false;
338
395
  if (changed) this.#data.options.camera.value = options.camera;
339
-
396
+ this.#log("debug", `#checkCameraChanged → ${changed}`, {
397
+ new: this.#data.options.camera.value,
398
+ old: this.#data.options.camera.changed?.oldValue ?? null,
399
+ });
340
400
  return changed;
341
401
  }
342
402
 
343
403
  #checkMaterialsChanged(options) {
344
- if (!options) {
345
- return false;
346
- }
404
+ if (!options) return false;
347
405
  let someChanged = false;
348
406
  Object.keys(this.#data.options.materials).forEach((material) => {
349
407
  const key = `${material}Material`;
350
408
  const materialChanged = options[key] && options[key] !== this.#data.options.materials[material].value ? true : false;
351
- this.#data.options.materials[material].changed = materialChanged ? { oldValue: this.#data.options.materials[material].value, success: false } : false;
409
+ this.#data.options.materials[material].changed = materialChanged
410
+ ? { oldValue: this.#data.options.materials[material].value, success: false }
411
+ : false;
352
412
  this.#data.options.materials[material].value = materialChanged ? options[key] : this.#data.options.materials[material].value;
353
413
  someChanged = someChanged || this.#data.options.materials[material].changed;
354
414
  });
415
+ this.#log("debug", `#checkMaterialsChanged → ${!!someChanged}`);
355
416
  return someChanged;
356
417
  }
357
418
 
@@ -369,41 +430,47 @@ class PrefViewer extends HTMLElement {
369
430
 
370
431
  // Babylon.js
371
432
  async #initializeBabylon() {
433
+ this.#timeStart("initializeBabylon");
372
434
  this.#engine = new Engine(this.#canvas, true, { alpha: true });
373
- this.#engine.disableUniformBuffers = true; // <- evita el límite de GL_MAX_*_UNIFORM_BUFFERS // PROVISIONAL, YA QUE ESTO ES UN POCO OVERKILL
374
-
435
+ this.#engine.disableUniformBuffers = true; // evita GL_MAX_*_UNIFORM_BUFFERS (workaround)
375
436
  this.#scene = new Scene(this.#engine);
376
437
  this.#scene.clearColor = new Color4(1, 1, 1, 1);
377
438
  this.#createCamera();
378
- this.#createLights();
439
+ this.#createLights(); // luces desactivadas
379
440
  this.#setupInteraction();
380
441
 
442
+ this.#log("info", "Engine y Scene creados", {
443
+ webgl: this.#engine.webGLVersion,
444
+ disableUBO: this.#engine.disableUniformBuffers,
445
+ });
446
+
381
447
  this.#engine.runRenderLoop(() => this.#scene && this.#scene.render());
382
448
  this.#canvasResizeObserver.observe(this.#canvas);
383
449
 
384
450
  await this.#createXRExperience();
451
+ this.#timeEnd("initializeBabylon");
385
452
  }
386
453
 
387
454
  addStylesToARButton() {
388
- 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"}';
455
+ const css =
456
+ '.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"}';
389
457
  const style = document.createElement("style");
390
458
  style.appendChild(document.createTextNode(css));
391
459
  this.#wrapper.appendChild(style);
392
460
  }
393
461
 
394
462
  async #createXRExperience() {
395
- if (this.#XRExperience) {
396
- return true;
397
- }
463
+ if (this.#XRExperience) return true;
398
464
 
399
465
  const sessionMode = "immersive-ar";
400
466
  const sessionSupported = await WebXRSessionManager.IsSessionSupportedAsync(sessionMode);
401
467
  if (!sessionSupported) {
402
- console.info("PrefViewer: WebXR in mode AR is not supported");
468
+ this.#log("info", `WebXR no soportado para ${sessionMode}`);
403
469
  return false;
404
470
  }
405
471
 
406
472
  try {
473
+ this.#log("debug", "Creando XR DefaultExperience…");
407
474
  const ground = MeshBuilder.CreateGround("ground", { width: 1000, height: 1000 }, this.#scene);
408
475
  ground.isVisible = false;
409
476
 
@@ -418,6 +485,7 @@ class PrefViewer extends HTMLElement {
418
485
  };
419
486
 
420
487
  this.#XRExperience = await WebXRDefaultExperience.CreateAsync(this.#scene, options);
488
+ this.#log("info", "XR DefaultExperience creado");
421
489
 
422
490
  const featuresManager = this.#XRExperience.baseExperience.featuresManager;
423
491
  featuresManager.enableFeature(WebXRFeatureName.TELEPORTATION, "stable", {
@@ -428,14 +496,19 @@ class PrefViewer extends HTMLElement {
428
496
 
429
497
  this.#XRExperience.baseExperience.sessionManager.onXRReady.add(() => {
430
498
  // Set the initial position of xrCamera: use nonVRCamera, which contains a copy of the original this.#scene.activeCamera before entering XR
431
- this.#XRExperience.baseExperience.camera.setTransformationFromNonVRCamera(this.#XRExperience.baseExperience._nonVRCamera);
499
+ this.#log("debug", "XR Ready → copiando pose inicial a xrCamera");
500
+ this.#XRExperience.baseExperience.camera.setTransformationFromNonVRCamera(
501
+ this.#XRExperience.baseExperience._nonVRCamera
502
+ );
432
503
  this.#XRExperience.baseExperience.camera.setTarget(Vector3.Zero());
433
- this.#XRExperience.baseExperience.onInitialXRPoseSetObservable.notifyObservers(this.#XRExperience.baseExperience.camera);
504
+ this.#XRExperience.baseExperience.onInitialXRPoseSetObservable.notifyObservers(
505
+ this.#XRExperience.baseExperience.camera
506
+ );
434
507
  });
435
508
 
436
509
  this.addStylesToARButton();
437
510
  } catch (error) {
438
- console.warn("PrefViewer: failed to create WebXR experience", error);
511
+ this.#log("warn", "Falló la creación de la experiencia WebXR", error);
439
512
  this.#XRExperience = null;
440
513
  }
441
514
  }
@@ -448,42 +521,30 @@ class PrefViewer extends HTMLElement {
448
521
  this.#camera.lowerBetaLimit = Math.PI * 0.25;
449
522
  this.#camera.lowerRadiusLimit = 5;
450
523
  this.#camera.upperRadiusLimit = 20;
451
- this.#camera.metadata = { locked: false }
524
+ this.#camera.metadata = { locked: false };
452
525
  this.#camera.attachControl(this.#canvas, true);
453
526
  this.#scene.activeCamera = this.#camera;
527
+ this.#log("debug", "Cámara creada y asignada como activa", {
528
+ name: this.#camera.name,
529
+ locked: this.#camera.metadata.locked,
530
+ });
454
531
  }
455
532
 
533
+ // [LIGHTS OFF] — sin luces del componente
456
534
  #createLights() {
457
- // 1) Stronger ambient fill
458
- this.#hemiLight = new HemisphericLight("hemiLight", new Vector3(-10, 10, -10), this.#scene);
459
- this.#hemiLight.intensity = 0.6;
460
-
461
- // 2) Directional light from the front-right, angled slightly down
462
- this.#dirLight = new DirectionalLight("dirLight", new Vector3(-10, 10, -10), this.#scene);
463
- this.#dirLight.position = new Vector3(5, 4, 5); // light is IN FRONT + ABOVE + to the RIGHT
464
- this.#dirLight.intensity = 0.6;
465
-
466
- // 3) Soft shadows
467
- this.#shadowGen = new ShadowGenerator(1024, this.#dirLight);
468
- this.#shadowGen.useBlurExponentialShadowMap = true;
469
- this.#shadowGen.blurKernel = 16;
470
- this.#shadowGen.darkness = 0.5;
471
-
472
- // 4) Camera‐attached headlight
473
- this.#cameraLight = new PointLight("pl", this.#camera.position, this.#scene);
474
- this.#cameraLight.parent = this.#camera;
475
- this.#cameraLight.intensity = 0.3;
535
+ this.#hemiLight = null;
536
+ this.#dirLight = null;
537
+ this.#cameraLight = null;
538
+ this.#shadowGen = null;
539
+ this.#log("info", "Luces internas desactivadas (#createLights). No se crean sombras.");
476
540
  }
477
541
 
478
542
  #setupInteraction() {
479
543
  this.#canvas.addEventListener("wheel", (event) => {
480
- if (!this.#scene || !this.#camera) {
481
- return false;
482
- }
483
- //const pick = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
484
- //this.#camera.target = pick.hit ? pick.pickedPoint.clone() : this.#camera.target;
544
+ if (!this.#scene || !this.#camera) return false;
485
545
  if (!this.#scene.activeCamera.metadata?.locked) {
486
- this.#scene.activeCamera.inertialRadiusOffset -= event.deltaY * this.#scene.activeCamera.wheelPrecision * 0.001;
546
+ this.#scene.activeCamera.inertialRadiusOffset -=
547
+ event.deltaY * this.#scene.activeCamera.wheelPrecision * 0.001;
487
548
  }
488
549
  event.preventDefault();
489
550
  });
@@ -492,15 +553,17 @@ class PrefViewer extends HTMLElement {
492
553
  #disposeEngine() {
493
554
  if (!this.#engine) return;
494
555
  this.#shadowGen?.dispose();
495
- this.#scene?.lights?.slice().forEach(l => l.dispose());
556
+ this.#scene?.lights?.slice().forEach((l) => l.dispose());
496
557
  this.#engine.dispose();
497
558
  this.#engine = this.#scene = this.#camera = null;
498
559
  this.#hemiLight = this.#dirLight = this.#cameraLight = null;
499
560
  this.#shadowGen = null;
561
+ this.#log("info", "Engine y recursos Babylon eliminados");
500
562
  }
501
563
 
502
564
  // Utility methods for loading gltf/glb
503
565
  async #getServerFileDataHeader(uri) {
566
+ this.#log("debug", `HEAD ${uri}`);
504
567
  return new Promise((resolve) => {
505
568
  const xhr = new XMLHttpRequest();
506
569
  xhr.open("HEAD", uri, true);
@@ -509,12 +572,15 @@ class PrefViewer extends HTMLElement {
509
572
  if (xhr.status === 200) {
510
573
  const size = parseInt(xhr.getResponseHeader("Content-Length"));
511
574
  const timeStamp = new Date(xhr.getResponseHeader("Last-Modified")).toISOString();
575
+ this.#log("debug", "HEAD ok", { size, timeStamp });
512
576
  resolve([size, timeStamp]);
513
577
  } else {
578
+ this.#log("warn", "HEAD non-200", { status: xhr.status });
514
579
  resolve([0, null]);
515
580
  }
516
581
  };
517
582
  xhr.onerror = () => {
583
+ this.#log("warn", "HEAD error");
518
584
  resolve([0, null]);
519
585
  };
520
586
  xhr.send();
@@ -528,6 +594,7 @@ class PrefViewer extends HTMLElement {
528
594
  }
529
595
 
530
596
  #decodeBase64(base64) {
597
+ this.#log("debug", "Decodificando Base64…");
531
598
  const [, payload] = base64.split(",");
532
599
  const raw = payload || base64;
533
600
  let decoded = "";
@@ -537,17 +604,19 @@ class PrefViewer extends HTMLElement {
537
604
  try {
538
605
  decoded = atob(raw);
539
606
  } catch {
607
+ this.#log("warn", "Base64 inválido (atob falló)");
540
608
  return { blob, extension, size };
541
609
  }
542
610
  let isJson = false;
543
611
  try {
544
612
  JSON.parse(decoded);
545
613
  isJson = true;
546
- } catch { }
614
+ } catch {}
547
615
  extension = isJson ? ".gltf" : ".glb";
548
616
  const type = isJson ? "model/gltf+json" : "model/gltf-binary";
549
617
  const array = Uint8Array.from(decoded, (c) => c.charCodeAt(0));
550
618
  blob = new Blob([array], { type });
619
+ this.#log("debug", "Base64 decodificado", { extension, size });
551
620
  return { blob, extension, size };
552
621
  }
553
622
 
@@ -556,6 +625,7 @@ class PrefViewer extends HTMLElement {
556
625
  return true;
557
626
  }
558
627
  await initDb(db, table);
628
+ this.#log("debug", "IndexedDB inicializado/abierto", { db, table });
559
629
  }
560
630
 
561
631
  // Methods for managing Asset Containers
@@ -565,7 +635,11 @@ class PrefViewer extends HTMLElement {
565
635
  }
566
636
  show = show !== undefined ? show : this.#data.containers.environment.visible;
567
637
  const prefixes = Object.values(this.#data.options.materials).map((material) => material.prefix);
568
- this.#data.containers.model.assetContainer.meshes.filter((meshToFilter) => prefixes.some((prefix) => meshToFilter.name.startsWith(prefix))).forEach((mesh) => mesh.setEnabled(show));
638
+ const meshes = this.#data.containers.model.assetContainer.meshes.filter((meshToFilter) =>
639
+ prefixes.some((prefix) => meshToFilter.name.startsWith(prefix))
640
+ );
641
+ meshes.forEach((mesh) => mesh.setEnabled(show));
642
+ this.#log("debug", "setVisibilityOfWallAndFloorInModel", { show, count: meshes.length });
569
643
  }
570
644
 
571
645
  #setOptionsMaterial(optionMaterial) {
@@ -573,16 +647,27 @@ class PrefViewer extends HTMLElement {
573
647
  return false;
574
648
  }
575
649
 
576
- const material = this.#data.containers.materials.assetContainer?.materials.find((mat) => mat.name === optionMaterial.value) || null;
650
+ const material =
651
+ this.#data.containers.materials.assetContainer?.materials.find((mat) => mat.name === optionMaterial.value) ||
652
+ null;
577
653
  if (!material) {
654
+ this.#log("warn", "Material no encontrado en contenedor 'materials'", { name: optionMaterial.value });
578
655
  return false;
579
656
  }
580
657
 
581
658
  const containers = [];
582
- if (this.#data.containers.model.assetContainer && (this.#data.containers.model.changed || this.#data.containers.materials.changed || optionMaterial.changed)) {
659
+ if (
660
+ this.#data.containers.model.assetContainer &&
661
+ (this.#data.containers.model.changed || this.#data.containers.materials.changed || optionMaterial.changed)
662
+ ) {
583
663
  containers.push(this.#data.containers.model.assetContainer);
584
664
  }
585
- if (this.#data.containers.environment.assetContainer && (this.#data.containers.environment.changed || this.#data.containers.materials.changed || optionMaterial.changed)) {
665
+ if (
666
+ this.#data.containers.environment.assetContainer &&
667
+ (this.#data.containers.environment.changed ||
668
+ this.#data.containers.materials.changed ||
669
+ optionMaterial.changed)
670
+ ) {
586
671
  containers.push(this.#data.containers.environment.assetContainer);
587
672
  }
588
673
  if (containers.length === 0) {
@@ -600,9 +685,10 @@ class PrefViewer extends HTMLElement {
600
685
  );
601
686
 
602
687
  if (someSetted) {
603
- optionMaterial.changed.success = true;
688
+ optionMaterial.changed && (optionMaterial.changed.success = true);
689
+ this.#log("debug", "Material aplicado", { prefix: optionMaterial.prefix, name: optionMaterial.value });
604
690
  } else {
605
- optionMaterial.value = optionMaterial.changed.oldValue;
691
+ optionMaterial.value = optionMaterial.changed?.oldValue ?? optionMaterial.value;
606
692
  }
607
693
 
608
694
  return someSetted;
@@ -618,14 +704,34 @@ class PrefViewer extends HTMLElement {
618
704
  }
619
705
 
620
706
  #setOptionsCamera() {
621
- if (!this.#data.options.camera.value || (!this.#data.options.camera.changed && !this.#data.containers.model.changed && !this.#data.containers.environment.changed)) {
707
+ if (
708
+ !this.#data.options.camera.value &&
709
+ !this.#data.options.camera.changed &&
710
+ !this.#data.containers.model.changed &&
711
+ !this.#data.containers.environment.changed
712
+ ) {
622
713
  return false;
623
714
  }
624
715
 
625
- 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;
716
+ let camera =
717
+ this.#data.containers.model.assetContainer?.cameras.find(
718
+ (thisCamera) => thisCamera.name === this.#data.options.camera.value
719
+ ) ||
720
+ this.#data.containers.environment.assetContainer?.cameras.find(
721
+ (thisCamera) => thisCamera.name === this.#data.options.camera.value
722
+ ) ||
723
+ null;
724
+
626
725
  if (!camera) {
627
726
  if (this.#data.options.camera.changed?.oldValue && this.#data.options.camera.changed?.oldValue !== this.#data.options.camera.value) {
628
- camera = this.#data.containers.model.assetContainer?.cameras.find((thisCamera) => thisCamera.name === this.#data.options.camera.changed.oldValue) || this.#data.containers.environment.assetContainer?.cameras.find((thisCamera) => thisCamera.name === this.#data.options.camera.changed.oldValue) || null;
727
+ camera =
728
+ this.#data.containers.model.assetContainer?.cameras.find(
729
+ (thisCamera) => thisCamera.name === this.#data.options.camera.changed.oldValue
730
+ ) ||
731
+ this.#data.containers.environment.assetContainer?.cameras.find(
732
+ (thisCamera) => thisCamera.name === this.#data.options.camera.changed.oldValue
733
+ ) ||
734
+ null;
629
735
  }
630
736
  if (camera) {
631
737
  camera.metadata = { locked: this.#data.options.camera.changed.oldLocked };
@@ -636,14 +742,19 @@ class PrefViewer extends HTMLElement {
636
742
  this.#data.options.camera.value = null;
637
743
  this.#data.options.camera.locked = this.#camera.metadata.locked;
638
744
  }
639
- this.#data.options.camera.changed.success = false;
745
+ this.#data.options.camera.changed && (this.#data.options.camera.changed.success = false);
640
746
  } else {
641
747
  camera.metadata = { locked: this.#data.options.camera.locked };
642
748
  }
749
+
643
750
  if (!this.#data.options.camera.locked && this.#data.options.camera.value !== null) {
644
751
  camera.attachControl(this.#canvas, true);
645
752
  }
646
753
  this.#scene.activeCamera = camera;
754
+ this.#log("debug", "Cámara actualizada", {
755
+ active: this.#scene?.activeCamera?.name ?? null,
756
+ locked: this.#scene?.activeCamera?.metadata?.locked ?? null,
757
+ });
647
758
  return true;
648
759
  }
649
760
 
@@ -651,6 +762,7 @@ class PrefViewer extends HTMLElement {
651
762
  if (container.assetContainer && !container.visible && container.show) {
652
763
  container.assetContainer.addAllToScene();
653
764
  container.visible = true;
765
+ this.#log("debug", `Añadido a escena: ${container.name}`);
654
766
  }
655
767
  }
656
768
 
@@ -658,6 +770,7 @@ class PrefViewer extends HTMLElement {
658
770
  if (container.assetContainer && container.visible) {
659
771
  container.assetContainer.removeAllFromScene();
660
772
  container.visible = false;
773
+ this.#log("debug", `Eliminado de escena: ${container.name}`);
661
774
  }
662
775
  }
663
776
 
@@ -665,24 +778,26 @@ class PrefViewer extends HTMLElement {
665
778
  // 1) quita y destruye el anterior si existía
666
779
  const old = container.assetContainer;
667
780
  if (old) {
668
- if (container.visible) { old.removeAllFromScene(); }
669
- old.dispose(); // <- importante
781
+ if (container.visible) {
782
+ old.removeAllFromScene();
783
+ }
784
+ old.dispose();
670
785
  }
671
786
 
672
787
  // 2) asigna el nuevo y prepara
673
788
  container.assetContainer = newAssetContainer;
674
789
 
675
- // Opcional: limitar luces por material para ganar margen
676
- container.assetContainer.materials?.forEach(m => {
790
+ // Limitar luces por material (no usamos luces, poner 0 asegura shaders más simples)
791
+ container.assetContainer.materials?.forEach((m) => {
677
792
  if ("maxSimultaneousLights" in m) {
678
- m.maxSimultaneousLights = 2; // 2–3 suele ir bien
793
+ m.maxSimultaneousLights = 0; // [LIGHTS OFF]
679
794
  }
680
795
  });
681
796
 
682
- // 3) sombras solo para los meshes que te interesen (mejor que todos)
683
- container.assetContainer.meshes.forEach(mesh => {
684
- mesh.receiveShadows = true;
685
- this.#shadowGen.addShadowCaster(mesh, true);
797
+ // 3) sombras sólo si existe generador (con luces OFF es null)
798
+ container.assetContainer.meshes.forEach((mesh) => {
799
+ mesh.receiveShadows = !!this.#shadowGen;
800
+ if (this.#shadowGen) this.#shadowGen.addShadowCaster(mesh, true);
686
801
  });
687
802
 
688
803
  // 4) añade a escena
@@ -690,29 +805,46 @@ class PrefViewer extends HTMLElement {
690
805
 
691
806
  // 5) fuerza recompilación con defines correctos del nuevo estado
692
807
  this.#scene.getEngine().releaseEffects();
808
+
809
+ this.#log("debug", `Container reemplazado: ${container.name}`, {
810
+ meshes: container.assetContainer.meshes?.length ?? 0,
811
+ materials: container.assetContainer.materials?.length ?? 0,
812
+ });
693
813
  }
694
814
 
695
815
  async #loadAssetContainer(container) {
696
- let storage = container?.storage;
816
+ const storage = container?.storage;
697
817
 
698
818
  if (!storage) {
819
+ this.#log("debug", `Sin storage para "${container?.name}"`);
699
820
  return false;
700
821
  }
701
822
 
823
+ this.#timeStart(`load:${container.name}`);
702
824
  let source = storage.url || null;
703
825
 
704
826
  if (storage.db && storage.table && storage.id) {
827
+ this.#log("info", `Cargando ${container.name} desde IndexedDB`, {
828
+ db: storage.db,
829
+ table: storage.table,
830
+ id: storage.id,
831
+ });
705
832
  await this.#initStorage(storage.db, storage.table);
706
833
  const object = await loadModel(storage.id, storage.table);
707
834
  source = object.data;
708
835
  if (object.timeStamp === container.timeStamp) {
836
+ this.#log("debug", `${container.name}: sin cambios en IndexedDB`);
837
+ this.#timeEnd(`load:${container.name}`);
709
838
  return false;
710
839
  } else {
711
840
  container.changed = { timeStamp: object.timeStamp, size: object.size, success: false };
841
+ this.#log("debug", `${container.name}: cambios detectados`, container.changed);
712
842
  }
713
843
  }
714
844
 
715
845
  if (!source) {
846
+ this.#log("warn", `${container.name}: no hay source`);
847
+ this.#timeEnd(`load:${container.name}`);
716
848
  return false;
717
849
  }
718
850
 
@@ -725,6 +857,8 @@ class PrefViewer extends HTMLElement {
725
857
  });
726
858
  if (!container.changed) {
727
859
  if (container.timeStamp === null && container.size === size) {
860
+ this.#log("debug", `${container.name}: Base64 sin cambios`);
861
+ this.#timeEnd(`load:${container.name}`);
728
862
  return false;
729
863
  } else {
730
864
  container.changed = { timeStamp: null, size: size, success: false };
@@ -735,6 +869,8 @@ class PrefViewer extends HTMLElement {
735
869
  extension = extMatch ? `.${extMatch[1].toLowerCase()}` : ".gltf";
736
870
  const [fileSize, fileTimeStamp] = await this.#getServerFileDataHeader(source);
737
871
  if (container.size === fileSize && container.timeStamp === fileTimeStamp) {
872
+ this.#log("debug", `${container.name}: URL sin cambios`);
873
+ this.#timeEnd(`load:${container.name}`);
738
874
  return false;
739
875
  } else {
740
876
  container.changed = { timeStamp: fileTimeStamp, size: fileSize, success: false };
@@ -750,11 +886,21 @@ class PrefViewer extends HTMLElement {
750
886
  },
751
887
  },
752
888
  };
889
+ this.#log("info", `LoadAssetContainerAsync ${container.name}`, { extension, changed: container.changed });
753
890
 
754
- return LoadAssetContainerAsync(file || source, this.#scene, options);
891
+ try {
892
+ const result = await LoadAssetContainerAsync(file || source, this.#scene, options);
893
+ this.#timeEnd(`load:${container.name}`);
894
+ return result;
895
+ } catch (e) {
896
+ this.#timeEnd(`load:${container.name}`);
897
+ this.#log("error", `LoadAssetContainerAsync falló para ${container.name}`, e);
898
+ throw e;
899
+ }
755
900
  }
756
901
 
757
902
  async #loadContainers(loadModel = true, loadEnvironment = true, loadMaterials = true) {
903
+ this.#log("info", "loadContainers()", { loadModel, loadEnvironment, loadMaterials });
758
904
  const promiseArray = [];
759
905
  promiseArray.push(loadModel ? this.#loadAssetContainer(this.#data.containers.model) : false);
760
906
  promiseArray.push(loadEnvironment ? this.#loadAssetContainer(this.#data.containers.environment) : false);
@@ -768,12 +914,20 @@ class PrefViewer extends HTMLElement {
768
914
  const environmentContainer = values[1];
769
915
  const materialsContainer = values[2];
770
916
 
917
+ this.#log(
918
+ "debug",
919
+ "Resultados Promise.allSettled",
920
+ values.map((v) => ({ status: v.status, hasValue: !!v.value }))
921
+ );
922
+
771
923
  if (modelContainer.status === "fulfilled" && modelContainer.value) {
772
924
  this.#stripImportedLights(modelContainer.value);
773
925
  this.#replaceContainer(this.#data.containers.model, modelContainer.value);
774
926
  this.#storeChangedFlagsForContainer(this.#data.containers.model);
775
927
  } else {
776
- this.#data.containers.model.show ? this.#addContainer(this.#data.containers.model) : this.#removeContainer(this.#data.containers.model);
928
+ this.#data.containers.model.show
929
+ ? this.#addContainer(this.#data.containers.model)
930
+ : this.#removeContainer(this.#data.containers.model);
777
931
  }
778
932
 
779
933
  if (environmentContainer.status === "fulfilled" && environmentContainer.value) {
@@ -781,7 +935,9 @@ class PrefViewer extends HTMLElement {
781
935
  this.#replaceContainer(this.#data.containers.environment, environmentContainer.value);
782
936
  this.#storeChangedFlagsForContainer(this.#data.containers.environment);
783
937
  } else {
784
- this.#data.containers.environment.show ? this.#addContainer(this.#data.containers.environment) : this.#removeContainer(this.#data.containers.environment);
938
+ this.#data.containers.environment.show
939
+ ? this.#addContainer(this.#data.containers.environment)
940
+ : this.#removeContainer(this.#data.containers.environment);
785
941
  }
786
942
 
787
943
  if (materialsContainer.status === "fulfilled" && materialsContainer.value) {
@@ -795,10 +951,11 @@ class PrefViewer extends HTMLElement {
795
951
  this.#setVisibilityOfWallAndFloorInModel();
796
952
  this.#setStatusSceneLoaded();
797
953
  this.#resetChangedFlags();
954
+ this.#log("info", "Escena cargada");
798
955
  })
799
956
  .catch((error) => {
800
957
  this.loaded = true;
801
- console.error("PrefViewer: failed to load model", error);
958
+ this.#log("error", "Failed to load containers", error);
802
959
  this.dispatchEvent(
803
960
  new CustomEvent("scene-error", {
804
961
  bubbles: true,
@@ -811,25 +968,27 @@ class PrefViewer extends HTMLElement {
811
968
  }
812
969
 
813
970
  #stripImportedLights(container) {
814
- // El glTF puede traer KHR_lights_punctual: bórralas antes de añadir a la escena
815
- if (container?.lights?.length) {
816
- // Clonar para no mutar mientras iteras
817
- container.lights.slice().forEach(l => l.dispose());
818
- }
971
+ const n = container?.lights?.length ?? 0;
972
+ if (n) container.lights.slice().forEach((l) => l.dispose());
973
+ this.#log("debug", `stripImportedLights(): ${n} 0`);
819
974
  }
820
975
 
821
976
  // Public Methods
822
977
  loadConfig(config) {
978
+ this.#log("info", "loadConfig()", typeof config === "string" ? "[string]" : config);
823
979
  config = typeof config === "string" ? JSON.parse(config) : config;
824
980
  if (!config) {
981
+ this.#log("warn", "loadConfig() → config vacío/nulo");
825
982
  return false;
826
983
  }
827
984
 
828
985
  // Containers
829
986
  this.#data.containers.model.storage = config.model?.storage || null;
830
- this.#data.containers.model.show = config.model?.visible !== undefined ? config.model.visible : this.#data.containers.model.show;
987
+ this.#data.containers.model.show =
988
+ config.model?.visible !== undefined ? config.model.visible : this.#data.containers.model.show;
831
989
  this.#data.containers.environment.storage = config.scene?.storage || null;
832
- this.#data.containers.environment.show = config.scene?.visible !== undefined ? config.scene.visible : this.#data.containers.environment.show;
990
+ this.#data.containers.environment.show =
991
+ config.scene?.visible !== undefined ? config.scene.visible : this.#data.containers.environment.show;
833
992
  this.#data.containers.materials.storage = config.materials?.storage || null;
834
993
 
835
994
  // Options
@@ -842,6 +1001,7 @@ class PrefViewer extends HTMLElement {
842
1001
  }
843
1002
 
844
1003
  setOptions(options) {
1004
+ this.#log("info", "setOptions()", options);
845
1005
  if (!options) {
846
1006
  return false;
847
1007
  }
@@ -863,22 +1023,28 @@ class PrefViewer extends HTMLElement {
863
1023
  }
864
1024
 
865
1025
  loadModel(model) {
1026
+ this.#log("info", "loadModel()", typeof model === "string" ? "[string]" : model);
866
1027
  model = typeof model === "string" ? JSON.parse(model) : model;
867
1028
  if (!model) {
1029
+ this.#log("warn", "loadModel() → model vacío/nulo");
868
1030
  return false;
869
1031
  }
870
1032
  this.#data.containers.model.storage = model.storage || null;
871
- this.#data.containers.model.show = model.visible !== undefined ? model.visible : this.#data.containers.model.show;
1033
+ this.#data.containers.model.show =
1034
+ model.visible !== undefined ? model.visible : this.#data.containers.model.show;
872
1035
  this.initialized && this.#loadContainers(true, false, false);
873
1036
  }
874
1037
 
875
1038
  loadScene(scene) {
1039
+ this.#log("info", "loadScene()", typeof scene === "string" ? "[string]" : scene);
876
1040
  scene = typeof scene === "string" ? JSON.parse(scene) : scene;
877
1041
  if (!scene) {
1042
+ this.#log("warn", "loadScene() → scene vacío/nulo");
878
1043
  return false;
879
1044
  }
880
1045
  this.#data.containers.environment.storage = scene.storage || null;
881
- this.#data.containers.environment.show = scene.visible !== undefined ? scene.visible : this.#data.containers.environment.show;
1046
+ this.#data.containers.environment.show =
1047
+ scene.visible !== undefined ? scene.visible : this.#data.containers.environment.show;
882
1048
  this.initialized && this.#loadContainers(false, true, false);
883
1049
  }
884
1050
 
@@ -906,7 +1072,9 @@ class PrefViewer extends HTMLElement {
906
1072
 
907
1073
  downloadModelGLB() {
908
1074
  const fileName = "model";
909
- GLTF2Export.GLBAsync(this.#data.containers.model.assetContainer, fileName, { exportWithoutWaitingForScene: true }).then((glb) => glb.downloadFiles());
1075
+ GLTF2Export.GLBAsync(this.#data.containers.model.assetContainer, fileName, {
1076
+ exportWithoutWaitingForScene: true,
1077
+ }).then((glb) => glb.downloadFiles());
910
1078
  }
911
1079
 
912
1080
  downloadModelUSDZ() {
@@ -929,7 +1097,9 @@ class PrefViewer extends HTMLElement {
929
1097
 
930
1098
  downloadModelAndSceneGLB() {
931
1099
  const fileName = "scene";
932
- GLTF2Export.GLBAsync(this.#scene, fileName, { exportWithoutWaitingForScene: true }).then((glb) => glb.downloadFiles());
1100
+ GLTF2Export.GLBAsync(this.#scene, fileName, { exportWithoutWaitingForScene: true }).then((glb) =>
1101
+ glb.downloadFiles()
1102
+ );
933
1103
  }
934
1104
  }
935
1105