@preference-sl/pref-viewer 2.10.0-beta.2 → 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.
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 } 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";
@@ -47,22 +63,76 @@ import { DracoCompression } from "@babylonjs/core/Meshes/Compression/dracoCompre
47
63
  import { initDb, loadModel } from "./gltf-storage.js";
48
64
 
49
65
  class PrefViewer extends HTMLElement {
50
- #initialized = false;
66
+ initialized = false;
67
+ loaded = false;
68
+ loading = false;
51
69
 
52
- #model = {
53
- container: null,
54
- show: true, // Show model by default
55
- storage: null,
56
- visible: false,
57
- };
58
-
59
- #environment = {
60
- container: null,
61
- show: true, // Show environment by default
62
- storage: null,
63
- visible: false,
70
+ #data = {
71
+ containers: {
72
+ model: {
73
+ name: "model",
74
+ container: null,
75
+ show: true,
76
+ storage: null,
77
+ visible: false,
78
+ size: null,
79
+ timeStamp: null,
80
+ changed: false,
81
+ },
82
+ environment: {
83
+ name: "environment",
84
+ container: null,
85
+ show: true,
86
+ storage: null,
87
+ visible: false,
88
+ size: null,
89
+ timeStamp: null,
90
+ changed: false,
91
+ },
92
+ materials: {
93
+ name: "materials",
94
+ container: null,
95
+ storage: null,
96
+ show: true,
97
+ visible: false,
98
+ size: null,
99
+ timeStamp: null,
100
+ changed: false,
101
+ },
102
+ },
103
+ options: {
104
+ camera: {
105
+ value: null,
106
+ locked: true,
107
+ changed: false,
108
+ },
109
+ materials: {
110
+ innerWall: {
111
+ value: null,
112
+ prefix: "innerWall",
113
+ changed: false,
114
+ },
115
+ outerWall: {
116
+ value: null,
117
+ prefix: "outerWall",
118
+ changed: false,
119
+ },
120
+ innerFloor: {
121
+ value: null,
122
+ prefix: "innerFloor",
123
+ changed: false,
124
+ },
125
+ outerFloor: {
126
+ value: null,
127
+ prefix: "outerFloor",
128
+ changed: false,
129
+ },
130
+ },
131
+ },
64
132
  };
65
133
 
134
+ // DOM elements
135
+ #wrapper = null;
66
136
  #canvas = null;
67
137
 
68
138
  // Babylon.js core objects
