@preference-sl/pref-viewer 2.10.0 → 2.11.0-beta.0
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 +3 -2
- package/src/file-storage.js +288 -0
- package/src/index.js +113 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@preference-sl/pref-viewer",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.0-beta.0",
|
|
4
4
|
"description": "Web Component to preview GLTF models with Babylon.js",
|
|
5
5
|
"author": "Alex Moreno Palacio <amoreno@preference.es>",
|
|
6
6
|
"scripts": {
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"@babylonjs/core": "^8.31.3",
|
|
38
38
|
"@babylonjs/loaders": "^8.31.3",
|
|
39
39
|
"@babylonjs/serializers": "^8.31.3",
|
|
40
|
-
"babylonjs-gltf2interface": "^8.31.3"
|
|
40
|
+
"babylonjs-gltf2interface": "^8.31.3",
|
|
41
|
+
"idb": "^8.0.3"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
44
|
"esbuild": "^0.25.10",
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileStore class to manage file storage in IndexedDB using a promise-based wrapper around the IndexedDB API.
|
|
3
|
+
* @see {@link https://github.com/jakearchibald/idb|GitHub - jakearchibald/idb: IndexedDB, but with promises}
|
|
4
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API|IndexedDB API - MDN}
|
|
5
|
+
* @see {@link https://www.npmjs.com/package/idb|idb - npm}
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { openDB } from "idb";
|
|
9
|
+
|
|
10
|
+
export class FileStorage {
|
|
11
|
+
#supported = "indexedDB" in window && openDB; // true if IndexedDB is available in the browser and the idb helper is loaded
|
|
12
|
+
#dbVersion = 1; // single DB version; cache is managed per-file
|
|
13
|
+
#db = undefined; // IndexedDB database handle
|
|
14
|
+
|
|
15
|
+
constructor(dbName = "FilesDB", osName = "FilesObjectStore") {
|
|
16
|
+
this.dbName = dbName; // database name
|
|
17
|
+
this.osName = osName; // object store name used for file cache
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create the object store if it does not exist.
|
|
22
|
+
* @param {IDBDatabase} db IndexedDB database instance
|
|
23
|
+
*/
|
|
24
|
+
#createObjectStore = (db, osName) => {
|
|
25
|
+
if (!db.objectStoreNames.contains(osName)) {
|
|
26
|
+
db.createObjectStore(osName);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Open the IndexedDB database (creating it if needed) and ensure the object store for files exists.
|
|
32
|
+
* @returns {Promise<IDBDatabase|null>} Resolves with the opened database or null if IndexedDB is not supported or opening fails.
|
|
33
|
+
*/
|
|
34
|
+
#openDB = async () => {
|
|
35
|
+
if (!this.#supported) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const self = this;
|
|
39
|
+
try {
|
|
40
|
+
return await openDB(this.dbName, this.#dbVersion, {
|
|
41
|
+
upgrade(db) {
|
|
42
|
+
self.#createObjectStore(db, self.osName);
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Download the file from the server (forced download).
|
|
52
|
+
* @param {String} uri File URI
|
|
53
|
+
* @returns {Promise<{blob: Blob, timeStamp: string}|undefined>} Resolves with:
|
|
54
|
+
* - { blob, timeStamp }: object with the downloaded Blob and the Last-Modified timestamp (ISO string) on success
|
|
55
|
+
* - undefined: if the download fails (non-200 status or network error)
|
|
56
|
+
*/
|
|
57
|
+
#getServerFile = async (uri) => {
|
|
58
|
+
let file = undefined;
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
const xhr = new XMLHttpRequest();
|
|
61
|
+
xhr.open("GET", uri, true);
|
|
62
|
+
xhr.responseType = "blob";
|
|
63
|
+
xhr.onload = () => {
|
|
64
|
+
if (xhr.status === 200) {
|
|
65
|
+
const blob = xhr.response;
|
|
66
|
+
const timeStamp = new Date(xhr.getResponseHeader("Last-Modified")).toISOString();
|
|
67
|
+
file = { blob: blob, timeStamp: timeStamp };
|
|
68
|
+
resolve(file);
|
|
69
|
+
} else {
|
|
70
|
+
resolve(file);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
xhr.onerror = () => {
|
|
74
|
+
resolve(file);
|
|
75
|
+
};
|
|
76
|
+
xhr.send();
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get the Last-Modified timestamp of the file on the server without downloading its content.
|
|
82
|
+
* @param {String} uri File URI
|
|
83
|
+
* @returns {Promise<string|null>} Resolves with:
|
|
84
|
+
* - ISO 8601 string from the Last-Modified header if available
|
|
85
|
+
* - null if the header is not present or on error
|
|
86
|
+
*/
|
|
87
|
+
#getServerFileTimeStamp = async (uri) => {
|
|
88
|
+
let timeStamp = null;
|
|
89
|
+
return new Promise((resolve) => {
|
|
90
|
+
const xhr = new XMLHttpRequest();
|
|
91
|
+
xhr.open("HEAD", uri, true);
|
|
92
|
+
xhr.responseType = "blob";
|
|
93
|
+
xhr.onload = () => {
|
|
94
|
+
if (xhr.status === 200) {
|
|
95
|
+
timeStamp = new Date(xhr.getResponseHeader("Last-Modified")).toISOString();
|
|
96
|
+
resolve(timeStamp);
|
|
97
|
+
} else {
|
|
98
|
+
resolve(timeStamp);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
xhr.onerror = () => {
|
|
102
|
+
resolve(timeStamp);
|
|
103
|
+
};
|
|
104
|
+
xhr.send();
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get the size of the file on the server without downloading its content.
|
|
110
|
+
* @param {String} uri File URI
|
|
111
|
+
* @returns {Promise<number>} Promise that resolves with:
|
|
112
|
+
* - number: Content-Length in bytes when the HEAD request succeeds and the header is present
|
|
113
|
+
* - 0: when the header is missing or the request fails
|
|
114
|
+
*/
|
|
115
|
+
#getServerFileSize = async (uri) => {
|
|
116
|
+
let size = 0;
|
|
117
|
+
return new Promise((resolve) => {
|
|
118
|
+
const xhr = new XMLHttpRequest();
|
|
119
|
+
xhr.open("HEAD", uri, true);
|
|
120
|
+
xhr.responseType = "blob";
|
|
121
|
+
xhr.onload = () => {
|
|
122
|
+
if (xhr.status === 200) {
|
|
123
|
+
size = parseInt(xhr.getResponseHeader("Content-Length"));
|
|
124
|
+
resolve(size);
|
|
125
|
+
} else {
|
|
126
|
+
resolve(size);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
xhr.onerror = () => {
|
|
130
|
+
resolve(size);
|
|
131
|
+
};
|
|
132
|
+
xhr.send();
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Stores a file record in IndexedDB under the provided key (uri).
|
|
138
|
+
* @param {Object|Blob} file The value to store (for example an object {blob, timeStamp} or a Blob).
|
|
139
|
+
* @param {String} uri Key to use for storage (the original URI).
|
|
140
|
+
* @returns {Promise<any|undefined>} Resolves with the value returned by the underlying idb store.put (usually the record key — string or number)
|
|
141
|
+
* - resolves to the put result on success
|
|
142
|
+
* - resolves to undefined if the database could not be opened
|
|
143
|
+
* - rejects if the underlying put operation throws
|
|
144
|
+
*/
|
|
145
|
+
#putFile = async (file, uri) => {
|
|
146
|
+
if (this.#db === undefined) {
|
|
147
|
+
this.#db = await this.#openDB();
|
|
148
|
+
}
|
|
149
|
+
if (this.#db === null) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
const transation = this.#db.transaction(this.osName, "readwrite");
|
|
153
|
+
const store = transation.objectStore(this.osName);
|
|
154
|
+
return store.put(file, uri);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Retrieve a file record from IndexedDB.
|
|
159
|
+
* @param {String} uri File URI
|
|
160
|
+
* @returns {Promise<{blob: Blob, timeStamp: string}|undefined|false>} Resolves with:
|
|
161
|
+
* - {blob, timeStamp}: object with the Blob and ISO timestamp if the entry exists in IndexedDB
|
|
162
|
+
* - undefined: if there is no stored entry for that URI
|
|
163
|
+
* - false: if the database could not be opened (IndexedDB unsupported or open failure)
|
|
164
|
+
*/
|
|
165
|
+
#getFile = async (uri) => {
|
|
166
|
+
if (this.#db === undefined) {
|
|
167
|
+
this.#db = await this.#openDB();
|
|
168
|
+
}
|
|
169
|
+
if (this.#db === null) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
const transation = this.#db.transaction(this.osName, "readonly");
|
|
173
|
+
const store = transation.objectStore(this.osName);
|
|
174
|
+
return store.get(uri);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get a URL to the file: either an object URL for the cached Blob, or the original URI if the server copy is available and IndexedDB isn't supported.
|
|
179
|
+
* @param {String} uri File URI
|
|
180
|
+
* @returns {Promise<string|boolean>} Resolves with:
|
|
181
|
+
* - object URL string for the cached Blob
|
|
182
|
+
* - original URI string if IndexedDB unsupported but server file exists
|
|
183
|
+
* - false if the file is not available on the server
|
|
184
|
+
*/
|
|
185
|
+
getURL = async (uri) => {
|
|
186
|
+
if (!this.#supported) {
|
|
187
|
+
return (await this.#getServerFileTimeStamp(uri)) ? uri : false;
|
|
188
|
+
}
|
|
189
|
+
const storedFile = await this.get(uri);
|
|
190
|
+
return storedFile ? URL.createObjectURL(storedFile.blob) : (await this.#getServerFileTimeStamp(uri)) ? uri : false;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get the cached or server Last-Modified timestamp for a file.
|
|
195
|
+
* @param {String} uri File URI
|
|
196
|
+
* @returns {Promise<string|null>} Resolves with:
|
|
197
|
+
* - ISO 8601 string: Last-Modified timestamp from the cached record (when using IndexedDB) or from the server HEAD response
|
|
198
|
+
* - null: when no timestamp is available or on error
|
|
199
|
+
*/
|
|
200
|
+
getTimeStamp = async (uri) => {
|
|
201
|
+
if (!this.#supported) {
|
|
202
|
+
const serverFileTimeStamp = await this.#getServerFileTimeStamp(uri);
|
|
203
|
+
return serverFileTimeStamp ? serverFileTimeStamp : null;
|
|
204
|
+
}
|
|
205
|
+
const storedFile = await this.get(uri);
|
|
206
|
+
return storedFile ? storedFile.timeStamp : null;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get the cached or server size of a file in bytes.
|
|
211
|
+
* @param {String} uri File URI
|
|
212
|
+
* @returns {Promise<number|null>} Resolves with:
|
|
213
|
+
* - number: size in bytes when available (from cache or server)
|
|
214
|
+
* - null: when size is not available or on error
|
|
215
|
+
*/
|
|
216
|
+
getSize = async (uri) => {
|
|
217
|
+
if (!this.#supported) {
|
|
218
|
+
const serverFileSize = await this.#getServerFileSize(uri);
|
|
219
|
+
return serverFileSize ? serverFileSize : null;
|
|
220
|
+
}
|
|
221
|
+
const storedFile = await this.get(uri);
|
|
222
|
+
return storedFile ? storedFile.blob.size : null;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Retrieve the Blob for a file from cache or server.
|
|
227
|
+
* @param {String} uri File URI
|
|
228
|
+
* @returns {Promise<Blob|false>} Resolves with:
|
|
229
|
+
* - Blob: the file blob when available (from cache or server)
|
|
230
|
+
* - false: when the file cannot be retrieved
|
|
231
|
+
*/
|
|
232
|
+
getBlob = async (uri) => {
|
|
233
|
+
if (!this.#supported) {
|
|
234
|
+
const serverFile = await this.#getServerFile(uri);
|
|
235
|
+
return serverFile ? serverFile.blob : false;
|
|
236
|
+
}
|
|
237
|
+
const storedFile = await this.get(uri);
|
|
238
|
+
return storedFile ? storedFile.blob : false;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Get the file Blob from IndexedDB if stored; otherwise download and store it, then return the Blob.
|
|
243
|
+
* @param {String} uri File URI
|
|
244
|
+
* @returns {Promise<Blob|undefined|false>} Resolves with:
|
|
245
|
+
* - Blob of the stored file
|
|
246
|
+
* - undefined if the download fails
|
|
247
|
+
* - false if uri is falsy
|
|
248
|
+
* @description
|
|
249
|
+
* The method controls the cache version of the file. If the server copy has a different timestamp than the stored one,
|
|
250
|
+
* the file is re-downloaded from the server and stored again in IndexedDB.
|
|
251
|
+
* If the server file is not available at that time to check its age but is stored, the stored file is returned.
|
|
252
|
+
*/
|
|
253
|
+
get = async (uri) => {
|
|
254
|
+
if (!uri) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
if (!this.#supported) {
|
|
258
|
+
return await this.#getServerFile(uri);
|
|
259
|
+
}
|
|
260
|
+
let storedFile = await this.#getFile(uri);
|
|
261
|
+
const serverFileTimeStamp = storedFile ? await this.#getServerFileTimeStamp(uri) : 0;
|
|
262
|
+
const storedFileTimeStamp = storedFile ? storedFile.timeStamp : 0;
|
|
263
|
+
if (!storedFile || (storedFile && serverFileTimeStamp !== null && serverFileTimeStamp !== storedFileTimeStamp)) {
|
|
264
|
+
const fileToStore = await this.#getServerFile(uri);
|
|
265
|
+
if (fileToStore && !!(await this.#putFile(fileToStore, uri))) {
|
|
266
|
+
storedFile = await this.#getFile(uri);
|
|
267
|
+
} else {
|
|
268
|
+
storedFile = fileToStore;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return storedFile;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Store the file in IndexedDB.
|
|
276
|
+
* @param {String} uri File URI
|
|
277
|
+
* @returns {Promise<boolean>} Resolves with:
|
|
278
|
+
* - true if the file was stored successfully
|
|
279
|
+
* - false if IndexedDB is not supported or storing failed
|
|
280
|
+
*/
|
|
281
|
+
put = async (uri) => {
|
|
282
|
+
if (!this.#supported) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
const fileToStore = await this.#getServerFile(uri);
|
|
286
|
+
return fileToStore ? !!(await this.#putFile(fileToStore, uri)) : false;
|
|
287
|
+
};
|
|
288
|
+
}
|
package/src/index.js
CHANGED
|
@@ -45,6 +45,7 @@ import { USDZExportAsync, GLTF2Export } from "@babylonjs/serializers";
|
|
|
45
45
|
import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_draco_mesh_compression";
|
|
46
46
|
import { DracoCompression } from "@babylonjs/core/Meshes/Compression/dracoCompression";
|
|
47
47
|
import { initDb, loadModel } from "./gltf-storage.js";
|
|
48
|
+
import { FileStorage } from "./file-storage.js";
|
|
48
49
|
|
|
49
50
|
class PrefViewerTask {
|
|
50
51
|
static Types = Object.freeze({
|
|
@@ -65,9 +66,7 @@ class PrefViewerTask {
|
|
|
65
66
|
const t = typeof type === "string" ? type.toLowerCase() : String(type).toLowerCase();
|
|
66
67
|
const allowed = Object.values(PrefViewerTask.Types);
|
|
67
68
|
if (!allowed.includes(t)) {
|
|
68
|
-
throw new TypeError(
|
|
69
|
-
`PrefViewerTask: invalid type "${type}". Allowed types: ${allowed.join(", ")}`
|
|
70
|
-
);
|
|
69
|
+
throw new TypeError(`PrefViewerTask: invalid type "${type}". Allowed types: ${allowed.join(", ")}`);
|
|
71
70
|
}
|
|
72
71
|
this.type = t;
|
|
73
72
|
|
|
@@ -80,6 +79,7 @@ class PrefViewer extends HTMLElement {
|
|
|
80
79
|
loaded = false;
|
|
81
80
|
loading = false;
|
|
82
81
|
#taskQueue = [];
|
|
82
|
+
#fileStorage = new FileStorage("PrefViewer", "Files");
|
|
83
83
|
|
|
84
84
|
#data = {
|
|
85
85
|
containers: {
|
|
@@ -306,7 +306,7 @@ class PrefViewer extends HTMLElement {
|
|
|
306
306
|
tried: toLoadDetail,
|
|
307
307
|
success: loadedDetail,
|
|
308
308
|
};
|
|
309
|
-
|
|
309
|
+
|
|
310
310
|
this.dispatchEvent(
|
|
311
311
|
new CustomEvent("scene-loaded", {
|
|
312
312
|
bubbles: true,
|
|
@@ -318,7 +318,7 @@ class PrefViewer extends HTMLElement {
|
|
|
318
318
|
|
|
319
319
|
await this.#scene.whenReadyAsync();
|
|
320
320
|
this.#engine.runRenderLoop(this.#renderLoop);
|
|
321
|
-
|
|
321
|
+
|
|
322
322
|
this.#resetChangedFlags();
|
|
323
323
|
|
|
324
324
|
if (this.hasAttribute("loading")) {
|
|
@@ -803,6 +803,94 @@ class PrefViewer extends HTMLElement {
|
|
|
803
803
|
return true;
|
|
804
804
|
}
|
|
805
805
|
|
|
806
|
+
/**
|
|
807
|
+
* Replace internal URIs in a glTF AssetContainer JSON with URLs pointing to files stored in IndexedDB.
|
|
808
|
+
* @param {JSON} assetContainerJSON AssetContainer in glTF (JSON) (modified in-place).
|
|
809
|
+
* @param {URL} [assetContainerURL] Optional URL of the AssetContainer. Used as the base path to resolve relative URIs.
|
|
810
|
+
* @returns {Promise<void>} Resolves when all applicable URIs have been resolved/replaced.
|
|
811
|
+
* @description
|
|
812
|
+
* - When provided, assetContainerURL is used as the base path for other scene files (binary buffers and all images).
|
|
813
|
+
* If not provided (null/undefined), it is because it is the assetContainer of the model or materials whose URIs are absolute.
|
|
814
|
+
* - According to the glTF 2.0 spec, only items inside the "buffers" and "images" arrays may have a "uri" property.
|
|
815
|
+
* - Data URIs (embedded base64) are ignored and left unchanged.
|
|
816
|
+
* - Matching asset URIs are normalized (backslashes converted to forward slashes) and passed to the FileStorage layer
|
|
817
|
+
* to obtain a usable URL (object URL or cached URL).
|
|
818
|
+
* - The function performs replacements in parallel and waits for all lookups to complete.
|
|
819
|
+
* - The JSON is updated in-place with the resolved URLs.
|
|
820
|
+
* @see {@link https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#uris|glTF™ 2.0 Specification - URIs}
|
|
821
|
+
*/
|
|
822
|
+
async #replaceSceneURIAsync(assetContainerJSON, assetContainerURL) {
|
|
823
|
+
if (!assetContainerJSON) {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
let sceneURLBase = assetContainerURL;
|
|
828
|
+
|
|
829
|
+
if (typeof assetContainerURL === "string") {
|
|
830
|
+
const lastIndexOfSlash = assetContainerURL.lastIndexOf("/");
|
|
831
|
+
if (lastIndexOfSlash !== -1) {
|
|
832
|
+
sceneURLBase = assetContainerURL.substring(0, lastIndexOfSlash + 1);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const arrayOfAssetsWithURI = [];
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Check whether a value is a syntactically absolute URL.
|
|
840
|
+
* @param {string} url Value to test.
|
|
841
|
+
* @returns {boolean} True when `url` is a string that can be parsed by the global URL constructor (i.e. a syntactically absolute URL); false otherwise.
|
|
842
|
+
* @description
|
|
843
|
+
* - Returns false for non-string inputs.
|
|
844
|
+
* - Uses the browser's URL parser, so protocol-relative URLs ("//host/...") and relative paths are considered non-absolute.
|
|
845
|
+
* - This is a syntactic check only — it does not perform network requests or validate reachability/CORS.
|
|
846
|
+
*/
|
|
847
|
+
var isURLAbsolute = function (url) {
|
|
848
|
+
if (typeof url !== "string") {
|
|
849
|
+
return false;
|
|
850
|
+
}
|
|
851
|
+
try {
|
|
852
|
+
new URL(url);
|
|
853
|
+
return true;
|
|
854
|
+
} catch {
|
|
855
|
+
return false;
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* Collect asset entries that have an external URI (non-data URI) and store a normalized absolute/relative-resolved URI for later replacement.
|
|
861
|
+
* @param {Object} asset glTF asset entry (an element of buffers[] or images[]).
|
|
862
|
+
* @param {number} index Index of the asset within its parent array.
|
|
863
|
+
* @param {Array} array Reference to the parent array (buffers or images).
|
|
864
|
+
* @returns {void} Side-effect: pushes a record into arrayOfAssetsWithURI when applicable { parent: <array>, index: <number>, uri: <string> }.
|
|
865
|
+
*/
|
|
866
|
+
var saveAssetData = function (asset, index, array) {
|
|
867
|
+
if (asset.uri && !asset.uri.startsWith("data:")) {
|
|
868
|
+
const assetData = {
|
|
869
|
+
parent: array,
|
|
870
|
+
index: index,
|
|
871
|
+
uri: `${!isURLAbsolute(asset.uri) && sceneURLBase ? sceneURLBase : ""}${asset.uri}`.replace(/\\\\|\\|\/\\/g, "/"),
|
|
872
|
+
};
|
|
873
|
+
arrayOfAssetsWithURI.push(assetData);
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
if (assetContainerJSON.buffers) {
|
|
878
|
+
assetContainerJSON.buffers.forEach((asset, index, array) => saveAssetData(asset, index, array));
|
|
879
|
+
}
|
|
880
|
+
if (assetContainerJSON.images) {
|
|
881
|
+
assetContainerJSON.images.forEach((asset, index, array) => saveAssetData(asset, index, array));
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Replace parallel URIs so that if files are not stored yet, they are downloaded in parallel
|
|
885
|
+
const promisesArray = arrayOfAssetsWithURI.map(async (asset) => {
|
|
886
|
+
const uri = await this.#fileStorage.getURL(asset.uri);
|
|
887
|
+
if (uri) {
|
|
888
|
+
asset.parent[asset.index].uri = uri;
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
await Promise.all(promisesArray);
|
|
892
|
+
}
|
|
893
|
+
|
|
806
894
|
async #loadAssetContainer(container) {
|
|
807
895
|
let storage = container?.storage;
|
|
808
896
|
|
|
@@ -832,9 +920,15 @@ class PrefViewer extends HTMLElement {
|
|
|
832
920
|
|
|
833
921
|
let { blob, extension, size } = this.#decodeBase64(source);
|
|
834
922
|
if (blob && extension) {
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
923
|
+
if ((container.name === "model" || container.name === "materials") && extension === ".gltf") {
|
|
924
|
+
const assetContainerJSON = JSON.parse(await blob.text());
|
|
925
|
+
await this.#replaceSceneURIAsync(assetContainerJSON, source);
|
|
926
|
+
source = `data:${JSON.stringify(assetContainerJSON)}`;
|
|
927
|
+
} else {
|
|
928
|
+
file = new File([blob], `${container.name}${extension}`, {
|
|
929
|
+
type: blob.type,
|
|
930
|
+
});
|
|
931
|
+
}
|
|
838
932
|
if (!container.changed.pending) {
|
|
839
933
|
if (container.timeStamp === null && container.size === size) {
|
|
840
934
|
container.changed = { pending: false, success: false };
|
|
@@ -846,12 +940,20 @@ class PrefViewer extends HTMLElement {
|
|
|
846
940
|
} else {
|
|
847
941
|
const extMatch = source.match(/\.(gltf|glb)(\?|#|$)/i);
|
|
848
942
|
extension = extMatch ? `.${extMatch[1].toLowerCase()}` : ".gltf";
|
|
849
|
-
|
|
943
|
+
let [fileSize, fileTimeStamp] = [await this.#fileStorage.getSize(source), await this.#fileStorage.getTimeStamp(source)];
|
|
850
944
|
if (container.size === fileSize && container.timeStamp === fileTimeStamp) {
|
|
851
945
|
container.changed = { pending: false, success: false };
|
|
852
946
|
return false;
|
|
853
947
|
} else {
|
|
854
948
|
container.changed = { pending: true, size: fileSize, success: false, timeStamp: fileTimeStamp };
|
|
949
|
+
if (extension === ".gltf") {
|
|
950
|
+
const assetContainerBlob = await this.#fileStorage.getBlob(source);
|
|
951
|
+
const assetContainerJSON = JSON.parse(await assetContainerBlob.text());
|
|
952
|
+
await this.#replaceSceneURIAsync(assetContainerJSON, source);
|
|
953
|
+
source = `data:${JSON.stringify(assetContainerJSON)}`;
|
|
954
|
+
} else {
|
|
955
|
+
source = await this.#fileStorage.getURL(source);
|
|
956
|
+
}
|
|
855
957
|
}
|
|
856
958
|
}
|
|
857
959
|
|
|
@@ -863,7 +965,7 @@ class PrefViewer extends HTMLElement {
|
|
|
863
965
|
compileMaterials: true,
|
|
864
966
|
loadAllMaterials: true,
|
|
865
967
|
loadOnlyMaterials: container.name === "materials",
|
|
866
|
-
preprocessUrlAsync: this.#transformUrl,
|
|
968
|
+
//preprocessUrlAsync: this.#transformUrl,
|
|
867
969
|
},
|
|
868
970
|
},
|
|
869
971
|
};
|
|
@@ -1050,7 +1152,7 @@ class PrefViewer extends HTMLElement {
|
|
|
1050
1152
|
if (this.#checkMaterialsChanged(options)) {
|
|
1051
1153
|
someSetted = someSetted || this.#setOptionsMaterials();
|
|
1052
1154
|
}
|
|
1053
|
-
|
|
1155
|
+
|
|
1054
1156
|
await this.#setStatusLoaded();
|
|
1055
1157
|
|
|
1056
1158
|
return someSetted;
|