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

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