@@ -75,6 +145,38 @@ class PrefViewer extends HTMLElement {
75
145
  #shadowGen = null;
76
146
  #XRExperience = null;
77
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
+
78
180
  constructor() {
79
181
  super();
80
182
  this.attachShadow({ mode: "open" });
@@ -97,6 +199,7 @@ class PrefViewer extends HTMLElement {
97
199
  }
98
200
 
99
201
  attributeChangedCallback(name, _old, value) {
202
+ this.#log("debug", `attributeChangedCallback: ${name}`, value);
100
203
  let data = null;
101
204
  switch (name) {
102
205
  case "config":
@@ -110,43 +213,47 @@ class PrefViewer extends HTMLElement {
110
213
  break;
111
214
  case "show-model":
112
215
  data = value.toLowerCase?.() === "true";
113
- if (this.#initialized) {
216
+ if (this.initialized) {
114
217
  data ? this.showModel() : this.hideModel();
115
218
  } else {
116
- this.#model.show = data;
219
+ this.#data.containers.model.show = data;
117
220
  }
118
221
  break;
119
222
  case "show-scene":
120
223
  data = value.toLowerCase?.() === "true";
121
- if (this.#initialized) {
224
+ if (this.initialized) {
122
225
  data ? this.showScene() : this.hideScene();
123
226
  } else {
124
- this.#environment.show = data;
227
+ this.#data.containers.environment.show = data;
125
228
  }
126
229
  break;
127
230
  }
128
231
  }
129
232
 
130
233
  connectedCallback() {
234
+ this.#log("info", "connectedCallback");
131
235
  if (!this.hasAttribute("config")) {
132
236
  const error = 'PrefViewer: provide "models" as array of model and environment';
237
+ this.#log("error", error);
133
238
  console.error(error);
134
239
  this.dispatchEvent(
135
- new CustomEvent("model-error", {
136
- detail: { error: new Error(error) },
240
+ new CustomEvent("scene-error", {
137
241
  bubbles: true,
242
+ cancelable: false,
138
243
  composed: true,
244
+ detail: { error: new Error(error) },
139
245
  })
140
246
  );
141
247
  return false;
142
248
  }
143
249
 
144
250
  this.#initializeBabylon();
145
- this.#loadContainers(true, true);
146
- this.#initialized = true;
251
+ this.initialized = true;
252
+ this.#loadContainers(true, true, true);
147
253
  }
148
254
 
149
255
  disconnectedCallback() {
256
+ this.#log("info", "disconnectedCallback → dispose engine");
150
257
  this.#disposeEngine();
151
258
  this.#canvasResizeObserver.disconnect();
152
259
  }
@@ -163,55 +270,245 @@ class PrefViewer extends HTMLElement {
163
270
  }
164
271
 
165
272
  #wrapCanvas() {
166
- const wrapper = document.createElement("div");
167
- Object.assign(wrapper.style, {
273
+ this.#wrapper = document.createElement("div");
274
+ Object.assign(this.#wrapper.style, {
168
275
  width: "100%",
169
276
  height: "100%",
170
277
  position: "relative",
171
278
  });
172
- wrapper.appendChild(this.#canvas);
173
- this.shadowRoot.append(wrapper);
279
+ this.#wrapper.appendChild(this.#canvas);
280
+ this.shadowRoot.append(this.#wrapper);
281
+ }
282
+
283
+ #setStatusSceneLoading() {
284
+ this.loaded = false;
285
+ this.loading = true;
286
+ if (this.hasAttribute("loaded")) {
287
+ this.removeAttribute("loaded");
288
+ }
289
+ this.setAttribute("loading", "");
290
+ this.#log("info", "Escena → loading");
291
+ this.dispatchEvent(
292
+ new CustomEvent("scene-loading", {
293
+ bubbles: true,
294
+ cancelable: false,
295
+ composed: true,
296
+ })
297
+ );
298
+ }
299
+
300
+ #setStatusSceneLoaded() {
301
+ this.loaded = true;
302
+ this.loading = false;
303
+
304
+ const toLoadDetail = {
305
+ container_model: !!this.#data.containers.model.changed,
306
+ container_environment: !!this.#data.containers.environment.changed,
307
+ container_materials: !!this.#data.containers.materials.changed,
308
+ options_camera: !!this.#data.options.camera.changed,
309
+ options_innerWallMaterial: !!this.#data.options.materials.innerWall.changed,
310
+ options_outerWallMaterial: !!this.#data.options.materials.outerWall.changed,
311
+ options_innerFloorMaterial: !!this.#data.options.materials.innerFloor.changed,
312
+ options_outerFloorMaterial: !!this.#data.options.materials.outerFloor.changed,
313
+ };
314
+ const loadedDetail = {
315
+ container_model: !!this.#data.containers.model.changed?.success,
316
+ container_environment: !!this.#data.containers.environment.changed?.success,
317
+ container_materials: !!this.#data.containers.materials.changed?.success,
318
+ options_camera: !!this.#data.options.camera.changed?.success,
319
+ options_innerWallMaterial: !!this.#data.options.materials.innerWall.changed?.success,
320
+ options_outerWallMaterial: !!this.#data.options.materials.outerWall.changed?.success,
321
+ options_innerFloorMaterial: !!this.#data.options.materials.innerFloor.changed?.success,
322
+ options_outerFloorMaterial: !!this.#data.options.materials.outerFloor.changed?.success,
323
+ };
324
+
325
+ const detail = {
326
+ tried: toLoadDetail,
327
+ success: loadedDetail,
328
+ };
329
+
330
+ if (this.hasAttribute("loading")) {
331
+ this.removeAttribute("loading");
332
+ }
333
+ this.setAttribute("loaded", "");
334
+ this.#log("info", "Escena → loaded", detail);
335
+ this.dispatchEvent(
336
+ new CustomEvent("scene-loaded", {
337
+ bubbles: true,
338
+ cancelable: false,
339
+ composed: true,
340
+ detail: detail,
341
+ })
342
+ );
343
+ }
344
+
345
+ #setStatusOptionsLoading() {
346
+ this.#log("info", "Opciones → loading");
347
+ this.dispatchEvent(
348
+ new CustomEvent("options-loading", {
349
+ bubbles: true,
350
+ cancelable: false,
351
+ composed: true,
352
+ })
353
+ );
354
+ }
355
+
356
+ #setStatusOptionsLoaded() {
357
+ const toLoadDetail = {
358
+ innerWallMaterial: !!this.#data.options.materials.innerWall.changed,
359
+ outerWallMaterial: !!this.#data.options.materials.outerWall.changed,
360
+ innerFloorMaterial: !!this.#data.options.materials.innerFloor.changed,
361
+ outerFloorMaterial: !!this.#data.options.materials.outerFloor.changed,
362
+ };
363
+ const loadedDetail = {
364
+ innerWallMaterial: !!this.#data.options.materials.innerWall.changed?.success,
365
+ outerWallMaterial: !!this.#data.options.materials.outerWall.changed?.success,
366
+ innerFloorMaterial: !!this.#data.options.materials.innerFloor.changed?.success,
367
+ outerFloorMaterial: !!this.#data.options.materials.outerFloor.changed?.success,
368
+ };
369
+
370
+ const detail = {
371
+ tried: toLoadDetail,
372
+ success: loadedDetail,
373
+ };
374
+
375
+ this.#log("info", "Opciones → loaded", detail);
376
+ this.dispatchEvent(
377
+ new CustomEvent("options-loaded", {
378
+ bubbles: true,
379
+ cancelable: false,
380
+ composed: true,
381
+ detail: detail,
382
+ })
383
+ );
384
+ }
385
+
386
+ // Data
387
+ #checkCameraChanged(options) {
388
+ if (!options || !options.camera) {
389
+ return false;
390
+ }
391
+ const changed = options.camera !== this.#data.options.camera.value;
392
+ this.#data.options.camera.changed = changed
393
+ ? { oldValue: this.#data.options.camera.value, oldLocked: this.#data.options.camera.locked, success: false }
394
+ : false;
395
+ if (changed) this.#data.options.camera.value = options.camera;
396
+ this.#log("debug", `#checkCameraChanged → ${changed}`, {
397
+ new: this.#data.options.camera.value,
398
+ old: this.#data.options.camera.changed?.oldValue ?? null,
399
+ });
400
+ return changed;
401
+ }
402
+
403
+ #checkMaterialsChanged(options) {
404
+ if (!options) return false;
405
+ let someChanged = false;
406
+ Object.keys(this.#data.options.materials).forEach((material) => {
407
+ const key = `${material}Material`;
408
+ const materialChanged = options[key] && options[key] !== this.#data.options.materials[material].value ? true : false;
409
+ this.#data.options.materials[material].changed = materialChanged
410
+ ? { oldValue: this.#data.options.materials[material].value, success: false }
411
+ : false;
412
+ this.#data.options.materials[material].value = materialChanged ? options[key] : this.#data.options.materials[material].value;
413
+ someChanged = someChanged || this.#data.options.materials[material].changed;
414
+ });
415
+ this.#log("debug", `#checkMaterialsChanged → ${!!someChanged}`);
416
+ return someChanged;
417
+ }
418
+
419
+ #storeChangedFlagsForContainer(container) {
420
+ container.timeStamp = container.changed.timeStamp;
421
+ container.size = container.changed.size;
422
+ container.changed.success = true;
174
423
  }
175
-
176
- // Bbylon.js
424
+
425
+ #resetChangedFlags() {
426
+ Object.values(this.#data.containers).forEach((container) => (container.changed = false));
427
+ Object.values(this.#data.options.materials).forEach((material) => (material.changed = false));
428
+ this.#data.options.camera.changed = false;
429
+ }
430
+
431
+ // Babylon.js
177
432
  async #initializeBabylon() {
433
+ this.#timeStart("initializeBabylon");
178
434
  this.#engine = new Engine(this.#canvas, true, { alpha: true });
435
+ this.#engine.disableUniformBuffers = true; // evita GL_MAX_*_UNIFORM_BUFFERS (workaround)
179
436
  this.#scene = new Scene(this.#engine);
180
437
  this.#scene.clearColor = new Color4(1, 1, 1, 1);
181
438
  this.#createCamera();
182
- this.#createLights();
439
+ this.#createLights(); // luces desactivadas
183
440
  this.#setupInteraction();
184
-
441
+
442
+ this.#log("info", "Engine y Scene creados", {
443
+ webgl: this.#engine.webGLVersion,
444
+ disableUBO: this.#engine.disableUniformBuffers,
445
+ });
446
+
185
447
  this.#engine.runRenderLoop(() => this.#scene && this.#scene.render());
