@preference-sl/pref-viewer 2.10.0-beta.1 → 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.
- package/package.json +1 -1
- package/src/index.js +684 -92
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* </pref-viewer>
|
|
40
40
|
* ```
|
|
41
41
|
*/
|
|
42
|
-
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";
|
|
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";
|
|
@@ -47,22 +47,141 @@ import { DracoCompression } from "@babylonjs/core/Meshes/Compression/dracoCompre
|
|
|
47
47
|
import { initDb, loadModel } from "./gltf-storage.js";
|
|
48
48
|
|
|
49
49
|
class PrefViewer extends HTMLElement {
|
|
50
|
+
static LOG_PREFIX = "[PrefViewer]";
|
|
51
|
+
static LOG_LEVELS = { none: 0, error: 1, warn: 2, info: 3, debug: 4 };
|
|
52
|
+
// Por defecto NO loggear: control únicamente vía atributo `log-level`
|
|
53
|
+
static DEFAULT_LOG_LEVEL = "none";
|
|
54
|
+
|
|
50
55
|
#initialized = false;
|
|
56
|
+
#logLevel = PrefViewer.DEFAULT_LOG_LEVEL;
|
|
51
57
|
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
},
|
|
57
120
|
};
|
|
58
121
|
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
};
|
|
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;
|
|
65
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
|
+
// DOM elements
|
|
184
|
+
#wrapper = null;
|
|
66
185
|
#canvas = null;
|
|
67
186
|
|
|
68
187
|
// Babylon.js core objects
|
|
@@ -73,9 +192,11 @@ class PrefViewer extends HTMLElement {
|
|
|
73
192
|
#dirLight = null;
|
|
74
193
|
#cameraLight = null;
|
|
75
194
|
#shadowGen = null;
|
|
195
|
+
#XRExperience = null;
|
|
76
196
|
|
|
77
197
|
constructor() {
|
|
78
198
|
super();
|
|
199
|
+
this.#logDebug("Constructing PrefViewer instance");
|
|
79
200
|
this.attachShadow({ mode: "open" });
|
|
80
201
|
this.#createCanvas();
|
|
81
202
|
this.#wrapCanvas();
|
|
@@ -91,11 +212,13 @@ class PrefViewer extends HTMLElement {
|
|
|
91
212
|
};
|
|
92
213
|
}
|
|
93
214
|
|
|
94
|
-
static get observedAttributes() {
|
|
95
|
-
return ["config", "model", "scene", "show-model", "show-scene"];
|
|
96
|
-
}
|
|
97
|
-
|
|
98
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) });
|
|
99
222
|
let data = null;
|
|
100
223
|
switch (name) {
|
|
101
224
|
case "config":
|
|
@@ -109,18 +232,20 @@ class PrefViewer extends HTMLElement {
|
|
|
109
232
|
break;
|
|
110
233
|
case "show-model":
|
|
111
234
|
data = value.toLowerCase?.() === "true";
|
|
235
|
+
this.#logDebug("Toggling model visibility from attribute", { visible: data, initialized: this.#initialized });
|
|
112
236
|
if (this.#initialized) {
|
|
113
237
|
data ? this.showModel() : this.hideModel();
|
|
114
238
|
} else {
|
|
115
|
-
this.#model.show = data;
|
|
239
|
+
this.#data.containers.model.show = data;
|
|
116
240
|
}
|
|
117
241
|
break;
|
|
118
242
|
case "show-scene":
|
|
119
243
|
data = value.toLowerCase?.() === "true";
|
|
244
|
+
this.#logDebug("Toggling scene visibility from attribute", { visible: data, initialized: this.#initialized });
|
|
120
245
|
if (this.#initialized) {
|
|
121
246
|
data ? this.showScene() : this.hideScene();
|
|
122
247
|
} else {
|
|
123
|
-
this.#environment.show = data;
|
|
248
|
+
this.#data.containers.environment.show = data;
|
|
124
249
|
}
|
|
125
250
|
break;
|
|
126
251
|
}
|
|
@@ -129,7 +254,7 @@ class PrefViewer extends HTMLElement {
|
|
|
129
254
|
connectedCallback() {
|
|
130
255
|
if (!this.hasAttribute("config")) {
|
|
131
256
|
const error = 'PrefViewer: provide "models" as array of model and environment';
|
|
132
|
-
|
|
257
|
+
this.#logError("Missing required config attribute", { error });
|
|
133
258
|
this.dispatchEvent(
|
|
134
259
|
new CustomEvent("model-error", {
|
|
135
260
|
detail: { error: new Error(error) },
|
|
@@ -140,18 +265,22 @@ class PrefViewer extends HTMLElement {
|
|
|
140
265
|
return false;
|
|
141
266
|
}
|
|
142
267
|
|
|
268
|
+
this.#logDebug("Connected to DOM, initializing Babylon");
|
|
143
269
|
this.#initializeBabylon();
|
|
144
|
-
this.#loadContainers(true, true);
|
|
270
|
+
this.#loadContainers(true, true, true);
|
|
145
271
|
this.#initialized = true;
|
|
272
|
+
this.#logInfo("Initialization completed", { initialized: this.#initialized });
|
|
146
273
|
}
|
|
147
274
|
|
|
148
275
|
disconnectedCallback() {
|
|
276
|
+
this.#logDebug("Disconnected from DOM, disposing resources");
|
|
149
277
|
this.#disposeEngine();
|
|
150
278
|
this.#canvasResizeObserver.disconnect();
|
|
151
279
|
}
|
|
152
280
|
|
|
153
281
|
// Web Component
|
|
154
282
|
#createCanvas() {
|
|
283
|
+
this.#logDebug("Creating rendering canvas");
|
|
155
284
|
this.#canvas = document.createElement("canvas");
|
|
156
285
|
Object.assign(this.#canvas.style, {
|
|
157
286
|
width: "100%",
|
|
@@ -159,45 +288,177 @@ class PrefViewer extends HTMLElement {
|
|
|
159
288
|
display: "block",
|
|
160
289
|
outline: "none",
|
|
161
290
|
});
|
|
291
|
+
this.#logDebug("Canvas element created and styled");
|
|
162
292
|
}
|
|
163
293
|
|
|
164
294
|
#wrapCanvas() {
|
|
165
|
-
|
|
166
|
-
|
|
295
|
+
this.#logDebug("Wrapping canvas inside container div");
|
|
296
|
+
this.#wrapper = document.createElement("div");
|
|
297
|
+
Object.assign(this.#wrapper.style, {
|
|
167
298
|
width: "100%",
|
|
168
299
|
height: "100%",
|
|
169
300
|
position: "relative",
|
|
170
301
|
});
|
|
171
|
-
wrapper.appendChild(this.#canvas);
|
|
172
|
-
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");
|
|
173
305
|
}
|
|
174
306
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Babylon.js
|
|
355
|
+
async #initializeBabylon() {
|
|
356
|
+
this.#logInfo("Initializing Babylon engine and scene");
|
|
178
357
|
this.#engine = new Engine(this.#canvas, true, { alpha: true });
|
|
179
358
|
this.#scene = new Scene(this.#engine);
|
|
180
359
|
this.#scene.clearColor = new Color4(1, 1, 1, 1);
|
|
181
360
|
this.#createCamera();
|
|
182
361
|
this.#createLights();
|
|
183
362
|
this.#setupInteraction();
|
|
184
|
-
|
|
363
|
+
|
|
185
364
|
this.#engine.runRenderLoop(() => this.#scene && this.#scene.render());
|
|
186
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");
|
|
379
|
+
}
|
|
380
|
+
|
|
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
|
+
}
|
|
187
433
|
}
|
|
188
434
|
|
|
189
|
-
#canvasResizeObserver = new ResizeObserver(() =>
|
|
435
|
+
#canvasResizeObserver = new ResizeObserver(() => {
|
|
436
|
+
if (this.#engine) {
|
|
437
|
+
this.#logDebug("Resize observer triggered");
|
|
438
|
+
this.#engine.resize();
|
|
439
|
+
}
|
|
440
|
+
});
|
|
190
441
|
|
|
191
442
|
#createCamera() {
|
|
192
|
-
this.#
|
|
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);
|
|
193
445
|
this.#camera.upperBetaLimit = Math.PI * 0.48;
|
|
194
446
|
this.#camera.lowerBetaLimit = Math.PI * 0.25;
|
|
195
447
|
this.#camera.lowerRadiusLimit = 5;
|
|
196
448
|
this.#camera.upperRadiusLimit = 20;
|
|
449
|
+
this.#camera.metadata = { locked: false }
|
|
450
|
+
this.#camera = this.#camera;
|
|
197
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
|
+
});
|
|
198
458
|
}
|
|
199
459
|
|
|
200
460
|
#createLights() {
|
|
461
|
+
this.#logDebug("Creating scene lights");
|
|
201
462
|
// 1) Stronger ambient fill
|
|
202
463
|
this.#hemiLight = new HemisphericLight("hemiLight", new Vector3(-10, 10, -10), this.#scene);
|
|
203
464
|
this.#hemiLight.intensity = 0.6;
|
|
@@ -217,161 +478,408 @@ class PrefViewer extends HTMLElement {
|
|
|
217
478
|
this.#cameraLight = new PointLight("pl", this.#camera.position, this.#scene);
|
|
218
479
|
this.#cameraLight.parent = this.#camera;
|
|
219
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
|
+
});
|
|
220
487
|
}
|
|
221
488
|
|
|
222
489
|
#setupInteraction() {
|
|
490
|
+
this.#logDebug("Setting up canvas interaction listeners");
|
|
223
491
|
this.#canvas.addEventListener("wheel", (event) => {
|
|
224
|
-
if (!this.#scene || !this.#camera)
|
|
225
|
-
|
|
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);
|
|
226
497
|
//this.#camera.target = pick.hit ? pick.pickedPoint.clone() : this.#camera.target;
|
|
227
|
-
this.#
|
|
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
|
+
}
|
|
228
507
|
event.preventDefault();
|
|
229
508
|
});
|
|
230
509
|
}
|
|
231
510
|
|
|
232
511
|
#disposeEngine() {
|
|
233
|
-
if (!this.#engine)
|
|
512
|
+
if (!this.#engine) {
|
|
513
|
+
this.#logDebug("Dispose engine called but engine already null");
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
this.#logDebug("Disposing Babylon resources");
|
|
234
517
|
this.#engine.dispose();
|
|
235
518
|
this.#engine = this.#scene = this.#camera = null;
|
|
236
519
|
this.#hemiLight = this.#dirLight = this.#cameraLight = null;
|
|
237
520
|
this.#shadowGen = null;
|
|
521
|
+
this.#logDebug("Babylon resources disposed");
|
|
238
522
|
}
|
|
239
523
|
|
|
240
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
|
+
|
|
241
550
|
#transformUrl(url) {
|
|
242
551
|
return new Promise((resolve) => {
|
|
243
|
-
|
|
552
|
+
const transformed = url.replace(/^blob:[^/]+\//i, "").replace(/\\/g, "/");
|
|
553
|
+
this.#logDebug("Transformed URL", { original: url, transformed });
|
|
554
|
+
resolve(transformed);
|
|
244
555
|
});
|
|
245
556
|
}
|
|
246
557
|
|
|
247
558
|
#decodeBase64(base64) {
|
|
559
|
+
this.#logDebug("Decoding Base64 payload", { length: base64 ? base64.length : 0 });
|
|
248
560
|
const [, payload] = base64.split(",");
|
|
249
561
|
const raw = payload || base64;
|
|
250
562
|
let decoded = "";
|
|
251
563
|
let blob = null;
|
|
252
564
|
let extension = null;
|
|
565
|
+
let size = raw.length;
|
|
253
566
|
try {
|
|
254
567
|
decoded = atob(raw);
|
|
255
568
|
} catch {
|
|
256
|
-
|
|
569
|
+
this.#logWarn("Failed to decode Base64 string");
|
|
570
|
+
return { blob, extension, size };
|
|
257
571
|
}
|
|
258
572
|
let isJson = false;
|
|
259
573
|
try {
|
|
260
574
|
JSON.parse(decoded);
|
|
261
575
|
isJson = true;
|
|
262
|
-
} catch {}
|
|
576
|
+
} catch { }
|
|
263
577
|
extension = isJson ? ".gltf" : ".glb";
|
|
264
578
|
const type = isJson ? "model/gltf+json" : "model/gltf-binary";
|
|
265
579
|
const array = Uint8Array.from(decoded, (c) => c.charCodeAt(0));
|
|
266
580
|
blob = new Blob([array], { type });
|
|
267
|
-
|
|
581
|
+
this.#logDebug("Decoded Base64 payload", { isJson, size, extension });
|
|
582
|
+
return { blob, extension, size };
|
|
268
583
|
}
|
|
269
584
|
|
|
270
585
|
async #initStorage(db, table) {
|
|
586
|
+
this.#logDebug("Initializing storage access", { db, table });
|
|
271
587
|
if (window.gltfDB && window.gltfDB.name === db && window.gltfDB.objectStoreNames.contains(table)) {
|
|
588
|
+
this.#logDebug("Reusing existing IndexedDB connection", { db, table });
|
|
272
589
|
return true;
|
|
273
590
|
}
|
|
274
591
|
await initDb(db, table);
|
|
592
|
+
this.#logDebug("IndexedDB initialized", { db, table });
|
|
275
593
|
}
|
|
276
594
|
|
|
277
595
|
// Methods for managing Asset Containers
|
|
278
596
|
#setVisibilityOfWallAndFloorInModel(show) {
|
|
279
|
-
if (this.#model.
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
.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;
|
|
286
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 });
|
|
287
608
|
}
|
|
288
609
|
|
|
289
|
-
#
|
|
290
|
-
if (
|
|
291
|
-
|
|
292
|
-
|
|
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);
|
|
293
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;
|
|
294
659
|
}
|
|
295
660
|
|
|
296
|
-
#
|
|
297
|
-
if (
|
|
298
|
-
|
|
299
|
-
|
|
661
|
+
#setOptionsMaterials() {
|
|
662
|
+
if (!this.#data.containers.materials.assetContainer) {
|
|
663
|
+
this.#logDebug("Skipping materials update; materials container is missing");
|
|
664
|
+
return false;
|
|
300
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;
|
|
301
675
|
}
|
|
302
676
|
|
|
303
|
-
#
|
|
304
|
-
this.#
|
|
305
|
-
|
|
306
|
-
|
|
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 });
|
|
699
|
+
}
|
|
700
|
+
this.#scene.activeCamera = camera;
|
|
701
|
+
this.#logDebug("Active camera set", { camera: camera.name });
|
|
702
|
+
|
|
703
|
+
return true;
|
|
704
|
+
}
|
|
705
|
+
|
|
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
|
+
});
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
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
|
+
});
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
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) => {
|
|
307
740
|
mesh.receiveShadows = true;
|
|
308
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,
|
|
309
748
|
});
|
|
310
|
-
this.#addContainer(group);
|
|
311
749
|
}
|
|
312
750
|
|
|
313
|
-
async #loadAssetContainer(
|
|
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
|
+
|
|
314
758
|
if (!storage) {
|
|
759
|
+
this.#logWarn("No storage configuration provided for container", { container: container?.name });
|
|
315
760
|
return false;
|
|
316
761
|
}
|
|
317
762
|
|
|
318
763
|
let source = storage.url || null;
|
|
319
764
|
|
|
320
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
|
+
});
|
|
321
772
|
await this.#initStorage(storage.db, storage.table);
|
|
322
773
|
const object = await loadModel(storage.id, storage.table);
|
|
323
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
|
+
}
|
|
324
782
|
}
|
|
325
783
|
|
|
326
784
|
if (!source) {
|
|
785
|
+
this.#logWarn("No source resolved for container", { container: container.name });
|
|
327
786
|
return false;
|
|
328
787
|
}
|
|
329
788
|
|
|
330
789
|
let file = null;
|
|
331
790
|
|
|
332
|
-
let { blob, extension } = this.#decodeBase64(source);
|
|
791
|
+
let { blob, extension, size } = this.#decodeBase64(source);
|
|
333
792
|
if (blob && extension) {
|
|
334
|
-
|
|
793
|
+
this.#logDebug("Source detected as Base64", { container: container.name, extension });
|
|
794
|
+
file = new File([blob], `${container.name}${extension}`, {
|
|
335
795
|
type: blob.type,
|
|
336
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
|
+
}
|
|
337
806
|
} else {
|
|
338
807
|
const extMatch = source.match(/\.(gltf|glb)(\?|#|$)/i);
|
|
339
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
|
+
}
|
|
340
821
|
}
|
|
341
822
|
|
|
342
823
|
let options = {
|
|
343
824
|
pluginExtension: extension,
|
|
344
825
|
pluginOptions: {
|
|
345
826
|
gltf: {
|
|
827
|
+
loadAllMaterials: true,
|
|
346
828
|
preprocessUrlAsync: this.#transformUrl,
|
|
347
829
|
},
|
|
348
830
|
},
|
|
349
831
|
};
|
|
350
832
|
|
|
833
|
+
this.#logInfo("Loading asset container", { container: container.name, extension });
|
|
351
834
|
return LoadAssetContainerAsync(file || source, this.#scene, options);
|
|
352
835
|
}
|
|
353
836
|
|
|
354
|
-
async #loadContainers(loadModel = true, loadEnvironment = true) {
|
|
837
|
+
async #loadContainers(loadModel = true, loadEnvironment = true, loadMaterials = true) {
|
|
838
|
+
this.#logInfo("Starting container load", { loadModel, loadEnvironment, loadMaterials });
|
|
355
839
|
const promiseArray = [];
|
|
356
|
-
|
|
357
|
-
promiseArray.push(
|
|
358
|
-
promiseArray.push(
|
|
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);
|
|
359
843
|
|
|
360
844
|
Promise.allSettled(promiseArray)
|
|
361
845
|
.then(async (values) => {
|
|
362
846
|
const modelContainer = values[0];
|
|
363
847
|
const environmentContainer = values[1];
|
|
848
|
+
const materialsContainer = values[2];
|
|
364
849
|
|
|
365
850
|
if (modelContainer.status === "fulfilled" && modelContainer.value) {
|
|
366
|
-
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 });
|
|
367
857
|
}
|
|
368
858
|
|
|
369
859
|
if (environmentContainer.status === "fulfilled" && environmentContainer.value) {
|
|
370
|
-
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 });
|
|
371
874
|
}
|
|
372
875
|
|
|
876
|
+
this.#setOptionsMaterials();
|
|
877
|
+
this.#setOptionsCamera();
|
|
373
878
|
this.#setVisibilityOfWallAndFloorInModel();
|
|
374
879
|
|
|
880
|
+
this.#resetChangedFlags();
|
|
881
|
+
|
|
882
|
+
this.#logInfo("Containers load routine completed");
|
|
375
883
|
this.dispatchEvent(
|
|
376
884
|
new CustomEvent("model-loaded", {
|
|
377
885
|
detail: { success: "" },
|
|
@@ -379,9 +887,10 @@ class PrefViewer extends HTMLElement {
|
|
|
379
887
|
composed: true,
|
|
380
888
|
})
|
|
381
889
|
);
|
|
890
|
+
this.#logDebug("Dispatched model-loaded event");
|
|
382
891
|
})
|
|
383
892
|
.catch((error) => {
|
|
384
|
-
|
|
893
|
+
this.#logError("Failed to load containers", { error });
|
|
385
894
|
this.dispatchEvent(
|
|
386
895
|
new CustomEvent("model-error", {
|
|
387
896
|
detail: { error: error },
|
|
@@ -394,87 +903,170 @@ class PrefViewer extends HTMLElement {
|
|
|
394
903
|
|
|
395
904
|
// Public Methods
|
|
396
905
|
loadConfig(config) {
|
|
397
|
-
|
|
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
|
+
}
|
|
398
915
|
if (!config) {
|
|
916
|
+
this.#logWarn("No config provided");
|
|
399
917
|
return false;
|
|
400
918
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
this.#
|
|
404
|
-
this.#
|
|
405
|
-
this.#
|
|
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;
|
|
406
960
|
}
|
|
407
961
|
|
|
408
962
|
loadModel(model) {
|
|
409
|
-
|
|
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
|
+
}
|
|
410
972
|
if (!model) {
|
|
973
|
+
this.#logWarn("No model payload provided");
|
|
411
974
|
return false;
|
|
412
975
|
}
|
|
413
|
-
this.#model.storage = model.storage || null;
|
|
414
|
-
this.#model.show = model.visible !== undefined ? model.visible : this.#model.show;
|
|
415
|
-
this.#
|
|
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);
|
|
416
983
|
}
|
|
417
984
|
|
|
418
985
|
loadScene(scene) {
|
|
419
|
-
|
|
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
|
+
}
|
|
420
995
|
if (!scene) {
|
|
996
|
+
this.#logWarn("No scene payload provided");
|
|
421
997
|
return false;
|
|
422
998
|
}
|
|
423
|
-
this.#environment.storage = scene.storage || null;
|
|
424
|
-
this.#environment.show = scene.visible !== undefined ? scene.visible : this.#environment.show;
|
|
425
|
-
this.#
|
|
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);
|
|
426
1006
|
}
|
|
427
1007
|
|
|
428
1008
|
showModel() {
|
|
429
|
-
this.#model.show = true;
|
|
430
|
-
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");
|
|
431
1012
|
}
|
|
432
1013
|
|
|
433
1014
|
hideModel() {
|
|
434
|
-
this.#model.show = false;
|
|
435
|
-
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");
|
|
436
1018
|
}
|
|
437
1019
|
|
|
438
1020
|
showScene() {
|
|
439
|
-
this.#environment.show = true;
|
|
440
|
-
this.#addContainer(this.#environment);
|
|
1021
|
+
this.#data.containers.environment.show = true;
|
|
1022
|
+
this.#addContainer(this.#data.containers.environment);
|
|
441
1023
|
this.#setVisibilityOfWallAndFloorInModel();
|
|
1024
|
+
this.#logInfo("Scene visibility set to true");
|
|
442
1025
|
}
|
|
443
1026
|
|
|
444
1027
|
hideScene() {
|
|
445
|
-
this.#environment.show = false;
|
|
446
|
-
this.#removeContainer(this.#environment);
|
|
1028
|
+
this.#data.containers.environment.show = false;
|
|
1029
|
+
this.#removeContainer(this.#data.containers.environment);
|
|
447
1030
|
this.#setVisibilityOfWallAndFloorInModel();
|
|
1031
|
+
this.#logInfo("Scene visibility set to false");
|
|
448
1032
|
}
|
|
449
1033
|
|
|
450
1034
|
downloadModelGLB() {
|
|
451
1035
|
const fileName = "model";
|
|
452
|
-
|
|
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 });
|
|
453
1039
|
glb.downloadFiles();
|
|
454
1040
|
});
|
|
455
1041
|
}
|
|
456
1042
|
|
|
457
1043
|
downloadModelUSDZ() {
|
|
458
1044
|
const fileName = "model";
|
|
459
|
-
|
|
1045
|
+
this.#logInfo("Initiating USDZ download for model", { fileName });
|
|
1046
|
+
USDZExportAsync(this.#data.containers.model.assetContainer).then((response) => {
|
|
460
1047
|
if (response) {
|
|
461
1048
|
Tools.Download(new Blob([response], { type: "model/vnd.usdz+zip" }), `${fileName}.usdz`);
|
|
1049
|
+
this.#logDebug("Model USDZ export ready", { fileName });
|
|
462
1050
|
}
|
|
463
1051
|
});
|
|
464
1052
|
}
|
|
465
1053
|
|
|
466
1054
|
downloadModelAndSceneUSDZ() {
|
|
467
1055
|
const fileName = "scene";
|
|
1056
|
+
this.#logInfo("Initiating USDZ download for scene", { fileName });
|
|
468
1057
|
USDZExportAsync(this.#scene).then((response) => {
|
|
469
1058
|
if (response) {
|
|
470
1059
|
Tools.Download(new Blob([response], { type: "model/vnd.usdz+zip" }), `${fileName}.usdz`);
|
|
1060
|
+
this.#logDebug("Scene USDZ export ready", { fileName });
|
|
471
1061
|
}
|
|
472
1062
|
});
|
|
473
1063
|
}
|
|
474
1064
|
|
|
475
1065
|
downloadModelAndSceneGLB() {
|
|
476
1066
|
const fileName = "scene";
|
|
1067
|
+
this.#logInfo("Initiating GLB download for scene", { fileName });
|
|
477
1068
|
GLTF2Export.GLBAsync(this.#scene, fileName, { exportWithoutWaitingForScene: true }).then((glb) => {
|
|
1069
|
+
this.#logDebug("Scene GLB export ready", { fileName });
|
|
478
1070
|
glb.downloadFiles();
|
|
479
1071
|
});
|
|
480
1072
|
}
|