@preference-sl/pref-viewer 2.10.0-beta.3 → 2.10.0-beta.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/gltf-storage.js +150 -71
- package/src/index.js +718 -112
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@preference-sl/pref-viewer",
|
|
3
|
-
"version": "2.10.0-beta.
|
|
3
|
+
"version": "2.10.0-beta.30",
|
|
4
4
|
"description": "Web Component to preview GLTF models with Babylon.js",
|
|
5
5
|
"author": "Alex Moreno Palacio <amoreno@preference.es>",
|
|
6
6
|
"scripts": {
|
|
@@ -34,10 +34,10 @@
|
|
|
34
34
|
"index.d.ts"
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@babylonjs/core": "^8.
|
|
38
|
-
"@babylonjs/loaders": "^8.
|
|
39
|
-
"@babylonjs/serializers": "^8.
|
|
40
|
-
"babylonjs-gltf2interface": "^8.
|
|
37
|
+
"@babylonjs/core": "^8.31.3",
|
|
38
|
+
"@babylonjs/loaders": "^8.31.3",
|
|
39
|
+
"@babylonjs/serializers": "^8.31.3",
|
|
40
|
+
"babylonjs-gltf2interface": "^8.31.3"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"esbuild": "^0.25.10",
|
package/src/gltf-storage.js
CHANGED
|
@@ -1,137 +1,214 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
1
|
+
// wwwroot/js/gltf-storage.js
|
|
2
|
+
|
|
3
|
+
// Public, inspectable namespace
|
|
4
|
+
const PC = (globalThis.PrefConfigurator ??= {});
|
|
5
|
+
PC.version = PC.version ?? "1.0.1"; // bump if your schema changes!!
|
|
6
|
+
PC.db = PC.db ?? null;
|
|
7
|
+
|
|
8
|
+
function _getDbOrThrow() {
|
|
9
|
+
const db = PC.db;
|
|
10
|
+
if (!db) throw new Error("Database not initialized. Call initDb(...) first.");
|
|
11
|
+
return db;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// --- internal helpers -------------------------------------------------------
|
|
15
|
+
function _openEnsuringStore(dbName, storeName) {
|
|
3
16
|
return new Promise((resolve, reject) => {
|
|
4
|
-
const
|
|
17
|
+
const open = indexedDB.open(dbName, 5);
|
|
5
18
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
resolve();
|
|
10
|
-
}
|
|
19
|
+
open.onblocked = () => reject(new Error("Open blocked by another tab or worker"));
|
|
20
|
+
open.onerror = () => reject(open.error);
|
|
21
|
+
open.onsuccess = (e) => {
|
|
22
|
+
resolve(e.target.result);
|
|
23
|
+
}
|
|
24
|
+
open.onupgradeneeded = (ev) => {
|
|
25
|
+
const upgradeDb = ev.target.result;
|
|
11
26
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const store = db.createObjectStore(storeName, { keyPath: "id" });
|
|
16
|
-
store.createIndex("type", "type", { unique: false });
|
|
17
|
-
store.createIndex("timestamp", "timestamp", { unique: false });
|
|
27
|
+
// Eliminar la object store si ya existe
|
|
28
|
+
if (upgradeDb.objectStoreNames.contains(storeName)) {
|
|
29
|
+
upgradeDb.deleteObjectStore(storeName);
|
|
18
30
|
}
|
|
31
|
+
|
|
32
|
+
const store = upgradeDb.createObjectStore(storeName, { keyPath: 'id' });
|
|
33
|
+
store.createIndex('expirationTimeStamp', 'expirationTimeStamp', { unique: false });
|
|
19
34
|
};
|
|
20
35
|
});
|
|
21
36
|
}
|
|
22
37
|
|
|
38
|
+
// --- public API -------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
// Inicializar IndexedDB y dejar el handle en PrefConfigurator.db (público)
|
|
41
|
+
export async function initDb(dbName, storeName) {
|
|
42
|
+
const db = await _openEnsuringStore(dbName, storeName);
|
|
43
|
+
// Close any previous handle to avoid versionchange blocking
|
|
44
|
+
try { PC.db?.close?.(); } catch { }
|
|
45
|
+
PC.db = db;
|
|
46
|
+
|
|
47
|
+
// If another tab upgrades, close gracefully so next call can re-init
|
|
48
|
+
db.onversionchange = () => {
|
|
49
|
+
try { db.close(); } catch { }
|
|
50
|
+
if (PC.db === db) PC.db = null;
|
|
51
|
+
console.warn("[PrefConfigurator] DB connection closed due to versionchange. Re-run initDb().");
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
23
55
|
// Guardar modelo
|
|
24
56
|
export async function saveModel(modelDataStr, storeName) {
|
|
25
57
|
return new Promise((resolve, reject) => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
58
|
+
let db;
|
|
59
|
+
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
60
|
+
|
|
61
|
+
let modelData;
|
|
62
|
+
try { modelData = JSON.parse(modelDataStr); }
|
|
63
|
+
catch (e) { reject(new Error("saveModel: modelDataStr is not valid JSON")); return; }
|
|
31
64
|
|
|
32
65
|
const dataToStore = {
|
|
33
66
|
...modelData,
|
|
34
67
|
data: modelData.data,
|
|
35
|
-
size: modelData
|
|
36
|
-
timestamp: new Date().toISOString(),
|
|
68
|
+
size: (modelData?.data?.length ?? 0)
|
|
37
69
|
};
|
|
38
70
|
|
|
39
|
-
const
|
|
40
|
-
const store =
|
|
41
|
-
const
|
|
71
|
+
const tx = db.transaction([storeName], "readwrite");
|
|
72
|
+
const store = tx.objectStore(storeName);
|
|
73
|
+
const req = store.put(dataToStore);
|
|
42
74
|
|
|
43
|
-
|
|
44
|
-
|
|
75
|
+
req.onerror = () => reject(req.error);
|
|
76
|
+
req.onsuccess = () => resolve();
|
|
45
77
|
});
|
|
46
78
|
}
|
|
47
79
|
|
|
48
80
|
// Cargar modelo
|
|
49
81
|
export function loadModel(modelId, storeName) {
|
|
50
82
|
return new Promise((resolve, reject) => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const tx = globalThis.gltfDB.transaction([storeName], "readonly");
|
|
83
|
+
let db;
|
|
84
|
+
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
85
|
+
|
|
86
|
+
const tx = db.transaction([storeName], "readonly");
|
|
56
87
|
const store = tx.objectStore(storeName);
|
|
57
88
|
const req = store.get(modelId);
|
|
89
|
+
|
|
58
90
|
req.onerror = () => reject(req.error);
|
|
59
91
|
req.onsuccess = () => resolve(req.result ?? null);
|
|
60
92
|
});
|
|
61
93
|
}
|
|
62
94
|
|
|
95
|
+
// Descargar archivo desde base64
|
|
63
96
|
export function downloadFileFromBytes(fileName, bytesBase64, mimeType) {
|
|
64
97
|
const link = document.createElement("a");
|
|
65
98
|
link.download = fileName;
|
|
66
99
|
link.href = `data:${mimeType};base64,${bytesBase64}`;
|
|
67
100
|
document.body.appendChild(link);
|
|
68
101
|
link.click();
|
|
69
|
-
|
|
102
|
+
link.remove();
|
|
70
103
|
}
|
|
71
104
|
|
|
72
105
|
// Obtener todos los modelos (solo metadata)
|
|
73
106
|
export async function getAllModels(storeName) {
|
|
74
107
|
return new Promise((resolve, reject) => {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
108
|
+
let db;
|
|
109
|
+
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
79
110
|
|
|
80
|
-
const
|
|
81
|
-
const store =
|
|
82
|
-
const
|
|
111
|
+
const tx = db.transaction([storeName], "readonly");
|
|
112
|
+
const store = tx.objectStore(storeName);
|
|
113
|
+
const req = store.getAll();
|
|
83
114
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const results =
|
|
115
|
+
req.onerror = () => reject(req.error);
|
|
116
|
+
req.onsuccess = () => {
|
|
117
|
+
const items = Array.isArray(req.result) ? req.result : [];
|
|
118
|
+
const results = items.map(item => ({
|
|
88
119
|
id: item.id,
|
|
89
120
|
metadata: item.metadata,
|
|
90
|
-
|
|
91
|
-
size: item.size
|
|
92
|
-
type: item.type,
|
|
121
|
+
timeStamp: item.timeStamp,
|
|
122
|
+
size: item.size
|
|
93
123
|
}));
|
|
124
|
+
// keep old behavior: return JSON string
|
|
94
125
|
resolve(JSON.stringify(results));
|
|
95
126
|
};
|
|
96
127
|
});
|
|
97
128
|
}
|
|
98
129
|
|
|
99
|
-
// Eliminar modelo
|
|
130
|
+
// Eliminar modelo por id
|
|
100
131
|
export async function deleteModel(modelId, storeName) {
|
|
101
132
|
return new Promise((resolve, reject) => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
133
|
+
let db;
|
|
134
|
+
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
106
135
|
|
|
107
|
-
const
|
|
108
|
-
const store =
|
|
109
|
-
const
|
|
136
|
+
const tx = db.transaction([storeName], "readwrite");
|
|
137
|
+
const store = tx.objectStore(storeName);
|
|
138
|
+
const req = store.delete(modelId);
|
|
110
139
|
|
|
111
|
-
|
|
112
|
-
|
|
140
|
+
req.onerror = () => reject(req.error);
|
|
141
|
+
req.onsuccess = () => resolve();
|
|
113
142
|
});
|
|
114
143
|
}
|
|
115
144
|
|
|
116
|
-
// Limpiar toda la
|
|
145
|
+
// Limpiar toda la store
|
|
117
146
|
export async function clearAll(storeName) {
|
|
118
147
|
return new Promise((resolve, reject) => {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
148
|
+
let db;
|
|
149
|
+
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
150
|
+
|
|
151
|
+
const tx = db.transaction([storeName], "readwrite");
|
|
152
|
+
const store = tx.objectStore(storeName);
|
|
153
|
+
const req = store.clear();
|
|
154
|
+
|
|
155
|
+
req.onerror = () => reject(req.error);
|
|
156
|
+
req.onsuccess = () => resolve();
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Borrar modelos expirados usando el índice "expirationTimeStamp"
|
|
161
|
+
export async function cleanExpiredModels(storeName) {
|
|
162
|
+
return new Promise((resolve, reject) => {
|
|
163
|
+
let db;
|
|
164
|
+
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
165
|
+
|
|
166
|
+
const tx = db.transaction([storeName], "readwrite");
|
|
167
|
+
const store = tx.objectStore(storeName);
|
|
168
|
+
|
|
169
|
+
const index = store.index("expirationTimeStamp");
|
|
170
|
+
const now = Date.now();
|
|
171
|
+
const range = IDBKeyRange.upperBound(now);
|
|
172
|
+
const cursorRequest = index.openCursor(range);
|
|
173
|
+
|
|
174
|
+
cursorRequest.onerror = () => reject(cursorRequest.error);
|
|
175
|
+
cursorRequest.onsuccess = (event) => {
|
|
176
|
+
const cursor = event.target.result;
|
|
177
|
+
if (cursor) {
|
|
178
|
+
cursor.delete();
|
|
179
|
+
cursor.continue();
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
tx.oncomplete = () => resolve();
|
|
184
|
+
tx.onerror = () => reject(tx.error);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
123
187
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
188
|
+
// Utilidades opcionales y visibles (por si las quieres usar en consola)
|
|
189
|
+
export function closeDb() {
|
|
190
|
+
if (PC.db) {
|
|
191
|
+
try { PC.db.close(); } catch { }
|
|
192
|
+
PC.db = null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
127
195
|
|
|
128
|
-
|
|
129
|
-
|
|
196
|
+
export function deleteDatabase(dbName) {
|
|
197
|
+
return new Promise((resolve, reject) => {
|
|
198
|
+
// Close current handle if points to this DB
|
|
199
|
+
if (PC.db && PC.db.name === dbName) {
|
|
200
|
+
try { PC.db.close(); } catch { }
|
|
201
|
+
PC.db = null;
|
|
202
|
+
}
|
|
203
|
+
const req = indexedDB.deleteDatabase(dbName);
|
|
204
|
+
req.onblocked = () => reject(new Error("Delete blocked by another tab or worker"));
|
|
205
|
+
req.onerror = () => reject(req.error);
|
|
206
|
+
req.onsuccess = () => resolve();
|
|
130
207
|
});
|
|
131
208
|
}
|
|
132
209
|
|
|
210
|
+
// Attach a frozen, public API (no private state)
|
|
133
211
|
(function attachPublicAPI(global) {
|
|
134
|
-
const root = (global.PrefConfigurator ??= {});
|
|
135
212
|
const storage = {
|
|
136
213
|
initDb,
|
|
137
214
|
saveModel,
|
|
@@ -139,10 +216,12 @@ export async function clearAll(storeName) {
|
|
|
139
216
|
getAllModels,
|
|
140
217
|
deleteModel,
|
|
141
218
|
clearAll,
|
|
219
|
+
cleanExpiredModels,
|
|
142
220
|
downloadFileFromBytes,
|
|
221
|
+
// extras
|
|
222
|
+
closeDb,
|
|
223
|
+
deleteDatabase,
|
|
143
224
|
};
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
root.version = root.version ?? "1.0.0";
|
|
147
|
-
root.storage = Object.freeze(storage);
|
|
225
|
+
// free to inspect PC.db in devtools
|
|
226
|
+
global.PrefConfigurator.storage = Object.freeze(storage);
|
|
148
227
|
})(globalThis);
|