186
448
  this.#canvasResizeObserver.observe(this.#canvas);
187
449
 
188
450
  await this.#createXRExperience();
451
+ this.#timeEnd("initializeBabylon");
452
+ }
453
+
454
+ addStylesToARButton() {
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"}';
457
+ const style = document.createElement("style");
458
+ style.appendChild(document.createTextNode(css));
459
+ this.#wrapper.appendChild(style);
189
460
  }
190
461
 
191
462
  async #createXRExperience() {
192
- if (this.#XRExperience) {
193
- return true;
194
- }
195
-
463
+ if (this.#XRExperience) return true;
464
+
196
465
  const sessionMode = "immersive-ar";
197
466
  const sessionSupported = await WebXRSessionManager.IsSessionSupportedAsync(sessionMode);
198
467
  if (!sessionSupported) {
199
- console.info("PrefViewer: WebXR in mode AR is not supported");
468
+ this.#log("info", `WebXR no soportado para ${sessionMode}`);
200
469
  return false;
201
470
  }
202
471
 
203
- const options = {
204
- uiOptions: {
205
- sessionMode: sessionMode,
206
- renderTarget: "xrLayer",
207
- referenceSpaceType: "local",
208
- },
209
- };
210
-
211
472
  try {
212
- this.#XRExperience = await WebXRDefaultExperience.CreateAsync(this.#scene, options);
473
+ this.#log("debug", "Creando XR DefaultExperience…");
474
+ const ground = MeshBuilder.CreateGround("ground", { width: 1000, height: 1000 }, this.#scene);
475
+ ground.isVisible = false;
476
+
477
+ const options = {
478
+ floorMeshes: [ground],
479
+ uiOptions: {
480
+ sessionMode: sessionMode,
481
+ renderTarget: "xrLayer",
482
+ referenceSpaceType: "local",
483
+ },
484
+ optionalFeatures: true,
485
+ };
486
+
487
+ this.#XRExperience = await WebXRDefaultExperience.CreateAsync(this.#scene, options);
488
+ this.#log("info", "XR DefaultExperience creado");
489
+
490
+ const featuresManager = this.#XRExperience.baseExperience.featuresManager;
491
+ featuresManager.enableFeature(WebXRFeatureName.TELEPORTATION, "stable", {
492
+ xrInput: this.#XRExperience.input,
493
+ floorMeshes: [ground],
494
+ timeToTeleport: 1500,
495
+ });
496
+
497
+ this.#XRExperience.baseExperience.sessionManager.onXRReady.add(() => {
498
+ // Set the initial position of xrCamera: use nonVRCamera, which contains a copy of the original this.#scene.activeCamera before entering XR
499
+ this.#log("debug", "XR Ready → copiando pose inicial a xrCamera");
500
+ this.#XRExperience.baseExperience.camera.setTransformationFromNonVRCamera(
501
+ this.#XRExperience.baseExperience._nonVRCamera
502
+ );
503
+ this.#XRExperience.baseExperience.camera.setTarget(Vector3.Zero());
504
+ this.#XRExperience.baseExperience.onInitialXRPoseSetObservable.notifyObservers(
505
+ this.#XRExperience.baseExperience.camera
506
+ );
507
+ });
508
+
509
+ this.addStylesToARButton();
213
510
  } catch (error) {
214
- console.warn("PrefViewer: failed to create WebXR experience", error);
511
+ this.#log("warn", "Falló la creación de la experiencia WebXR", error);
215
512
  this.#XRExperience = null;
216
513
  }
