@preference-sl/pref-viewer 2.13.0-beta.2 → 2.13.0-beta.21
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/Readme.md +1 -1
- package/package.json +5 -5
- package/src/babylonjs-animation-controller.js +187 -76
- package/src/babylonjs-animation-opening.js +58 -2
- package/src/babylonjs-controller.js +1008 -359
- package/src/file-storage.js +405 -24
- package/src/gltf-resolver.js +65 -9
- package/src/gltf-storage.js +47 -35
- package/src/localization/i18n.js +1 -1
- package/src/localization/translations.js +3 -3
- package/src/pref-viewer-3d-data.js +102 -52
- package/src/pref-viewer-3d.js +71 -15
- package/src/pref-viewer-menu-3d.js +44 -3
- package/src/pref-viewer.js +134 -17
- package/src/styles.js +21 -5
package/src/gltf-resolver.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { initDb, loadModel } from "./gltf-storage.js";
|
|
1
|
+
import FileStorage from "./file-storage.js";
|
|
2
|
+
import { closeDb, initDb, loadModel } from "./gltf-storage.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* GLTFResolver - Utility class for resolving, decoding, and preparing glTF/GLB assets from various sources.
|
|
@@ -9,6 +9,7 @@ import { initDb, loadModel } from "./gltf-storage.js";
|
|
|
9
9
|
* - Decodes base64 data and determines file type (.gltf or .glb).
|
|
10
10
|
* - Normalizes and replaces asset URIs with resolved URLs from FileStorage.
|
|
11
11
|
* - Handles cache validation using asset size and timestamp to avoid unnecessary reloads.
|
|
12
|
+
* - Tracks generated object URLs so callers can revoke them after Babylon finishes loading.
|
|
12
13
|
* - Provides methods for initializing storage, decoding assets, and preparing sources for viewer components.
|
|
13
14
|
*
|
|
14
15
|
* Usage:
|
|
@@ -17,11 +18,14 @@ import { initDb, loadModel } from "./gltf-storage.js";
|
|
|
17
18
|
*
|
|
18
19
|
* Public Methods:
|
|
19
20
|
* - getSource(storage, currentSize, currentTimeStamp): Resolves and prepares a glTF/GLB source for loading.
|
|
21
|
+
* - revokeObjectURLs(objectURLs): Releases temporary blob URLs generated during source resolution.
|
|
22
|
+
* - dispose(): Releases resolver resources and closes the internal FileStorage DB handle.
|
|
20
23
|
*
|
|
21
24
|
* Private Methods:
|
|
22
25
|
* - #initializeStorage(db, table): Ensures IndexedDB store is initialized.
|
|
23
26
|
* - #decodeBase64(base64): Decodes base64 data and determines file type.
|
|
24
27
|
* - #isURLAbsolute(url): Checks if a URL is syntactically absolute.
|
|
28
|
+
* - #isBlobURL(url): Checks if a URL is a blob object URL.
|
|
25
29
|
* - #saveAssetData(asset, index, parent, assetArray): Collects asset entries with external URIs.
|
|
26
30
|
* - #replaceSceneURIAsync(assetContainerJSON, assetContainerURL): Replaces internal URIs in glTF JSON with resolved URLs.
|
|
27
31
|
*
|
|
@@ -124,6 +128,16 @@ export default class GLTFResolver {
|
|
|
124
128
|
}
|
|
125
129
|
}
|
|
126
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Check whether a URL is a browser object URL.
|
|
133
|
+
* @private
|
|
134
|
+
* @param {string} url - Value to test.
|
|
135
|
+
* @returns {boolean} True when `url` is a blob object URL.
|
|
136
|
+
*/
|
|
137
|
+
#isBlobURL(url) {
|
|
138
|
+
return typeof url === "string" && url.startsWith("blob:");
|
|
139
|
+
}
|
|
140
|
+
|
|
127
141
|
/**
|
|
128
142
|
* Collects asset entries that have an external URI (non-data URI) and stores a normalized absolute or relative-resolved URI for later replacement.
|
|
129
143
|
* @private
|
|
@@ -154,6 +168,7 @@ export default class GLTFResolver {
|
|
|
154
168
|
* @private
|
|
155
169
|
* @param {JSON} assetContainerJSON - AssetContainer in glTF (JSON) (modified in-place).
|
|
156
170
|
* @param {URL} [assetContainerURL] - Optional URL of the AssetContainer. Used as the base path to resolve relative URIs.
|
|
171
|
+
* @param {string[]} [objectURLs=[]] - Collector array where generated blob URLs are appended for later cleanup.
|
|
157
172
|
* @returns {Promise<void>} Resolves when all applicable URIs have been resolved/replaced.
|
|
158
173
|
* @description
|
|
159
174
|
* - When provided, assetContainerURL is used as the base path for other scene files (binary buffers and all images).
|
|
@@ -162,11 +177,12 @@ export default class GLTFResolver {
|
|
|
162
177
|
* - Data URIs (embedded base64) are ignored and left unchanged.
|
|
163
178
|
* - Matching asset URIs are normalized (backslashes converted to forward slashes) and passed to the FileStorage layer
|
|
164
179
|
* to obtain a usable URL (object URL or cached URL).
|
|
180
|
+
* - Any generated blob URL is appended to `objectURLs` so the caller can revoke it later.
|
|
165
181
|
* - The function performs replacements in parallel and waits for all lookups to complete.
|
|
166
182
|
* - The JSON is updated in-place with the resolved URLs.
|
|
167
183
|
* @see {@link https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#uris|glTF™ 2.0 Specification - URIs}
|
|
168
184
|
*/
|
|
169
|
-
async #replaceSceneURIAsync(assetContainerJSON, assetContainerURL) {
|
|
185
|
+
async #replaceSceneURIAsync(assetContainerJSON, assetContainerURL, objectURLs = []) {
|
|
170
186
|
if (!assetContainerJSON) {
|
|
171
187
|
return;
|
|
172
188
|
}
|
|
@@ -193,11 +209,40 @@ export default class GLTFResolver {
|
|
|
193
209
|
const uri = await this.#fileStorage.getURL(asset.uri);
|
|
194
210
|
if (uri) {
|
|
195
211
|
asset.parent[asset.index].uri = uri;
|
|
212
|
+
if (this.#isBlobURL(uri)) {
|
|
213
|
+
objectURLs.push(uri);
|
|
214
|
+
}
|
|
196
215
|
}
|
|
197
216
|
});
|
|
198
217
|
await Promise.all(promisesArray);
|
|
199
218
|
}
|
|
200
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Revoke browser object URLs and clear the provided list.
|
|
222
|
+
* @public
|
|
223
|
+
* @param {string[]} objectURLs - URLs previously created via URL.createObjectURL.
|
|
224
|
+
* @returns {void}
|
|
225
|
+
*/
|
|
226
|
+
revokeObjectURLs(objectURLs = []) {
|
|
227
|
+
objectURLs.forEach((url) => {
|
|
228
|
+
if (this.#isBlobURL(url)) {
|
|
229
|
+
URL.revokeObjectURL(url);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
objectURLs.length = 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Disposes resolver-owned resources.
|
|
237
|
+
* @public
|
|
238
|
+
* @returns {void}
|
|
239
|
+
*/
|
|
240
|
+
dispose() {
|
|
241
|
+
this.#fileStorage?.dispose?.();
|
|
242
|
+
this.#fileStorage = null;
|
|
243
|
+
closeDb();
|
|
244
|
+
}
|
|
245
|
+
|
|
201
246
|
/**
|
|
202
247
|
* Resolves and prepares a glTF/GLB source from various storage backends.
|
|
203
248
|
* Supports IndexedDB, direct URLs, and base64-encoded data.
|
|
@@ -206,21 +251,22 @@ export default class GLTFResolver {
|
|
|
206
251
|
* @param {object} storage - Storage descriptor containing url, db, table, and id properties.
|
|
207
252
|
* @param {number|null} currentSize - The current cached size of the asset, or null if not cached.
|
|
208
253
|
* @param {string|null} currentTimeStamp - The current cached timestamp of the asset, or null if not cached.
|
|
209
|
-
* @returns {Promise<false|{source: File|string, size: number, timeStamp: string|null, extension: string}>}
|
|
254
|
+
* @returns {Promise<false|{source: File|string, size: number, timeStamp: string|null, extension: string, metadata: object, objectURLs: string[]}>}
|
|
210
255
|
* - Resolves to false if no update is needed or on error.
|
|
211
|
-
* - Resolves to an object containing
|
|
256
|
+
* - Resolves to an object containing source, cache metadata, and tracked object URLs if an update is required.
|
|
212
257
|
* @description
|
|
213
258
|
* - If storage specifies IndexedDB (db, table, id), loads the asset from IndexedDB and checks for updates.
|
|
214
259
|
* - If storage specifies a direct URL or base64 data, decodes and validates the asset.
|
|
215
260
|
* - For .gltf files, replaces internal URIs with resolved URLs from FileStorage.
|
|
216
261
|
* - Performs cache validation using size and timestamp to avoid unnecessary reloads.
|
|
217
|
-
* - Returns the prepared source, size, timestamp, and
|
|
262
|
+
* - Returns the prepared source, size, timestamp, extension, metadata, and objectURLs for further processing.
|
|
218
263
|
*/
|
|
219
264
|
async getSource(storage, currentSize, currentTimeStamp) {
|
|
220
265
|
let source = storage.url || null;
|
|
221
266
|
let newSize, newTimeStamp;
|
|
222
267
|
let pending = false;
|
|
223
268
|
let metadata = {};
|
|
269
|
+
const objectURLs = [];
|
|
224
270
|
|
|
225
271
|
if (storage.db && storage.table && storage.id) {
|
|
226
272
|
await this.#initializeStorage(storage.db, storage.table);
|
|
@@ -249,7 +295,7 @@ export default class GLTFResolver {
|
|
|
249
295
|
if (blob && extension) {
|
|
250
296
|
if (extension === ".gltf") {
|
|
251
297
|
const assetContainerJSON = JSON.parse(await blob.text());
|
|
252
|
-
await this.#replaceSceneURIAsync(assetContainerJSON, source);
|
|
298
|
+
await this.#replaceSceneURIAsync(assetContainerJSON, source, objectURLs);
|
|
253
299
|
source = `data:${JSON.stringify(assetContainerJSON)}`;
|
|
254
300
|
} else {
|
|
255
301
|
file = new File([blob], `file${extension}`, {
|
|
@@ -278,13 +324,23 @@ export default class GLTFResolver {
|
|
|
278
324
|
if (extension === ".gltf") {
|
|
279
325
|
const assetContainerBlob = await this.#fileStorage.getBlob(source);
|
|
280
326
|
const assetContainerJSON = JSON.parse(await assetContainerBlob.text());
|
|
281
|
-
await this.#replaceSceneURIAsync(assetContainerJSON, source);
|
|
327
|
+
await this.#replaceSceneURIAsync(assetContainerJSON, source, objectURLs);
|
|
282
328
|
source = `data:${JSON.stringify(assetContainerJSON)}`;
|
|
283
329
|
} else {
|
|
284
330
|
source = await this.#fileStorage.getURL(source);
|
|
331
|
+
if (this.#isBlobURL(source)) {
|
|
332
|
+
objectURLs.push(source);
|
|
333
|
+
}
|
|
285
334
|
}
|
|
286
335
|
}
|
|
287
336
|
}
|
|
288
|
-
return {
|
|
337
|
+
return {
|
|
338
|
+
source: file || source,
|
|
339
|
+
size: newSize,
|
|
340
|
+
timeStamp: newTimeStamp,
|
|
341
|
+
extension: extension,
|
|
342
|
+
metadata: metadata,
|
|
343
|
+
objectURLs: objectURLs,
|
|
344
|
+
};
|
|
289
345
|
}
|
|
290
346
|
}
|
package/src/gltf-storage.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
const PC = (globalThis.PrefConfigurator ??= {});
|
|
5
5
|
PC.version = PC.version ?? "1.0.1"; // bump if your schema changes!!
|
|
6
6
|
PC.db = PC.db ?? null;
|
|
7
|
+
PC.dbName = PC.dbName ?? null;
|
|
8
|
+
PC.storeName = PC.storeName ?? null;
|
|
7
9
|
|
|
8
10
|
function _getDbOrThrow() {
|
|
9
11
|
const db = PC.db;
|
|
@@ -23,22 +25,50 @@ function _openEnsuringStore(dbName, storeName) {
|
|
|
23
25
|
}
|
|
24
26
|
open.onupgradeneeded = (ev) => {
|
|
25
27
|
const upgradeDb = ev.target.result;
|
|
28
|
+
const tx = ev.target.transaction;
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
if (upgradeDb.objectStoreNames.contains(storeName)) {
|
|
29
|
-
upgradeDb.
|
|
30
|
+
let store;
|
|
31
|
+
if (!upgradeDb.objectStoreNames.contains(storeName)) {
|
|
32
|
+
store = upgradeDb.createObjectStore(storeName, { keyPath: 'id' });
|
|
33
|
+
} else if (tx) {
|
|
34
|
+
store = tx.objectStore(storeName);
|
|
30
35
|
}
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
if (store && !store.indexNames.contains('expirationTimeStamp')) {
|
|
38
|
+
store.createIndex('expirationTimeStamp', 'expirationTimeStamp', { unique: false });
|
|
39
|
+
}
|
|
34
40
|
};
|
|
35
41
|
});
|
|
36
42
|
}
|
|
37
43
|
|
|
44
|
+
function _isRecoverableDbError(err) {
|
|
45
|
+
const msg = String(err?.message ?? err ?? "");
|
|
46
|
+
if (!msg) return false;
|
|
47
|
+
return msg.includes("Database not initialized") ||
|
|
48
|
+
msg.includes("The database connection is closing") ||
|
|
49
|
+
msg.includes("InvalidStateError") ||
|
|
50
|
+
msg.includes("NotFoundError");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function _withDbRetry(work) {
|
|
54
|
+
try {
|
|
55
|
+
return await work(_getDbOrThrow());
|
|
56
|
+
} catch (e) {
|
|
57
|
+
if (!_isRecoverableDbError(e) || !PC.dbName || !PC.storeName) {
|
|
58
|
+
throw e;
|
|
59
|
+
}
|
|
60
|
+
await initDb(PC.dbName, PC.storeName);
|
|
61
|
+
return await work(_getDbOrThrow());
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
38
65
|
// --- public API -------------------------------------------------------------
|
|
39
66
|
|
|
40
67
|
// Inicializar IndexedDB y dejar el handle en PrefConfigurator.db (público)
|
|
41
68
|
export async function initDb(dbName, storeName) {
|
|
69
|
+
PC.dbName = dbName;
|
|
70
|
+
PC.storeName = storeName;
|
|
71
|
+
|
|
42
72
|
const db = await _openEnsuringStore(dbName, storeName);
|
|
43
73
|
// Close any previous handle to avoid versionchange blocking
|
|
44
74
|
try { PC.db?.close?.(); } catch { }
|
|
@@ -54,10 +84,7 @@ export async function initDb(dbName, storeName) {
|
|
|
54
84
|
|
|
55
85
|
// Guardar modelo
|
|
56
86
|
export async function saveModel(modelDataStr, storeName) {
|
|
57
|
-
return new Promise((resolve, reject) => {
|
|
58
|
-
let db;
|
|
59
|
-
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
60
|
-
|
|
87
|
+
return _withDbRetry((db) => new Promise((resolve, reject) => {
|
|
61
88
|
let modelData;
|
|
62
89
|
try { modelData = JSON.parse(modelDataStr); }
|
|
63
90
|
catch (e) { reject(new Error("saveModel: modelDataStr is not valid JSON")); return; }
|
|
@@ -74,22 +101,19 @@ export async function saveModel(modelDataStr, storeName) {
|
|
|
74
101
|
|
|
75
102
|
req.onerror = () => reject(req.error);
|
|
76
103
|
req.onsuccess = () => resolve();
|
|
77
|
-
});
|
|
104
|
+
}));
|
|
78
105
|
}
|
|
79
106
|
|
|
80
107
|
// Cargar modelo
|
|
81
108
|
export function loadModel(modelId, storeName) {
|
|
82
|
-
return new Promise((resolve, reject) => {
|
|
83
|
-
let db;
|
|
84
|
-
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
85
|
-
|
|
109
|
+
return _withDbRetry((db) => new Promise((resolve, reject) => {
|
|
86
110
|
const tx = db.transaction([storeName], "readonly");
|
|
87
111
|
const store = tx.objectStore(storeName);
|
|
88
112
|
const req = store.get(modelId);
|
|
89
113
|
|
|
90
114
|
req.onerror = () => reject(req.error);
|
|
91
115
|
req.onsuccess = () => resolve(req.result ?? null);
|
|
92
|
-
});
|
|
116
|
+
}));
|
|
93
117
|
}
|
|
94
118
|
|
|
95
119
|
// Descargar archivo desde base64
|
|
@@ -104,10 +128,7 @@ export function downloadFileFromBytes(fileName, bytesBase64, mimeType) {
|
|
|
104
128
|
|
|
105
129
|
// Obtener todos los modelos (solo metadata)
|
|
106
130
|
export async function getAllModels(storeName) {
|
|
107
|
-
return new Promise((resolve, reject) => {
|
|
108
|
-
let db;
|
|
109
|
-
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
110
|
-
|
|
131
|
+
return _withDbRetry((db) => new Promise((resolve, reject) => {
|
|
111
132
|
const tx = db.transaction([storeName], "readonly");
|
|
112
133
|
const store = tx.objectStore(storeName);
|
|
113
134
|
const req = store.getAll();
|
|
@@ -124,45 +145,36 @@ export async function getAllModels(storeName) {
|
|
|
124
145
|
// keep old behavior: return JSON string
|
|
125
146
|
resolve(JSON.stringify(results));
|
|
126
147
|
};
|
|
127
|
-
});
|
|
148
|
+
}));
|
|
128
149
|
}
|
|
129
150
|
|
|
130
151
|
// Eliminar modelo por id
|
|
131
152
|
export async function deleteModel(modelId, storeName) {
|
|
132
|
-
return new Promise((resolve, reject) => {
|
|
133
|
-
let db;
|
|
134
|
-
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
135
|
-
|
|
153
|
+
return _withDbRetry((db) => new Promise((resolve, reject) => {
|
|
136
154
|
const tx = db.transaction([storeName], "readwrite");
|
|
137
155
|
const store = tx.objectStore(storeName);
|
|
138
156
|
const req = store.delete(modelId);
|
|
139
157
|
|
|
140
158
|
req.onerror = () => reject(req.error);
|
|
141
159
|
req.onsuccess = () => resolve();
|
|
142
|
-
});
|
|
160
|
+
}));
|
|
143
161
|
}
|
|
144
162
|
|
|
145
163
|
// Limpiar toda la store
|
|
146
164
|
export async function clearAll(storeName) {
|
|
147
|
-
return new Promise((resolve, reject) => {
|
|
148
|
-
let db;
|
|
149
|
-
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
150
|
-
|
|
165
|
+
return _withDbRetry((db) => new Promise((resolve, reject) => {
|
|
151
166
|
const tx = db.transaction([storeName], "readwrite");
|
|
152
167
|
const store = tx.objectStore(storeName);
|
|
153
168
|
const req = store.clear();
|
|
154
169
|
|
|
155
170
|
req.onerror = () => reject(req.error);
|
|
156
171
|
req.onsuccess = () => resolve();
|
|
157
|
-
});
|
|
172
|
+
}));
|
|
158
173
|
}
|
|
159
174
|
|
|
160
175
|
// Borrar modelos expirados usando el índice "expirationTimeStamp"
|
|
161
176
|
export async function cleanExpiredModels(storeName) {
|
|
162
|
-
return new Promise((resolve, reject) => {
|
|
163
|
-
let db;
|
|
164
|
-
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
165
|
-
|
|
177
|
+
return _withDbRetry((db) => new Promise((resolve, reject) => {
|
|
166
178
|
const tx = db.transaction([storeName], "readwrite");
|
|
167
179
|
const store = tx.objectStore(storeName);
|
|
168
180
|
|
|
@@ -182,7 +194,7 @@ export async function cleanExpiredModels(storeName) {
|
|
|
182
194
|
|
|
183
195
|
tx.oncomplete = () => resolve();
|
|
184
196
|
tx.onerror = () => reject(tx.error);
|
|
185
|
-
});
|
|
197
|
+
}));
|
|
186
198
|
}
|
|
187
199
|
|
|
188
200
|
// Utilidades opcionales y visibles (por si las quieres usar en consola)
|
package/src/localization/i18n.js
CHANGED
|
@@ -22,7 +22,7 @@ export const translations = {
|
|
|
22
22
|
helper: "Reduction of ambient light in corners and areas close to surfaces.",
|
|
23
23
|
},
|
|
24
24
|
iblEnabled: {
|
|
25
|
-
label: "IBL",
|
|
25
|
+
label: "Image Based Lighting (IBL)",
|
|
26
26
|
helper: "Uses the HDR environment to light the scene.",
|
|
27
27
|
},
|
|
28
28
|
shadowsEnabled: {
|
|
@@ -68,11 +68,11 @@ export const translations = {
|
|
|
68
68
|
helper: "Suaviza aristas y líneas.",
|
|
69
69
|
},
|
|
70
70
|
ambientOcclusionEnabled: {
|
|
71
|
-
label: "
|
|
71
|
+
label: "Oclusión ambiental",
|
|
72
72
|
helper: "Atenuación de la luz ambiental en rincones y zonas cercanas entre superficies.",
|
|
73
73
|
},
|
|
74
74
|
iblEnabled: {
|
|
75
|
-
label: "IBL",
|
|
75
|
+
label: "Iluminación basada en imagen (IBL)",
|
|
76
76
|
helper: "Usa la imagen HDR de entorno para iluminar la escena.",
|
|
77
77
|
},
|
|
78
78
|
shadowsEnabled: {
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ContainerData - Represents
|
|
2
|
+
* ContainerData - Represents state, cache metadata, and update lifecycle for a 3D asset container
|
|
3
|
+
* (for example: model, environment, or materials).
|
|
3
4
|
*
|
|
4
5
|
* Responsibilities:
|
|
5
|
-
* - Tracks container
|
|
6
|
-
* -
|
|
7
|
-
* - Provides
|
|
6
|
+
* - Tracks persistent container fields (`storage`, `show`, `size`, `timeStamp`, `metadata`, `visible`).
|
|
7
|
+
* - Tracks staged update data in `update` (`pending`, `success`, staged storage/show/cache info).
|
|
8
|
+
* - Provides helpers to stage updates (`setPending`, `setPendingCacheData`, `setPendingWithCurrentStorage`)
|
|
9
|
+
* and commit them (`setSuccess(true)`).
|
|
8
10
|
*
|
|
9
11
|
* Usage:
|
|
10
|
-
* - Instantiate with a name: new ContainerData("model")
|
|
11
|
-
* -
|
|
12
|
-
* -
|
|
12
|
+
* - Instantiate with a name: `new ContainerData("model")`.
|
|
13
|
+
* - Stage a reload with `setPending(...)`, optionally seed cache metadata, then finalize with `setSuccess(true)`.
|
|
14
|
+
* - Use `setPendingWithCurrentStorage()` to request a reload from the currently stored source.
|
|
15
|
+
* - Inspect flags via `isPending`, `isSuccess`, `isVisible`, and `mustBeShown`.
|
|
13
16
|
*/
|
|
14
17
|
export class ContainerData {
|
|
15
18
|
constructor(name = "") {
|
|
@@ -70,9 +73,7 @@ export class ContainerData {
|
|
|
70
73
|
}
|
|
71
74
|
const targetShow = this.update.show !== null ? this.update.show : this.show;
|
|
72
75
|
this.setPending(storedSource, targetShow);
|
|
73
|
-
this.
|
|
74
|
-
this.update.timeStamp = this.timeStamp;
|
|
75
|
-
this.update.metadata = { ...(this.metadata ?? {}) };
|
|
76
|
+
this.setPendingCacheData(this.size, this.timeStamp, this.metadata);
|
|
76
77
|
return true;
|
|
77
78
|
}
|
|
78
79
|
get isPending() {
|
|
@@ -90,17 +91,18 @@ export class ContainerData {
|
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
/**
|
|
93
|
-
* MaterialData - Represents
|
|
94
|
+
* MaterialData - Represents a material option plus its update lifecycle in the 3D viewer.
|
|
94
95
|
*
|
|
95
96
|
* Responsibilities:
|
|
96
|
-
* -
|
|
97
|
-
* -
|
|
98
|
-
* -
|
|
97
|
+
* - Stores material identity and targeting info (`name`, `nodeNames`, `nodePrefixes`).
|
|
98
|
+
* - Stores current applied material value (`value`).
|
|
99
|
+
* - Tracks staged material changes in `update` (`pending`, `success`, `value`).
|
|
99
100
|
*
|
|
100
101
|
* Usage:
|
|
101
|
-
* - Instantiate with
|
|
102
|
-
* -
|
|
103
|
-
* -
|
|
102
|
+
* - Instantiate with initial values: `new MaterialData("innerWall", value, nodeNames, nodePrefixes)`.
|
|
103
|
+
* - Stage a change with `setPending(...)`, then commit with `setSuccess(true)`.
|
|
104
|
+
* - Use `setPendingWithCurrent()` to reapply the currently stored material value.
|
|
105
|
+
* - Inspect flags via `isPending` and `isSuccess`.
|
|
104
106
|
*/
|
|
105
107
|
export class MaterialData {
|
|
106
108
|
constructor(name = "", value = null, nodeNames = [], nodePrefixes = []) {
|
|
@@ -148,17 +150,18 @@ export class MaterialData {
|
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
/**
|
|
151
|
-
* CameraData - Represents
|
|
153
|
+
* CameraData - Represents camera selection state and camera-lock behavior for the 3D viewer.
|
|
152
154
|
*
|
|
153
155
|
* Responsibilities:
|
|
154
|
-
* -
|
|
155
|
-
* -
|
|
156
|
-
* - Provides
|
|
156
|
+
* - Stores current camera selection (`value`) and lock state (`locked`).
|
|
157
|
+
* - Tracks staged camera updates in `update` (`pending`, `success`, `value`, `locked`).
|
|
158
|
+
* - Provides helpers to stage (`setPending`, `setPendingWithCurrent`) and commit (`setSuccess(true)`) updates.
|
|
157
159
|
*
|
|
158
160
|
* Usage:
|
|
159
|
-
* - Instantiate with a name and optional value: new CameraData("camera", value, locked)
|
|
160
|
-
* -
|
|
161
|
-
* -
|
|
161
|
+
* - Instantiate with a name and optional value: `new CameraData("camera", value, locked)`.
|
|
162
|
+
* - Stage a camera update with `setPending(value, locked)`.
|
|
163
|
+
* - Pass `null` as value to request an unlocked/default camera behavior.
|
|
164
|
+
* - Inspect flags via `isPending` and `isSuccess`.
|
|
162
165
|
*/
|
|
163
166
|
export class CameraData {
|
|
164
167
|
defaultLocked = true;
|
|
@@ -212,51 +215,98 @@ export class CameraData {
|
|
|
212
215
|
}
|
|
213
216
|
|
|
214
217
|
/**
|
|
215
|
-
* IBLData -
|
|
218
|
+
* IBLData - Stores image-based lighting configuration for HDR environment rendering.
|
|
216
219
|
*
|
|
217
220
|
* Responsibilities:
|
|
218
|
-
* - Stores
|
|
219
|
-
* -
|
|
220
|
-
* -
|
|
221
|
+
* - Stores source and cache references (`url`, `cachedUrl`) for the HDR environment map.
|
|
222
|
+
* - Tracks whether the HDR texture is already valid and reusable without needing `cachedUrl` (`valid`).
|
|
223
|
+
* - Stores runtime tuning values (`intensity`, `shadows`) and cache identity (`timeStamp`).
|
|
224
|
+
* - Provides helpers to fully reset IBL state (`reset`) or partially update known fields (`setValues`).
|
|
225
|
+
* - Provides a helper to consume and clear temporary cached URLs after texture creation (`consumeCachedUrl`).
|
|
221
226
|
*
|
|
222
227
|
* Usage:
|
|
223
|
-
* - Instantiate with defaults: `const ibl = new IBLData()
|
|
224
|
-
* - Call `
|
|
225
|
-
* -
|
|
228
|
+
* - Instantiate with defaults: `const ibl = new IBLData();`.
|
|
229
|
+
* - Call `setValues(...)` with only the fields you want to update (`undefined` preserves current values).
|
|
230
|
+
* - Lifecycle: set a fresh `cachedUrl` via `setValues(...)`, create/clone the Babylon HDR texture, then call
|
|
231
|
+
* `consumeCachedUrl(true)` to revoke temporary blob URLs, set `cachedUrl` to null, and keep `valid=true`.
|
|
232
|
+
* - Call `reset()` to clear the current IBL environment and restore default intensity/shadows.
|
|
226
233
|
*/
|
|
227
234
|
export class IBLData {
|
|
228
|
-
|
|
235
|
+
defaultIntensity = 1.0;
|
|
236
|
+
defaultShadows = false;
|
|
237
|
+
constructor(url = null, cachedUrl = null, intensity = this.defaultIntensity, shadows = this.defaultShadows, timeStamp = null, valid = false) {
|
|
229
238
|
this.url = url;
|
|
239
|
+
this.cachedUrl = cachedUrl;
|
|
230
240
|
this.intensity = intensity;
|
|
231
241
|
this.shadows = shadows;
|
|
232
242
|
this.timeStamp = timeStamp;
|
|
233
|
-
this.
|
|
243
|
+
this.valid = valid;
|
|
234
244
|
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Revokes object URLs (`blob:`) to release browser memory.
|
|
248
|
+
* @private
|
|
249
|
+
* @param {string|null|undefined} url URL to revoke when applicable.
|
|
250
|
+
* @returns {void}
|
|
251
|
+
*/
|
|
252
|
+
#revokeIfBlobURL(url) {
|
|
253
|
+
if (typeof url === "string" && url.startsWith("blob:")) {
|
|
254
|
+
URL.revokeObjectURL(url);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Resets IBL state and revokes any temporary cached object URL.
|
|
260
|
+
* @public
|
|
261
|
+
* @returns {void}
|
|
262
|
+
*/
|
|
235
263
|
reset() {
|
|
236
|
-
this.
|
|
237
|
-
this.
|
|
264
|
+
this.#revokeIfBlobURL(this.cachedUrl);
|
|
265
|
+
this.url = null
|
|
266
|
+
this.cachedUrl = null;
|
|
267
|
+
this.intensity = this.defaultIntensity;
|
|
268
|
+
this.shadows = this.defaultShadows;
|
|
269
|
+
this.timeStamp = null;
|
|
270
|
+
this.valid = false;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Consumes the temporary cached URL after Babylon texture creation.
|
|
275
|
+
* Revokes the URL when it is a `blob:` object URL, clears `cachedUrl`,
|
|
276
|
+
* and marks whether a reusable in-memory texture is available.
|
|
277
|
+
* @public
|
|
278
|
+
* @param {boolean} [valid=true] Whether the corresponding HDR texture has been validated and cached in memory.
|
|
279
|
+
* @returns {void}
|
|
280
|
+
*/
|
|
281
|
+
consumeCachedUrl(valid = true) {
|
|
282
|
+
this.#revokeIfBlobURL(this.cachedUrl);
|
|
283
|
+
this.cachedUrl = null;
|
|
284
|
+
this.valid = !!valid;
|
|
238
285
|
}
|
|
239
|
-
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Updates selected IBL fields while preserving unspecified values.
|
|
289
|
+
* When a new non-empty `cachedUrl` is supplied, `valid` is reset to `false`
|
|
290
|
+
* until the URL is consumed into a Babylon texture.
|
|
291
|
+
* @public
|
|
292
|
+
* @param {string|null|undefined} url Source URL identifier.
|
|
293
|
+
* @param {string|null|undefined} cachedUrl Resolved URL (including possible `blob:` URL).
|
|
294
|
+
* @param {number|undefined} intensity IBL intensity.
|
|
295
|
+
* @param {boolean|undefined} shadows Shadow toggle for IBL path.
|
|
296
|
+
* @param {string|null|undefined} timeStamp Last-modified marker.
|
|
297
|
+
* @returns {void}
|
|
298
|
+
*/
|
|
299
|
+
setValues(url, cachedUrl, intensity, shadows, timeStamp) {
|
|
300
|
+
if (cachedUrl !== undefined && cachedUrl !== this.cachedUrl) {
|
|
301
|
+
this.#revokeIfBlobURL(this.cachedUrl);
|
|
302
|
+
}
|
|
240
303
|
this.url = url !== undefined ? url : this.url;
|
|
304
|
+
this.cachedUrl = cachedUrl !== undefined ? cachedUrl : this.cachedUrl;
|
|
241
305
|
this.intensity = intensity !== undefined ? intensity : this.intensity;
|
|
242
306
|
this.shadows = shadows !== undefined ? shadows : this.shadows;
|
|
243
307
|
this.timeStamp = timeStamp !== undefined ? timeStamp : this.timeStamp;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (success) {
|
|
247
|
-
this.success = true;
|
|
248
|
-
} else {
|
|
249
|
-
this.success = false;
|
|
308
|
+
if (cachedUrl !== undefined) {
|
|
309
|
+
this.valid = typeof cachedUrl === "string" && cachedUrl.length > 0 ? false : this.valid;
|
|
250
310
|
}
|
|
251
311
|
}
|
|
252
|
-
setPending() {
|
|
253
|
-
this.pending = true;
|
|
254
|
-
this.success = false;
|
|
255
|
-
}
|
|
256
|
-
get isPending() {
|
|
257
|
-
return this.pending === true;
|
|
258
|
-
}
|
|
259
|
-
get isSuccess() {
|
|
260
|
-
return this.success === true;
|
|
261
|
-
}
|
|
262
312
|
}
|