@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.
- package/package.json +1 -1
- package/src/index.js +729 -113
package/package.json
CHANGED
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
|
|
10
|
-
* • Supports loading via remote
|
|
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
|
|
14
|
+
* • Emits 'model-loaded' and 'model-error' events for integration.
|
|
13
15
|
*
|
|
14
16
|
* Usage
|
|
15
17
|
* -----
|
|
16
|
-
* Load from
|
|
18
|
+
* Load model from IndexedDB:
|
|
17
19
|
* ```html
|
|
18
20
|
* <pref-viewer
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
-
*
|
|
28
|
-
*
|
|
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
|
-
#
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
#
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
#
|
|
199
|
-
|
|
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.#
|
|
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)
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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(
|
|
297
|
-
if (
|
|
298
|
-
|
|
299
|
-
|
|
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(
|
|
304
|
-
if (
|
|
305
|
-
|
|
306
|
-
|
|
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(
|
|
311
|
-
this.#
|
|
312
|
-
|
|
313
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
365
|
-
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);
|
|
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.#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|