217
514
  }
@@ -219,55 +516,77 @@ class PrefViewer extends HTMLElement {
219
516
  #canvasResizeObserver = new ResizeObserver(() => this.#engine && this.#engine.resize());
220
517
 
221
518
  #createCamera() {
222
- this.#camera = new ArcRotateCamera("camera", 3 * Math.PI / 2, Math.PI * 0.47, 10, Vector3.Zero(), this.#scene);
519
+ this.#camera = new ArcRotateCamera("camera", (3 * Math.PI) / 2, Math.PI * 0.47, 10, Vector3.Zero(), this.#scene);
223
520
  this.#camera.upperBetaLimit = Math.PI * 0.48;
224
521
  this.#camera.lowerBetaLimit = Math.PI * 0.25;
225
522
  this.#camera.lowerRadiusLimit = 5;
226
523
  this.#camera.upperRadiusLimit = 20;
524
+ this.#camera.metadata = { locked: false };
227
525
  this.#camera.attachControl(this.#canvas, true);
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
+ });
228
531
  }
229
532
 
533
+ // [LIGHTS OFF] — sin luces del componente
230
534
  #createLights() {
231
- // 1) Stronger ambient fill
232
- this.#hemiLight = new HemisphericLight("hemiLight", new Vector3(-10, 10, -10), this.#scene);
233
- this.#hemiLight.intensity = 0.6;
234
-
235
- // 2) Directional light from the front-right, angled slightly down
236
- this.#dirLight = new DirectionalLight("dirLight", new Vector3(-10, 10, -10), this.#scene);
237
- this.#dirLight.position = new Vector3(5, 4, 5); // light is IN FRONT + ABOVE + to the RIGHT
238
- this.#dirLight.intensity = 0.6;
239
-
240
- // 3) Soft shadows
241
- this.#shadowGen = new ShadowGenerator(1024, this.#dirLight);
242
- this.#shadowGen.useBlurExponentialShadowMap = true;
243
- this.#shadowGen.blurKernel = 16;
244
- this.#shadowGen.darkness = 0.5;
245
-
246
- // 4) Camera‐attached headlight
247
- this.#cameraLight = new PointLight("pl", this.#camera.position, this.#scene);
248
- this.#cameraLight.parent = this.#camera;
249
- 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.");
250
540
  }
251
541
 
252
542
  #setupInteraction() {
253
543
  this.#canvas.addEventListener("wheel", (event) => {
254
- if (!this.#scene || !this.#camera) return;
255
- const pick = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
256
- //this.#camera.target = pick.hit ? pick.pickedPoint.clone() : this.#camera.target;
257
- this.#camera.inertialRadiusOffset -= event.deltaY * this.#camera.wheelPrecision * 0.001;
544
+ if (!this.#scene || !this.#camera) return false;
545
+ if (!this.#scene.activeCamera.metadata?.locked) {
546
+ this.#scene.activeCamera.inertialRadiusOffset -=
547
+ event.deltaY * this.#scene.activeCamera.wheelPrecision * 0.001;
548
+ }
258
549
  event.preventDefault();
259
550
  });
260
551
  }
261
552
 
262
553
  #disposeEngine() {
263
554
  if (!this.#engine) return;
555
+ this.#shadowGen?.dispose();
556
+ this.#scene?.lights?.slice().forEach((l) => l.dispose());
264
557
  this.#engine.dispose();
265
558
  this.#engine = this.#scene = this.#camera = null;
266
559
  this.#hemiLight = this.#dirLight = this.#cameraLight = null;
267
560
  this.#shadowGen = null;
561
+ this.#log("info", "Engine y recursos Babylon eliminados");
268
562
  }
269
563
 
270
564
  // Utility methods for loading gltf/glb
565
+ async #getServerFileDataHeader(uri) {
566
+ this.#log("debug", `HEAD ${uri}`);
567
+ return new Promise((resolve) => {
568
+ const xhr = new XMLHttpRequest();
569
+ xhr.open("HEAD", uri, true);
570
+ xhr.responseType = "blob";
571
+ xhr.onload = () => {
572
+ if (xhr.status === 200) {
573
+ const size = parseInt(xhr.getResponseHeader("Content-Length"));
574
+ const timeStamp = new Date(xhr.getResponseHeader("Last-Modified")).toISOString();
575
+ this.#log("debug", "HEAD ok", { size, timeStamp });
576
+ resolve([size, timeStamp]);
577
+ } else {
578
+ this.#log("warn", "HEAD non-200", { status: xhr.status });
579
+ resolve([0, null]);
580
+ }
581
+ };
582
+ xhr.onerror = () => {
583
+ this.#log("warn", "HEAD error");
584
+ resolve([0, null]);
585
+ };
586
+ xhr.send();
587
+ });
588
+ }
589
+
271
590
  #transformUrl(url) {
