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

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