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