272
591
  return new Promise((resolve) => {
273
592
  resolve(url.replace(/^blob:[^/]+\//i, "").replace(/\\/g, "/"));
@@ -275,15 +594,18 @@ class PrefViewer extends HTMLElement {
275
594
  }
276
595
 
277
596
  #decodeBase64(base64) {
597
+ this.#log("debug", "Decodificando Base64…");
278
598
  const [, payload] = base64.split(",");
279
599
  const raw = payload || base64;
280
600
  let decoded = "";
281
601
  let blob = null;
282
602
  let extension = null;
603
+ let size = raw.length;
283
604
  try {
284
605
  decoded = atob(raw);
285
606
  } catch {
286
- return { blob, extension };
607
+ this.#log("warn", "Base64 inválido (atob falló)");
608
+ return { blob, extension, size };
287
609
  }
288
610
  let isJson = false;
289
611
  try {
@@ -294,7 +616,8 @@ class PrefViewer extends HTMLElement {
294
616
  const type = isJson ? "model/gltf+json" : "model/gltf-binary";
295
617
  const array = Uint8Array.from(decoded, (c) => c.charCodeAt(0));
296
618
  blob = new Blob([array], { type });
297
- return { blob, extension };
619
+ this.#log("debug", "Base64 decodificado", { extension, size });
620
+ return { blob, extension, size };
298
621
  }
299
622
 
300
623
  async #initStorage(db, table) {
@@ -302,191 +625,461 @@ class PrefViewer extends HTMLElement {
302
625
  return true;
303
626
  }
304
627
  await initDb(db, table);
628
+ this.#log("debug", "IndexedDB inicializado/abierto", { db, table });
305
629
  }
306
630
 
307
631
  // Methods for managing Asset Containers
308
632
  #setVisibilityOfWallAndFloorInModel(show) {
309
- if (this.#model.container && this.#model.visible) {
310
- const names = ["outer_0", "inner_1", "outerFloor", "innerFloor"];
311
- const nodes = this.#model.container.getNodes();
312
- this.#model.container
313
- .getNodes()
314
- .filter((filter) => names.includes(filter.name))
315
- .forEach((node) => node.setEnabled(show !== undefined ? show : this.#environment.show));
633
+ if (!this.#data.containers.model.assetContainer || !this.#data.containers.model.visible) {
634
+ return false;
316
635
  }
636
+ show = show !== undefined ? show : this.#data.containers.environment.visible;
637
+ const prefixes = Object.values(this.#data.options.materials).map((material) => material.prefix);
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 });
317
643
  }
318
644
 
319
- #addContainer(group) {
320
- if (group.container && !group.visible && group.show) {
321
- group.container.addAllToScene();
322
- group.visible = true;
645
+ #setOptionsMaterial(optionMaterial) {
646
+ if (!optionMaterial || !optionMaterial.prefix || !optionMaterial.value) {
647
+ return false;
648
+ }
649
+
650
+ const material =
651
+ this.#data.containers.materials.assetContainer?.materials.find((mat) => mat.name === optionMaterial.value) ||
652
+ null;
653
+ if (!material) {
654
+ this.#log("warn", "Material no encontrado en contenedor 'materials'", { name: optionMaterial.value });
655
+ return false;
656
+ }
657
+
658
+ const containers = [];
659
+ if (
660
+ this.#data.containers.model.assetContainer &&
661
+ (this.#data.containers.model.changed || this.#data.containers.materials.changed || optionMaterial.changed)
662
+ ) {
663
+ containers.push(this.#data.containers.model.assetContainer);
664
+ }
665
+ if (
666
+ this.#data.containers.environment.assetContainer &&
667
+ (this.#data.containers.environment.changed ||
668
+ this.#data.containers.materials.changed ||
669
+ optionMaterial.changed)
670
+ ) {
671
+ containers.push(this.#data.containers.environment.assetContainer);
323
672
  }
673
+ if (containers.length === 0) {
674
+ return false;
675
+ }
676
+
677
+ let someSetted = false;
678
+ containers.forEach((container) =>
679
+ container.meshes
680
+ .filter((meshToFilter) => meshToFilter.name.startsWith(optionMaterial.prefix))
681
+ .forEach((mesh) => {
682
+ mesh.material = material;
683
+ someSetted = true;
684
+ })
685
+ );
686
+
687
+ if (someSetted) {
688
+ optionMaterial.changed && (optionMaterial.changed.success = true);
689
+ this.#log("debug", "Material aplicado", { prefix: optionMaterial.prefix, name: optionMaterial.value });
690
+ } else {
691
+ optionMaterial.value = optionMaterial.changed?.oldValue ?? optionMaterial.value;
692
+ }
693
+
694
+ return someSetted;
324
695
  }
325
696
 
326
- #removeContainer(group) {
327
- if (group.container && group.visible) {
328
- group.container.removeAllFromScene();
329
- group.visible = false;
697
+ #setOptionsMaterials() {
698
+ let someSetted = false;
699
+ Object.values(this.#data.options.materials).forEach((material) => {
700
+ let settedMaterial = this.#setOptionsMaterial(material);
701
+ someSetted = someSetted || settedMaterial;
702
+ });
703
+ return someSetted;
704
+ }
705
+
706
+ #setOptionsCamera() {
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
+ ) {
713
+ return false;
714
+ }
715
+
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
+
725
+ if (!camera) {
726
+ if (this.#data.options.camera.changed?.oldValue && this.#data.options.camera.changed?.oldValue !== this.#data.options.camera.value) {
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;
735
+ }
736
+ if (camera) {
737
+ camera.metadata = { locked: this.#data.options.camera.changed.oldLocked };
738
+ this.#data.options.camera.value = this.#data.options.camera.changed.oldValue;
739
+ this.#data.options.camera.locked = this.#data.options.camera.changed.oldLocked;
740
+ } else {
741
+ camera = this.#camera;
742
+ this.#data.options.camera.value = null;
743
+ this.#data.options.camera.locked = this.#camera.metadata.locked;
744
+ }
745
+ this.#data.options.camera.changed && (this.#data.options.camera.changed.success = false);
746
+ } else {
747
+ camera.metadata = { locked: this.#data.options.camera.locked };
748
+ }
749
+
750
+ if (!this.#data.options.camera.locked && this.#data.options.camera.value !== null) {
751
+ camera.attachControl(this.#canvas, true);
752
+ }
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
+ });
758
+ return true;
759
+ }
760
+
761
+ #addContainer(container) {
762
+ if (container.assetContainer && !container.visible && container.show) {
763
+ container.assetContainer.addAllToScene();
764
+ container.visible = true;
765
+ this.#log("debug", `Añadido a escena: ${container.name}`);
766
+ }
767
+ }
768
+
769
+ #removeContainer(container) {
770
+ if (container.assetContainer && container.visible) {
771
+ container.assetContainer.removeAllFromScene();
772
+ container.visible = false;
773
+ this.#log("debug", `Eliminado de escena: ${container.name}`);
330
774
  }
331
775
  }
332
776
 
333
- #replaceContainer(group, newContainer) {
334
- this.#removeContainer(group);
335
- group.container = newContainer;
336
- group.container.meshes.forEach((mesh) => {
337
- mesh.receiveShadows = true;
338
- this.#shadowGen.addShadowCaster(mesh, true);
777
+ #replaceContainer(container, newAssetContainer) {
778
+ // 1) quita y destruye el anterior si existía
779
+ const old = container.assetContainer;
780
+ if (old) {
781
+ if (container.visible) {
782
+ old.removeAllFromScene();
783
+ }
784
+ old.dispose();
785
+ }
786
+
787
+ // 2) asigna el nuevo y prepara
788
+ container.assetContainer = newAssetContainer;
789
+
790
+ // Limitar luces por material (no usamos luces, poner 0 asegura shaders más simples)
791
+ container.assetContainer.materials?.forEach((m) => {
792
+ if ("maxSimultaneousLights" in m) {
793
+ m.maxSimultaneousLights = 0; // [LIGHTS OFF]
794
+ }
795
+ });
796
+
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);
801
+ });
802
+
803
+ // 4) añade a escena
804
+ this.#addContainer(container);
805
+
806
+ // 5) fuerza recompilación con defines correctos del nuevo estado
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,
339
812
  });
340
- this.#addContainer(group);
341
813
  }
