@preference-sl/pref-viewer 2.13.0-beta.0 → 2.13.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/babylonjs-animation-controller.js +185 -77
- package/src/babylonjs-animation-opening.js +58 -2
- package/src/babylonjs-controller.js +1182 -404
- package/src/file-storage.js +399 -21
- package/src/gltf-resolver.js +63 -8
- package/src/index.js +2 -0
- package/src/localization/i18n.js +94 -0
- package/src/localization/translations.js +104 -0
- package/src/pref-viewer-3d-data.js +127 -49
- package/src/pref-viewer-3d.js +85 -17
- package/src/pref-viewer-menu-3d.js +598 -0
- package/src/pref-viewer-task.js +1 -0
- package/src/pref-viewer.js +549 -200
- package/src/styles.js +318 -10
package/src/gltf-resolver.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import FileStorage from "./file-storage.js";
|
|
2
2
|
import { initDb, loadModel } from "./gltf-storage.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -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,39 @@ 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
|
+
}
|
|
244
|
+
|
|
201
245
|
/**
|
|
202
246
|
* Resolves and prepares a glTF/GLB source from various storage backends.
|
|
203
247
|
* Supports IndexedDB, direct URLs, and base64-encoded data.
|
|
@@ -206,21 +250,22 @@ export default class GLTFResolver {
|
|
|
206
250
|
* @param {object} storage - Storage descriptor containing url, db, table, and id properties.
|
|
207
251
|
* @param {number|null} currentSize - The current cached size of the asset, or null if not cached.
|
|
208
252
|
* @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}>}
|
|
253
|
+
* @returns {Promise<false|{source: File|string, size: number, timeStamp: string|null, extension: string, metadata: object, objectURLs: string[]}>}
|
|
210
254
|
* - Resolves to false if no update is needed or on error.
|
|
211
|
-
* - Resolves to an object containing
|
|
255
|
+
* - Resolves to an object containing source, cache metadata, and tracked object URLs if an update is required.
|
|
212
256
|
* @description
|
|
213
257
|
* - If storage specifies IndexedDB (db, table, id), loads the asset from IndexedDB and checks for updates.
|
|
214
258
|
* - If storage specifies a direct URL or base64 data, decodes and validates the asset.
|
|
215
259
|
* - For .gltf files, replaces internal URIs with resolved URLs from FileStorage.
|
|
216
260
|
* - Performs cache validation using size and timestamp to avoid unnecessary reloads.
|
|
217
|
-
* - Returns the prepared source, size, timestamp, and
|
|
261
|
+
* - Returns the prepared source, size, timestamp, extension, metadata, and objectURLs for further processing.
|
|
218
262
|
*/
|
|
219
263
|
async getSource(storage, currentSize, currentTimeStamp) {
|
|
220
264
|
let source = storage.url || null;
|
|
221
265
|
let newSize, newTimeStamp;
|
|
222
266
|
let pending = false;
|
|
223
267
|
let metadata = {};
|
|
268
|
+
const objectURLs = [];
|
|
224
269
|
|
|
225
270
|
if (storage.db && storage.table && storage.id) {
|
|
226
271
|
await this.#initializeStorage(storage.db, storage.table);
|
|
@@ -249,7 +294,7 @@ export default class GLTFResolver {
|
|
|
249
294
|
if (blob && extension) {
|
|
250
295
|
if (extension === ".gltf") {
|
|
251
296
|
const assetContainerJSON = JSON.parse(await blob.text());
|
|
252
|
-
await this.#replaceSceneURIAsync(assetContainerJSON, source);
|
|
297
|
+
await this.#replaceSceneURIAsync(assetContainerJSON, source, objectURLs);
|
|
253
298
|
source = `data:${JSON.stringify(assetContainerJSON)}`;
|
|
254
299
|
} else {
|
|
255
300
|
file = new File([blob], `file${extension}`, {
|
|
@@ -278,13 +323,23 @@ export default class GLTFResolver {
|
|
|
278
323
|
if (extension === ".gltf") {
|
|
279
324
|
const assetContainerBlob = await this.#fileStorage.getBlob(source);
|
|
280
325
|
const assetContainerJSON = JSON.parse(await assetContainerBlob.text());
|
|
281
|
-
await this.#replaceSceneURIAsync(assetContainerJSON, source);
|
|
326
|
+
await this.#replaceSceneURIAsync(assetContainerJSON, source, objectURLs);
|
|
282
327
|
source = `data:${JSON.stringify(assetContainerJSON)}`;
|
|
283
328
|
} else {
|
|
284
329
|
source = await this.#fileStorage.getURL(source);
|
|
330
|
+
if (this.#isBlobURL(source)) {
|
|
331
|
+
objectURLs.push(source);
|
|
332
|
+
}
|
|
285
333
|
}
|
|
286
334
|
}
|
|
287
335
|
}
|
|
288
|
-
return {
|
|
336
|
+
return {
|
|
337
|
+
source: file || source,
|
|
338
|
+
size: newSize,
|
|
339
|
+
timeStamp: newTimeStamp,
|
|
340
|
+
extension: extension,
|
|
341
|
+
metadata: metadata,
|
|
342
|
+
objectURLs: objectURLs,
|
|
343
|
+
};
|
|
289
344
|
}
|
|
290
345
|
}
|
package/src/index.js
CHANGED
|
@@ -3,11 +3,13 @@ import PrefViewer from "./pref-viewer.js";
|
|
|
3
3
|
import PrefViewer2D from "./pref-viewer-2d.js";
|
|
4
4
|
import PrefViewer3D from "./pref-viewer-3d.js";
|
|
5
5
|
import PrefViewerDialog from "./pref-viewer-dialog.js";
|
|
6
|
+
import PrefViewerMenu3D from "./pref-viewer-menu-3d.js";
|
|
6
7
|
|
|
7
8
|
// Defines custom elements for use as HTML tags in the application.
|
|
8
9
|
customElements.define("pref-viewer-2d", PrefViewer2D);
|
|
9
10
|
customElements.define("pref-viewer-3d", PrefViewer3D);
|
|
10
11
|
customElements.define("pref-viewer-dialog", PrefViewerDialog);
|
|
12
|
+
customElements.define("pref-viewer-menu-3d", PrefViewerMenu3D);
|
|
11
13
|
customElements.define("pref-viewer", PrefViewer);
|
|
12
14
|
|
|
13
15
|
// Exposes selected PrefViewer classes globally for external JavaScript use.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import translations from "./translations.js";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_LOCALE = "en-EN";
|
|
4
|
+
|
|
5
|
+
export const SUPPORTED_LOCALES = Object.keys(translations);
|
|
6
|
+
const listeners = new Set();
|
|
7
|
+
let currentLocale = DEFAULT_LOCALE;
|
|
8
|
+
|
|
9
|
+
const findLocaleKey = (localeId) => {
|
|
10
|
+
if (typeof localeId !== "string") {
|
|
11
|
+
return DEFAULT_LOCALE;
|
|
12
|
+
}
|
|
13
|
+
const normalized = localeId.trim().toLowerCase();
|
|
14
|
+
const match = SUPPORTED_LOCALES.find((locale) => locale.toLowerCase() === normalized);
|
|
15
|
+
return match ?? DEFAULT_LOCALE;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const getBundle = (localeId = currentLocale) => {
|
|
19
|
+
const key = findLocaleKey(localeId);
|
|
20
|
+
return translations[key] ?? translations[DEFAULT_LOCALE];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const resolveValue = (bundle, parts) => parts.reduce((acc, part) => (
|
|
24
|
+
acc && Object.prototype.hasOwnProperty.call(acc, part) ? acc[part] : undefined
|
|
25
|
+
), bundle);
|
|
26
|
+
|
|
27
|
+
export const availableLocales = () => [...SUPPORTED_LOCALES];
|
|
28
|
+
|
|
29
|
+
export const getLocale = () => currentLocale;
|
|
30
|
+
|
|
31
|
+
export const resolveLocale = (localeId) => findLocaleKey(localeId);
|
|
32
|
+
|
|
33
|
+
export const setLocale = (localeId) => {
|
|
34
|
+
const resolved = findLocaleKey(localeId);
|
|
35
|
+
if (resolved === currentLocale) {
|
|
36
|
+
return currentLocale;
|
|
37
|
+
}
|
|
38
|
+
currentLocale = resolved;
|
|
39
|
+
listeners.forEach((listener) => {
|
|
40
|
+
try {
|
|
41
|
+
listener(currentLocale);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.warn("PrefViewer i18n listener failed", error);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return currentLocale;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const onLocaleChange = (listener) => {
|
|
50
|
+
if (typeof listener !== "function") {
|
|
51
|
+
return () => {};
|
|
52
|
+
}
|
|
53
|
+
listeners.add(listener);
|
|
54
|
+
return () => listeners.delete(listener);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const translate = (key, options = {}) => {
|
|
58
|
+
if (!key) {
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
const { locale, fallback } = options;
|
|
62
|
+
const parts = Array.isArray(key) ? key : key.split(".");
|
|
63
|
+
const bundle = getBundle(locale);
|
|
64
|
+
let value = resolveValue(bundle, parts);
|
|
65
|
+
if (value !== undefined) {
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
if (bundle !== translations[DEFAULT_LOCALE]) {
|
|
69
|
+
value = resolveValue(translations[DEFAULT_LOCALE], parts);
|
|
70
|
+
if (value !== undefined) {
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return fallback ?? "";
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const getNamespace = (namespace, options = {}) => {
|
|
78
|
+
if (!namespace) {
|
|
79
|
+
return getBundle(options.locale);
|
|
80
|
+
}
|
|
81
|
+
return translate(namespace, options);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export default {
|
|
85
|
+
availableLocales,
|
|
86
|
+
DEFAULT_LOCALE,
|
|
87
|
+
getLocale,
|
|
88
|
+
getNamespace,
|
|
89
|
+
onLocaleChange,
|
|
90
|
+
resolveLocale,
|
|
91
|
+
setLocale,
|
|
92
|
+
SUPPORTED_LOCALES,
|
|
93
|
+
translate,
|
|
94
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export const translations = {
|
|
2
|
+
"en-EN": {
|
|
3
|
+
prefViewer: {
|
|
4
|
+
menu3d: {
|
|
5
|
+
title: "3D render settings",
|
|
6
|
+
subtitle: "Toggle visual effects on or off.",
|
|
7
|
+
pendingLabel: "Pending changes",
|
|
8
|
+
actions: {
|
|
9
|
+
apply: "Apply",
|
|
10
|
+
applying: "Applying...",
|
|
11
|
+
},
|
|
12
|
+
status: {
|
|
13
|
+
error: "Couldn't apply the changes.",
|
|
14
|
+
},
|
|
15
|
+
switches: {
|
|
16
|
+
antiAliasingEnabled: {
|
|
17
|
+
label: "Antialiasing",
|
|
18
|
+
helper: "Softens jagged edges and lines.",
|
|
19
|
+
},
|
|
20
|
+
ambientOcclusionEnabled: {
|
|
21
|
+
label: "Ambient Occlusion",
|
|
22
|
+
helper: "Reduction of ambient light in corners and areas close to surfaces.",
|
|
23
|
+
},
|
|
24
|
+
iblEnabled: {
|
|
25
|
+
label: "Image Based Lighting (IBL)",
|
|
26
|
+
helper: "Uses the HDR environment to light the scene.",
|
|
27
|
+
},
|
|
28
|
+
shadowsEnabled: {
|
|
29
|
+
label: "Shadows",
|
|
30
|
+
helper: "Enables shadows.",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
downloadDialog: {
|
|
35
|
+
title: "Download 3D Scene",
|
|
36
|
+
sections: {
|
|
37
|
+
content: "Content",
|
|
38
|
+
format: "Format",
|
|
39
|
+
},
|
|
40
|
+
options: {
|
|
41
|
+
model: "Model",
|
|
42
|
+
scene: "Scene",
|
|
43
|
+
both: "Both",
|
|
44
|
+
},
|
|
45
|
+
buttons: {
|
|
46
|
+
download: "Download",
|
|
47
|
+
cancel: "Cancel",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
"es-ES": {
|
|
53
|
+
prefViewer: {
|
|
54
|
+
menu3d: {
|
|
55
|
+
title: "Ajustes de render 3D",
|
|
56
|
+
subtitle: "Activa o desactiva los efectos visuales.",
|
|
57
|
+
pendingLabel: "Pendiente de aplicar",
|
|
58
|
+
actions: {
|
|
59
|
+
apply: "Aplicar",
|
|
60
|
+
applying: "Aplicando...",
|
|
61
|
+
},
|
|
62
|
+
status: {
|
|
63
|
+
error: "No se pudo aplicar los cambios.",
|
|
64
|
+
},
|
|
65
|
+
switches: {
|
|
66
|
+
antiAliasingEnabled: {
|
|
67
|
+
label: "Antialiasing",
|
|
68
|
+
helper: "Suaviza aristas y líneas.",
|
|
69
|
+
},
|
|
70
|
+
ambientOcclusionEnabled: {
|
|
71
|
+
label: "Oclusión ambiental",
|
|
72
|
+
helper: "Atenuación de la luz ambiental en rincones y zonas cercanas entre superficies.",
|
|
73
|
+
},
|
|
74
|
+
iblEnabled: {
|
|
75
|
+
label: "Iluminación basada en imagen (IBL)",
|
|
76
|
+
helper: "Usa la imagen HDR de entorno para iluminar la escena.",
|
|
77
|
+
},
|
|
78
|
+
shadowsEnabled: {
|
|
79
|
+
label: "Sombras",
|
|
80
|
+
helper: "Activa sombras.",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
downloadDialog: {
|
|
85
|
+
title: "Descargar escena 3D",
|
|
86
|
+
sections: {
|
|
87
|
+
content: "Contenido",
|
|
88
|
+
format: "Formato",
|
|
89
|
+
},
|
|
90
|
+
options: {
|
|
91
|
+
model: "Modelo",
|
|
92
|
+
scene: "Escena",
|
|
93
|
+
both: "Ambos",
|
|
94
|
+
},
|
|
95
|
+
buttons: {
|
|
96
|
+
download: "Descargar",
|
|
97
|
+
cancel: "Cancelar",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export default translations;
|
|
@@ -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 = "") {
|
|
@@ -18,6 +21,7 @@ export class ContainerData {
|
|
|
18
21
|
this.show = true;
|
|
19
22
|
this.size = 0;
|
|
20
23
|
this.timeStamp = null;
|
|
24
|
+
this.storage = null;
|
|
21
25
|
this.visible = false;
|
|
22
26
|
// Set initial information about ongoing update
|
|
23
27
|
this.reset();
|
|
@@ -40,6 +44,9 @@ export class ContainerData {
|
|
|
40
44
|
this.size = this.update.size;
|
|
41
45
|
this.timeStamp = this.update.timeStamp;
|
|
42
46
|
this.metadata = { ...(this.update.metadata ?? {}) };
|
|
47
|
+
if (this.update.storage !== undefined && this.update.storage !== null) {
|
|
48
|
+
this.storage = this.update.storage;
|
|
49
|
+
}
|
|
43
50
|
} else {
|
|
44
51
|
this.update.success = false;
|
|
45
52
|
}
|
|
@@ -50,12 +57,25 @@ export class ContainerData {
|
|
|
50
57
|
this.update.storage = storage;
|
|
51
58
|
this.update.success = false;
|
|
52
59
|
this.update.show = show !== undefined ? show : this.update.show !== null ? this.update.show : this.show;
|
|
60
|
+
if (storage !== undefined && storage !== null) {
|
|
61
|
+
this.storage = storage;
|
|
62
|
+
}
|
|
53
63
|
}
|
|
54
64
|
setPendingCacheData(size = 0, timeStamp = null, metadata = {}) {
|
|
55
65
|
this.update.size = size;
|
|
56
66
|
this.update.timeStamp = timeStamp;
|
|
57
67
|
this.update.metadata = { ...(metadata ?? {}) };
|
|
58
68
|
}
|
|
69
|
+
setPendingWithCurrentStorage() {
|
|
70
|
+
const storedSource = this.storage ?? null;
|
|
71
|
+
if (!storedSource) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
const targetShow = this.update.show !== null ? this.update.show : this.show;
|
|
75
|
+
this.setPending(storedSource, targetShow);
|
|
76
|
+
this.setPendingCacheData(this.size, this.timeStamp, this.metadata);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
59
79
|
get isPending() {
|
|
60
80
|
return this.update.pending === true;
|
|
61
81
|
}
|
|
@@ -71,17 +91,18 @@ export class ContainerData {
|
|
|
71
91
|
}
|
|
72
92
|
|
|
73
93
|
/**
|
|
74
|
-
* MaterialData - Represents
|
|
94
|
+
* MaterialData - Represents a material option plus its update lifecycle in the 3D viewer.
|
|
75
95
|
*
|
|
76
96
|
* Responsibilities:
|
|
77
|
-
* -
|
|
78
|
-
* -
|
|
79
|
-
* -
|
|
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`).
|
|
80
100
|
*
|
|
81
101
|
* Usage:
|
|
82
|
-
* - Instantiate with
|
|
83
|
-
* -
|
|
84
|
-
* -
|
|
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`.
|
|
85
106
|
*/
|
|
86
107
|
export class MaterialData {
|
|
87
108
|
constructor(name = "", value = null, nodeNames = [], nodePrefixes = []) {
|
|
@@ -114,7 +135,11 @@ export class MaterialData {
|
|
|
114
135
|
this.update.value = value;
|
|
115
136
|
}
|
|
116
137
|
setPendingWithCurrent() {
|
|
138
|
+
if (this.value === undefined) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
117
141
|
this.setPending(this.value);
|
|
142
|
+
return true;
|
|
118
143
|
}
|
|
119
144
|
get isPending() {
|
|
120
145
|
return this.update.pending === true;
|
|
@@ -125,17 +150,18 @@ export class MaterialData {
|
|
|
125
150
|
}
|
|
126
151
|
|
|
127
152
|
/**
|
|
128
|
-
* CameraData - Represents
|
|
153
|
+
* CameraData - Represents camera selection state and camera-lock behavior for the 3D viewer.
|
|
129
154
|
*
|
|
130
155
|
* Responsibilities:
|
|
131
|
-
* -
|
|
132
|
-
* -
|
|
133
|
-
* - 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.
|
|
134
159
|
*
|
|
135
160
|
* Usage:
|
|
136
|
-
* - Instantiate with a name and optional value: new CameraData("camera", value, locked)
|
|
137
|
-
* -
|
|
138
|
-
* -
|
|
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`.
|
|
139
165
|
*/
|
|
140
166
|
export class CameraData {
|
|
141
167
|
defaultLocked = true;
|
|
@@ -175,6 +201,11 @@ export class CameraData {
|
|
|
175
201
|
this.update.value = value;
|
|
176
202
|
this.update.locked = locked;
|
|
177
203
|
}
|
|
204
|
+
setPendingWithCurrent() {
|
|
205
|
+
const currentValue = this.value !== undefined ? this.value : null;
|
|
206
|
+
this.setPending(currentValue, this.locked);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
178
209
|
get isPending() {
|
|
179
210
|
return this.update.pending === true;
|
|
180
211
|
}
|
|
@@ -184,51 +215,98 @@ export class CameraData {
|
|
|
184
215
|
}
|
|
185
216
|
|
|
186
217
|
/**
|
|
187
|
-
* IBLData -
|
|
218
|
+
* IBLData - Stores image-based lighting configuration for HDR environment rendering.
|
|
188
219
|
*
|
|
189
220
|
* Responsibilities:
|
|
190
|
-
* - Stores
|
|
191
|
-
* -
|
|
192
|
-
* -
|
|
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`).
|
|
193
226
|
*
|
|
194
227
|
* Usage:
|
|
195
|
-
* - Instantiate with defaults: `const ibl = new IBLData()
|
|
196
|
-
* - Call `
|
|
197
|
-
* -
|
|
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.
|
|
198
233
|
*/
|
|
199
234
|
export class IBLData {
|
|
200
|
-
|
|
235
|
+
defaultIntensity = 1.0;
|
|
236
|
+
defaultShadows = false;
|
|
237
|
+
constructor(url = null, cachedUrl = null, intensity = this.defaultIntensity, shadows = this.defaultShadows, timeStamp = null, valid = false) {
|
|
201
238
|
this.url = url;
|
|
239
|
+
this.cachedUrl = cachedUrl;
|
|
202
240
|
this.intensity = intensity;
|
|
203
241
|
this.shadows = shadows;
|
|
204
242
|
this.timeStamp = timeStamp;
|
|
205
|
-
this.
|
|
243
|
+
this.valid = valid;
|
|
206
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
|
+
*/
|
|
207
263
|
reset() {
|
|
208
|
-
this.
|
|
209
|
-
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;
|
|
210
271
|
}
|
|
211
|
-
|
|
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;
|
|
285
|
+
}
|
|
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
|
+
}
|
|
212
303
|
this.url = url !== undefined ? url : this.url;
|
|
304
|
+
this.cachedUrl = cachedUrl !== undefined ? cachedUrl : this.cachedUrl;
|
|
213
305
|
this.intensity = intensity !== undefined ? intensity : this.intensity;
|
|
214
306
|
this.shadows = shadows !== undefined ? shadows : this.shadows;
|
|
215
307
|
this.timeStamp = timeStamp !== undefined ? timeStamp : this.timeStamp;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (success) {
|
|
219
|
-
this.success = true;
|
|
220
|
-
} else {
|
|
221
|
-
this.success = false;
|
|
308
|
+
if (cachedUrl !== undefined) {
|
|
309
|
+
this.valid = typeof cachedUrl === "string" && cachedUrl.length > 0 ? false : this.valid;
|
|
222
310
|
}
|
|
223
311
|
}
|
|
224
|
-
setPending() {
|
|
225
|
-
this.pending = true;
|
|
226
|
-
this.success = false;
|
|
227
|
-
}
|
|
228
|
-
get isPending() {
|
|
229
|
-
return this.pending === true;
|
|
230
|
-
}
|
|
231
|
-
get isSuccess() {
|
|
232
|
-
return this.success === true;
|
|
233
|
-
}
|
|
234
312
|
}
|