@preference-sl/pref-viewer 2.10.0-beta.2 → 2.10.0-beta.20

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preference-sl/pref-viewer",
3
- "version": "2.10.0-beta.2",
3
+ "version": "2.10.0-beta.20",
4
4
  "description": "Web Component to preview GLTF models with Babylon.js",
5
5
  "author": "Alex Moreno Palacio <amoreno@preference.es>",
6
6
  "scripts": {
@@ -1,137 +1,241 @@
1
- // Inicializar IndexedDB
2
- export async function initDb(dbName, storeName) {
1
+ // wwwroot/js/gltf-storage.js
2
+
3
+ // Public, inspectable namespace
4
+ const PC = (globalThis.PrefConfigurator ??= {});
5
+ PC.version = PC.version ?? "1.0.1"; // bump if your schema changes!!
6
+ PC.db = PC.db ?? null;
7
+
8
+ function _getDbOrThrow() {
9
+ const db = PC.db;
10
+ if (!db) throw new Error("Database not initialized. Call initDb(...) first.");
11
+ return db;
12
+ }
13
+
14
+ // --- internal helpers -------------------------------------------------------
15
+
16
+ function _createStoreIfMissing(db, storeName) {
17
+ if (!db.objectStoreNames.contains(storeName)) {
18
+ const store = db.createObjectStore(storeName, { keyPath: "id" });
19
+ store.createIndex("expirationTimeStamp", "expirationTimeStamp", { unique: false });
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Opens a DB without specifying a version. If the required object store doesn't
25
+ * exist, it re-opens with (currentVersion + 1) to create it.
26
+ */
27
+ function _openEnsuringStore(dbName, storeName) {
3
28
  return new Promise((resolve, reject) => {
4
- const request = indexedDB.open(dbName, 1);
29
+ const open = indexedDB.open(dbName);
30
+
31
+ open.onblocked = () => reject(new Error("Open blocked by another tab or worker"));
32
+ open.onerror = () => reject(open.error);
5
33
 
6
- request.onerror = () => reject(request.error);
7
- request.onsuccess = () => {
8
- window.gltfDB = request.result;
9
- resolve();
34
+ open.onupgradeneeded = (e) => {
35
+ // First creation path (brand-new DB)
36
+ const db = e.target.result;
37
+ _createStoreIfMissing(db, storeName);
10
38
  };
11
39
 
12
- request.onupgradeneeded = (event) => {
13
- const db = event.target.result;
14
- if (!db.objectStoreNames.contains(storeName)) {
15
- const store = db.createObjectStore(storeName, { keyPath: "id" });
16
- store.createIndex("type", "type", { unique: false });
17
- store.createIndex("timestamp", "timestamp", { unique: false });
40
+ open.onsuccess = (e) => {
41
+ const db = e.target.result;
42
+
43
+ // If the store exists, we're done.
44
+ if (db.objectStoreNames.contains(storeName)) {
45
+ resolve(db);
46
+ return;
18
47
  }
48
+
49
+ // Otherwise bump version just enough to add the store.
50
+ const nextVersion = db.version + 1;
51
+ db.close();
52
+
53
+ const upgrade = indexedDB.open(dbName, nextVersion);
54
+ upgrade.onblocked = () => reject(new Error("Upgrade blocked by another tab or worker"));
55
+ upgrade.onerror = () => reject(upgrade.error);
56
+ upgrade.onupgradeneeded = (ev) => {
57
+ const upDb = ev.target.result;
58
+ _createStoreIfMissing(upDb, storeName);
59
+ };
60
+ upgrade.onsuccess = (ev) => resolve(ev.target.result);
19
61
  };
20
62
  });
21
63
  }
22
64
 
65
+ // --- public API -------------------------------------------------------------
66
+
67
+ // Inicializar IndexedDB y dejar el handle en PrefConfigurator.db (público)
68
+ export async function initDb(dbName, storeName) {
69
+ const db = await _openEnsuringStore(dbName, storeName);
70
+ // Close any previous handle to avoid versionchange blocking
71
+ try { PC.db?.close?.(); } catch { }
72
+ PC.db = db;
73
+
74
+ // If another tab upgrades, close gracefully so next call can re-init
75
+ db.onversionchange = () => {
76
+ try { db.close(); } catch { }
77
+ if (PC.db === db) PC.db = null;
78
+ console.warn("[PrefConfigurator] DB connection closed due to versionchange. Re-run initDb().");
79
+ };
80
+ }
81
+
23
82
  // Guardar modelo
24
83
  export async function saveModel(modelDataStr, storeName) {
25
84
  return new Promise((resolve, reject) => {
26
- if (!window.gltfDB) {
27
- reject(new Error("Database not initialized"));
28
- return;
29
- }
30
- let modelData = JSON.parse(modelDataStr);
85
+ let db;
86
+ try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
87
+
88
+ let modelData;
89
+ try { modelData = JSON.parse(modelDataStr); }
90
+ catch (e) { reject(new Error("saveModel: modelDataStr is not valid JSON")); return; }
31
91
 
32
92
  const dataToStore = {
33
93
  ...modelData,
34
94
  data: modelData.data,
35
- size: modelData.data.length,
36
- timestamp: new Date().toISOString(),
95
+ size: (modelData?.data?.length ?? 0)
37
96
  };
38
97
 
39
- const transaction = window.gltfDB.transaction([storeName], "readwrite");
40
- const store = transaction.objectStore(storeName);
41
- const request = store.put(dataToStore);
98
+ const tx = db.transaction([storeName], "readwrite");
99
+ const store = tx.objectStore(storeName);
100
+ const req = store.put(dataToStore);
42
101
 
43
- request.onerror = () => reject(request.error);
44
- request.onsuccess = () => resolve();
102
+ req.onerror = () => reject(req.error);
103
+ req.onsuccess = () => resolve();
45
104
  });
46
105
  }
47
106
 
48
107
  // Cargar modelo
49
108
  export function loadModel(modelId, storeName) {
50
109
  return new Promise((resolve, reject) => {
51
- if (!globalThis.gltfDB) {
52
- reject(new Error("Database not initialized"));
53
- return;
54
- }
55
- const tx = globalThis.gltfDB.transaction([storeName], "readonly");
110
+ let db;
111
+ try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
112
+
113
+ const tx = db.transaction([storeName], "readonly");
56
114
  const store = tx.objectStore(storeName);
57
115
  const req = store.get(modelId);
116
+
58
117
  req.onerror = () => reject(req.error);
59
118
  req.onsuccess = () => resolve(req.result ?? null);
60
119
  });
61
120
  }
62
121
 
122
+ // Descargar archivo desde base64
63
123
  export function downloadFileFromBytes(fileName, bytesBase64, mimeType) {
64
124
  const link = document.createElement("a");
65
125
  link.download = fileName;
66
126
  link.href = `data:${mimeType};base64,${bytesBase64}`;
67
127
  document.body.appendChild(link);
68
128
  link.click();
69
- document.body.removeChild(link);
129
+ link.remove();
70
130
  }
71
131
 
72
132
  // Obtener todos los modelos (solo metadata)
73
133
  export async function getAllModels(storeName) {
74
134
  return new Promise((resolve, reject) => {
75
- if (!window.gltfDB) {
76
- reject(new Error("Database not initialized"));
77
- return;
78
- }
135
+ let db;
136
+ try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
79
137
 
80
- const transaction = window.gltfDB.transaction([storeName], "readonly");
81
- const store = transaction.objectStore(storeName);
82
- const request = store.getAll();
138
+ const tx = db.transaction([storeName], "readonly");
139
+ const store = tx.objectStore(storeName);
140
+ const req = store.getAll();
83
141
 
84
- request.onerror = () => reject(request.error);
85
- request.onsuccess = () => {
86
- // Excluir los datos binarios para evitar transferir demasiados datos
87
- const results = request.result.map((item) => ({
142
+ req.onerror = () => reject(req.error);
143
+ req.onsuccess = () => {
144
+ const items = Array.isArray(req.result) ? req.result : [];
145
+ const results = items.map(item => ({
88
146
  id: item.id,
89
147
  metadata: item.metadata,
90
- timestamp: item.timestamp,
91
- size: item.size,
92
- type: item.type,
148
+ timeStamp: item.timeStamp,
149
+ size: item.size
93
150
  }));
151
+ // keep old behavior: return JSON string
94
152
  resolve(JSON.stringify(results));
95
153
  };
96
154
  });
97
155
  }
98
156
 
99
- // Eliminar modelo
157
+ // Eliminar modelo por id
100
158
  export async function deleteModel(modelId, storeName) {
101
159
  return new Promise((resolve, reject) => {
102
- if (!window.gltfDB) {
103
- reject(new Error("Database not initialized"));
104
- return;
105
- }
160
+ let db;
161
+ try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
106
162
 
107
- const transaction = window.gltfDB.transaction([storeName], "readwrite");
108
- const store = transaction.objectStore(storeName);
109
- const request = store.delete(modelId);
163
+ const tx = db.transaction([storeName], "readwrite");
164
+ const store = tx.objectStore(storeName);
165
+ const req = store.delete(modelId);
110
166
 
111
- request.onerror = () => reject(request.error);
112
- request.onsuccess = () => resolve();
167
+ req.onerror = () => reject(req.error);
168
+ req.onsuccess = () => resolve();
113
169
  });
114
170
  }
115
171
 
116
- // Limpiar toda la base de datos
172
+ // Limpiar toda la store
117
173
  export async function clearAll(storeName) {
118
174
  return new Promise((resolve, reject) => {
119
- if (!window.gltfDB) {
120
- reject(new Error("Database not initialized"));
121
- return;
122
- }
175
+ let db;
176
+ try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
123
177
 
124
- const transaction = window.gltfDB.transaction([storeName], "readwrite");
125
- const store = transaction.objectStore(storeName);
126
- const request = store.clear();
178
+ const tx = db.transaction([storeName], "readwrite");
179
+ const store = tx.objectStore(storeName);
180
+ const req = store.clear();
127
181
 
128
- request.onerror = () => reject(request.error);
129
- request.onsuccess = () => resolve();
182
+ req.onerror = () => reject(req.error);
183
+ req.onsuccess = () => resolve();
130
184
  });
131
185
  }
132
186
 
187
+ // Borrar modelos expirados usando el índice "expirationTimeStamp"
188
+ export async function cleanExpiredModels(storeName) {
189
+ return new Promise((resolve, reject) => {
190
+ let db;
191
+ try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
192
+
193
+ const tx = db.transaction([storeName], "readwrite");
194
+ const store = tx.objectStore(storeName);
195
+
196
+ const index = store.index("expirationTimeStamp");
197
+ const now = Date.now();
198
+ const range = IDBKeyRange.upperBound(now);
199
+ const cursorRequest = index.openCursor(range);
200
+
201
+ cursorRequest.onerror = () => reject(cursorRequest.error);
202
+ cursorRequest.onsuccess = (event) => {
203
+ const cursor = event.target.result;
204
+ if (cursor) {
205
+ cursor.delete();
206
+ cursor.continue();
207
+ }
208
+ };
209
+
210
+ tx.oncomplete = () => resolve();
211
+ tx.onerror = () => reject(tx.error);
212
+ });
213
+ }
214
+
215
+ // Utilidades opcionales y visibles (por si las quieres usar en consola)
216
+ export function closeDb() {
217
+ if (PC.db) {
218
+ try { PC.db.close(); } catch { }
219
+ PC.db = null;
220
+ }
221
+ }
222
+
223
+ export function deleteDatabase(dbName) {
224
+ return new Promise((resolve, reject) => {
225
+ // Close current handle if points to this DB
226
+ if (PC.db && PC.db.name === dbName) {
227
+ try { PC.db.close(); } catch { }
228
+ PC.db = null;
229
+ }
230
+ const req = indexedDB.deleteDatabase(dbName);
231
+ req.onblocked = () => reject(new Error("Delete blocked by another tab or worker"));
232
+ req.onerror = () => reject(req.error);
233
+ req.onsuccess = () => resolve();
234
+ });
235
+ }
236
+
237
+ // Attach a frozen, public API (no private state)
133
238
  (function attachPublicAPI(global) {
134
- const root = (global.PrefConfigurator ??= {});
135
239
  const storage = {
136
240
  initDb,
137
241
  saveModel,
@@ -139,10 +243,12 @@ export async function clearAll(storeName) {
139
243
  getAllModels,
140
244
  deleteModel,
141
245
  clearAll,
246
+ cleanExpiredModels,
142
247
  downloadFileFromBytes,
248
+ // extras
249
+ closeDb,
250
+ deleteDatabase,
143
251
  };
144
-
145
- // versionado del módulo público
146
- root.version = root.version ?? "1.0.0";
147
- root.storage = Object.freeze(storage);
252
+ // free to inspect PC.db in devtools
253
+ global.PrefConfigurator.storage = Object.freeze(storage);
148
254
  })(globalThis);
package/src/index.js CHANGED
@@ -39,7 +39,7 @@
39
39
  * </pref-viewer>
40
40
  * ```
41
41
  */
42
- import { Engine, Scene, ArcRotateCamera, Vector3, Color4, HemisphericLight, DirectionalLight, PointLight, ShadowGenerator, LoadAssetContainerAsync, Tools, WebXRSessionManager, WebXRDefaultExperience } from "@babylonjs/core";
42
+ import { Engine, Scene, ArcRotateCamera, Vector3, Color4, HemisphericLight, DirectionalLight, PointLight, ShadowGenerator, LoadAssetContainerAsync, Tools, WebXRSessionManager, WebXRDefaultExperience, MeshBuilder, WebXRFeatureName } from "@babylonjs/core";
43
43
  import "@babylonjs/loaders";
44
44
  import { USDZExportAsync, GLTF2Export } from "@babylonjs/serializers";
45
45
  import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_draco_mesh_compression";
@@ -47,22 +47,76 @@ import { DracoCompression } from "@babylonjs/core/Meshes/Compression/dracoCompre
47
47
  import { initDb, loadModel } from "./gltf-storage.js";
48
48
 
49
49
  class PrefViewer extends HTMLElement {
50
- #initialized = false;
51
-
52
- #model = {
53
- container: null,
54
- show: true, // Show model by default
55
- storage: null,
56
- visible: false,
57
- };
58
-
59
- #environment = {
60
- container: null,
61
- show: true, // Show environment by default
62
- storage: null,
63
- visible: false,
50
+ initialized = false;
51
+ loaded = false;
52
+ loading = false;
53
+
54
+ #data = {
55
+ containers: {
56
+ model: {
57
+ name: "model",
58
+ container: null,
59
+ show: true,
60
+ storage: null,
61
+ visible: false,
62
+ size: null,
63
+ timeStamp: null,
64
+ changed: false,
65
+ },
66
+ environment: {
67
+ name: "environment",
68
+ container: null,
69
+ show: true,
70
+ storage: null,
71
+ visible: false,
72
+ size: null,
73
+ timeStamp: null,
74
+ changed: false,
75
+ },
76
+ materials: {
77
+ name: "materials",
78
+ container: null,
79
+ storage: null,
80
+ show: true,
81
+ visible: false,
82
+ size: null,
83
+ timeStamp: null,
84
+ changed: false,
85
+ },
86
+ },
87
+ options: {
88
+ camera: {
89
+ value: null,
90
+ locked: true,
91
+ changed: false,
92
+ },
93
+ materials: {
94
+ innerWall: {
95
+ value: null,
96
+ prefix: "innerWall",
97
+ changed: false,
98
+ },
99
+ outerWall: {
100
+ value: null,
101
+ prefix: "outerWall",
102
+ changed: false,
103
+ },
104
+ innerFloor: {
105
+ value: null,
106
+ prefix: "innerFloor",
107
+ changed: false,
108
+ },
109
+ outerFloor: {
110
+ value: null,
111
+ prefix: "outerFloor",
112
+ changed: false,
113
+ },
114
+ },
115
+ },
64
116
  };
65
117
 
118
+ // DOM elements
119
+ #wrapper = null;
66
120
  #canvas = null;
67
121
 
68
122
  // Babylon.js core objects
@@ -110,18 +164,18 @@ class PrefViewer extends HTMLElement {
110
164
  break;
111
165
  case "show-model":
112
166
  data = value.toLowerCase?.() === "true";
113
- if (this.#initialized) {
167
+ if (this.initialized) {
114
168
  data ? this.showModel() : this.hideModel();
115
169
  } else {
116
- this.#model.show = data;
170
+ this.#data.containers.model.show = data;
117
171
  }
118
172
  break;
119
173
  case "show-scene":
120
174
  data = value.toLowerCase?.() === "true";
121
- if (this.#initialized) {
175
+ if (this.initialized) {
122
176
  data ? this.showScene() : this.hideScene();
123
177
  } else {
124
- this.#environment.show = data;
178
+ this.#data.containers.environment.show = data;
125
179
  }
126
180
  break;
127
181
  }
@@ -132,18 +186,19 @@ class PrefViewer extends HTMLElement {
132
186
  const error = 'PrefViewer: provide "models" as array of model and environment';
133
187
  console.error(error);
134
188
  this.dispatchEvent(
135
- new CustomEvent("model-error", {
136
- detail: { error: new Error(error) },
189
+ new CustomEvent("scene-error", {
137
190
  bubbles: true,
191
+ cancelable: false,
138
192
  composed: true,
193
+ detail: { error: new Error(error) },
139
194
  })
140
195
  );
141
196
  return false;
142
197
  }
143
198
 
144
199
  this.#initializeBabylon();
145
- this.#loadContainers(true, true);
146
- this.#initialized = true;
200
+ this.initialized = true;
201
+ this.#loadContainers(true, true, true);
147
202
  }
148
203
 
149
204
  disconnectedCallback() {
@@ -163,36 +218,184 @@ class PrefViewer extends HTMLElement {
163
218
  }
164
219
 
165
220
  #wrapCanvas() {
166
- const wrapper = document.createElement("div");
167
- Object.assign(wrapper.style, {
221
+ this.#wrapper = document.createElement("div");
222
+ Object.assign(this.#wrapper.style, {
168
223
  width: "100%",
169
224
  height: "100%",
170
225
  position: "relative",
171
226
  });
172
- wrapper.appendChild(this.#canvas);
173
- this.shadowRoot.append(wrapper);
227
+ this.#wrapper.appendChild(this.#canvas);
228
+ this.shadowRoot.append(this.#wrapper);
229
+ }
230
+
231
+ #setStatusSceneLoading() {
232
+ this.loaded = false;
233
+ this.loading = true;
234
+ if (this.hasAttribute("loaded")) {
235
+ this.removeAttribute("loaded");
236
+ }
237
+ this.setAttribute("loading", "");
238
+ this.dispatchEvent(
239
+ new CustomEvent("scene-loading", {
240
+ bubbles: true,
241
+ cancelable: false,
242
+ composed: true,
243
+ })
244
+ );
245
+ }
246
+
247
+ #setStatusSceneLoaded() {
248
+ this.loaded = true;
249
+ this.loading = false;
250
+
251
+ const toLoadDetail = {
252
+ container_model: !!this.#data.containers.model.changed,
253
+ container_environment: !!this.#data.containers.environment.changed,
254
+ container_materials: !!this.#data.containers.materials.changed,
255
+ options_camera: !!this.#data.options.camera.changed,
256
+ options_innerWallMaterial: !!this.#data.options.materials.innerWall.changed,
257
+ options_outerWallMaterial: !!this.#data.options.materials.outerWall.changed,
258
+ options_innerFloorMaterial: !!this.#data.options.materials.innerFloor.changed,
259
+ options_outerFloorMaterial: !!this.#data.options.materials.outerFloor.changed,
260
+ };
261
+ const loadedDetail = {
262
+ container_model: !!this.#data.containers.model.changed?.success,
263
+ container_environment: !!this.#data.containers.environment.changed?.success,
264
+ container_materials: !!this.#data.containers.materials.changed?.success,
265
+ options_camera: !!this.#data.options.camera.changed?.success,
266
+ options_innerWallMaterial: !!this.#data.options.materials.innerWall.changed?.success,
267
+ options_outerWallMaterial: !!this.#data.options.materials.outerWall.changed?.success,
268
+ options_innerFloorMaterial: !!this.#data.options.materials.innerFloor.changed?.success,
269
+ options_outerFloorMaterial: !!this.#data.options.materials.outerFloor.changed?.success,
270
+ };
271
+
272
+ const detail = {
273
+ tried: toLoadDetail,
274
+ success: loadedDetail,
275
+ };
276
+
277
+ if (this.hasAttribute("loading")) {
278
+ this.removeAttribute("loading");
279
+ }
280
+ this.setAttribute("loaded", "");
281
+ this.dispatchEvent(
282
+ new CustomEvent("scene-loaded", {
283
+ bubbles: true,
284
+ cancelable: false,
285
+ composed: true,
286
+ detail: detail,
287
+ })
288
+ );
289
+ }
290
+
291
+ #setStatusOptionsLoading() {
292
+ this.dispatchEvent(
293
+ new CustomEvent("options-loading", {
294
+ bubbles: true,
295
+ cancelable: false,
296
+ composed: true,
297
+ })
298
+ );
299
+ }
300
+
301
+ #setStatusOptionsLoaded() {
302
+ const toLoadDetail = {
303
+ innerWallMaterial: !!this.#data.options.materials.innerWall.changed,
304
+ outerWallMaterial: !!this.#data.options.materials.outerWall.changed,
305
+ innerFloorMaterial: !!this.#data.options.materials.innerFloor.changed,
306
+ outerFloorMaterial: !!this.#data.options.materials.outerFloor.changed,
307
+ };
308
+ const loadedDetail = {
309
+ innerWallMaterial: !!this.#data.options.materials.innerWall.changed?.success,
310
+ outerWallMaterial: !!this.#data.options.materials.outerWall.changed?.success,
311
+ innerFloorMaterial: !!this.#data.options.materials.innerFloor.changed?.success,
312
+ outerFloorMaterial: !!this.#data.options.materials.outerFloor.changed?.success,
313
+ };
314
+
315
+ const detail = {
316
+ tried: toLoadDetail,
317
+ success: loadedDetail,
318
+ };
319
+
320
+ this.dispatchEvent(
321
+ new CustomEvent("options-loaded", {
322
+ bubbles: true,
323
+ cancelable: false,
324
+ composed: true,
325
+ detail: detail,
326
+ })
327
+ );
328
+ }
329
+
330
+ // Data
331
+ #checkCameraChanged(options) {
332
+ if (!options || !options.camera) {
333
+ return false;
334
+ }
335
+
336
+ const changed = options.camera !== this.#data.options.camera.value;
337
+ this.#data.options.camera.changed = changed ? { oldValue: this.#data.options.camera.value, oldLocked: this.#data.options.camera.locked, success: false } : false;
338
+ if (changed) this.#data.options.camera.value = options.camera;
339
+
340
+ return changed;
341
+ }
342
+
343
+ #checkMaterialsChanged(options) {
344
+ if (!options) {
345
+ return false;
346
+ }
347
+ let someChanged = false;
348
+ Object.keys(this.#data.options.materials).forEach((material) => {
349
+ const key = `${material}Material`;
350
+ const materialChanged = options[key] && options[key] !== this.#data.options.materials[material].value ? true : false;
351
+ this.#data.options.materials[material].changed = materialChanged ? { oldValue: this.#data.options.materials[material].value, success: false } : false;
352
+ this.#data.options.materials[material].value = materialChanged ? options[key] : this.#data.options.materials[material].value;
353
+ someChanged = someChanged || this.#data.options.materials[material].changed;
354
+ });
355
+ return someChanged;
174
356
  }
175
-
176
- // Bbylon.js
357
+
358
+ #storeChangedFlagsForContainer(container) {
359
+ container.timeStamp = container.changed.timeStamp;
360
+ container.size = container.changed.size;
361
+ container.changed.success = true;
362
+ }
363
+
364
+ #resetChangedFlags() {
365
+ Object.values(this.#data.containers).forEach((container) => (container.changed = false));
366
+ Object.values(this.#data.options.materials).forEach((material) => (material.changed = false));
367
+ this.#data.options.camera.changed = false;
368
+ }
369
+
370
+ // Babylon.js
177
371
  async #initializeBabylon() {
178
372
  this.#engine = new Engine(this.#canvas, true, { alpha: true });
373
+ this.#engine.disableUniformBuffers = true; // <- evita el límite de GL_MAX_*_UNIFORM_BUFFERS // PROVISIONAL, YA QUE ESTO ES UN POCO OVERKILL
374
+
179
375
  this.#scene = new Scene(this.#engine);
180
376
  this.#scene.clearColor = new Color4(1, 1, 1, 1);
181
377
  this.#createCamera();
182
378
  this.#createLights();
183
379
  this.#setupInteraction();
184
-
380
+
185
381
  this.#engine.runRenderLoop(() => this.#scene && this.#scene.render());
186
382
  this.#canvasResizeObserver.observe(this.#canvas);
187
383
 
188
384
  await this.#createXRExperience();
189
385
  }
190
386
 
387
+ addStylesToARButton() {
388
+ 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"}';
389
+ const style = document.createElement("style");
390
+ style.appendChild(document.createTextNode(css));
391
+ this.#wrapper.appendChild(style);
392
+ }
393
+
191
394
  async #createXRExperience() {
192
395
  if (this.#XRExperience) {
193
396
  return true;
194
397
  }
195
-
398
+
196
399
  const sessionMode = "immersive-ar";
197
400
  const sessionSupported = await WebXRSessionManager.IsSessionSupportedAsync(sessionMode);
198
401
  if (!sessionSupported) {
@@ -200,16 +403,37 @@ class PrefViewer extends HTMLElement {
200
403
  return false;
201
404
  }
202
405
 
203
- const options = {
204
- uiOptions: {
205
- sessionMode: sessionMode,
206
- renderTarget: "xrLayer",
207
- referenceSpaceType: "local",
208
- },
209
- };
210
-
211
406
  try {
212
- this.#XRExperience = await WebXRDefaultExperience.CreateAsync(this.#scene, options);
407
+ const ground = MeshBuilder.CreateGround("ground", { width: 1000, height: 1000 }, this.#scene);
408
+ ground.isVisible = false;
409
+
410
+ const options = {
411
+ floorMeshes: [ground],
412
+ uiOptions: {
413
+ sessionMode: sessionMode,
414
+ renderTarget: "xrLayer",
415
+ referenceSpaceType: "local",
416
+ },
417
+ optionalFeatures: true,
418
+ };
419
+
420
+ this.#XRExperience = await WebXRDefaultExperience.CreateAsync(this.#scene, options);
421
+
422
+ const featuresManager = this.#XRExperience.baseExperience.featuresManager;
423
+ featuresManager.enableFeature(WebXRFeatureName.TELEPORTATION, "stable", {
424
+ xrInput: this.#XRExperience.input,
425
+ floorMeshes: [ground],
426
+ timeToTeleport: 1500,
427
+ });
428
+
429
+ this.#XRExperience.baseExperience.sessionManager.onXRReady.add(() => {
430
+ // Set the initial position of xrCamera: use nonVRCamera, which contains a copy of the original this.#scene.activeCamera before entering XR
431
+ this.#XRExperience.baseExperience.camera.setTransformationFromNonVRCamera(this.#XRExperience.baseExperience._nonVRCamera);
432
+ this.#XRExperience.baseExperience.camera.setTarget(Vector3.Zero());
433
+ this.#XRExperience.baseExperience.onInitialXRPoseSetObservable.notifyObservers(this.#XRExperience.baseExperience.camera);
434
+ });
435
+
436
+ this.addStylesToARButton();
213
437
  } catch (error) {
214
438
  console.warn("PrefViewer: failed to create WebXR experience", error);
215
439
  this.#XRExperience = null;
@@ -219,12 +443,14 @@ class PrefViewer extends HTMLElement {
219
443
  #canvasResizeObserver = new ResizeObserver(() => this.#engine && this.#engine.resize());
220
444
 
221
445
  #createCamera() {
222
- this.#camera = new ArcRotateCamera("camera", 3 * Math.PI / 2, Math.PI * 0.47, 10, Vector3.Zero(), this.#scene);
446
+ this.#camera = new ArcRotateCamera("camera", (3 * Math.PI) / 2, Math.PI * 0.47, 10, Vector3.Zero(), this.#scene);
223
447
  this.#camera.upperBetaLimit = Math.PI * 0.48;
224
448
  this.#camera.lowerBetaLimit = Math.PI * 0.25;
225
449
  this.#camera.lowerRadiusLimit = 5;
226
450
  this.#camera.upperRadiusLimit = 20;
451
+ this.#camera.metadata = { locked: false }
227
452
  this.#camera.attachControl(this.#canvas, true);
453
+ this.#scene.activeCamera = this.#camera;
228
454
  }
229
455
 
230
456
  #createLights() {
@@ -251,16 +477,22 @@ class PrefViewer extends HTMLElement {
251
477
 
252
478
  #setupInteraction() {
253
479
  this.#canvas.addEventListener("wheel", (event) => {
254
- if (!this.#scene || !this.#camera) return;
255
- const pick = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
480
+ if (!this.#scene || !this.#camera) {
481
+ return false;
482
+ }
483
+ //const pick = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
256
484
  //this.#camera.target = pick.hit ? pick.pickedPoint.clone() : this.#camera.target;
257
- this.#camera.inertialRadiusOffset -= event.deltaY * this.#camera.wheelPrecision * 0.001;
485
+ if (!this.#scene.activeCamera.metadata?.locked) {
486
+ this.#scene.activeCamera.inertialRadiusOffset -= event.deltaY * this.#scene.activeCamera.wheelPrecision * 0.001;
487
+ }
258
488
  event.preventDefault();
259
489
  });
260
490
  }
261
491
 
262
492
  #disposeEngine() {
263
493
  if (!this.#engine) return;
494
+ this.#shadowGen?.dispose();
495
+ this.#scene?.lights?.slice().forEach(l => l.dispose());
264
496
  this.#engine.dispose();
265
497
  this.#engine = this.#scene = this.#camera = null;
266
498
  this.#hemiLight = this.#dirLight = this.#cameraLight = null;
@@ -268,6 +500,27 @@ class PrefViewer extends HTMLElement {
268
500
  }
269
501
 
270
502
  // Utility methods for loading gltf/glb
503
+ async #getServerFileDataHeader(uri) {
504
+ return new Promise((resolve) => {
505
+ const xhr = new XMLHttpRequest();
506
+ xhr.open("HEAD", uri, true);
507
+ xhr.responseType = "blob";
508
+ xhr.onload = () => {
509
+ if (xhr.status === 200) {
510
+ const size = parseInt(xhr.getResponseHeader("Content-Length"));
511
+ const timeStamp = new Date(xhr.getResponseHeader("Last-Modified")).toISOString();
512
+ resolve([size, timeStamp]);
513
+ } else {
514
+ resolve([0, null]);
515
+ }
516
+ };
517
+ xhr.onerror = () => {
518
+ resolve([0, null]);
519
+ };
520
+ xhr.send();
521
+ });
522
+ }
523
+
271
524
  #transformUrl(url) {
272
525
  return new Promise((resolve) => {
273
526
  resolve(url.replace(/^blob:[^/]+\//i, "").replace(/\\/g, "/"));
@@ -280,21 +533,22 @@ class PrefViewer extends HTMLElement {
280
533
  let decoded = "";
281
534
  let blob = null;
282
535
  let extension = null;
536
+ let size = raw.length;
283
537
  try {
284
538
  decoded = atob(raw);
285
539
  } catch {
286
- return { blob, extension };
540
+ return { blob, extension, size };
287
541
  }
288
542
  let isJson = false;
289
543
  try {
290
544
  JSON.parse(decoded);
291
545
  isJson = true;
292
- } catch {}
546
+ } catch { }
293
547
  extension = isJson ? ".gltf" : ".glb";
294
548
  const type = isJson ? "model/gltf+json" : "model/gltf-binary";
295
549
  const array = Uint8Array.from(decoded, (c) => c.charCodeAt(0));
296
550
  blob = new Blob([array], { type });
297
- return { blob, extension };
551
+ return { blob, extension, size };
298
552
  }
299
553
 
300
554
  async #initStorage(db, table) {
@@ -306,41 +560,141 @@ class PrefViewer extends HTMLElement {
306
560
 
307
561
  // Methods for managing Asset Containers
308
562
  #setVisibilityOfWallAndFloorInModel(show) {
309
- if (this.#model.container && this.#model.visible) {
310
- const names = ["outer_0", "inner_1", "outerFloor", "innerFloor"];
311
- const nodes = this.#model.container.getNodes();
312
- this.#model.container
313
- .getNodes()
314
- .filter((filter) => names.includes(filter.name))
315
- .forEach((node) => node.setEnabled(show !== undefined ? show : this.#environment.show));
563
+ if (!this.#data.containers.model.assetContainer || !this.#data.containers.model.visible) {
564
+ return false;
316
565
  }
566
+ show = show !== undefined ? show : this.#data.containers.environment.visible;
567
+ const prefixes = Object.values(this.#data.options.materials).map((material) => material.prefix);
568
+ this.#data.containers.model.assetContainer.meshes.filter((meshToFilter) => prefixes.some((prefix) => meshToFilter.name.startsWith(prefix))).forEach((mesh) => mesh.setEnabled(show));
317
569
  }
318
570
 
319
- #addContainer(group) {
320
- if (group.container && !group.visible && group.show) {
321
- group.container.addAllToScene();
322
- group.visible = true;
571
+ #setOptionsMaterial(optionMaterial) {
572
+ if (!optionMaterial || !optionMaterial.prefix || !optionMaterial.value) {
573
+ return false;
574
+ }
575
+
576
+ const material = this.#data.containers.materials.assetContainer?.materials.find((mat) => mat.name === optionMaterial.value) || null;
577
+ if (!material) {
578
+ return false;
579
+ }
580
+
581
+ const containers = [];
582
+ if (this.#data.containers.model.assetContainer && (this.#data.containers.model.changed || this.#data.containers.materials.changed || optionMaterial.changed)) {
583
+ containers.push(this.#data.containers.model.assetContainer);
584
+ }
585
+ if (this.#data.containers.environment.assetContainer && (this.#data.containers.environment.changed || this.#data.containers.materials.changed || optionMaterial.changed)) {
586
+ containers.push(this.#data.containers.environment.assetContainer);
587
+ }
588
+ if (containers.length === 0) {
589
+ return false;
590
+ }
591
+
592
+ let someSetted = false;
593
+ containers.forEach((container) =>
594
+ container.meshes
595
+ .filter((meshToFilter) => meshToFilter.name.startsWith(optionMaterial.prefix))
596
+ .forEach((mesh) => {
597
+ mesh.material = material;
598
+ someSetted = true;
599
+ })
600
+ );
601
+
602
+ if (someSetted) {
603
+ optionMaterial.changed.success = true;
604
+ } else {
605
+ optionMaterial.value = optionMaterial.changed.oldValue;
606
+ }
607
+
608
+ return someSetted;
609
+ }
610
+
611
+ #setOptionsMaterials() {
612
+ let someSetted = false;
613
+ Object.values(this.#data.options.materials).forEach((material) => {
614
+ let settedMaterial = this.#setOptionsMaterial(material);
615
+ someSetted = someSetted || settedMaterial;
616
+ });
617
+ return someSetted;
618
+ }
619
+
620
+ #setOptionsCamera() {
621
+ if (!this.#data.options.camera.value || (!this.#data.options.camera.changed && !this.#data.containers.model.changed && !this.#data.containers.environment.changed)) {
622
+ return false;
623
+ }
624
+
625
+ 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;
626
+ if (!camera) {
627
+ if (this.#data.options.camera.changed?.oldValue && this.#data.options.camera.changed?.oldValue !== this.#data.options.camera.value) {
628
+ camera = this.#data.containers.model.assetContainer?.cameras.find((thisCamera) => thisCamera.name === this.#data.options.camera.changed.oldValue) || this.#data.containers.environment.assetContainer?.cameras.find((thisCamera) => thisCamera.name === this.#data.options.camera.changed.oldValue) || null;
629
+ }
630
+ if (camera) {
631
+ camera.metadata = { locked: this.#data.options.camera.changed.oldLocked };
632
+ this.#data.options.camera.value = this.#data.options.camera.changed.oldValue;
633
+ this.#data.options.camera.locked = this.#data.options.camera.changed.oldLocked;
634
+ } else {
635
+ camera = this.#camera;
636
+ this.#data.options.camera.value = null;
637
+ this.#data.options.camera.locked = this.#camera.metadata.locked;
638
+ }
639
+ this.#data.options.camera.changed.success = false;
640
+ } else {
641
+ camera.metadata = { locked: this.#data.options.camera.locked };
323
642
  }
643
+ if (!this.#data.options.camera.locked && this.#data.options.camera.value !== null) {
644
+ camera.attachControl(this.#canvas, true);
645
+ }
646
+ this.#scene.activeCamera = camera;
647
+ return true;
324
648
  }
325
649
 
326
- #removeContainer(group) {
327
- if (group.container && group.visible) {
328
- group.container.removeAllFromScene();
329
- group.visible = false;
650
+ #addContainer(container) {
651
+ if (container.assetContainer && !container.visible && container.show) {
652
+ container.assetContainer.addAllToScene();
653
+ container.visible = true;
330
654
  }
331
655
  }
332
656
 
333
- #replaceContainer(group, newContainer) {
334
- this.#removeContainer(group);
335
- group.container = newContainer;
336
- group.container.meshes.forEach((mesh) => {
657
+ #removeContainer(container) {
658
+ if (container.assetContainer && container.visible) {
659
+ container.assetContainer.removeAllFromScene();
660
+ container.visible = false;
661
+ }
662
+ }
663
+
664
+ #replaceContainer(container, newAssetContainer) {
665
+ // 1) quita y destruye el anterior si existía
666
+ const old = container.assetContainer;
667
+ if (old) {
668
+ if (container.visible) { old.removeAllFromScene(); }
669
+ old.dispose(); // <- importante
670
+ }
671
+
672
+ // 2) asigna el nuevo y prepara
673
+ container.assetContainer = newAssetContainer;
674
+
675
+ // Opcional: limitar luces por material para ganar margen
676
+ container.assetContainer.materials?.forEach(m => {
677
+ if ("maxSimultaneousLights" in m) {
678
+ m.maxSimultaneousLights = 2; // 2–3 suele ir bien
679
+ }
680
+ });
681
+
682
+ // 3) sombras solo para los meshes que te interesen (mejor que todos)
683
+ container.assetContainer.meshes.forEach(mesh => {
337
684
  mesh.receiveShadows = true;
338
685
  this.#shadowGen.addShadowCaster(mesh, true);
339
686
  });
340
- this.#addContainer(group);
687
+
688
+ // 4) añade a escena
689
+ this.#addContainer(container);
690
+
691
+ // 5) fuerza recompilación con defines correctos del nuevo estado
692
+ this.#scene.getEngine().releaseEffects();
341
693
  }
342
694
 
343
- async #loadAssetContainer(storage) {
695
+ async #loadAssetContainer(container) {
696
+ let storage = container?.storage;
697
+
344
698
  if (!storage) {
345
699
  return false;
346
700
  }
@@ -351,6 +705,11 @@ class PrefViewer extends HTMLElement {
351
705
  await this.#initStorage(storage.db, storage.table);
352
706
  const object = await loadModel(storage.id, storage.table);
353
707
  source = object.data;
708
+ if (object.timeStamp === container.timeStamp) {
709
+ return false;
710
+ } else {
711
+ container.changed = { timeStamp: object.timeStamp, size: object.size, success: false };
712
+ }
354
713
  }
355
714
 
356
715
  if (!source) {
@@ -359,20 +718,34 @@ class PrefViewer extends HTMLElement {
359
718
 
360
719
  let file = null;
361
720
 
362
- let { blob, extension } = this.#decodeBase64(source);
721
+ let { blob, extension, size } = this.#decodeBase64(source);
363
722
  if (blob && extension) {
364
- file = new File([blob], `model${extension}`, {
723
+ file = new File([blob], `${container.name}${extension}`, {
365
724
  type: blob.type,
366
725
  });
726
+ if (!container.changed) {
727
+ if (container.timeStamp === null && container.size === size) {
728
+ return false;
729
+ } else {
730
+ container.changed = { timeStamp: null, size: size, success: false };
731
+ }
732
+ }
367
733
  } else {
368
734
  const extMatch = source.match(/\.(gltf|glb)(\?|#|$)/i);
369
735
  extension = extMatch ? `.${extMatch[1].toLowerCase()}` : ".gltf";
736
+ const [fileSize, fileTimeStamp] = await this.#getServerFileDataHeader(source);
737
+ if (container.size === fileSize && container.timeStamp === fileTimeStamp) {
738
+ return false;
739
+ } else {
740
+ container.changed = { timeStamp: fileTimeStamp, size: fileSize, success: false };
741
+ }
370
742
  }
371
743
 
372
744
  let options = {
373
745
  pluginExtension: extension,
374
746
  pluginOptions: {
375
747
  gltf: {
748
+ loadAllMaterials: true,
376
749
  preprocessUrlAsync: this.#transformUrl,
377
750
  },
378
751
  },
@@ -381,58 +754,112 @@ class PrefViewer extends HTMLElement {
381
754
  return LoadAssetContainerAsync(file || source, this.#scene, options);
382
755
  }
383
756
 
384
- async #loadContainers(loadModel = true, loadEnvironment = true) {
757
+ async #loadContainers(loadModel = true, loadEnvironment = true, loadMaterials = true) {
385
758
  const promiseArray = [];
759
+ promiseArray.push(loadModel ? this.#loadAssetContainer(this.#data.containers.model) : false);
760
+ promiseArray.push(loadEnvironment ? this.#loadAssetContainer(this.#data.containers.environment) : false);
761
+ promiseArray.push(loadMaterials ? this.#loadAssetContainer(this.#data.containers.materials) : false);
386
762
 
387
- promiseArray.push(loadModel ? this.#loadAssetContainer(this.#model.storage) : false);
388
- promiseArray.push(loadEnvironment ? this.#loadAssetContainer(this.#environment.storage) : false);
763
+ this.#setStatusSceneLoading();
389
764
 
390
765
  Promise.allSettled(promiseArray)
391
766
  .then(async (values) => {
392
767
  const modelContainer = values[0];
393
768
  const environmentContainer = values[1];
769
+ const materialsContainer = values[2];
394
770
 
395
771
  if (modelContainer.status === "fulfilled" && modelContainer.value) {
396
- this.#replaceContainer(this.#model, modelContainer.value);
772
+ this.#stripImportedLights(modelContainer.value);
773
+ this.#replaceContainer(this.#data.containers.model, modelContainer.value);
774
+ this.#storeChangedFlagsForContainer(this.#data.containers.model);
775
+ } else {
776
+ this.#data.containers.model.show ? this.#addContainer(this.#data.containers.model) : this.#removeContainer(this.#data.containers.model);
397
777
  }
398
778
 
399
779
  if (environmentContainer.status === "fulfilled" && environmentContainer.value) {
400
- this.#replaceContainer(this.#environment, environmentContainer.value);
780
+ this.#stripImportedLights(environmentContainer.value);
781
+ this.#replaceContainer(this.#data.containers.environment, environmentContainer.value);
782
+ this.#storeChangedFlagsForContainer(this.#data.containers.environment);
783
+ } else {
784
+ this.#data.containers.environment.show ? this.#addContainer(this.#data.containers.environment) : this.#removeContainer(this.#data.containers.environment);
401
785
  }
402
786
 
403
- this.#setVisibilityOfWallAndFloorInModel();
787
+ if (materialsContainer.status === "fulfilled" && materialsContainer.value) {
788
+ this.#stripImportedLights(materialsContainer.value);
789
+ this.#replaceContainer(this.#data.containers.materials, materialsContainer.value);
790
+ this.#storeChangedFlagsForContainer(this.#data.containers.materials);
791
+ }
404
792
 
405
- this.dispatchEvent(
406
- new CustomEvent("model-loaded", {
407
- detail: { success: "" },
408
- bubbles: true,
409
- composed: true,
410
- })
411
- );
793
+ this.#setOptionsMaterials();
794
+ this.#setOptionsCamera();
795
+ this.#setVisibilityOfWallAndFloorInModel();
796
+ this.#setStatusSceneLoaded();
797
+ this.#resetChangedFlags();
412
798
  })
413
799
  .catch((error) => {
800
+ this.loaded = true;
414
801
  console.error("PrefViewer: failed to load model", error);
415
802
  this.dispatchEvent(
416
- new CustomEvent("model-error", {
417
- detail: { error: error },
803
+ new CustomEvent("scene-error", {
418
804
  bubbles: true,
805
+ cancelable: false,
419
806
  composed: true,
807
+ detail: { error: error },
420
808
  })
421
809
  );
422
810
  });
423
811
  }
424
812
 
813
+ #stripImportedLights(container) {
814
+ // El glTF puede traer KHR_lights_punctual: bórralas antes de añadir a la escena
815
+ if (container?.lights?.length) {
816
+ // Clonar para no mutar mientras iteras
817
+ container.lights.slice().forEach(l => l.dispose());
818
+ }
819
+ }
820
+
425
821
  // Public Methods
426
822
  loadConfig(config) {
427
823
  config = typeof config === "string" ? JSON.parse(config) : config;
428
824
  if (!config) {
429
825
  return false;
430
826
  }
431
- this.#model.storage = config.model?.storage || null;
432
- this.#model.show = config.model?.visible !== undefined ? config.model.visible : this.#model.show;
433
- this.#environment.storage = config.scene?.storage || null;
434
- this.#environment.show = config.scene?.visible !== undefined ? config.scene.visible : this.#environment.show;
435
- this.#initialized && this.#loadContainers(true, true);
827
+
828
+ // Containers
829
+ this.#data.containers.model.storage = config.model?.storage || null;
830
+ this.#data.containers.model.show = config.model?.visible !== undefined ? config.model.visible : this.#data.containers.model.show;
831
+ this.#data.containers.environment.storage = config.scene?.storage || null;
832
+ this.#data.containers.environment.show = config.scene?.visible !== undefined ? config.scene.visible : this.#data.containers.environment.show;
833
+ this.#data.containers.materials.storage = config.materials?.storage || null;
834
+
835
+ // Options
836
+ if (config.options) {
837
+ this.#checkCameraChanged(config.options);
838
+ this.#checkMaterialsChanged(config.options);
839
+ }
840
+
841
+ this.initialized && this.#loadContainers(true, true, true);
842
+ }
843
+
844
+ setOptions(options) {
845
+ if (!options) {
846
+ return false;
847
+ }
848
+
849
+ this.#setStatusOptionsLoading();
850
+
851
+ let someSetted = false;
852
+ if (this.#checkCameraChanged(options)) {
853
+ someSetted = someSetted || this.#setOptionsCamera();
854
+ }
855
+ if (this.#checkMaterialsChanged(options)) {
856
+ someSetted = someSetted || this.#setOptionsMaterials();
857
+ }
858
+
859
+ this.#setStatusOptionsLoaded();
860
+ this.#resetChangedFlags();
861
+
862
+ return someSetted;
436
863
  }
437
864
 
438
865
  loadModel(model) {
@@ -440,9 +867,9 @@ class PrefViewer extends HTMLElement {
440
867
  if (!model) {
441
868
  return false;
442
869
  }
443
- this.#model.storage = model.storage || null;
444
- this.#model.show = model.visible !== undefined ? model.visible : this.#model.show;
445
- this.#initialized && this.#loadContainers(true, false);
870
+ this.#data.containers.model.storage = model.storage || null;
871
+ this.#data.containers.model.show = model.visible !== undefined ? model.visible : this.#data.containers.model.show;
872
+ this.initialized && this.#loadContainers(true, false, false);
446
873
  }
447
874
 
448
875
  loadScene(scene) {
@@ -450,43 +877,41 @@ class PrefViewer extends HTMLElement {
450
877
  if (!scene) {
451
878
  return false;
452
879
  }
453
- this.#environment.storage = scene.storage || null;
454
- this.#environment.show = scene.visible !== undefined ? scene.visible : this.#environment.show;
455
- this.#initialized && this.#loadContainers(false, true);
880
+ this.#data.containers.environment.storage = scene.storage || null;
881
+ this.#data.containers.environment.show = scene.visible !== undefined ? scene.visible : this.#data.containers.environment.show;
882
+ this.initialized && this.#loadContainers(false, true, false);
456
883
  }
457
884
 
458
885
  showModel() {
459
- this.#model.show = true;
460
- this.#addContainer(this.#model);
886
+ this.#data.containers.model.show = true;
887
+ this.#addContainer(this.#data.containers.model);
461
888
  }
462
889
 
463
890
  hideModel() {
464
- this.#model.show = false;
465
- this.#removeContainer(this.#model);
891
+ this.#data.containers.model.show = false;
892
+ this.#removeContainer(this.#data.containers.model);
466
893
  }
467
894
 
468
895
  showScene() {
469
- this.#environment.show = true;
470
- this.#addContainer(this.#environment);
896
+ this.#data.containers.environment.show = true;
897
+ this.#addContainer(this.#data.containers.environment);
471
898
  this.#setVisibilityOfWallAndFloorInModel();
472
899
  }
473
900
 
474
901
  hideScene() {
475
- this.#environment.show = false;
476
- this.#removeContainer(this.#environment);
902
+ this.#data.containers.environment.show = false;
903
+ this.#removeContainer(this.#data.containers.environment);
477
904
  this.#setVisibilityOfWallAndFloorInModel();
478
905
  }
479
906
 
480
907
  downloadModelGLB() {
481
908
  const fileName = "model";
482
- GLTF2Export.GLBAsync(this.#model.container, fileName, { exportWithoutWaitingForScene: true }).then((glb) => {
483
- glb.downloadFiles();
484
- });
909
+ GLTF2Export.GLBAsync(this.#data.containers.model.assetContainer, fileName, { exportWithoutWaitingForScene: true }).then((glb) => glb.downloadFiles());
485
910
  }
486
911
 
487
912
  downloadModelUSDZ() {
488
913
  const fileName = "model";
489
- USDZExportAsync(this.#model.container).then((response) => {
914
+ USDZExportAsync(this.#data.containers.model.assetContainer).then((response) => {
490
915
  if (response) {
491
916
  Tools.Download(new Blob([response], { type: "model/vnd.usdz+zip" }), `${fileName}.usdz`);
492
917
  }
@@ -504,9 +929,7 @@ class PrefViewer extends HTMLElement {
504
929
 
505
930
  downloadModelAndSceneGLB() {
506
931
  const fileName = "scene";
507
- GLTF2Export.GLBAsync(this.#scene, fileName, { exportWithoutWaitingForScene: true }).then((glb) => {
508
- glb.downloadFiles();
509
- });
932
+ GLTF2Export.GLBAsync(this.#scene, fileName, { exportWithoutWaitingForScene: true }).then((glb) => glb.downloadFiles());
510
933
  }
511
934
  }
512
935