342
814
 
343
- async #loadAssetContainer(storage) {
815
+ async #loadAssetContainer(container) {
816
+ const storage = container?.storage;
817
+
344
818
  if (!storage) {
819
+ this.#log("debug", `Sin storage para "${container?.name}"`);
345
820
  return false;
346
821
  }
347
822
 
823
+ this.#timeStart(`load:${container.name}`);
348
824
  let source = storage.url || null;
349
825
 
350
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
+ });
351
832
  await this.#initStorage(storage.db, storage.table);
352
833
  const object = await loadModel(storage.id, storage.table);
353
834
  source = object.data;
835
+ if (object.timeStamp === container.timeStamp) {
836
+ this.#log("debug", `${container.name}: sin cambios en IndexedDB`);
837
+ this.#timeEnd(`load:${container.name}`);
838
+ return false;
839
+ } else {
840
+ container.changed = { timeStamp: object.timeStamp, size: object.size, success: false };
841
+ this.#log("debug", `${container.name}: cambios detectados`, container.changed);
842
+ }
354
843
  }
355
844
 
356
845
  if (!source) {
846
+ this.#log("warn", `${container.name}: no hay source`);
847
+ this.#timeEnd(`load:${container.name}`);
357
848
  return false;
358
849
  }
359
850
 
360
851
  let file = null;
361
852
 
