@preference-sl/pref-viewer 2.16.0-beta.1 → 2.16.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/LICENSE.md +13 -0
- package/README.md +463 -0
- package/package.json +7 -7
- package/src/babylonjs-controller.js +122 -847
- package/src/file-storage.js +5 -126
- package/src/gltf-resolver.js +24 -58
- package/src/localization/translations.js +5 -17
- package/src/material-utils.js +72 -0
- package/src/pref-viewer-3d.js +24 -112
- package/src/pref-viewer-menu-3d.js +133 -260
- package/src/pref-viewer.js +4 -150
- package/src/render-math.js +127 -0
- package/src/styles.js +71 -163
- package/Readme.md +0 -8
package/src/file-storage.js
CHANGED
|
@@ -129,10 +129,6 @@ export default class FileStorage {
|
|
|
129
129
|
#minimumFreeSpaceRatio = 0.05; // try to keep at least 5% of quota free
|
|
130
130
|
#maxQuotaEvictionRetries = 5; // max retries after quota error
|
|
131
131
|
#lastCleanupAt = 0; // timestamp of last automatic cleanup run
|
|
132
|
-
// In-memory blob cache — eliminates repeated IDB round-trips for the same URI within a session.
|
|
133
|
-
// Keyed by URI; values are normalized records (blob kept in RAM). Avoids both the readonly read
|
|
134
|
-
// and the readwrite #touchFile transaction on every getURL() call during #replaceSceneURIAsync.
|
|
135
|
-
#memoryCache = new Map();
|
|
136
132
|
|
|
137
133
|
constructor(dbName = "FilesDB", osName = "FilesObjectStore") {
|
|
138
134
|
this.dbName = dbName; // database name
|
|
@@ -262,7 +258,6 @@ export default class FileStorage {
|
|
|
262
258
|
const store = transaction.objectStore(this.osName);
|
|
263
259
|
keys.forEach((key) => {
|
|
264
260
|
store.delete(key);
|
|
265
|
-
this.#memoryCache.delete(String(key));
|
|
266
261
|
});
|
|
267
262
|
await transaction.done;
|
|
268
263
|
return keys.length;
|
|
@@ -613,7 +608,6 @@ export default class FileStorage {
|
|
|
613
608
|
const store = transaction.objectStore(this.osName);
|
|
614
609
|
const result = await store.put(record, uri);
|
|
615
610
|
await transaction.done;
|
|
616
|
-
this.#memoryCache.set(uri, record);
|
|
617
611
|
return result;
|
|
618
612
|
} catch (error) {
|
|
619
613
|
if (!this.#isQuotaExceededError(error) || attempt === this.#maxQuotaEvictionRetries) {
|
|
@@ -641,19 +635,6 @@ export default class FileStorage {
|
|
|
641
635
|
* - false: if the database could not be opened (IndexedDB unsupported or open failure)
|
|
642
636
|
*/
|
|
643
637
|
async #getFile(uri, { touch = true, allowExpired = false } = {}) {
|
|
644
|
-
// In-memory cache hit: skip IDB entirely, update lastAccess in RAM only.
|
|
645
|
-
const inMemory = this.#memoryCache.get(uri);
|
|
646
|
-
if (inMemory) {
|
|
647
|
-
if (!allowExpired && this.#isExpired(inMemory)) {
|
|
648
|
-
this.#memoryCache.delete(uri);
|
|
649
|
-
} else {
|
|
650
|
-
if (touch) {
|
|
651
|
-
inMemory.lastAccess = this.#now();
|
|
652
|
-
}
|
|
653
|
-
return inMemory;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
|
|
657
638
|
if (this.#db === undefined) {
|
|
658
639
|
this.#db = await this.#openDB();
|
|
659
640
|
}
|
|
@@ -670,68 +651,13 @@ export default class FileStorage {
|
|
|
670
651
|
await this.#deleteKeys([uri]);
|
|
671
652
|
return undefined;
|
|
672
653
|
}
|
|
673
|
-
this.#memoryCache.set(uri, record);
|
|
674
654
|
if (touch) {
|
|
675
|
-
|
|
676
|
-
void this.#touchFile(uri, record);
|
|
655
|
+
await this.#touchFile(uri, record);
|
|
677
656
|
record.lastAccess = this.#now();
|
|
678
657
|
}
|
|
679
658
|
return record;
|
|
680
659
|
}
|
|
681
660
|
|
|
682
|
-
/**
|
|
683
|
-
* Retrieves multiple file records from the in-memory cache and IDB in a single readonly
|
|
684
|
-
* transaction, populating the in-memory cache for all hits.
|
|
685
|
-
* @private
|
|
686
|
-
* @param {string[]} uris - URIs to look up.
|
|
687
|
-
* @returns {Promise<Map<string, object>>} Map of uri → normalized record for found entries.
|
|
688
|
-
*/
|
|
689
|
-
async #getFileBatch(uris) {
|
|
690
|
-
const resultMap = new Map();
|
|
691
|
-
if (!uris.length) return resultMap;
|
|
692
|
-
|
|
693
|
-
const idbMisses = [];
|
|
694
|
-
for (const uri of uris) {
|
|
695
|
-
const cached = this.#memoryCache.get(uri);
|
|
696
|
-
if (cached) {
|
|
697
|
-
if (this.#isExpired(cached)) {
|
|
698
|
-
this.#memoryCache.delete(uri);
|
|
699
|
-
idbMisses.push(uri);
|
|
700
|
-
} else {
|
|
701
|
-
resultMap.set(uri, cached);
|
|
702
|
-
}
|
|
703
|
-
} else {
|
|
704
|
-
idbMisses.push(uri);
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
if (!idbMisses.length) return resultMap;
|
|
709
|
-
|
|
710
|
-
if (this.#db === undefined) {
|
|
711
|
-
this.#db = await this.#openDB();
|
|
712
|
-
}
|
|
713
|
-
if (this.#db === null) return resultMap;
|
|
714
|
-
|
|
715
|
-
// One readonly transaction for all IDB misses — single lock acquisition vs N transactions.
|
|
716
|
-
const tx = this.#db.transaction(this.osName, "readonly");
|
|
717
|
-
const store = tx.objectStore(this.osName);
|
|
718
|
-
const rawRecords = await Promise.all(idbMisses.map((uri) => store.get(uri)));
|
|
719
|
-
|
|
720
|
-
const now = this.#now();
|
|
721
|
-
idbMisses.forEach((uri, i) => {
|
|
722
|
-
const record = this.#normalizeRecord(rawRecords[i]);
|
|
723
|
-
if (!record) return;
|
|
724
|
-
if (this.#isExpired(record, now)) {
|
|
725
|
-
void this.#deleteKeys([uri]);
|
|
726
|
-
return;
|
|
727
|
-
}
|
|
728
|
-
this.#memoryCache.set(uri, record);
|
|
729
|
-
resultMap.set(uri, record);
|
|
730
|
-
});
|
|
731
|
-
|
|
732
|
-
return resultMap;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
661
|
/**
|
|
736
662
|
* ---------------------------
|
|
737
663
|
* Public methods
|
|
@@ -756,44 +682,6 @@ export default class FileStorage {
|
|
|
756
682
|
return storedFile ? URL.createObjectURL(storedFile.blob) : (await this.#getServerFileTimeStamp(uri)) ? uri : false;
|
|
757
683
|
}
|
|
758
684
|
|
|
759
|
-
/**
|
|
760
|
-
* Resolve multiple URIs to object URLs in a single IDB batch transaction.
|
|
761
|
-
* Hits in-memory cache first, then IDB in one transaction, then falls back to individual
|
|
762
|
-
* server downloads for any URIs not found. Returns a Map<uri, objectURL|uri>.
|
|
763
|
-
* @public
|
|
764
|
-
* @param {string[]} uris - URIs to resolve.
|
|
765
|
-
* @returns {Promise<Map<string, string>>} Map of uri → resolved URL (blob: or original URI).
|
|
766
|
-
*/
|
|
767
|
-
async getURLBatch(uris) {
|
|
768
|
-
if (!uris.length) return new Map();
|
|
769
|
-
if (!this.#supported) {
|
|
770
|
-
return new Map(uris.map((uri) => [uri, uri]));
|
|
771
|
-
}
|
|
772
|
-
await this.#runCleanupIfDue();
|
|
773
|
-
const records = await this.#getFileBatch(uris);
|
|
774
|
-
const urlMap = new Map();
|
|
775
|
-
const serverFetches = [];
|
|
776
|
-
|
|
777
|
-
for (const uri of uris) {
|
|
778
|
-
const record = records.get(uri);
|
|
779
|
-
if (record?.blob) {
|
|
780
|
-
urlMap.set(uri, URL.createObjectURL(record.blob));
|
|
781
|
-
} else {
|
|
782
|
-
serverFetches.push(uri);
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
// For URIs absent from IDB, fall back to individual get() which downloads and caches them.
|
|
787
|
-
await Promise.all(
|
|
788
|
-
serverFetches.map(async (uri) => {
|
|
789
|
-
const url = await this.getURL(uri);
|
|
790
|
-
if (url) urlMap.set(uri, url);
|
|
791
|
-
})
|
|
792
|
-
);
|
|
793
|
-
|
|
794
|
-
return urlMap;
|
|
795
|
-
}
|
|
796
|
-
|
|
797
685
|
/**
|
|
798
686
|
* Get the cached or server Last-Modified timestamp for a file.
|
|
799
687
|
* @public
|
|
@@ -874,18 +762,10 @@ export default class FileStorage {
|
|
|
874
762
|
}
|
|
875
763
|
if (!storedFile) {
|
|
876
764
|
const fileToStore = await this.#getServerFile(uri);
|
|
877
|
-
if (fileToStore) {
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
if (freshRecord) {
|
|
882
|
-
this.#memoryCache.set(uri, freshRecord);
|
|
883
|
-
}
|
|
884
|
-
if (!!(await this.#putFile(fileToStore, uri))) {
|
|
885
|
-
storedFile = await this.#getFile(uri);
|
|
886
|
-
} else {
|
|
887
|
-
storedFile = freshRecord ?? fileToStore;
|
|
888
|
-
}
|
|
765
|
+
if (fileToStore && !!(await this.#putFile(fileToStore, uri))) {
|
|
766
|
+
storedFile = await this.#getFile(uri);
|
|
767
|
+
} else {
|
|
768
|
+
storedFile = fileToStore;
|
|
889
769
|
}
|
|
890
770
|
}
|
|
891
771
|
return storedFile;
|
|
@@ -921,6 +801,5 @@ export default class FileStorage {
|
|
|
921
801
|
} catch {}
|
|
922
802
|
}
|
|
923
803
|
this.#db = undefined;
|
|
924
|
-
this.#memoryCache.clear();
|
|
925
804
|
}
|
|
926
805
|
}
|
package/src/gltf-resolver.js
CHANGED
|
@@ -82,27 +82,27 @@ export default class GLTFResolver {
|
|
|
82
82
|
* - If parsing fails, returns a Blob with MIME type "model/gltf-binary" and extension ".glb".
|
|
83
83
|
* - If decoding fails, returns nulls for blob and extension.
|
|
84
84
|
*/
|
|
85
|
-
|
|
85
|
+
#decodeBase64(base64) {
|
|
86
86
|
const [, payload] = base64.split(",");
|
|
87
87
|
const raw = payload || base64;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
// Detect format from first base64 chars — O(1), no full decode needed.
|
|
91
|
-
// GLB magic bytes 0x676c5446 ("glTF") encode to "Z2x" in base64.
|
|
92
|
-
// GLTF JSON always starts with '{' (0x7B), which encodes to "ey" in base64.
|
|
93
|
-
const isGlb = raw.startsWith("Z2x");
|
|
94
|
-
const extension = isGlb ? ".glb" : ".gltf";
|
|
95
|
-
const type = isGlb ? "model/gltf-binary" : "model/gltf+json";
|
|
96
|
-
|
|
88
|
+
let decoded = "";
|
|
97
89
|
let blob = null;
|
|
90
|
+
let extension = null;
|
|
91
|
+
let size = raw.length;
|
|
98
92
|
try {
|
|
99
|
-
|
|
100
|
-
// main thread. Avoids the per-character Uint8Array loop and the full JSON.parse
|
|
101
|
-
// that the previous sync implementation used to detect the file type.
|
|
102
|
-
blob = await fetch(`data:${type};base64,${raw}`).then((r) => r.blob());
|
|
93
|
+
decoded = atob(raw);
|
|
103
94
|
} catch {
|
|
104
|
-
return { blob
|
|
95
|
+
return { blob, extension, size };
|
|
105
96
|
}
|
|
97
|
+
let isJson = false;
|
|
98
|
+
try {
|
|
99
|
+
JSON.parse(decoded);
|
|
100
|
+
isJson = true;
|
|
101
|
+
} catch {}
|
|
102
|
+
extension = isJson ? ".gltf" : ".glb";
|
|
103
|
+
const type = isJson ? "model/gltf+json" : "model/gltf-binary";
|
|
104
|
+
const array = Uint8Array.from(decoded, (c) => c.charCodeAt(0));
|
|
105
|
+
blob = new Blob([array], { type });
|
|
106
106
|
return { blob, extension, size };
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -138,27 +138,6 @@ export default class GLTFResolver {
|
|
|
138
138
|
return typeof url === "string" && url.startsWith("blob:");
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
/**
|
|
142
|
-
* Check whether a source should be treated as embedded base64/data URI.
|
|
143
|
-
* @private
|
|
144
|
-
* @param {string} source - Value to test.
|
|
145
|
-
* @returns {boolean} True when the source is a data URI or raw base64 payload.
|
|
146
|
-
*/
|
|
147
|
-
#isEmbeddedBase64Source(source) {
|
|
148
|
-
if (typeof source !== "string") {
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
const normalized = source.trim();
|
|
152
|
-
if (!normalized) {
|
|
153
|
-
return false;
|
|
154
|
-
}
|
|
155
|
-
if (normalized.startsWith("data:")) {
|
|
156
|
-
return true;
|
|
157
|
-
}
|
|
158
|
-
// Raw base64 payloads use the standard alphabet and have no URL separators.
|
|
159
|
-
return /^[A-Za-z0-9+/=_-]+$/.test(normalized);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
141
|
/**
|
|
163
142
|
* Collects asset entries that have an external URI (non-data URI) and stores a normalized absolute or relative-resolved URI for later replacement.
|
|
164
143
|
* @private
|
|
@@ -225,19 +204,17 @@ export default class GLTFResolver {
|
|
|
225
204
|
assetContainerJSON.images.forEach((asset, index, array) => this.#saveAssetData(asset, index, array, sceneURLBase, arrayOfAssetsWithURI));
|
|
226
205
|
}
|
|
227
206
|
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
asset.parent[asset.index].uri = resolvedUrl;
|
|
236
|
-
if (this.#isBlobURL(resolvedUrl)) {
|
|
237
|
-
objectURLs.push(resolvedUrl);
|
|
207
|
+
// Replace parallel URIs so that if files are not stored yet, they are downloaded in parallel
|
|
208
|
+
const promisesArray = arrayOfAssetsWithURI.map(async (asset) => {
|
|
209
|
+
const uri = await this.#fileStorage.getURL(asset.uri);
|
|
210
|
+
if (uri) {
|
|
211
|
+
asset.parent[asset.index].uri = uri;
|
|
212
|
+
if (this.#isBlobURL(uri)) {
|
|
213
|
+
objectURLs.push(uri);
|
|
238
214
|
}
|
|
239
215
|
}
|
|
240
216
|
});
|
|
217
|
+
await Promise.all(promisesArray);
|
|
241
218
|
}
|
|
242
219
|
|
|
243
220
|
/**
|
|
@@ -293,9 +270,7 @@ export default class GLTFResolver {
|
|
|
293
270
|
|
|
294
271
|
if (storage.db && storage.table && storage.id) {
|
|
295
272
|
await this.#initializeStorage(storage.db, storage.table);
|
|
296
|
-
const tIdb = performance.now();
|
|
297
273
|
const object = await loadModel(storage.id, storage.table);
|
|
298
|
-
console.log(`[perf][gltf:${storage.id}] IDB read ${Math.round(performance.now() - tIdb)}ms`);
|
|
299
274
|
if (!object || !object.data || !object.timeStamp || !object.size) {
|
|
300
275
|
return false;
|
|
301
276
|
}
|
|
@@ -315,21 +290,12 @@ export default class GLTFResolver {
|
|
|
315
290
|
}
|
|
316
291
|
|
|
317
292
|
let file = null;
|
|
318
|
-
let blob =
|
|
319
|
-
let extension = null;
|
|
320
|
-
let size = 0;
|
|
321
|
-
if (this.#isEmbeddedBase64Source(source)) {
|
|
322
|
-
const tDecode = performance.now();
|
|
323
|
-
({ blob, extension, size } = await this.#decodeBase64(source));
|
|
324
|
-
console.log(`[perf][gltf:${storage.id ?? "url"}] decode(${extension}) ${Math.round(performance.now() - tDecode)}ms`);
|
|
325
|
-
}
|
|
293
|
+
let { blob, extension, size } = this.#decodeBase64(source);
|
|
326
294
|
|
|
327
295
|
if (blob && extension) {
|
|
328
296
|
if (extension === ".gltf") {
|
|
329
297
|
const assetContainerJSON = JSON.parse(await blob.text());
|
|
330
|
-
const tReplace = performance.now();
|
|
331
298
|
await this.#replaceSceneURIAsync(assetContainerJSON, source, objectURLs);
|
|
332
|
-
console.log(`[perf][gltf:${storage.id ?? "url"}] replaceURIs ${Math.round(performance.now() - tReplace)}ms (${objectURLs.length} assets)`);
|
|
333
299
|
source = `data:${JSON.stringify(assetContainerJSON)}`;
|
|
334
300
|
} else {
|
|
335
301
|
file = new File([blob], `file${extension}`, {
|
|
@@ -3,18 +3,12 @@ export const translations = {
|
|
|
3
3
|
prefViewer: {
|
|
4
4
|
menu3d: {
|
|
5
5
|
title: "3D render settings",
|
|
6
|
-
subtitle: "
|
|
7
|
-
pendingLabel: "Pending changes",
|
|
6
|
+
subtitle: "Modify the visual effects.",
|
|
8
7
|
actions: {
|
|
9
|
-
apply: "Apply",
|
|
10
|
-
applying: "Applying...",
|
|
11
8
|
play: "Play",
|
|
12
9
|
pause: "Pause",
|
|
13
10
|
},
|
|
14
|
-
|
|
15
|
-
error: "Couldn't apply the changes.",
|
|
16
|
-
},
|
|
17
|
-
switches: {
|
|
11
|
+
features: {
|
|
18
12
|
antiAliasingEnabled: {
|
|
19
13
|
label: "Antialiasing",
|
|
20
14
|
helper: "Softens jagged edges and lines.",
|
|
@@ -70,18 +64,12 @@ export const translations = {
|
|
|
70
64
|
prefViewer: {
|
|
71
65
|
menu3d: {
|
|
72
66
|
title: "Ajustes de render 3D",
|
|
73
|
-
subtitle: "
|
|
74
|
-
pendingLabel: "Pendiente de aplicar",
|
|
67
|
+
subtitle: "Modifica los efectos visuales.",
|
|
75
68
|
actions: {
|
|
76
|
-
apply: "Aplicar",
|
|
77
|
-
applying: "Aplicando...",
|
|
78
69
|
play: "Reproducir",
|
|
79
70
|
pause: "Pausar",
|
|
80
71
|
},
|
|
81
|
-
|
|
82
|
-
error: "No se pudo aplicar los cambios.",
|
|
83
|
-
},
|
|
84
|
-
switches: {
|
|
72
|
+
features: {
|
|
85
73
|
antiAliasingEnabled: {
|
|
86
74
|
label: "Antialiasing",
|
|
87
75
|
helper: "Suaviza aristas y líneas.",
|
|
@@ -100,7 +88,7 @@ export const translations = {
|
|
|
100
88
|
},
|
|
101
89
|
highlightEnabled: {
|
|
102
90
|
label: "Resaltado",
|
|
103
|
-
helper: "
|
|
91
|
+
helper: "Resalta elementos animados al pasar el ratón.",
|
|
104
92
|
},
|
|
105
93
|
highlightColor: {
|
|
106
94
|
label: "Color de resaltado",
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { PBRMaterial } from "@babylonjs/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Utilities for classifying and tagging PBR materials by their transparency/transmission mode.
|
|
5
|
+
* Used by BabylonJSController at load time and by pref-viewer-3d for render-order decisions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const PREF_VIEWER_SIMPLIFIED_TRANSMISSION_METADATA_KEY = "prefViewerSimplifiedTransmission";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Tags a PBR material as having been converted from physical KHR_materials_transmission to simple
|
|
12
|
+
* alpha blending. The tag is stored in `mat.metadata` under an internal key so that downstream
|
|
13
|
+
* render-order passes can still identify these materials as transparent candidates even after
|
|
14
|
+
* their `subSurface.isRefractionEnabled` flag has been cleared.
|
|
15
|
+
*
|
|
16
|
+
* Returns the same material instance (or the original value when `mat` is falsy) so calls can be
|
|
17
|
+
* chained.
|
|
18
|
+
* @param {import("@babylonjs/core").PBRMaterial|null|undefined} mat - Material to tag.
|
|
19
|
+
* @returns {import("@babylonjs/core").PBRMaterial|null|undefined} The same `mat` value that was passed in.
|
|
20
|
+
*/
|
|
21
|
+
export function markPrefViewerSimplifiedTransmissionMaterial(mat) {
|
|
22
|
+
if (!mat) {
|
|
23
|
+
return mat;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
mat.metadata = {
|
|
27
|
+
...(mat.metadata ?? {}),
|
|
28
|
+
[PREF_VIEWER_SIMPLIFIED_TRANSMISSION_METADATA_KEY]: true,
|
|
29
|
+
};
|
|
30
|
+
return mat;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Returns true when the material uses Babylon.js physical glass rendering — i.e. either
|
|
35
|
+
* KHR_materials_transmission (refraction) or KHR_materials_translucency is active.
|
|
36
|
+
*
|
|
37
|
+
* This is intentionally narrower than `isPrefViewerTransparentRenderOrderMaterial`: it only
|
|
38
|
+
* matches full physical transmission, not plain alpha blending or simplified environment glass.
|
|
39
|
+
* @param {import("@babylonjs/core").Material|null|undefined} mat - Material to test.
|
|
40
|
+
* @returns {boolean}
|
|
41
|
+
*/
|
|
42
|
+
export function isPrefViewerPhysicalGlassMaterial(mat) {
|
|
43
|
+
return Boolean(mat?.subSurface?.isRefractionEnabled || mat?.subSurface?.isTranslucencyEnabled);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Returns true when a material should be placed in the transparent rendering group (renderingGroupId=1).
|
|
48
|
+
*
|
|
49
|
+
* A material qualifies when any of the following is true:
|
|
50
|
+
* - It is a physical glass material (KHR_materials_transmission / translucency).
|
|
51
|
+
* - It has been tagged as simplified environment transmission via `markPrefViewerSimplifiedTransmissionMaterial`.
|
|
52
|
+
* - Its effective alpha is below 1, its `transparencyMode` is alpha-blend or alpha-test-and-blend,
|
|
53
|
+
* or it self-reports as needing blending/testing through the Babylon.js material API.
|
|
54
|
+
* @param {import("@babylonjs/core").Material|null|undefined} mat - Material to test.
|
|
55
|
+
* @returns {boolean}
|
|
56
|
+
*/
|
|
57
|
+
export function isPrefViewerTransparentRenderOrderMaterial(mat) {
|
|
58
|
+
if (!mat) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
const isTransmissive = isPrefViewerPhysicalGlassMaterial(mat);
|
|
62
|
+
const isSimplifiedTransmission = mat.metadata?.[PREF_VIEWER_SIMPLIFIED_TRANSMISSION_METADATA_KEY] === true;
|
|
63
|
+
const alpha = typeof mat.alpha === "number" ? mat.alpha : 1;
|
|
64
|
+
const isAlphaBlended =
|
|
65
|
+
alpha < 1 ||
|
|
66
|
+
mat.transparencyMode === PBRMaterial.PBRMATERIAL_ALPHABLEND ||
|
|
67
|
+
mat.transparencyMode === PBRMaterial.PBRMATERIAL_ALPHATESTANDBLEND ||
|
|
68
|
+
mat.needAlphaBlending?.() === true ||
|
|
69
|
+
mat.needAlphaTesting?.() === true;
|
|
70
|
+
|
|
71
|
+
return Boolean(isTransmissive || isSimplifiedTransmission || isAlphaBlended);
|
|
72
|
+
}
|
package/src/pref-viewer-3d.js
CHANGED
|
@@ -10,28 +10,32 @@ import FileStorage from "./file-storage.js";
|
|
|
10
10
|
* - Encapsulates a Babylon.js-powered 3D viewer for displaying models, environments, and materials.
|
|
11
11
|
* - Manages internal state for containers (model, environment, materials) and options (camera, materials, IBL, render toggles).
|
|
12
12
|
* - Handles asset loading, configuration, and option updates (camera, materials, IBL) through attributes and public methods.
|
|
13
|
-
* - Exposes getters/setters for render settings so PrefViewerMenu3D or other UIs can
|
|
13
|
+
* - Exposes getters/setters for render settings so PrefViewerMenu3D or other UIs can toggle AA/SSAO/IBL/shadows,
|
|
14
|
+
* hover highlight, and time-of-day lighting.
|
|
14
15
|
* - Provides API for showing/hiding the viewer, model, and environment, and for downloading assets.
|
|
15
16
|
* - Emits custom events for loading, loaded, option-setting, and render-setting workflows.
|
|
16
17
|
*
|
|
17
18
|
* Usage:
|
|
18
19
|
* - Use as a custom HTML element: <pref-viewer-3d ...>
|
|
19
20
|
* - Configure via attributes (config, options, show-model, show-scene, visible).
|
|
20
|
-
* - Call getRenderSettings()/applyRenderSettings() to synchronize UI
|
|
21
|
+
* - Call getRenderSettings()/applyRenderSettings() to synchronize UI controls with BabylonJSController settings.
|
|
21
22
|
* - Control viewer state and assets via public methods.
|
|
22
23
|
*
|
|
23
24
|
* Observed Attributes:
|
|
24
25
|
* - show-model: Show or hide the 3D model ("true"/"false").
|
|
25
26
|
* - show-scene: Show or hide the 3D environment ("true"/"false").
|
|
26
27
|
* - visible: Show or hide the entire viewer ("true"/"false").
|
|
28
|
+
* - show-animation-menu: Show or hide animation controls in the 3D controller.
|
|
29
|
+
* - highlight-color: Hex color used for hover highlight on animated meshes.
|
|
30
|
+
* - highlight-enabled: Enable or disable hover highlight ("true"/"false").
|
|
27
31
|
*
|
|
28
32
|
* Public Methods:
|
|
29
33
|
* - show(): Shows the 3D viewer component.
|
|
30
34
|
* - hide(): Hides the 3D viewer component.
|
|
31
35
|
* - load(config): Loads the provided configuration into the viewer.
|
|
32
36
|
* - setOptions(options): Sets viewer options such as camera, materials, and image-based lighting (IBL).
|
|
33
|
-
* - getRenderSettings(): Returns the current
|
|
34
|
-
* - applyRenderSettings(settings):
|
|
37
|
+
* - getRenderSettings(): Returns the current render settings snapshot, including highlight and lighting values.
|
|
38
|
+
* - applyRenderSettings(settings): Applies render settings, reloading only when a setting requires it.
|
|
35
39
|
* - showModel(): Shows the 3D model.
|
|
36
40
|
* - hideModel(): Hides the 3D model.
|
|
37
41
|
* - showEnvironment(): Shows the 3D environment/scene.
|
|
@@ -62,7 +66,6 @@ import FileStorage from "./file-storage.js";
|
|
|
62
66
|
* Events:
|
|
63
67
|
* - "scene-loading": Dispatched when a loading operation starts.
|
|
64
68
|
* - "scene-loaded": Dispatched when a loading operation completes.
|
|
65
|
-
* - "model-first-painted": Dispatched when the model-only preview has actually painted a frame.
|
|
66
69
|
* - "scene-setting-options": Dispatched when viewer options are being set.
|
|
67
70
|
* - "scene-set-options": Dispatched when viewer options have been set.
|
|
68
71
|
*
|
|
@@ -77,7 +80,6 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
77
80
|
#isLoaded = false;
|
|
78
81
|
#isVisible = false;
|
|
79
82
|
#lastLoadTimestamp = undefined;
|
|
80
|
-
#suppressVisibilityAttributeCallback = false;
|
|
81
83
|
|
|
82
84
|
#wrapper = null;
|
|
83
85
|
#canvas = null;
|
|
@@ -112,11 +114,9 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
112
114
|
* - "show-model": shows or hides the model according to the attribute value ("true"/"false").
|
|
113
115
|
* - "show-scene": shows or hides the scene according to the attribute value ("true"/"false").
|
|
114
116
|
* - "visible": shows or hides the component according to the attribute value ("true"/"false").
|
|
117
|
+
* - "show-animation-menu", "highlight-color", and "highlight-enabled": forward runtime UI/highlight settings to BabylonJSController.
|
|
115
118
|
*/
|
|
116
119
|
attributeChangedCallback(name, _old, value) {
|
|
117
|
-
if (this.#suppressVisibilityAttributeCallback && (name === "show-model" || name === "show-scene")) {
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
120
|
switch (name) {
|
|
121
121
|
case "show-model":
|
|
122
122
|
if (_old === value) {
|
|
@@ -721,62 +721,6 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
721
721
|
return { success: someSetted, detail: detail };
|
|
722
722
|
}
|
|
723
723
|
|
|
724
|
-
/**
|
|
725
|
-
* Loads the model container independently in the background while the render loop keeps running.
|
|
726
|
-
* Merges the model into the scene atomically when ready, without stopping camera-dependent effects.
|
|
727
|
-
* @public
|
|
728
|
-
* @param {object} payload - Model payload with storage pointer and optional timestamp.
|
|
729
|
-
* @returns {Promise<{success: boolean, error: any, loadedContainers: string[]}>}
|
|
730
|
-
*/
|
|
731
|
-
async loadModelIndependent(payload) {
|
|
732
|
-
if (!this.#babylonJSController) {
|
|
733
|
-
return { success: false, error: "Controller not initialized", loadedContainers: [] };
|
|
734
|
-
}
|
|
735
|
-
// #checkNeedToUpdateContainers passed as callback — called AFTER any in-progress #loadContainers
|
|
736
|
-
// finishes so the scene load doesn't also pick up and consume the model container.
|
|
737
|
-
return await this.#babylonJSController.loadContainerIndependent("model", () => {
|
|
738
|
-
this.#checkNeedToUpdateContainers({ model: payload });
|
|
739
|
-
}, { loadSessionId: payload?.loadSessionId ?? null });
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
/**
|
|
743
|
-
* Loads the environment/scene container independently in the background, in parallel with any
|
|
744
|
-
* ongoing model load. Applies IBL (HDR) if the scene payload includes an ibl property.
|
|
745
|
-
* Dispatches "scene-loading" and "scene-loaded" (or "scene-error") events directly.
|
|
746
|
-
* @public
|
|
747
|
-
* @param {object|string} scene - Scene payload: { storage, ibl?, visible? }
|
|
748
|
-
* @returns {Promise<{success: boolean, error: any, loadedContainers: string[]}>}
|
|
749
|
-
*/
|
|
750
|
-
async loadSceneIndependent(scene) {
|
|
751
|
-
if (!this.#babylonJSController) {
|
|
752
|
-
return { success: false, error: "Controller not initialized", loadedContainers: [] };
|
|
753
|
-
}
|
|
754
|
-
// Pre-mark IBL as pending before entering loadContainerIndependent — the onReady callback
|
|
755
|
-
// is synchronous so async IBL resolution must happen here, not inside it.
|
|
756
|
-
if (scene?.ibl !== undefined) {
|
|
757
|
-
await this.#checkNeedToUpdateIBL({ ibl: scene.ibl });
|
|
758
|
-
}
|
|
759
|
-
return await this.#babylonJSController.loadContainerIndependent("environment", () => {
|
|
760
|
-
this.#checkNeedToUpdateContainers({ scene });
|
|
761
|
-
});
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
/**
|
|
765
|
-
* Loads the materials container independently in the background, in parallel with any
|
|
766
|
-
* ongoing model or environment load.
|
|
767
|
-
* @public
|
|
768
|
-
* @param {object|string} materials - Materials payload: { storage }
|
|
769
|
-
* @returns {Promise<{success: boolean, error: any, loadedContainers: string[]}>}
|
|
770
|
-
*/
|
|
771
|
-
async loadMaterialsIndependent(materials) {
|
|
772
|
-
if (!this.#babylonJSController) {
|
|
773
|
-
return { success: false, error: "Controller not initialized", loadedContainers: [] };
|
|
774
|
-
}
|
|
775
|
-
return await this.#babylonJSController.loadContainerIndependent("materials", () => {
|
|
776
|
-
this.#checkNeedToUpdateContainers({ materials });
|
|
777
|
-
});
|
|
778
|
-
}
|
|
779
|
-
|
|
780
724
|
/**
|
|
781
725
|
* Shows the 3D model within the viewer.
|
|
782
726
|
* @public
|
|
@@ -825,31 +769,6 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
825
769
|
this.#babylonJSController.setContainerVisibility("environment", false);
|
|
826
770
|
}
|
|
827
771
|
|
|
828
|
-
/**
|
|
829
|
-
* Reflects the current visibility of a container without re-triggering the command queue.
|
|
830
|
-
* Used by BabylonJSController when the visibility change originated from the renderer itself.
|
|
831
|
-
* @public
|
|
832
|
-
* @param {string} name - Container name ("model" or "environment").
|
|
833
|
-
* @param {boolean} isVisible - Whether the container should be reflected as visible.
|
|
834
|
-
* @returns {void}
|
|
835
|
-
*/
|
|
836
|
-
syncVisibility(name, isVisible) {
|
|
837
|
-
const attrName = name === "model" ? "show-model" : name === "environment" ? "show-scene" : null;
|
|
838
|
-
if (!attrName) {
|
|
839
|
-
return;
|
|
840
|
-
}
|
|
841
|
-
const nextValue = isVisible ? "true" : "false";
|
|
842
|
-
if (this.getAttribute(attrName) === nextValue) {
|
|
843
|
-
return;
|
|
844
|
-
}
|
|
845
|
-
this.#suppressVisibilityAttributeCallback = true;
|
|
846
|
-
try {
|
|
847
|
-
this.setAttribute(attrName, nextValue);
|
|
848
|
-
} finally {
|
|
849
|
-
this.#suppressVisibilityAttributeCallback = false;
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
|
|
853
772
|
/**
|
|
854
773
|
* Downloads the current 3D model as a GLB file.
|
|
855
774
|
* @public
|
|
@@ -960,8 +879,9 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
960
879
|
|
|
961
880
|
/**
|
|
962
881
|
* Returns the current render settings used by the BabylonJSController.
|
|
882
|
+
* The snapshot is suitable for hydrating PrefViewerMenu3D controls.
|
|
963
883
|
* @public
|
|
964
|
-
* @returns {object|null} A shallow copy of the render settings or null if unavailable.
|
|
884
|
+
* @returns {object|null} A shallow copy of the render settings, including quality toggles, highlight state, and lighting time, or null if unavailable.
|
|
965
885
|
*/
|
|
966
886
|
getRenderSettings() {
|
|
967
887
|
if (!this.#babylonJSController) {
|
|
@@ -1030,10 +950,20 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
1030
950
|
}
|
|
1031
951
|
|
|
1032
952
|
/**
|
|
1033
|
-
* Applies render settings
|
|
953
|
+
* Applies render settings received from PrefViewerMenu3D or another UI.
|
|
954
|
+
* Highlight and time-of-day lighting values are pushed directly to BabylonJSController; AA/SSAO/IBL/shadow changes
|
|
955
|
+
* are scheduled through the render-settings reload path when needed.
|
|
1034
956
|
* @public
|
|
1035
|
-
* @param {object} settings - Partial settings
|
|
1036
|
-
* @
|
|
957
|
+
* @param {object} settings - Partial render settings payload.
|
|
958
|
+
* @param {boolean} [settings.antiAliasingEnabled] - Enables or disables anti-aliasing.
|
|
959
|
+
* @param {boolean} [settings.ambientOcclusionEnabled] - Enables or disables SSAO.
|
|
960
|
+
* @param {boolean} [settings.iblEnabled] - Enables or disables image-based lighting.
|
|
961
|
+
* @param {boolean} [settings.shadowsEnabled] - Enables or disables scene shadows.
|
|
962
|
+
* @param {boolean|string} [settings.highlightEnabled] - Enables or disables hover highlight; string "false" is normalized to false.
|
|
963
|
+
* @param {string} [settings.highlightColor] - Hex color used for hover highlight.
|
|
964
|
+
* @param {number} [settings.lightingTimeOfDay] - Normalized 0..1 time-of-day value.
|
|
965
|
+
* @param {boolean} [settings.persist=true] - False for transient lighting updates such as playback frames.
|
|
966
|
+
* @returns {Promise<{changed: boolean, success: boolean, detail?: object, controller?: object}>} Resolves with the apply/reload summary.
|
|
1037
967
|
*/
|
|
1038
968
|
async applyRenderSettings(settings) {
|
|
1039
969
|
if (!this.#babylonJSController) {
|
|
@@ -1101,22 +1031,4 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
1101
1031
|
get isVisible() {
|
|
1102
1032
|
return this.#isVisible;
|
|
1103
1033
|
}
|
|
1104
|
-
|
|
1105
|
-
/**
|
|
1106
|
-
* Forces the active camera to frame the visible model.
|
|
1107
|
-
* @public
|
|
1108
|
-
* @returns {boolean}
|
|
1109
|
-
*/
|
|
1110
|
-
focusModel() {
|
|
1111
|
-
return this.#babylonJSController?.focusModel?.() ?? false;
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
/**
|
|
1115
|
-
* Returns a debug snapshot of the internal 3D state, including camera data.
|
|
1116
|
-
* @public
|
|
1117
|
-
* @returns {object|null}
|
|
1118
|
-
*/
|
|
1119
|
-
getDebugState() {
|
|
1120
|
-
return this.#babylonJSController?.getDebugState?.() ?? null;
|
|
1121
|
-
}
|
|
1122
1034
|
}
|