@preference-sl/pref-viewer 2.10.0-beta.4 → 2.10.0-beta.6
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 +1 -1
- package/src/gltf-storage.js +226 -121
- package/src/index.js +47 -23
package/package.json
CHANGED
package/src/gltf-storage.js
CHANGED
|
@@ -1,148 +1,253 @@
|
|
|
1
|
-
//
|
|
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 you change schema
|
|
6
|
+
PC.db = PC.db ?? null; // public handle (no private state)
|
|
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
|
+
|
|
16
|
+
function _createStoreIfMissing(db, storeName) {
|
|
17
|
+
if (!db.objectStoreNames.contains(storeName)) {
|
|
18
|
+
const store = db.createObjectStore(storeName, { keyPath: "id" });
|
|
19
|
+
store.createIndex("expirationTimeStamp", "expirationTimeStamp", { unique: false });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Opens a DB without specifying a version. If the required object store doesn't
|
|
25
|
+
* exist, it re-opens with (currentVersion + 1) to create it.
|
|
26
|
+
*/
|
|
27
|
+
function _openEnsuringStore(dbName, storeName) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const open = indexedDB.open(dbName);
|
|
30
|
+
|
|
31
|
+
open.onblocked = () => reject(new Error("Open blocked by another tab or worker"));
|
|
32
|
+
open.onerror = () => reject(open.error);
|
|
33
|
+
|
|
34
|
+
open.onupgradeneeded = (e) => {
|
|
35
|
+
// First creation path (brand-new DB)
|
|
36
|
+
const db = e.target.result;
|
|
37
|
+
_createStoreIfMissing(db, storeName);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
open.onsuccess = (e) => {
|
|
41
|
+
const db = e.target.result;
|
|
42
|
+
|
|
43
|
+
// If the store exists, we're done.
|
|
44
|
+
if (db.objectStoreNames.contains(storeName)) {
|
|
45
|
+
resolve(db);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Otherwise bump version just enough to add the store.
|
|
50
|
+
const nextVersion = db.version + 1;
|
|
51
|
+
db.close();
|
|
52
|
+
|
|
53
|
+
const upgrade = indexedDB.open(dbName, nextVersion);
|
|
54
|
+
upgrade.onblocked = () => reject(new Error("Upgrade blocked by another tab or worker"));
|
|
55
|
+
upgrade.onerror = () => reject(upgrade.error);
|
|
56
|
+
upgrade.onupgradeneeded = (ev) => {
|
|
57
|
+
const upDb = ev.target.result;
|
|
58
|
+
_createStoreIfMissing(upDb, storeName);
|
|
59
|
+
};
|
|
60
|
+
upgrade.onsuccess = (ev) => resolve(ev.target.result);
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// --- public API -------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
// Inicializar IndexedDB y dejar el handle en PrefConfigurator.db (público)
|
|
2
68
|
export async function initDb(dbName, storeName) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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 });
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
});
|
|
69
|
+
const db = await _openEnsuringStore(dbName, storeName);
|
|
70
|
+
|
|
71
|
+
// Close any previous handle to avoid versionchange blocking
|
|
72
|
+
try { PC.db?.close?.(); } catch {}
|
|
73
|
+
PC.db = db;
|
|
74
|
+
|
|
75
|
+
// If another tab upgrades, close gracefully so next call can re-init
|
|
76
|
+
db.onversionchange = () => {
|
|
77
|
+
try { db.close(); } catch {}
|
|
78
|
+
if (PC.db === db) PC.db = null;
|
|
79
|
+
console.warn("[PrefConfigurator] DB connection closed due to versionchange. Re-run initDb().");
|
|
80
|
+
};
|
|
21
81
|
}
|
|
22
82
|
|
|
23
83
|
// Guardar modelo
|
|
24
84
|
export async function saveModel(modelDataStr, storeName) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
let db;
|
|
87
|
+
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
88
|
+
|
|
89
|
+
let modelData;
|
|
90
|
+
try { modelData = JSON.parse(modelDataStr); }
|
|
91
|
+
catch (e) { reject(new Error("saveModel: modelDataStr is not valid JSON")); return; }
|
|
92
|
+
|
|
93
|
+
const dataToStore = {
|
|
94
|
+
...modelData,
|
|
95
|
+
data: modelData.data,
|
|
96
|
+
size: (modelData?.data?.length ?? 0)
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const tx = db.transaction([storeName], "readwrite");
|
|
100
|
+
const store = tx.objectStore(storeName);
|
|
101
|
+
const req = store.put(dataToStore);
|
|
102
|
+
|
|
103
|
+
req.onerror = () => reject(req.error);
|
|
104
|
+
req.onsuccess = () => resolve();
|
|
105
|
+
});
|
|
46
106
|
}
|
|
47
107
|
|
|
48
108
|
// Cargar modelo
|
|
49
109
|
export function loadModel(modelId, storeName) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
let db;
|
|
112
|
+
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
113
|
+
|
|
114
|
+
const tx = db.transaction([storeName], "readonly");
|
|
115
|
+
const store = tx.objectStore(storeName);
|
|
116
|
+
const req = store.get(modelId);
|
|
117
|
+
|
|
118
|
+
req.onerror = () => reject(req.error);
|
|
119
|
+
req.onsuccess = () => resolve(req.result ?? null);
|
|
120
|
+
});
|
|
61
121
|
}
|
|
62
122
|
|
|
123
|
+
// Descargar archivo desde base64
|
|
63
124
|
export function downloadFileFromBytes(fileName, bytesBase64, mimeType) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
125
|
+
const link = document.createElement("a");
|
|
126
|
+
link.download = fileName;
|
|
127
|
+
link.href = `data:${mimeType};base64,${bytesBase64}`;
|
|
128
|
+
document.body.appendChild(link);
|
|
129
|
+
link.click();
|
|
130
|
+
link.remove();
|
|
70
131
|
}
|
|
71
132
|
|
|
72
133
|
// Obtener todos los modelos (solo metadata)
|
|
73
134
|
export async function getAllModels(storeName) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
};
|
|
96
|
-
});
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
let db;
|
|
137
|
+
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
138
|
+
|
|
139
|
+
const tx = db.transaction([storeName], "readonly");
|
|
140
|
+
const store = tx.objectStore(storeName);
|
|
141
|
+
const req = store.getAll();
|
|
142
|
+
|
|
143
|
+
req.onerror = () => reject(req.error);
|
|
144
|
+
req.onsuccess = () => {
|
|
145
|
+
const items = Array.isArray(req.result) ? req.result : [];
|
|
146
|
+
const results = items.map(item => ({
|
|
147
|
+
id: item.id,
|
|
148
|
+
metadata: item.metadata,
|
|
149
|
+
timeStamp: item.timeStamp,
|
|
150
|
+
size: item.size
|
|
151
|
+
}));
|
|
152
|
+
// keep old behavior: return JSON string
|
|
153
|
+
resolve(JSON.stringify(results));
|
|
154
|
+
};
|
|
155
|
+
});
|
|
97
156
|
}
|
|
98
157
|
|
|
99
|
-
// Eliminar modelo
|
|
158
|
+
// Eliminar modelo por id
|
|
100
159
|
export async function deleteModel(modelId, storeName) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
request.onsuccess = () => resolve();
|
|
113
|
-
});
|
|
160
|
+
return new Promise((resolve, reject) => {
|
|
161
|
+
let db;
|
|
162
|
+
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
163
|
+
|
|
164
|
+
const tx = db.transaction([storeName], "readwrite");
|
|
165
|
+
const store = tx.objectStore(storeName);
|
|
166
|
+
const req = store.delete(modelId);
|
|
167
|
+
|
|
168
|
+
req.onerror = () => reject(req.error);
|
|
169
|
+
req.onsuccess = () => resolve();
|
|
170
|
+
});
|
|
114
171
|
}
|
|
115
172
|
|
|
116
|
-
// Limpiar toda la
|
|
173
|
+
// Limpiar toda la store
|
|
117
174
|
export async function clearAll(storeName) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
request.onsuccess = () => resolve();
|
|
130
|
-
});
|
|
175
|
+
return new Promise((resolve, reject) => {
|
|
176
|
+
let db;
|
|
177
|
+
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
178
|
+
|
|
179
|
+
const tx = db.transaction([storeName], "readwrite");
|
|
180
|
+
const store = tx.objectStore(storeName);
|
|
181
|
+
const req = store.clear();
|
|
182
|
+
|
|
183
|
+
req.onerror = () => reject(req.error);
|
|
184
|
+
req.onsuccess = () => resolve();
|
|
185
|
+
});
|
|
131
186
|
}
|
|
132
187
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
188
|
+
// Borrar modelos expirados usando el índice "expirationTimeStamp"
|
|
189
|
+
export async function cleanExpiredModels(storeName) {
|
|
190
|
+
return new Promise((resolve, reject) => {
|
|
191
|
+
let db;
|
|
192
|
+
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
193
|
+
|
|
194
|
+
const tx = db.transaction([storeName], "readwrite");
|
|
195
|
+
const store = tx.objectStore(storeName);
|
|
196
|
+
|
|
197
|
+
const index = store.index("expirationTimeStamp");
|
|
198
|
+
const now = Date.now();
|
|
199
|
+
const range = IDBKeyRange.upperBound(now);
|
|
200
|
+
const cursorRequest = index.openCursor(range);
|
|
201
|
+
|
|
202
|
+
cursorRequest.onerror = () => reject(cursorRequest.error);
|
|
203
|
+
cursorRequest.onsuccess = (event) => {
|
|
204
|
+
const cursor = event.target.result;
|
|
205
|
+
if (cursor) {
|
|
206
|
+
cursor.delete();
|
|
207
|
+
cursor.continue();
|
|
208
|
+
}
|
|
143
209
|
};
|
|
144
210
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
211
|
+
tx.oncomplete = () => resolve();
|
|
212
|
+
tx.onerror = () => reject(tx.error);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Utilidades opcionales y visibles (para usar en consola)
|
|
217
|
+
export function closeDb() {
|
|
218
|
+
if (PC.db) {
|
|
219
|
+
try { PC.db.close(); } catch {}
|
|
220
|
+
PC.db = null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function deleteDatabase(dbName) {
|
|
225
|
+
return new Promise((resolve, reject) => {
|
|
226
|
+
if (PC.db && PC.db.name === dbName) {
|
|
227
|
+
try { PC.db.close(); } catch {}
|
|
228
|
+
PC.db = null;
|
|
229
|
+
}
|
|
230
|
+
const req = indexedDB.deleteDatabase(dbName);
|
|
231
|
+
req.onblocked = () => reject(new Error("Delete blocked by another tab or worker"));
|
|
232
|
+
req.onerror = () => reject(req.error);
|
|
233
|
+
req.onsuccess = () => resolve();
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Attach a frozen, public API (no private state)
|
|
238
|
+
(function attachPublicAPI(global) {
|
|
239
|
+
const storage = {
|
|
240
|
+
initDb,
|
|
241
|
+
saveModel,
|
|
242
|
+
loadModel,
|
|
243
|
+
getAllModels,
|
|
244
|
+
deleteModel,
|
|
245
|
+
clearAll,
|
|
246
|
+
cleanExpiredModels,
|
|
247
|
+
downloadFileFromBytes,
|
|
248
|
+
// extras
|
|
249
|
+
closeDb,
|
|
250
|
+
deleteDatabase,
|
|
251
|
+
};
|
|
252
|
+
global.PrefConfigurator.storage = Object.freeze(storage);
|
|
148
253
|
})(globalThis);
|
package/src/index.js
CHANGED
|
@@ -232,17 +232,21 @@ class PrefViewer extends HTMLElement {
|
|
|
232
232
|
}
|
|
233
233
|
this.#data.options.camera.changed = options.camera && options.camera !== this.#data.options.camera.value ? true : false;
|
|
234
234
|
this.#data.options.camera.value = this.#data.options.camera.changed ? options.camera : this.#data.options.camera.value;
|
|
235
|
+
return this.#data.options.camera.changed;
|
|
235
236
|
}
|
|
236
237
|
|
|
237
238
|
#checkMaterialsChanged(options) {
|
|
238
239
|
if (!options) {
|
|
239
240
|
return false;
|
|
240
241
|
}
|
|
242
|
+
let someChanged = false;
|
|
241
243
|
Object.keys(this.#data.options.materials).forEach((material) => {
|
|
242
244
|
const key = `${material}Material`;
|
|
243
245
|
this.#data.options.materials[material].changed = options[key] && options[key] !== this.#data.options.materials[material].value ? true : false;
|
|
244
246
|
this.#data.options.materials[material].value = this.#data.options.materials[material].changed ? options[key] : this.#data.options.materials[material].value;
|
|
247
|
+
someChanged = someChanged || this.#data.options.materials[material].changed;
|
|
245
248
|
});
|
|
249
|
+
return someChanged;
|
|
246
250
|
}
|
|
247
251
|
|
|
248
252
|
#storeChangedFlagsForContainer(container) {
|
|
@@ -474,37 +478,45 @@ class PrefViewer extends HTMLElement {
|
|
|
474
478
|
return false;
|
|
475
479
|
}
|
|
476
480
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
481
|
+
let someSetted = false;
|
|
482
|
+
containers.forEach((container) =>
|
|
483
|
+
container.meshes
|
|
484
|
+
.filter((meshToFilter) => meshToFilter.name.startsWith(optionMaterial.prefix))
|
|
485
|
+
.forEach((mesh) => {
|
|
486
|
+
mesh.material = material;
|
|
487
|
+
someSetted = true;
|
|
488
|
+
})
|
|
489
|
+
);
|
|
485
490
|
|
|
486
|
-
return
|
|
491
|
+
return someSetted;
|
|
487
492
|
}
|
|
488
493
|
|
|
489
494
|
#setOptionsMaterials() {
|
|
490
|
-
|
|
495
|
+
let someSetted = false;
|
|
496
|
+
Object.values(this.#data.options.materials).forEach((material) => {
|
|
497
|
+
let settedMaterial = this.#setOptionsMaterial(material);
|
|
498
|
+
someSetted = someSetted || settedMaterial;
|
|
499
|
+
});
|
|
500
|
+
return someSetted;
|
|
491
501
|
}
|
|
492
502
|
|
|
493
503
|
#setOptionsCamera() {
|
|
494
504
|
if (!this.#data.options.camera.value || (!this.#data.options.camera.changed && !this.#data.containers.model.assetContainer.changed)) {
|
|
495
505
|
return false;
|
|
496
506
|
}
|
|
507
|
+
|
|
497
508
|
let camera = this.#data.containers.model.assetContainer?.cameras.find((cam) => cam.name === this.#data.options.camera.value) || null;
|
|
498
509
|
if (!camera) {
|
|
499
|
-
|
|
500
|
-
} else {
|
|
501
|
-
camera.metadata = { locked: this.#data.options.camera.locked };
|
|
502
|
-
if (!this.#data.options.camera.locked) {
|
|
503
|
-
camera.attachControl(this.#canvas, true);
|
|
504
|
-
}
|
|
510
|
+
return false;
|
|
505
511
|
}
|
|
506
512
|
|
|
513
|
+
camera.metadata = { locked: this.#data.options.camera.locked };
|
|
514
|
+
if (!this.#data.options.camera.locked) {
|
|
515
|
+
camera.attachControl(this.#canvas, true);
|
|
516
|
+
}
|
|
507
517
|
this.#scene.activeCamera = camera;
|
|
518
|
+
|
|
519
|
+
return true;
|
|
508
520
|
}
|
|
509
521
|
|
|
510
522
|
#addContainer(container) {
|
|
@@ -657,7 +669,7 @@ class PrefViewer extends HTMLElement {
|
|
|
657
669
|
return false;
|
|
658
670
|
}
|
|
659
671
|
|
|
660
|
-
//
|
|
672
|
+
// Containers
|
|
661
673
|
this.#data.containers.model.storage = config.model?.storage || null;
|
|
662
674
|
this.#data.containers.model.show = config.model?.visible !== undefined ? config.model.visible : this.#data.containers.model.show;
|
|
663
675
|
this.#data.containers.environment.storage = config.scene?.storage || null;
|
|
@@ -673,6 +685,22 @@ class PrefViewer extends HTMLElement {
|
|
|
673
685
|
this.#initialized && this.#loadContainers(true, true, true);
|
|
674
686
|
}
|
|
675
687
|
|
|
688
|
+
setOptions(options) {
|
|
689
|
+
if (!options) {
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
let someSetted = false;
|
|
693
|
+
if (this.#checkCameraChanged(options)) {
|
|
694
|
+
someSetted = someSetted || this.#setOptionsCamera();
|
|
695
|
+
}
|
|
696
|
+
if (this.#checkMaterialsChanged(options)) {
|
|
697
|
+
someSetted = someSetted || this.#setOptionsMaterials();
|
|
698
|
+
}
|
|
699
|
+
this.#resetChangedFlags();
|
|
700
|
+
debugger;
|
|
701
|
+
return someSetted;
|
|
702
|
+
}
|
|
703
|
+
|
|
676
704
|
loadModel(model) {
|
|
677
705
|
model = typeof model === "string" ? JSON.parse(model) : model;
|
|
678
706
|
if (!model) {
|
|
@@ -717,9 +745,7 @@ class PrefViewer extends HTMLElement {
|
|
|
717
745
|
|
|
718
746
|
downloadModelGLB() {
|
|
719
747
|
const fileName = "model";
|
|
720
|
-
GLTF2Export.GLBAsync(this.#data.containers.model.assetContainer, fileName, { exportWithoutWaitingForScene: true }).then((glb) =>
|
|
721
|
-
glb.downloadFiles();
|
|
722
|
-
});
|
|
748
|
+
GLTF2Export.GLBAsync(this.#data.containers.model.assetContainer, fileName, { exportWithoutWaitingForScene: true }).then((glb) => glb.downloadFiles());
|
|
723
749
|
}
|
|
724
750
|
|
|
725
751
|
downloadModelUSDZ() {
|
|
@@ -742,9 +768,7 @@ class PrefViewer extends HTMLElement {
|
|
|
742
768
|
|
|
743
769
|
downloadModelAndSceneGLB() {
|
|
744
770
|
const fileName = "scene";
|
|
745
|
-
GLTF2Export.GLBAsync(this.#scene, fileName, { exportWithoutWaitingForScene: true }).then((glb) =>
|
|
746
|
-
glb.downloadFiles();
|
|
747
|
-
});
|
|
771
|
+
GLTF2Export.GLBAsync(this.#scene, fileName, { exportWithoutWaitingForScene: true }).then((glb) => glb.downloadFiles());
|
|
748
772
|
}
|
|
749
773
|
}
|
|
750
774
|
|