362
- let { blob, extension } = this.#decodeBase64(source);
853
+ let { blob, extension, size } = this.#decodeBase64(source);
363
854
  if (blob && extension) {
364
- file = new File([blob], `model${extension}`, {
855
+ file = new File([blob], `${container.name}${extension}`, {
365
856
  type: blob.type,
366
857
  });
858
+ if (!container.changed) {
859
+ if (container.timeStamp === null && container.size === size) {
860
+ this.#log("debug", `${container.name}: Base64 sin cambios`);
861
+ this.#timeEnd(`load:${container.name}`);
862
+ return false;
863
+ } else {
864
+ container.changed = { timeStamp: null, size: size, success: false };
865
+ }
866
+ }
367
867
  } else {
368
868
  const extMatch = source.match(/\.(gltf|glb)(\?|#|$)/i);
369
869
  extension = extMatch ? `.${extMatch[1].toLowerCase()}` : ".gltf";
870
+ const [fileSize, fileTimeStamp] = await this.#getServerFileDataHeader(source);
871
+ if (container.size === fileSize && container.timeStamp === fileTimeStamp) {
872
+ this.#log("debug", `${container.name}: URL sin cambios`);
873
+ this.#timeEnd(`load:${container.name}`);
874
+ return false;
875
+ } else {
876
+ container.changed = { timeStamp: fileTimeStamp, size: fileSize, success: false };
877
+ }
370
878
  }
371
879
 
372
880
  let options = {
373
881
  pluginExtension: extension,
374
882
  pluginOptions: {
375
883
  gltf: {
884
+ loadAllMaterials: true,
376
885
  preprocessUrlAsync: this.#transformUrl,
377
886
  },
378
887
  },
379
888
  };
889
+ this.#log("info", `LoadAssetContainerAsync ${container.name}`, { extension, changed: container.changed });
380
890
 
381
- 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
+ }
382
900
  }
383
901
 
384
- async #loadContainers(loadModel = true, loadEnvironment = true) {
902
+ async #loadContainers(loadModel = true, loadEnvironment = true, loadMaterials = true) {
903
+ this.#log("info", "loadContainers()", { loadModel, loadEnvironment, loadMaterials });
385
904
  const promiseArray = [];
905
+ promiseArray.push(loadModel ? this.#loadAssetContainer(this.#data.containers.model) : false);
906
+ promiseArray.push(loadEnvironment ? this.#loadAssetContainer(this.#data.containers.environment) : false);
907
+ promiseArray.push(loadMaterials ? this.#loadAssetContainer(this.#data.containers.materials) : false);
386
908
 
387
- promiseArray.push(loadModel ? this.#loadAssetContainer(this.#model.storage) : false);
388
- promiseArray.push(loadEnvironment ? this.#loadAssetContainer(this.#environment.storage) : false);
909
+ this.#setStatusSceneLoading();
389
910
 
390
911
  Promise.allSettled(promiseArray)
391
912
  .then(async (values) => {
392
913
  const modelContainer = values[0];
393
914
  const environmentContainer = values[1];
915
+ const materialsContainer = values[2];
916
+
917
+ this.#log(
918
+ "debug",
919
+ "Resultados Promise.allSettled",
920
+ values.map((v) => ({ status: v.status, hasValue: !!v.value }))
921
+ );
394
922
 
395
923
  if (modelContainer.status === "fulfilled" && modelContainer.value) {
396
- this.#replaceContainer(this.#model, modelContainer.value);
924
+ this.#stripImportedLights(modelContainer.value);
925
+ this.#replaceContainer(this.#data.containers.model, modelContainer.value);
926
+ this.#storeChangedFlagsForContainer(this.#data.containers.model);
927
+ } else {
928
+ this.#data.containers.model.show
929
+ ? this.#addContainer(this.#data.containers.model)
930
+ : this.#removeContainer(this.#data.containers.model);
397
931
  }
398
932
 
399
933
  if (environmentContainer.status === "fulfilled" && environmentContainer.value) {
400
- this.#replaceContainer(this.#environment, environmentContainer.value);
934
+ this.#stripImportedLights(environmentContainer.value);
935
+ this.#replaceContainer(this.#data.containers.environment, environmentContainer.value);
936
+ this.#storeChangedFlagsForContainer(this.#data.containers.environment);
937
+ } else {
938
+ this.#data.containers.environment.show
939
+ ? this.#addContainer(this.#data.containers.environment)
940
+ : this.#removeContainer(this.#data.containers.environment);
401
941
  }
402
942
 
403
- this.#setVisibilityOfWallAndFloorInModel();
943
+ if (materialsContainer.status === "fulfilled" && materialsContainer.value) {
944
+ this.#stripImportedLights(materialsContainer.value);
945
+ this.#replaceContainer(this.#data.containers.materials, materialsContainer.value);
946
+ this.#storeChangedFlagsForContainer(this.#data.containers.materials);
947
+ }
404
948
 
405
- this.dispatchEvent(
406
- new CustomEvent("model-loaded", {
407
- detail: { success: "" },
408
- bubbles: true,
409
- composed: true,
410
- })
411
- );
949
+ this.#setOptionsMaterials();
950
+ this.#setOptionsCamera();
951
+ this.#setVisibilityOfWallAndFloorInModel();
952
+ this.#setStatusSceneLoaded();
953
+ this.#resetChangedFlags();
954
+ this.#log("info", "Escena cargada");
412
955
  })
413
956
  .catch((error) => {
414
- console.error("PrefViewer: failed to load model", error);
957
+ this.loaded = true;
958
+ this.#log("error", "Failed to load containers", error);
415
959
  this.dispatchEvent(
416
- new CustomEvent("model-error", {
417
- detail: { error: error },
960
+ new CustomEvent("scene-error", {
418
961
  bubbles: true,
962
+ cancelable: false,
419
963
  composed: true,
964
+ detail: { error: error },
420
965
  })
421
966
  );
422
967
  });
423
968
  }
424
969
 
970
+ #stripImportedLights(container) {
971
+ const n = container?.lights?.length ?? 0;
972
+ if (n) container.lights.slice().forEach((l) => l.dispose());
973
+ this.#log("debug", `stripImportedLights(): ${n} → 0`);
974
+ }
975
+
425
976
  // Public Methods
426
977
  loadConfig(config) {
978
+ this.#log("info", "loadConfig()", typeof config === "string" ? "[string]" : config);
427
979
  config = typeof config === "string" ? JSON.parse(config) : config;
428
980
  if (!config) {
981
+ this.#log("warn", "loadConfig() → config vacío/nulo");
982
+ return false;
983
+ }
984
+
985
+ // Containers
986
+ this.#data.containers.model.storage = config.model?.storage || null;
987
+ this.#data.containers.model.show =
988
+ config.model?.visible !== undefined ? config.model.visible : this.#data.containers.model.show;
989
+ this.#data.containers.environment.storage = config.scene?.storage || null;
990
+ this.#data.containers.environment.show =
991
+ config.scene?.visible !== undefined ? config.scene.visible : this.#data.containers.environment.show;
992
+ this.#data.containers.materials.storage = config.materials?.storage || null;
993
+
994
+ // Options
995
+ if (config.options) {
996
+ this.#checkCameraChanged(config.options);
997
+ this.#checkMaterialsChanged(config.options);
998
+ }
999
+
1000
+ this.initialized && this.#loadContainers(true, true, true);
1001
+ }
1002
+
1003
+ setOptions(options) {
1004
+ this.#log("info", "setOptions()", options);
1005
+ if (!options) {
429
1006
  return false;
430
1007
  }
431
- this.#model.storage = config.model?.storage || null;
432
- this.#model.show = config.model?.visible !== undefined ? config.model.visible : this.#model.show;
433
- this.#environment.storage = config.scene?.storage || null;
434
- this.#environment.show = config.scene?.visible !== undefined ? config.scene.visible : this.#environment.show;
435
- this.#initialized && this.#loadContainers(true, true);
1008
+
1009
+ this.#setStatusOptionsLoading();
1010
+
1011
+ let someSetted = false;
1012
+ if (this.#checkCameraChanged(options)) {
1013
+ someSetted = someSetted || this.#setOptionsCamera();
1014
+ }
1015
+ if (this.#checkMaterialsChanged(options)) {
1016
+ someSetted = someSetted || this.#setOptionsMaterials();
1017
+ }
1018
+
1019
+ this.#setStatusOptionsLoaded();
1020
+ this.#resetChangedFlags();
1021
+
1022
+ return someSetted;
436
1023
  }
437
1024
 
438
1025
  loadModel(model) {
1026
+ this.#log("info", "loadModel()", typeof model === "string" ? "[string]" : model);
439
1027
  model = typeof model === "string" ? JSON.parse(model) : model;
440
1028
  if (!model) {
1029
+ this.#log("warn", "loadModel() → model vacío/nulo");
441
1030
  return false;
442
1031
  }
443
- this.#model.storage = model.storage || null;
444
- this.#model.show = model.visible !== undefined ? model.visible : this.#model.show;
445
- this.#initialized && this.#loadContainers(true, false);
1032
+ this.#data.containers.model.storage = model.storage || null;
1033
+ this.#data.containers.model.show =
1034
+ model.visible !== undefined ? model.visible : this.#data.containers.model.show;
1035
+ this.initialized && this.#loadContainers(true, false, false);
446
1036
  }
447
1037
 
448
1038
  loadScene(scene) {
1039
+ this.#log("info", "loadScene()", typeof scene === "string" ? "[string]" : scene);
449
1040
  scene = typeof scene === "string" ? JSON.parse(scene) : scene;
450
1041
  if (!scene) {
1042
+ this.#log("warn", "loadScene() → scene vacío/nulo");
451
1043
  return false;
452
1044
  }
453
- this.#environment.storage = scene.storage || null;
454
- this.#environment.show = scene.visible !== undefined ? scene.visible : this.#environment.show;
455
- this.#initialized && this.#loadContainers(false, true);
1045
+ this.#data.containers.environment.storage = scene.storage || null;
1046
+ this.#data.containers.environment.show =
1047
+ scene.visible !== undefined ? scene.visible : this.#data.containers.environment.show;
1048
+ this.initialized && this.#loadContainers(false, true, false);
456
1049
  }
457
1050
 
458
1051
  showModel() {
459
- this.#model.show = true;
460
- this.#addContainer(this.#model);
1052
+ this.#data.containers.model.show = true;
1053
+ this.#addContainer(this.#data.containers.model);
461
1054
  }
462
1055
 
463
1056
  hideModel() {
464
- this.#model.show = false;
465
- this.#removeContainer(this.#model);
1057
+ this.#data.containers.model.show = false;
1058
+ this.#removeContainer(this.#data.containers.model);
466
1059
  }
467
1060
 
468
1061
  showScene() {
469
- this.#environment.show = true;
470
- this.#addContainer(this.#environment);
1062
+ this.#data.containers.environment.show = true;
1063
+ this.#addContainer(this.#data.containers.environment);
471
1064
  this.#setVisibilityOfWallAndFloorInModel();
472
1065
  }
473
1066
 
474
1067
  hideScene() {
475
- this.#environment.show = false;
476
- this.#removeContainer(this.#environment);
1068
+ this.#data.containers.environment.show = false;
1069
+ this.#removeContainer(this.#data.containers.environment);
477
1070
  this.#setVisibilityOfWallAndFloorInModel();
478
1071
  }
479
1072
 
480
1073
  downloadModelGLB() {
481
1074
  const fileName = "model";
482
- GLTF2Export.GLBAsync(this.#model.container, fileName, { exportWithoutWaitingForScene: true }).then((glb) => {
483
- glb.downloadFiles();
484
- });
1075
+ GLTF2Export.GLBAsync(this.#data.containers.model.assetContainer, fileName, {
1076
+ exportWithoutWaitingForScene: true,
1077
+ }).then((glb) => glb.downloadFiles());
485
1078
  }
486
1079
 
487
1080
  downloadModelUSDZ() {
488
1081
  const fileName = "model";
489
- USDZExportAsync(this.#model.container).then((response) => {
1082
+ USDZExportAsync(this.#data.containers.model.assetContainer).then((response) => {
490
1083
  if (response) {
491
1084
  Tools.Download(new Blob([response], { type: "model/vnd.usdz+zip" }), `${fileName}.usdz`);
492
1085
  }
@@ -504,9 +1097,9 @@ class PrefViewer extends HTMLElement {
504
1097
 
505
1098
  downloadModelAndSceneGLB() {
506
1099
  const fileName = "scene";
507
- GLTF2Export.GLBAsync(this.#scene, fileName, { exportWithoutWaitingForScene: true }).then((glb) => {
508
- glb.downloadFiles();
509
- });
1100
+ GLTF2Export.GLBAsync(this.#scene, fileName, { exportWithoutWaitingForScene: true }).then((glb) =>
1101
+ glb.downloadFiles()
1102
+ );
510
1103
  }
511
1104
  }
512
1105