@preference-sl/pref-viewer 3.1.0-beta.3 → 3.1.0-beta.4
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 +1 -1
- package/src/babylonjs-ar-glass.js +1 -1
- package/src/babylonjs-controller.js +2 -2
- package/src/ibl-source-resolver.js +43 -9
- package/src/material-utils.js +3 -0
- package/src/pref-viewer-3d-data.js +38 -7
- package/src/pref-viewer-3d.js +3 -15
package/README.md
CHANGED
|
@@ -591,7 +591,7 @@ The model's transmissive glass (`KHR_materials_transmission`, as authored by the
|
|
|
591
591
|
| `.env` | Prefiltered Babylon environment (`CubeTexture.CreateFromPrefilteredData`) | No prefiltering in the browser; a 4K environment is typically under 1 MB. |
|
|
592
592
|
| anything else | Radiance HDR (`HDRCubeTexture`) | Prefiltered on load, which costs time and memory for large files. |
|
|
593
593
|
|
|
594
|
-
Either file is downloaded once, cached in IndexedDB, and reused across reloads. The cached bytes reach Babylon as an in-memory file, never as a `blob:` URL: iOS WebKit answers requests to such URLs with status 0, and the environment then never became ready, so the 3D load never finished. A cached record that is empty or cannot be read within 5 s is purged and the server URL is used for that load.
|
|
594
|
+
Either file is downloaded once, cached in IndexedDB, and reused across reloads. The cached bytes reach Babylon as an in-memory file, never as a `blob:` URL: iOS WebKit answers requests to such URLs with status 0, and the environment then never became ready, so the 3D load never finished. A cached record that is empty or cannot be read within 5 s is purged and the server URL is used for that load; a first download that does not answer within 5 s also falls back to the server URL instead of leaving the scene without an environment. File names are reduced to a URL-safe alphabet, so spaces, `%` and accents in the original name are harmless.
|
|
595
595
|
|
|
596
596
|
Directional and spot lights that come with the scene glTF (`KHR_lights_punctual`) cast shadows when `render.shadowsEnabled` is `true` and `ibl.shadows` is off, alongside the built-in light's. Every scene and model mesh casts unless its node sets `extras.castShadows: false`, and receives unless it sets `extras.receiveShadows: false`. A light's shadow darkness comes from its node's `extras.shadowDarkness`, from `0` (black) to `1` (no shadow), and is `0.5` when absent:
|
|
597
597
|
|
package/package.json
CHANGED
|
@@ -69,7 +69,7 @@ export default class BabylonJSARGlass {
|
|
|
69
69
|
sub.isRefractionEnabled = false;
|
|
70
70
|
sub.isTranslucencyEnabled = false;
|
|
71
71
|
const authored = linked ? null : MaterialUtils.authoredGlassAlpha(material);
|
|
72
|
-
material.alpha = authored !== null && authored < 1
|
|
72
|
+
material.alpha = authored !== null && authored > 0 && authored < 1
|
|
73
73
|
? authored
|
|
74
74
|
: (linked ? 1 : alpha) * Math.max(BabylonJSARGlass.#MIN_GLASS_OPACITY,
|
|
75
75
|
1 - transmission * BabylonJSARGlass.#TRANSMISSION_REDUCTION);
|
|
@@ -1651,7 +1651,7 @@ export default class BabylonJSController {
|
|
|
1651
1651
|
* Lifecycle implemented here:
|
|
1652
1652
|
* 1. If `options.ibl.cachedUrl` exists, create `HDRCubeTexture` from it (a prefiltered `CubeTexture` when `options.ibl.url` is a `.env`).
|
|
1653
1653
|
* 2. Wait for readiness, clone it into `#hdrTexture` for reuse.
|
|
1654
|
-
* 3. Call `options.ibl.consumeCachedUrl(true)` to
|
|
1654
|
+
* 3. Call `options.ibl.consumeCachedUrl(true)` to release the temporary `file:` source and clear `cachedUrl`.
|
|
1655
1655
|
* 4. On following reloads, if `options.ibl.valid === true` and no `cachedUrl` is present, use `#hdrTexture.clone()`.
|
|
1656
1656
|
*/
|
|
1657
1657
|
async #initializeEnvironmentTexture() {
|
|
@@ -1664,7 +1664,7 @@ export default class BabylonJSController {
|
|
|
1664
1664
|
if (this.#options.ibl?.cachedUrl) {
|
|
1665
1665
|
const hdrTextureURI = this.#options.ibl.cachedUrl;
|
|
1666
1666
|
if (/\.env$/i.test(String(this.#options.ibl.url ?? "").split(/[?#]/)[0])) {
|
|
1667
|
-
// The cached file is a
|
|
1667
|
+
// The cached file is a file: URL whose name may not end in .env, so the loader is told it is a .env.
|
|
1668
1668
|
hdrTexture = CubeTexture.CreateFromPrefilteredData(hdrTextureURI, this.#scene, ".env");
|
|
1669
1669
|
} else {
|
|
1670
1670
|
const envTextureSize = BabylonJSController.config.quality.environmentTextureSize || 1024;
|
|
@@ -9,6 +9,11 @@ import FileStorage from "./file-storage.js";
|
|
|
9
9
|
* 3D load never finished. Like `GLTFResolver` does for `.glb` files, the cached bytes are read here into memory and
|
|
10
10
|
* handed to Babylon as a `File` through its `file:` scheme (`FilesInputStore`), so no XHR touches a blob. A record
|
|
11
11
|
* that is empty or cannot be read in time is purged and the server URL is used instead, so the load always completes.
|
|
12
|
+
* The same goes for a cache read-through that does not answer in time (a slow first download): Babylon then loads
|
|
13
|
+
* the server URL itself instead of the scene being left without an environment.
|
|
14
|
+
*
|
|
15
|
+
* File names are reduced to a URL-safe alphabet, because Babylon runs `decodeURIComponent` on the `file:` URL before
|
|
16
|
+
* looking the name up and a space, `%` or accent in the original file name would otherwise miss the store.
|
|
12
17
|
*
|
|
13
18
|
* Every resolution registers its own file name; the owner releases it with `release` once Babylon has consumed it.
|
|
14
19
|
*/
|
|
@@ -26,6 +31,8 @@ export default class IBLSourceResolver {
|
|
|
26
31
|
static CACHE_READ_TIMEOUT_MS = 5000;
|
|
27
32
|
|
|
28
33
|
/**
|
|
34
|
+
* Creates a resolver bound to one cache.
|
|
35
|
+
* @public
|
|
29
36
|
* @param {FileStorage} [fileStorage] - Cache to read from; defaults to the viewer's shared store.
|
|
30
37
|
*/
|
|
31
38
|
constructor(fileStorage = new FileStorage("PrefViewer", "Files")) {
|
|
@@ -37,15 +44,16 @@ export default class IBLSourceResolver {
|
|
|
37
44
|
* @public
|
|
38
45
|
* @param {string} url - Server URL of the `.env` or `.hdr` file.
|
|
39
46
|
* @returns {Promise<{cachedUrl: string, timeStamp: string|null}|false>} The `file:` source of the in-memory copy
|
|
40
|
-
* (or the server URL when the cache cannot serve it) with the record's time stamp, or false when the
|
|
41
|
-
* unavailable both in the cache and on the server.
|
|
47
|
+
* (or the server URL when the cache cannot serve it in time) with the record's time stamp, or false when the
|
|
48
|
+
* file is unavailable both in the cache and on the server. Every `file:` source returned must reach `release`.
|
|
42
49
|
*/
|
|
43
50
|
async resolve(url) {
|
|
44
51
|
let record;
|
|
45
52
|
try {
|
|
46
53
|
record = await IBLSourceResolver.#withTimeout(this.#fileStorage.get(url), IBLSourceResolver.CACHE_READ_TIMEOUT_MS);
|
|
47
|
-
} catch {
|
|
48
|
-
|
|
54
|
+
} catch (error) {
|
|
55
|
+
// A slow read-through is not a missing file: let Babylon download the server URL itself.
|
|
56
|
+
return error?.timedOut === true ? { cachedUrl: url, timeStamp: null } : false;
|
|
49
57
|
}
|
|
50
58
|
if (!record || !record.blob) {
|
|
51
59
|
return false;
|
|
@@ -64,8 +72,7 @@ export default class IBLSourceResolver {
|
|
|
64
72
|
void Promise.resolve(this.#fileStorage.remove(url)).catch(() => {});
|
|
65
73
|
return { cachedUrl: url, timeStamp };
|
|
66
74
|
}
|
|
67
|
-
const
|
|
68
|
-
const name = `${IBLSourceResolver.#FILE_PREFIX}${++IBLSourceResolver.#sequence}-${baseName}`;
|
|
75
|
+
const name = `${IBLSourceResolver.#FILE_PREFIX}${++IBLSourceResolver.#sequence}-${IBLSourceResolver.#safeBaseName(url)}`;
|
|
69
76
|
FilesInputStore.FilesToLoad[name.toLowerCase()] = new File([bytes], name, { type: record.blob.type || "" });
|
|
70
77
|
return { cachedUrl: `file:${name}`, timeStamp };
|
|
71
78
|
}
|
|
@@ -83,24 +90,51 @@ export default class IBLSourceResolver {
|
|
|
83
90
|
return;
|
|
84
91
|
}
|
|
85
92
|
if (cachedUrl.startsWith("file:")) {
|
|
86
|
-
|
|
93
|
+
const name = cachedUrl.slice("file:".length);
|
|
94
|
+
let decoded = name;
|
|
95
|
+
try {
|
|
96
|
+
decoded = decodeURIComponent(name);
|
|
97
|
+
} catch {
|
|
98
|
+
// Not percent-encoded: the raw name is the key.
|
|
99
|
+
}
|
|
100
|
+
delete FilesInputStore.FilesToLoad[decoded.toLowerCase()];
|
|
87
101
|
} else if (cachedUrl.startsWith("blob:")) {
|
|
88
102
|
URL.revokeObjectURL(cachedUrl);
|
|
89
103
|
}
|
|
90
104
|
}
|
|
91
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Reduces the last path segment of a URL to letters, digits, dot, underscore and dash, so the name survives
|
|
108
|
+
* Babylon's `decodeURIComponent` of `file:` URLs unchanged.
|
|
109
|
+
* @private
|
|
110
|
+
* @static
|
|
111
|
+
* @param {string} url - Server URL of the environment file.
|
|
112
|
+
* @returns {string} Safe, non-empty base name.
|
|
113
|
+
*/
|
|
114
|
+
static #safeBaseName(url) {
|
|
115
|
+
const segment = url.split(/[?#]/)[0].split("/").pop() || "";
|
|
116
|
+
let decoded = segment;
|
|
117
|
+
try {
|
|
118
|
+
decoded = decodeURIComponent(segment);
|
|
119
|
+
} catch {
|
|
120
|
+
// Keep the raw segment; the replacement below removes the stray percent sign.
|
|
121
|
+
}
|
|
122
|
+
const safe = decoded.normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
123
|
+
return safe.replace(/^_+|_+$/g, "") || "environment";
|
|
124
|
+
}
|
|
125
|
+
|
|
92
126
|
/**
|
|
93
127
|
* Bounds a cache read so an IndexedDB transaction that never settles cannot block the load.
|
|
94
128
|
* @private
|
|
95
129
|
* @static
|
|
96
130
|
* @param {Promise<any>} promise - Read to bound.
|
|
97
131
|
* @param {number} ms - Time allowed before rejecting.
|
|
98
|
-
* @returns {Promise<any>} The read's result, or a rejection after `ms`.
|
|
132
|
+
* @returns {Promise<any>} The read's result, or a rejection after `ms` whose error has `timedOut === true`.
|
|
99
133
|
*/
|
|
100
134
|
static #withTimeout(promise, ms) {
|
|
101
135
|
let timer = null;
|
|
102
136
|
const timeout = new Promise((resolve, reject) => {
|
|
103
|
-
timer = setTimeout(() => reject(new Error(`Cache read timed out after ${ms}ms`)), ms);
|
|
137
|
+
timer = setTimeout(() => reject(Object.assign(new Error(`Cache read timed out after ${ms}ms`), { timedOut: true })), ms);
|
|
104
138
|
});
|
|
105
139
|
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
106
140
|
}
|
package/src/material-utils.js
CHANGED
|
@@ -58,6 +58,9 @@ export default class MaterialUtils {
|
|
|
58
58
|
* 1, raising the dielectric F0 and re-enabling the over-alpha reflections lets the environment
|
|
59
59
|
* show up on the glass while transmission keeps it see-through.
|
|
60
60
|
*
|
|
61
|
+
* Before raising the alpha, the alpha the SDK authored is stored once in the material metadata so the
|
|
62
|
+
* AR adapter can restore the authored see-through look in Quick Look / WebXR (see `authoredGlassAlpha`).
|
|
63
|
+
*
|
|
61
64
|
* Only physical glass (see `isPrefViewerPhysicalGlassMaterial`) is touched; `transparencyMode`
|
|
62
65
|
* and `subSurface` are left as authored so the glass stays in the transparent pass.
|
|
63
66
|
* @public
|
|
@@ -386,7 +386,7 @@ export class CameraData {
|
|
|
386
386
|
* - Instantiate with defaults: `const ibl = new IBLData();`.
|
|
387
387
|
* - Call `setValues(...)` with only the fields you want to update (`undefined` preserves current values).
|
|
388
388
|
* - Lifecycle: set a fresh `cachedUrl` via `setValues(...)`, create/clone the Babylon HDR texture, then call
|
|
389
|
-
* `consumeCachedUrl(true)` to
|
|
389
|
+
* `consumeCachedUrl(true)` to release the temporary `file:` source, set `cachedUrl` to null, and keep `valid=true`.
|
|
390
390
|
* - Call `reset()` to clear the current IBL environment and restore default intensity/shadows.
|
|
391
391
|
*/
|
|
392
392
|
export class IBLData {
|
|
@@ -418,7 +418,7 @@ export class IBLData {
|
|
|
418
418
|
* @param {string|null|undefined} url Cached source to release when applicable.
|
|
419
419
|
* @returns {void}
|
|
420
420
|
*/
|
|
421
|
-
#
|
|
421
|
+
#releaseSource(url) {
|
|
422
422
|
IBLSourceResolver.release(url);
|
|
423
423
|
}
|
|
424
424
|
|
|
@@ -428,7 +428,7 @@ export class IBLData {
|
|
|
428
428
|
* @returns {void}
|
|
429
429
|
*/
|
|
430
430
|
reset() {
|
|
431
|
-
this.#
|
|
431
|
+
this.#releaseSource(this.cachedUrl);
|
|
432
432
|
this.url = null
|
|
433
433
|
this.cachedUrl = null;
|
|
434
434
|
this.intensity = this.defaultIntensity;
|
|
@@ -439,14 +439,14 @@ export class IBLData {
|
|
|
439
439
|
|
|
440
440
|
/**
|
|
441
441
|
* Consumes the temporary cached URL after Babylon texture creation.
|
|
442
|
-
*
|
|
442
|
+
* Releases the source (a `file:` entry or a legacy `blob:` object URL), clears `cachedUrl`,
|
|
443
443
|
* and marks whether a reusable in-memory texture is available.
|
|
444
444
|
* @public
|
|
445
445
|
* @param {boolean} [valid=true] Whether the corresponding HDR texture has been validated and cached in memory.
|
|
446
446
|
* @returns {void}
|
|
447
447
|
*/
|
|
448
448
|
consumeCachedUrl(valid = true) {
|
|
449
|
-
this.#
|
|
449
|
+
this.#releaseSource(this.cachedUrl);
|
|
450
450
|
this.cachedUrl = null;
|
|
451
451
|
this.valid = !!valid;
|
|
452
452
|
}
|
|
@@ -457,7 +457,7 @@ export class IBLData {
|
|
|
457
457
|
* until the URL is consumed into a Babylon texture.
|
|
458
458
|
* @public
|
|
459
459
|
* @param {string|null|undefined} url Source URL identifier.
|
|
460
|
-
* @param {string|null|undefined} cachedUrl
|
|
460
|
+
* @param {string|null|undefined} cachedUrl Loadable source from `IBLSourceResolver` (a `file:` source or the server URL; a legacy `blob:` URL is also released).
|
|
461
461
|
* @param {number|undefined} intensity IBL intensity.
|
|
462
462
|
* @param {boolean|undefined} shadows Shadow toggle for IBL path.
|
|
463
463
|
* @param {string|null|undefined} timeStamp Last-modified marker.
|
|
@@ -465,7 +465,7 @@ export class IBLData {
|
|
|
465
465
|
*/
|
|
466
466
|
setValues(url, cachedUrl, intensity, shadows, timeStamp) {
|
|
467
467
|
if (cachedUrl !== undefined && cachedUrl !== this.cachedUrl) {
|
|
468
|
-
this.#
|
|
468
|
+
this.#releaseSource(this.cachedUrl);
|
|
469
469
|
}
|
|
470
470
|
this.url = url !== undefined ? url : this.url;
|
|
471
471
|
this.cachedUrl = cachedUrl !== undefined ? cachedUrl : this.cachedUrl;
|
|
@@ -476,4 +476,35 @@ export class IBLData {
|
|
|
476
476
|
this.valid = typeof cachedUrl === "string" && cachedUrl.length > 0 ? false : this.valid;
|
|
477
477
|
}
|
|
478
478
|
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Applies freshly resolved IBL options and tells whether the environment must be reloaded.
|
|
482
|
+
* A `null` `url` or `cachedUrl` clears the environment. When nothing differs from the current state, the
|
|
483
|
+
* `cachedUrl` the caller just resolved is released here, so an in-memory copy registered for nothing never
|
|
484
|
+
* outlives the call. Otherwise ownership of `cachedUrl` passes to this state until it is consumed or replaced.
|
|
485
|
+
* @public
|
|
486
|
+
* @param {object} [resolved] Resolved IBL options; `undefined` fields keep the current value.
|
|
487
|
+
* @param {string|null} [resolved.url] Source URL identifier, or `null` to clear the environment.
|
|
488
|
+
* @param {string|null} [resolved.cachedUrl] Loadable source produced by `IBLSourceResolver`, or `null`.
|
|
489
|
+
* @param {string|null} [resolved.timeStamp] Source modification marker.
|
|
490
|
+
* @param {boolean} [resolved.shadows] IBL shadow toggle.
|
|
491
|
+
* @param {number} [resolved.intensity] IBL intensity.
|
|
492
|
+
* @returns {boolean} True when the state changed and the environment must be reloaded.
|
|
493
|
+
*/
|
|
494
|
+
applyResolved({ url, cachedUrl, timeStamp, shadows, intensity } = {}) {
|
|
495
|
+
const needUpdate = url !== undefined && url !== this.url ||
|
|
496
|
+
timeStamp !== undefined && timeStamp !== this.timeStamp ||
|
|
497
|
+
shadows !== undefined && shadows !== this.shadows ||
|
|
498
|
+
shadows === undefined && this.shadows !== this.defaultShadows ||
|
|
499
|
+
intensity !== undefined && intensity !== this.intensity ||
|
|
500
|
+
intensity === undefined && this.intensity !== this.defaultIntensity;
|
|
501
|
+
if (!needUpdate) {
|
|
502
|
+
this.#releaseSource(cachedUrl);
|
|
503
|
+
} else if (url === null || cachedUrl === null) {
|
|
504
|
+
this.reset();
|
|
505
|
+
} else {
|
|
506
|
+
this.setValues(url, cachedUrl, intensity, shadows, timeStamp);
|
|
507
|
+
}
|
|
508
|
+
return needUpdate;
|
|
509
|
+
}
|
|
479
510
|
}
|
package/src/pref-viewer-3d.js
CHANGED
|
@@ -429,6 +429,8 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
429
429
|
* This allows updating other IBL properties (such as intensity or shadows) without requiring a new URL.
|
|
430
430
|
* If `options.ibl.url` is explicitly provided as `null` (or resolves to an unavailable URL), it is treated as a request to
|
|
431
431
|
* clear the current IBL environment, and the IBL state is reset so Babylon can remove the environment map.
|
|
432
|
+
* The cached environment is resolved by `IBLSourceResolver` into a `file:` source; the comparison and the release of a
|
|
433
|
+
* source that turned out not to be needed are delegated to `IBLData.applyResolved`, so repeated identical options leave nothing behind.
|
|
432
434
|
* @private
|
|
433
435
|
* @param {object} options - Options payload that may contain an `ibl` block with url, intensity, or shadow flags.
|
|
434
436
|
* @returns {Promise<boolean>} Resolves to true when any IBL property differs from the cached state, otherwise false.
|
|
@@ -467,21 +469,7 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
467
469
|
intensity = options.ibl.intensity;
|
|
468
470
|
}
|
|
469
471
|
|
|
470
|
-
|
|
471
|
-
timeStamp !== undefined && timeStamp !== iblState.timeStamp ||
|
|
472
|
-
shadows !== undefined && shadows !== iblState.shadows ||
|
|
473
|
-
shadows === undefined && iblState.shadows !== iblState.defaultShadows ||
|
|
474
|
-
intensity !== undefined && intensity !== iblState.intensity ||
|
|
475
|
-
intensity === undefined && iblState.intensity !== iblState.defaultIntensity;
|
|
476
|
-
if (needUpdate) {
|
|
477
|
-
if (url === null || cachedUrl === null) {
|
|
478
|
-
iblState.reset();
|
|
479
|
-
} else {
|
|
480
|
-
iblState.setValues(url, cachedUrl, intensity, shadows, timeStamp);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
return needUpdate;
|
|
472
|
+
return iblState.applyResolved({ url, cachedUrl, timeStamp, shadows, intensity });
|
|
485
473
|
}
|
|
486
474
|
|
|
487
475
|
/**
|