@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.
@@ -1,4 +1,4 @@
1
- import { FileStorage } from "./file-storage.js";
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 the resolved source (File or data URI), size, timestamp, and extension if an update is required.
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 extension for further processing.
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 { source: file || source, size: newSize, timeStamp: newTimeStamp, extension: extension, metadata: metadata, };
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 the state and metadata of a asset container in the 3D viewer (e.g., model, environment, materials).
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 name, show (if must be visible), visibility (if currently visible), size, and timestamp.
6
- * - Manages update state for asynchronous operations (pending, success, etc.).
7
- * - Provides methods to reset, set pending, and set success states.
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
- * - Use setPending(), setSuccess(), and reset() to manage update state.
12
- * - Access status via isPending, isSuccess, isVisible getters.
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 the state and metadata of a material in the 3D viewer.
94
+ * MaterialData - Represents a material option plus its update lifecycle in the 3D viewer.
75
95
  *
76
96
  * Responsibilities:
77
- * - Tracks material name, value, node names, and node prefixes.
78
- * - Manages update state for asynchronous operations (pending, success, etc.).
79
- * - Provides methods to reset, set pending, and set success states.
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 a name and optional value: new MaterialData("innerWall", value, nodeNames, nodePrefixes)
83
- * - Use setPending(), setSuccess(), and reset() to manage update state.
84
- * - Access status via isPending, isSuccess getters.
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 the state and metadata of a camera in the 3D viewer.
153
+ * CameraData - Represents camera selection state and camera-lock behavior for the 3D viewer.
129
154
  *
130
155
  * Responsibilities:
131
- * - Tracks camera name, value, and locked status.
132
- * - Manages update state for asynchronous operations (pending, success, etc.).
133
- * - Provides methods to reset, set pending, and set success states.
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
- * - Use setPending(), setSuccess(), and reset() to manage update state.
138
- * - Access status via isPending, isSuccess getters.
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 - Tracks configurable settings for image-based lighting assets (IBL/HDR environments).
218
+ * IBLData - Stores image-based lighting configuration for HDR environment rendering.
188
219
  *
189
220
  * Responsibilities:
190
- * - Stores the HDR url, intensity scalar, whether environment-provided shadows should render, and cache timestamp.
191
- * - Exposes a lightweight pending/success state machine so UI flows know when a new IBL selection is being fetched.
192
- * - Provides helpers to update values atomically via `setValues`, toggle pending state, and mark completion.
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 `setPending()` before kicking off an async download, `setValues()` as metadata streams in, and `setSuccess(true)` once loading finishes.
197
- * - Inspect `isPending`/`isSuccess` to drive UI or re-render logic.
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
- constructor(url = null, intensity = 1.0, shadows = false, timeStamp = null) {
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.reset();
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.pending = false;
209
- this.success = false;
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
- setValues(url, intensity, shadows, timeStamp) {
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
- setSuccess(success = false) {
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
  }