@preference-sl/pref-viewer 2.10.0-beta.2 → 2.10.0-beta.20
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 +176 -70
- package/src/index.js +534 -111
package/package.json
CHANGED
package/src/gltf-storage.js
CHANGED
|
@@ -1,137 +1,241 @@
|
|
|
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
|
+
|
|
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) {
|
|
3
28
|
return new Promise((resolve, reject) => {
|
|
4
|
-
const
|
|
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);
|
|
5
33
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
34
|
+
open.onupgradeneeded = (e) => {
|
|
35
|
+
// First creation path (brand-new DB)
|
|
36
|
+
const db = e.target.result;
|
|
37
|
+
_createStoreIfMissing(db, storeName);
|
|
10
38
|
};
|
|
11
39
|
|
|
12
|
-
|
|
13
|
-
const db =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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;
|
|
18
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);
|
|
19
61
|
};
|
|
20
62
|
});
|
|
21
63
|
}
|
|
22
64
|
|
|
65
|
+
// --- public API -------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
// Inicializar IndexedDB y dejar el handle en PrefConfigurator.db (público)
|
|
68
|
+
export async function initDb(dbName, storeName) {
|
|
69
|
+
const db = await _openEnsuringStore(dbName, storeName);
|
|
70
|
+
// Close any previous handle to avoid versionchange blocking
|
|
71
|
+
try { PC.db?.close?.(); } catch { }
|
|
72
|
+
PC.db = db;
|
|
73
|
+
|
|
74
|
+
// If another tab upgrades, close gracefully so next call can re-init
|
|
75
|
+
db.onversionchange = () => {
|
|
76
|
+
try { db.close(); } catch { }
|
|
77
|
+
if (PC.db === db) PC.db = null;
|
|
78
|
+
console.warn("[PrefConfigurator] DB connection closed due to versionchange. Re-run initDb().");
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
23
82
|
// Guardar modelo
|
|
24
83
|
export async function saveModel(modelDataStr, storeName) {
|
|
25
84
|
return new Promise((resolve, reject) => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
85
|
+
let db;
|
|
86
|
+
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
87
|
+
|
|
88
|
+
let modelData;
|
|
89
|
+
try { modelData = JSON.parse(modelDataStr); }
|
|
90
|
+
catch (e) { reject(new Error("saveModel: modelDataStr is not valid JSON")); return; }
|
|
31
91
|
|
|
32
92
|
const dataToStore = {
|
|
33
93
|
...modelData,
|
|
34
94
|
data: modelData.data,
|
|
35
|
-
size: modelData
|
|
36
|
-
timestamp: new Date().toISOString(),
|
|
95
|
+
size: (modelData?.data?.length ?? 0)
|
|
37
96
|
};
|
|
38
97
|
|
|
39
|
-
const
|
|
40
|
-
const store =
|
|
41
|
-
const
|
|
98
|
+
const tx = db.transaction([storeName], "readwrite");
|
|
99
|
+
const store = tx.objectStore(storeName);
|
|
100
|
+
const req = store.put(dataToStore);
|
|
42
101
|
|
|
43
|
-
|
|
44
|
-
|
|
102
|
+
req.onerror = () => reject(req.error);
|
|
103
|
+
req.onsuccess = () => resolve();
|
|
45
104
|
});
|
|
46
105
|
}
|
|
47
106
|
|
|
48
107
|
// Cargar modelo
|
|
49
108
|
export function loadModel(modelId, storeName) {
|
|
50
109
|
return new Promise((resolve, reject) => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const tx = globalThis.gltfDB.transaction([storeName], "readonly");
|
|
110
|
+
let db;
|
|
111
|
+
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
112
|
+
|
|
113
|
+
const tx = db.transaction([storeName], "readonly");
|
|
56
114
|
const store = tx.objectStore(storeName);
|
|
57
115
|
const req = store.get(modelId);
|
|
116
|
+
|
|
58
117
|
req.onerror = () => reject(req.error);
|
|
59
118
|
req.onsuccess = () => resolve(req.result ?? null);
|
|
60
119
|
});
|
|
61
120
|
}
|
|
62
121
|
|
|
122
|
+
// Descargar archivo desde base64
|
|
63
123
|
export function downloadFileFromBytes(fileName, bytesBase64, mimeType) {
|
|
64
124
|
const link = document.createElement("a");
|
|
65
125
|
link.download = fileName;
|
|
66
126
|
link.href = `data:${mimeType};base64,${bytesBase64}`;
|
|
67
127
|
document.body.appendChild(link);
|
|
68
128
|
link.click();
|
|
69
|
-
|
|
129
|
+
link.remove();
|
|
70
130
|
}
|
|
71
131
|
|
|
72
132
|
// Obtener todos los modelos (solo metadata)
|
|
73
133
|
export async function getAllModels(storeName) {
|
|
74
134
|
return new Promise((resolve, reject) => {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
135
|
+
let db;
|
|
136
|
+
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
79
137
|
|
|
80
|
-
const
|
|
81
|
-
const store =
|
|
82
|
-
const
|
|
138
|
+
const tx = db.transaction([storeName], "readonly");
|
|
139
|
+
const store = tx.objectStore(storeName);
|
|
140
|
+
const req = store.getAll();
|
|
83
141
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const results =
|
|
142
|
+
req.onerror = () => reject(req.error);
|
|
143
|
+
req.onsuccess = () => {
|
|
144
|
+
const items = Array.isArray(req.result) ? req.result : [];
|
|
145
|
+
const results = items.map(item => ({
|
|
88
146
|
id: item.id,
|
|
89
147
|
metadata: item.metadata,
|
|
90
|
-
|
|
91
|
-
size: item.size
|
|
92
|
-
type: item.type,
|
|
148
|
+
timeStamp: item.timeStamp,
|
|
149
|
+
size: item.size
|
|
93
150
|
}));
|
|
151
|
+
// keep old behavior: return JSON string
|
|
94
152
|
resolve(JSON.stringify(results));
|
|
95
153
|
};
|
|
96
154
|
});
|
|
97
155
|
}
|
|
98
156
|
|
|
99
|
-
// Eliminar modelo
|
|
157
|
+
// Eliminar modelo por id
|
|
100
158
|
export async function deleteModel(modelId, storeName) {
|
|
101
159
|
return new Promise((resolve, reject) => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
160
|
+
let db;
|
|
161
|
+
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
106
162
|
|
|
107
|
-
const
|
|
108
|
-
const store =
|
|
109
|
-
const
|
|
163
|
+
const tx = db.transaction([storeName], "readwrite");
|
|
164
|
+
const store = tx.objectStore(storeName);
|
|
165
|
+
const req = store.delete(modelId);
|
|
110
166
|
|
|
111
|
-
|
|
112
|
-
|
|
167
|
+
req.onerror = () => reject(req.error);
|
|
168
|
+
req.onsuccess = () => resolve();
|
|
113
169
|
});
|
|
114
170
|
}
|
|
115
171
|
|
|
116
|
-
// Limpiar toda la
|
|
172
|
+
// Limpiar toda la store
|
|
117
173
|
export async function clearAll(storeName) {
|
|
118
174
|
return new Promise((resolve, reject) => {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
175
|
+
let db;
|
|
176
|
+
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
123
177
|
|
|
124
|
-
const
|
|
125
|
-
const store =
|
|
126
|
-
const
|
|
178
|
+
const tx = db.transaction([storeName], "readwrite");
|
|
179
|
+
const store = tx.objectStore(storeName);
|
|
180
|
+
const req = store.clear();
|
|
127
181
|
|
|
128
|
-
|
|
129
|
-
|
|
182
|
+
req.onerror = () => reject(req.error);
|
|
183
|
+
req.onsuccess = () => resolve();
|
|
130
184
|
});
|
|
131
185
|
}
|
|
132
186
|
|
|
187
|
+
// Borrar modelos expirados usando el índice "expirationTimeStamp"
|
|
188
|
+
export async function cleanExpiredModels(storeName) {
|
|
189
|
+
return new Promise((resolve, reject) => {
|
|
190
|
+
let db;
|
|
191
|
+
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
192
|
+
|
|
193
|
+
const tx = db.transaction([storeName], "readwrite");
|
|
194
|
+
const store = tx.objectStore(storeName);
|
|
195
|
+
|
|
196
|
+
const index = store.index("expirationTimeStamp");
|
|
197
|
+
const now = Date.now();
|
|
198
|
+
const range = IDBKeyRange.upperBound(now);
|
|
199
|
+
const cursorRequest = index.openCursor(range);
|
|
200
|
+
|
|
201
|
+
cursorRequest.onerror = () => reject(cursorRequest.error);
|
|
202
|
+
cursorRequest.onsuccess = (event) => {
|
|
203
|
+
const cursor = event.target.result;
|
|
204
|
+
if (cursor) {
|
|
205
|
+
cursor.delete();
|
|
206
|
+
cursor.continue();
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
tx.oncomplete = () => resolve();
|
|
211
|
+
tx.onerror = () => reject(tx.error);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Utilidades opcionales y visibles (por si las quieres usar en consola)
|
|
216
|
+
export function closeDb() {
|
|
217
|
+
if (PC.db) {
|
|
218
|
+
try { PC.db.close(); } catch { }
|
|
219
|
+
PC.db = null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function deleteDatabase(dbName) {
|
|
224
|
+
return new Promise((resolve, reject) => {
|
|
225
|
+
// Close current handle if points to this DB
|
|
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)
|
|
133
238
|
(function attachPublicAPI(global) {
|
|
134
|
-
const root = (global.PrefConfigurator ??= {});
|
|
135
239
|
const storage = {
|
|
136
240
|
initDb,
|
|
137
241
|
saveModel,
|
|
@@ -139,10 +243,12 @@ export async function clearAll(storeName) {
|
|
|
139
243
|
getAllModels,
|
|
140
244
|
deleteModel,
|
|
141
245
|
clearAll,
|
|
246
|
+
cleanExpiredModels,
|
|
142
247
|
downloadFileFromBytes,
|
|
248
|
+
// extras
|
|
249
|
+
closeDb,
|
|
250
|
+
deleteDatabase,
|
|
143
251
|
};
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
root.version = root.version ?? "1.0.0";
|
|
147
|
-
root.storage = Object.freeze(storage);
|
|
252
|
+
// free to inspect PC.db in devtools
|
|
253
|
+
global.PrefConfigurator.storage = Object.freeze(storage);
|
|
148
254
|
})(globalThis);
|
package/src/index.js
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* </pref-viewer>
|
|
40
40
|
* ```
|
|
41
41
|
*/
|
|
42
|
-
import { Engine, Scene, ArcRotateCamera, Vector3, Color4, HemisphericLight, DirectionalLight, PointLight, ShadowGenerator, LoadAssetContainerAsync, Tools, WebXRSessionManager, WebXRDefaultExperience } from "@babylonjs/core";
|
|
42
|
+
import { Engine, Scene, ArcRotateCamera, Vector3, Color4, HemisphericLight, DirectionalLight, PointLight, ShadowGenerator, LoadAssetContainerAsync, Tools, WebXRSessionManager, WebXRDefaultExperience, MeshBuilder, WebXRFeatureName } from "@babylonjs/core";
|
|
43
43
|
import "@babylonjs/loaders";
|
|
44
44
|
import { USDZExportAsync, GLTF2Export } from "@babylonjs/serializers";
|
|
45
45
|
import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_draco_mesh_compression";
|
|
@@ -47,22 +47,76 @@ import { DracoCompression } from "@babylonjs/core/Meshes/Compression/dracoCompre
|
|
|
47
47
|
import { initDb, loadModel } from "./gltf-storage.js";
|
|
48
48
|
|
|
49
49
|
class PrefViewer extends HTMLElement {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
50
|
+
initialized = false;
|
|
51
|
+
loaded = false;
|
|
52
|
+
loading = false;
|
|
53
|
+
|
|
54
|
+
#data = {
|
|
55
|
+
containers: {
|
|
56
|
+
model: {
|
|
57
|
+
name: "model",
|
|
58
|
+
container: null,
|
|
59
|
+
show: true,
|
|
60
|
+
storage: null,
|
|
61
|
+
visible: false,
|
|
62
|
+
size: null,
|
|
63
|
+
timeStamp: null,
|
|
64
|
+
changed: false,
|
|
65
|
+
},
|
|
66
|
+
environment: {
|
|
67
|
+
name: "environment",
|
|
68
|
+
container: null,
|
|
69
|
+
show: true,
|
|
70
|
+
storage: null,
|
|
71
|
+
visible: false,
|
|
72
|
+
size: null,
|
|
73
|
+
timeStamp: null,
|
|
74
|
+
changed: false,
|
|
75
|
+
},
|
|
76
|
+
materials: {
|
|
77
|
+
name: "materials",
|
|
78
|
+
container: null,
|
|
79
|
+
storage: null,
|
|
80
|
+
show: true,
|
|
81
|
+
visible: false,
|
|
82
|
+
size: null,
|
|
83
|
+
timeStamp: null,
|
|
84
|
+
changed: false,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
options: {
|
|
88
|
+
camera: {
|
|
89
|
+
value: null,
|
|
90
|
+
locked: true,
|
|
91
|
+
changed: false,
|
|
92
|
+
},
|
|
93
|
+
materials: {
|
|
94
|
+
innerWall: {
|
|
95
|
+
value: null,
|
|
96
|
+
prefix: "innerWall",
|
|
97
|
+
changed: false,
|
|
98
|
+
},
|
|
99
|
+
outerWall: {
|
|
100
|
+
value: null,
|
|
101
|
+
prefix: "outerWall",
|
|
102
|
+
changed: false,
|
|
103
|
+
},
|
|
104
|
+
innerFloor: {
|
|
105
|
+
value: null,
|
|
106
|
+
prefix: "innerFloor",
|
|
107
|
+
changed: false,
|
|
108
|
+
},
|
|
109
|
+
outerFloor: {
|
|
110
|
+
value: null,
|
|
111
|
+
prefix: "outerFloor",
|
|
112
|
+
changed: false,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
64
116
|
};
|
|
65
117
|
|
|
118
|
+
// DOM elements
|
|
119
|
+
#wrapper = null;
|
|
66
120
|
#canvas = null;
|
|
67
121
|
|
|
68
122
|
// Babylon.js core objects
|
|
@@ -110,18 +164,18 @@ class PrefViewer extends HTMLElement {
|
|
|
110
164
|
break;
|
|
111
165
|
case "show-model":
|
|
112
166
|
data = value.toLowerCase?.() === "true";
|
|
113
|
-
if (this
|
|
167
|
+
if (this.initialized) {
|
|
114
168
|
data ? this.showModel() : this.hideModel();
|
|
115
169
|
} else {
|
|
116
|
-
this.#model.show = data;
|
|
170
|
+
this.#data.containers.model.show = data;
|
|
117
171
|
}
|
|
118
172
|
break;
|
|
119
173
|
case "show-scene":
|
|
120
174
|
data = value.toLowerCase?.() === "true";
|
|
121
|
-
if (this
|
|
175
|
+
if (this.initialized) {
|
|
122
176
|
data ? this.showScene() : this.hideScene();
|
|
123
177
|
} else {
|
|
124
|
-
this.#environment.show = data;
|
|
178
|
+
this.#data.containers.environment.show = data;
|
|
125
179
|
}
|
|
126
180
|
break;
|
|
127
181
|
}
|
|
@@ -132,18 +186,19 @@ class PrefViewer extends HTMLElement {
|
|
|
132
186
|
const error = 'PrefViewer: provide "models" as array of model and environment';
|
|
133
187
|
console.error(error);
|
|
134
188
|
this.dispatchEvent(
|
|
135
|
-
new CustomEvent("
|
|
136
|
-
detail: { error: new Error(error) },
|
|
189
|
+
new CustomEvent("scene-error", {
|
|
137
190
|
bubbles: true,
|
|
191
|
+
cancelable: false,
|
|
138
192
|
composed: true,
|
|
193
|
+
detail: { error: new Error(error) },
|
|
139
194
|
})
|
|
140
195
|
);
|
|
141
196
|
return false;
|
|
142
197
|
}
|
|
143
198
|
|
|
144
199
|
this.#initializeBabylon();
|
|
145
|
-
this
|
|
146
|
-
this.#
|
|
200
|
+
this.initialized = true;
|
|
201
|
+
this.#loadContainers(true, true, true);
|
|
147
202
|
}
|
|
148
203
|
|
|
149
204
|
disconnectedCallback() {
|
|
@@ -163,36 +218,184 @@ class PrefViewer extends HTMLElement {
|
|
|
163
218
|
}
|
|
164
219
|
|
|
165
220
|
#wrapCanvas() {
|
|
166
|
-
|
|
167
|
-
Object.assign(wrapper.style, {
|
|
221
|
+
this.#wrapper = document.createElement("div");
|
|
222
|
+
Object.assign(this.#wrapper.style, {
|
|
168
223
|
width: "100%",
|
|
169
224
|
height: "100%",
|
|
170
225
|
position: "relative",
|
|
171
226
|
});
|
|
172
|
-
wrapper.appendChild(this.#canvas);
|
|
173
|
-
this.shadowRoot.append(wrapper);
|
|
227
|
+
this.#wrapper.appendChild(this.#canvas);
|
|
228
|
+
this.shadowRoot.append(this.#wrapper);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
#setStatusSceneLoading() {
|
|
232
|
+
this.loaded = false;
|
|
233
|
+
this.loading = true;
|
|
234
|
+
if (this.hasAttribute("loaded")) {
|
|
235
|
+
this.removeAttribute("loaded");
|
|
236
|
+
}
|
|
237
|
+
this.setAttribute("loading", "");
|
|
238
|
+
this.dispatchEvent(
|
|
239
|
+
new CustomEvent("scene-loading", {
|
|
240
|
+
bubbles: true,
|
|
241
|
+
cancelable: false,
|
|
242
|
+
composed: true,
|
|
243
|
+
})
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
#setStatusSceneLoaded() {
|
|
248
|
+
this.loaded = true;
|
|
249
|
+
this.loading = false;
|
|
250
|
+
|
|
251
|
+
const toLoadDetail = {
|
|
252
|
+
container_model: !!this.#data.containers.model.changed,
|
|
253
|
+
container_environment: !!this.#data.containers.environment.changed,
|
|
254
|
+
container_materials: !!this.#data.containers.materials.changed,
|
|
255
|
+
options_camera: !!this.#data.options.camera.changed,
|
|
256
|
+
options_innerWallMaterial: !!this.#data.options.materials.innerWall.changed,
|
|
257
|
+
options_outerWallMaterial: !!this.#data.options.materials.outerWall.changed,
|
|
258
|
+
options_innerFloorMaterial: !!this.#data.options.materials.innerFloor.changed,
|
|
259
|
+
options_outerFloorMaterial: !!this.#data.options.materials.outerFloor.changed,
|
|
260
|
+
};
|
|
261
|
+
const loadedDetail = {
|
|
262
|
+
container_model: !!this.#data.containers.model.changed?.success,
|
|
263
|
+
container_environment: !!this.#data.containers.environment.changed?.success,
|
|
264
|
+
container_materials: !!this.#data.containers.materials.changed?.success,
|
|
265
|
+
options_camera: !!this.#data.options.camera.changed?.success,
|
|
266
|
+
options_innerWallMaterial: !!this.#data.options.materials.innerWall.changed?.success,
|
|
267
|
+
options_outerWallMaterial: !!this.#data.options.materials.outerWall.changed?.success,
|
|
268
|
+
options_innerFloorMaterial: !!this.#data.options.materials.innerFloor.changed?.success,
|
|
269
|
+
options_outerFloorMaterial: !!this.#data.options.materials.outerFloor.changed?.success,
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const detail = {
|
|
273
|
+
tried: toLoadDetail,
|
|
274
|
+
success: loadedDetail,
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
if (this.hasAttribute("loading")) {
|
|
278
|
+
this.removeAttribute("loading");
|
|
279
|
+
}
|
|
280
|
+
this.setAttribute("loaded", "");
|
|
281
|
+
this.dispatchEvent(
|
|
282
|
+
new CustomEvent("scene-loaded", {
|
|
283
|
+
bubbles: true,
|
|
284
|
+
cancelable: false,
|
|
285
|
+
composed: true,
|
|
286
|
+
detail: detail,
|
|
287
|
+
})
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
#setStatusOptionsLoading() {
|
|
292
|
+
this.dispatchEvent(
|
|
293
|
+
new CustomEvent("options-loading", {
|
|
294
|
+
bubbles: true,
|
|
295
|
+
cancelable: false,
|
|
296
|
+
composed: true,
|
|
297
|
+
})
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
#setStatusOptionsLoaded() {
|
|
302
|
+
const toLoadDetail = {
|
|
303
|
+
innerWallMaterial: !!this.#data.options.materials.innerWall.changed,
|
|
304
|
+
outerWallMaterial: !!this.#data.options.materials.outerWall.changed,
|
|
305
|
+
innerFloorMaterial: !!this.#data.options.materials.innerFloor.changed,
|
|
306
|
+
outerFloorMaterial: !!this.#data.options.materials.outerFloor.changed,
|
|
307
|
+
};
|
|
308
|
+
const loadedDetail = {
|
|
309
|
+
innerWallMaterial: !!this.#data.options.materials.innerWall.changed?.success,
|
|
310
|
+
outerWallMaterial: !!this.#data.options.materials.outerWall.changed?.success,
|
|
311
|
+
innerFloorMaterial: !!this.#data.options.materials.innerFloor.changed?.success,
|
|
312
|
+
outerFloorMaterial: !!this.#data.options.materials.outerFloor.changed?.success,
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const detail = {
|
|
316
|
+
tried: toLoadDetail,
|
|
317
|
+
success: loadedDetail,
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
this.dispatchEvent(
|
|
321
|
+
new CustomEvent("options-loaded", {
|
|
322
|
+
bubbles: true,
|
|
323
|
+
cancelable: false,
|
|
324
|
+
composed: true,
|
|
325
|
+
detail: detail,
|
|
326
|
+
})
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Data
|
|
331
|
+
#checkCameraChanged(options) {
|
|
332
|
+
if (!options || !options.camera) {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const changed = options.camera !== this.#data.options.camera.value;
|
|
337
|
+
this.#data.options.camera.changed = changed ? { oldValue: this.#data.options.camera.value, oldLocked: this.#data.options.camera.locked, success: false } : false;
|
|
338
|
+
if (changed) this.#data.options.camera.value = options.camera;
|
|
339
|
+
|
|
340
|
+
return changed;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
#checkMaterialsChanged(options) {
|
|
344
|
+
if (!options) {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
let someChanged = false;
|
|
348
|
+
Object.keys(this.#data.options.materials).forEach((material) => {
|
|
349
|
+
const key = `${material}Material`;
|
|
350
|
+
const materialChanged = options[key] && options[key] !== this.#data.options.materials[material].value ? true : false;
|
|
351
|
+
this.#data.options.materials[material].changed = materialChanged ? { oldValue: this.#data.options.materials[material].value, success: false } : false;
|
|
352
|
+
this.#data.options.materials[material].value = materialChanged ? options[key] : this.#data.options.materials[material].value;
|
|
353
|
+
someChanged = someChanged || this.#data.options.materials[material].changed;
|
|
354
|
+
});
|
|
355
|
+
return someChanged;
|
|
174
356
|
}
|
|
175
|
-
|
|
176
|
-
|
|
357
|
+
|
|
358
|
+
#storeChangedFlagsForContainer(container) {
|
|
359
|
+
container.timeStamp = container.changed.timeStamp;
|
|
360
|
+
container.size = container.changed.size;
|
|
361
|
+
container.changed.success = true;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
#resetChangedFlags() {
|
|
365
|
+
Object.values(this.#data.containers).forEach((container) => (container.changed = false));
|
|
366
|
+
Object.values(this.#data.options.materials).forEach((material) => (material.changed = false));
|
|
367
|
+
this.#data.options.camera.changed = false;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Babylon.js
|
|
177
371
|
async #initializeBabylon() {
|
|
178
372
|
this.#engine = new Engine(this.#canvas, true, { alpha: true });
|
|
373
|
+
this.#engine.disableUniformBuffers = true; // <- evita el límite de GL_MAX_*_UNIFORM_BUFFERS // PROVISIONAL, YA QUE ESTO ES UN POCO OVERKILL
|
|
374
|
+
|
|
179
375
|
this.#scene = new Scene(this.#engine);
|
|
180
376
|
this.#scene.clearColor = new Color4(1, 1, 1, 1);
|
|
181
377
|
this.#createCamera();
|
|
182
378
|
this.#createLights();
|
|
183
379
|
this.#setupInteraction();
|
|
184
|
-
|
|
380
|
+
|
|
185
381
|
this.#engine.runRenderLoop(() => this.#scene && this.#scene.render());
|
|
186
382
|
this.#canvasResizeObserver.observe(this.#canvas);
|
|
187
383
|
|
|
188
384
|
await this.#createXRExperience();
|
|
189
385
|
}
|
|
190
386
|
|
|
387
|
+
addStylesToARButton() {
|
|
388
|
+
const css = '.babylonVRicon { color: #868686; border-color: #868686; border-style: solid; margin-left: 10px; height: 50px; width: 80px; background-color: rgba(51,51,51,0.7); background-image: url(data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%222048%22%20height%3D%221152%22%20viewBox%3D%220%200%202048%201152%22%20version%3D%221.1%22%3E%3Cpath%20transform%3D%22rotate%28180%201024%2C576.0000000000001%29%22%20d%3D%22m1109%2C896q17%2C0%2030%2C-12t13%2C-30t-12.5%2C-30.5t-30.5%2C-12.5l-170%2C0q-18%2C0%20-30.5%2C12.5t-12.5%2C30.5t13%2C30t30%2C12l170%2C0zm-85%2C256q59%2C0%20132.5%2C-1.5t154.5%2C-5.5t164.5%2C-11.5t163%2C-20t150%2C-30t124.5%2C-41.5q23%2C-11%2042%2C-24t38%2C-30q27%2C-25%2041%2C-61.5t14%2C-72.5l0%2C-257q0%2C-123%20-47%2C-232t-128%2C-190t-190%2C-128t-232%2C-47l-81%2C0q-37%2C0%20-68.5%2C14t-60.5%2C34.5t-55.5%2C45t-53%2C45t-53%2C34.5t-55.5%2C14t-55.5%2C-14t-53%2C-34.5t-53%2C-45t-55.5%2C-45t-60.5%2C-34.5t-68.5%2C-14l-81%2C0q-123%2C0%20-232%2C47t-190%2C128t-128%2C190t-47%2C232l0%2C257q0%2C68%2038%2C115t97%2C73q54%2C24%20124.5%2C41.5t150%2C30t163%2C20t164.5%2C11.5t154.5%2C5.5t132.5%2C1.5zm939%2C-298q0%2C39%20-24.5%2C67t-58.5%2C42q-54%2C23%20-122%2C39.5t-143.5%2C28t-155.5%2C19t-157%2C11t-148.5%2C5t-129.5%2C1.5q-59%2C0%20-130%2C-1.5t-148%2C-5t-157%2C-11t-155.5%2C-19t-143.5%2C-28t-122%2C-39.5q-34%2C-14%20-58.5%2C-42t-24.5%2C-67l0%2C-257q0%2C-106%2040.5%2C-199t110%2C-162.5t162.5%2C-109.5t199%2C-40l81%2C0q27%2C0%2052%2C14t50%2C34.5t51%2C44.5t55.5%2C44.5t63.5%2C34.5t74%2C14t74%2C-14t63.5%2C-34.5t55.5%2C-44.5t51%2C-44.5t50%2C-34.5t52%2C-14l14%2C0q37%2C0%2070%2C0.5t64.5%2C4.5t63.5%2C12t68%2C23q71%2C30%20128.5%2C78.5t98.5%2C110t63.5%2C133.5t22.5%2C149l0%2C257z%22%20fill%3D%22white%22%20/%3E%3C/svg%3E%0A); background-size: 80%; background-repeat:no-repeat; background-position: center; border: none; outline: none; transition: transform 0.125s ease-out } .babylonVRicon:hover { transform: scale(1.05) } .babylonVRicon:active {background-color: rgba(51,51,51,1) } .babylonVRicon:focus {background-color: rgba(51,51,51,1) }.babylonVRicon.vrdisplaypresenting { background-image: none;} .vrdisplaypresenting::after { content: "EXIT"} .xr-error::after { content: "ERROR"}';
|
|
389
|
+
const style = document.createElement("style");
|
|
390
|
+
style.appendChild(document.createTextNode(css));
|
|
391
|
+
this.#wrapper.appendChild(style);
|
|
392
|
+
}
|
|
393
|
+
|
|
191
394
|
async #createXRExperience() {
|
|
192
395
|
if (this.#XRExperience) {
|
|
193
396
|
return true;
|
|
194
397
|
}
|
|
195
|
-
|
|
398
|
+
|
|
196
399
|
const sessionMode = "immersive-ar";
|
|
197
400
|
const sessionSupported = await WebXRSessionManager.IsSessionSupportedAsync(sessionMode);
|
|
198
401
|
if (!sessionSupported) {
|
|
@@ -200,16 +403,37 @@ class PrefViewer extends HTMLElement {
|
|
|
200
403
|
return false;
|
|
201
404
|
}
|
|
202
405
|
|
|
203
|
-
const options = {
|
|
204
|
-
uiOptions: {
|
|
205
|
-
sessionMode: sessionMode,
|
|
206
|
-
renderTarget: "xrLayer",
|
|
207
|
-
referenceSpaceType: "local",
|
|
208
|
-
},
|
|
209
|
-
};
|
|
210
|
-
|
|
211
406
|
try {
|
|
212
|
-
|
|
407
|
+
const ground = MeshBuilder.CreateGround("ground", { width: 1000, height: 1000 }, this.#scene);
|
|
408
|
+
ground.isVisible = false;
|
|
409
|
+
|
|
410
|
+
const options = {
|
|
411
|
+
floorMeshes: [ground],
|
|
412
|
+
uiOptions: {
|
|
413
|
+
sessionMode: sessionMode,
|
|
414
|
+
renderTarget: "xrLayer",
|
|
415
|
+
referenceSpaceType: "local",
|
|
416
|
+
},
|
|
417
|
+
optionalFeatures: true,
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
this.#XRExperience = await WebXRDefaultExperience.CreateAsync(this.#scene, options);
|
|
421
|
+
|
|
422
|
+
const featuresManager = this.#XRExperience.baseExperience.featuresManager;
|
|
423
|
+
featuresManager.enableFeature(WebXRFeatureName.TELEPORTATION, "stable", {
|
|
424
|
+
xrInput: this.#XRExperience.input,
|
|
425
|
+
floorMeshes: [ground],
|
|
426
|
+
timeToTeleport: 1500,
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
this.#XRExperience.baseExperience.sessionManager.onXRReady.add(() => {
|
|
430
|
+
// Set the initial position of xrCamera: use nonVRCamera, which contains a copy of the original this.#scene.activeCamera before entering XR
|
|
431
|
+
this.#XRExperience.baseExperience.camera.setTransformationFromNonVRCamera(this.#XRExperience.baseExperience._nonVRCamera);
|
|
432
|
+
this.#XRExperience.baseExperience.camera.setTarget(Vector3.Zero());
|
|
433
|
+
this.#XRExperience.baseExperience.onInitialXRPoseSetObservable.notifyObservers(this.#XRExperience.baseExperience.camera);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
this.addStylesToARButton();
|
|
213
437
|
} catch (error) {
|
|
214
438
|
console.warn("PrefViewer: failed to create WebXR experience", error);
|
|
215
439
|
this.#XRExperience = null;
|
|
@@ -219,12 +443,14 @@ class PrefViewer extends HTMLElement {
|
|
|
219
443
|
#canvasResizeObserver = new ResizeObserver(() => this.#engine && this.#engine.resize());
|
|
220
444
|
|
|
221
445
|
#createCamera() {
|
|
222
|
-
this.#camera = new ArcRotateCamera("camera", 3 * Math.PI / 2, Math.PI * 0.47, 10, Vector3.Zero(), this.#scene);
|
|
446
|
+
this.#camera = new ArcRotateCamera("camera", (3 * Math.PI) / 2, Math.PI * 0.47, 10, Vector3.Zero(), this.#scene);
|
|
223
447
|
this.#camera.upperBetaLimit = Math.PI * 0.48;
|
|
224
448
|
this.#camera.lowerBetaLimit = Math.PI * 0.25;
|
|
225
449
|
this.#camera.lowerRadiusLimit = 5;
|
|
226
450
|
this.#camera.upperRadiusLimit = 20;
|
|
451
|
+
this.#camera.metadata = { locked: false }
|
|
227
452
|
this.#camera.attachControl(this.#canvas, true);
|
|
453
|
+
this.#scene.activeCamera = this.#camera;
|
|
228
454
|
}
|
|
229
455
|
|
|
230
456
|
#createLights() {
|
|
@@ -251,16 +477,22 @@ class PrefViewer extends HTMLElement {
|
|
|
251
477
|
|
|
252
478
|
#setupInteraction() {
|
|
253
479
|
this.#canvas.addEventListener("wheel", (event) => {
|
|
254
|
-
if (!this.#scene || !this.#camera)
|
|
255
|
-
|
|
480
|
+
if (!this.#scene || !this.#camera) {
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
//const pick = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
|
|
256
484
|
//this.#camera.target = pick.hit ? pick.pickedPoint.clone() : this.#camera.target;
|
|
257
|
-
this.#
|
|
485
|
+
if (!this.#scene.activeCamera.metadata?.locked) {
|
|
486
|
+
this.#scene.activeCamera.inertialRadiusOffset -= event.deltaY * this.#scene.activeCamera.wheelPrecision * 0.001;
|
|
487
|
+
}
|
|
258
488
|
event.preventDefault();
|
|
259
489
|
});
|
|
260
490
|
}
|
|
261
491
|
|
|
262
492
|
#disposeEngine() {
|
|
263
493
|
if (!this.#engine) return;
|
|
494
|
+
this.#shadowGen?.dispose();
|
|
495
|
+
this.#scene?.lights?.slice().forEach(l => l.dispose());
|
|
264
496
|
this.#engine.dispose();
|
|
265
497
|
this.#engine = this.#scene = this.#camera = null;
|
|
266
498
|
this.#hemiLight = this.#dirLight = this.#cameraLight = null;
|
|
@@ -268,6 +500,27 @@ class PrefViewer extends HTMLElement {
|
|
|
268
500
|
}
|
|
269
501
|
|
|
270
502
|
// Utility methods for loading gltf/glb
|
|
503
|
+
async #getServerFileDataHeader(uri) {
|
|
504
|
+
return new Promise((resolve) => {
|
|
505
|
+
const xhr = new XMLHttpRequest();
|
|
506
|
+
xhr.open("HEAD", uri, true);
|
|
507
|
+
xhr.responseType = "blob";
|
|
508
|
+
xhr.onload = () => {
|
|
509
|
+
if (xhr.status === 200) {
|
|
510
|
+
const size = parseInt(xhr.getResponseHeader("Content-Length"));
|
|
511
|
+
const timeStamp = new Date(xhr.getResponseHeader("Last-Modified")).toISOString();
|
|
512
|
+
resolve([size, timeStamp]);
|
|
513
|
+
} else {
|
|
514
|
+
resolve([0, null]);
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
xhr.onerror = () => {
|
|
518
|
+
resolve([0, null]);
|
|
519
|
+
};
|
|
520
|
+
xhr.send();
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
271
524
|
#transformUrl(url) {
|
|
272
525
|
return new Promise((resolve) => {
|
|
273
526
|
resolve(url.replace(/^blob:[^/]+\//i, "").replace(/\\/g, "/"));
|
|
@@ -280,21 +533,22 @@ class PrefViewer extends HTMLElement {
|
|
|
280
533
|
let decoded = "";
|
|
281
534
|
let blob = null;
|
|
282
535
|
let extension = null;
|
|
536
|
+
let size = raw.length;
|
|
283
537
|
try {
|
|
284
538
|
decoded = atob(raw);
|
|
285
539
|
} catch {
|
|
286
|
-
return { blob, extension };
|
|
540
|
+
return { blob, extension, size };
|
|
287
541
|
}
|
|
288
542
|
let isJson = false;
|
|
289
543
|
try {
|
|
290
544
|
JSON.parse(decoded);
|
|
291
545
|
isJson = true;
|
|
292
|
-
} catch {}
|
|
546
|
+
} catch { }
|
|
293
547
|
extension = isJson ? ".gltf" : ".glb";
|
|
294
548
|
const type = isJson ? "model/gltf+json" : "model/gltf-binary";
|
|
295
549
|
const array = Uint8Array.from(decoded, (c) => c.charCodeAt(0));
|
|
296
550
|
blob = new Blob([array], { type });
|
|
297
|
-
return { blob, extension };
|
|
551
|
+
return { blob, extension, size };
|
|
298
552
|
}
|
|
299
553
|
|
|
300
554
|
async #initStorage(db, table) {
|
|
@@ -306,41 +560,141 @@ class PrefViewer extends HTMLElement {
|
|
|
306
560
|
|
|
307
561
|
// Methods for managing Asset Containers
|
|
308
562
|
#setVisibilityOfWallAndFloorInModel(show) {
|
|
309
|
-
if (this.#model.
|
|
310
|
-
|
|
311
|
-
const nodes = this.#model.container.getNodes();
|
|
312
|
-
this.#model.container
|
|
313
|
-
.getNodes()
|
|
314
|
-
.filter((filter) => names.includes(filter.name))
|
|
315
|
-
.forEach((node) => node.setEnabled(show !== undefined ? show : this.#environment.show));
|
|
563
|
+
if (!this.#data.containers.model.assetContainer || !this.#data.containers.model.visible) {
|
|
564
|
+
return false;
|
|
316
565
|
}
|
|
566
|
+
show = show !== undefined ? show : this.#data.containers.environment.visible;
|
|
567
|
+
const prefixes = Object.values(this.#data.options.materials).map((material) => material.prefix);
|
|
568
|
+
this.#data.containers.model.assetContainer.meshes.filter((meshToFilter) => prefixes.some((prefix) => meshToFilter.name.startsWith(prefix))).forEach((mesh) => mesh.setEnabled(show));
|
|
317
569
|
}
|
|
318
570
|
|
|
319
|
-
#
|
|
320
|
-
if (
|
|
321
|
-
|
|
322
|
-
|
|
571
|
+
#setOptionsMaterial(optionMaterial) {
|
|
572
|
+
if (!optionMaterial || !optionMaterial.prefix || !optionMaterial.value) {
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const material = this.#data.containers.materials.assetContainer?.materials.find((mat) => mat.name === optionMaterial.value) || null;
|
|
577
|
+
if (!material) {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const containers = [];
|
|
582
|
+
if (this.#data.containers.model.assetContainer && (this.#data.containers.model.changed || this.#data.containers.materials.changed || optionMaterial.changed)) {
|
|
583
|
+
containers.push(this.#data.containers.model.assetContainer);
|
|
584
|
+
}
|
|
585
|
+
if (this.#data.containers.environment.assetContainer && (this.#data.containers.environment.changed || this.#data.containers.materials.changed || optionMaterial.changed)) {
|
|
586
|
+
containers.push(this.#data.containers.environment.assetContainer);
|
|
587
|
+
}
|
|
588
|
+
if (containers.length === 0) {
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
let someSetted = false;
|
|
593
|
+
containers.forEach((container) =>
|
|
594
|
+
container.meshes
|
|
595
|
+
.filter((meshToFilter) => meshToFilter.name.startsWith(optionMaterial.prefix))
|
|
596
|
+
.forEach((mesh) => {
|
|
597
|
+
mesh.material = material;
|
|
598
|
+
someSetted = true;
|
|
599
|
+
})
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
if (someSetted) {
|
|
603
|
+
optionMaterial.changed.success = true;
|
|
604
|
+
} else {
|
|
605
|
+
optionMaterial.value = optionMaterial.changed.oldValue;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return someSetted;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
#setOptionsMaterials() {
|
|
612
|
+
let someSetted = false;
|
|
613
|
+
Object.values(this.#data.options.materials).forEach((material) => {
|
|
614
|
+
let settedMaterial = this.#setOptionsMaterial(material);
|
|
615
|
+
someSetted = someSetted || settedMaterial;
|
|
616
|
+
});
|
|
617
|
+
return someSetted;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
#setOptionsCamera() {
|
|
621
|
+
if (!this.#data.options.camera.value || (!this.#data.options.camera.changed && !this.#data.containers.model.changed && !this.#data.containers.environment.changed)) {
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
let camera = this.#data.containers.model.assetContainer?.cameras.find((thisCamera) => thisCamera.name === this.#data.options.camera.value) || this.#data.containers.environment.assetContainer?.cameras.find((thisCamera) => thisCamera.name === this.#data.options.camera.value) || null;
|
|
626
|
+
if (!camera) {
|
|
627
|
+
if (this.#data.options.camera.changed?.oldValue && this.#data.options.camera.changed?.oldValue !== this.#data.options.camera.value) {
|
|
628
|
+
camera = this.#data.containers.model.assetContainer?.cameras.find((thisCamera) => thisCamera.name === this.#data.options.camera.changed.oldValue) || this.#data.containers.environment.assetContainer?.cameras.find((thisCamera) => thisCamera.name === this.#data.options.camera.changed.oldValue) || null;
|
|
629
|
+
}
|
|
630
|
+
if (camera) {
|
|
631
|
+
camera.metadata = { locked: this.#data.options.camera.changed.oldLocked };
|
|
632
|
+
this.#data.options.camera.value = this.#data.options.camera.changed.oldValue;
|
|
633
|
+
this.#data.options.camera.locked = this.#data.options.camera.changed.oldLocked;
|
|
634
|
+
} else {
|
|
635
|
+
camera = this.#camera;
|
|
636
|
+
this.#data.options.camera.value = null;
|
|
637
|
+
this.#data.options.camera.locked = this.#camera.metadata.locked;
|
|
638
|
+
}
|
|
639
|
+
this.#data.options.camera.changed.success = false;
|
|
640
|
+
} else {
|
|
641
|
+
camera.metadata = { locked: this.#data.options.camera.locked };
|
|
323
642
|
}
|
|
643
|
+
if (!this.#data.options.camera.locked && this.#data.options.camera.value !== null) {
|
|
644
|
+
camera.attachControl(this.#canvas, true);
|
|
645
|
+
}
|
|
646
|
+
this.#scene.activeCamera = camera;
|
|
647
|
+
return true;
|
|
324
648
|
}
|
|
325
649
|
|
|
326
|
-
#
|
|
327
|
-
if (
|
|
328
|
-
|
|
329
|
-
|
|
650
|
+
#addContainer(container) {
|
|
651
|
+
if (container.assetContainer && !container.visible && container.show) {
|
|
652
|
+
container.assetContainer.addAllToScene();
|
|
653
|
+
container.visible = true;
|
|
330
654
|
}
|
|
331
655
|
}
|
|
332
656
|
|
|
333
|
-
#
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
657
|
+
#removeContainer(container) {
|
|
658
|
+
if (container.assetContainer && container.visible) {
|
|
659
|
+
container.assetContainer.removeAllFromScene();
|
|
660
|
+
container.visible = false;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
#replaceContainer(container, newAssetContainer) {
|
|
665
|
+
// 1) quita y destruye el anterior si existía
|
|
666
|
+
const old = container.assetContainer;
|
|
667
|
+
if (old) {
|
|
668
|
+
if (container.visible) { old.removeAllFromScene(); }
|
|
669
|
+
old.dispose(); // <- importante
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// 2) asigna el nuevo y prepara
|
|
673
|
+
container.assetContainer = newAssetContainer;
|
|
674
|
+
|
|
675
|
+
// Opcional: limitar luces por material para ganar margen
|
|
676
|
+
container.assetContainer.materials?.forEach(m => {
|
|
677
|
+
if ("maxSimultaneousLights" in m) {
|
|
678
|
+
m.maxSimultaneousLights = 2; // 2–3 suele ir bien
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
// 3) sombras solo para los meshes que te interesen (mejor que todos)
|
|
683
|
+
container.assetContainer.meshes.forEach(mesh => {
|
|
337
684
|
mesh.receiveShadows = true;
|
|
338
685
|
this.#shadowGen.addShadowCaster(mesh, true);
|
|
339
686
|
});
|
|
340
|
-
|
|
687
|
+
|
|
688
|
+
// 4) añade a escena
|
|
689
|
+
this.#addContainer(container);
|
|
690
|
+
|
|
691
|
+
// 5) fuerza recompilación con defines correctos del nuevo estado
|
|
692
|
+
this.#scene.getEngine().releaseEffects();
|
|
341
693
|
}
|
|
342
694
|
|
|
343
|
-
async #loadAssetContainer(
|
|
695
|
+
async #loadAssetContainer(container) {
|
|
696
|
+
let storage = container?.storage;
|
|
697
|
+
|
|
344
698
|
if (!storage) {
|
|
345
699
|
return false;
|
|
346
700
|
}
|
|
@@ -351,6 +705,11 @@ class PrefViewer extends HTMLElement {
|
|
|
351
705
|
await this.#initStorage(storage.db, storage.table);
|
|
352
706
|
const object = await loadModel(storage.id, storage.table);
|
|
353
707
|
source = object.data;
|
|
708
|
+
if (object.timeStamp === container.timeStamp) {
|
|
709
|
+
return false;
|
|
710
|
+
} else {
|
|
711
|
+
container.changed = { timeStamp: object.timeStamp, size: object.size, success: false };
|
|
712
|
+
}
|
|
354
713
|
}
|
|
355
714
|
|
|
356
715
|
if (!source) {
|
|
@@ -359,20 +718,34 @@ class PrefViewer extends HTMLElement {
|
|
|
359
718
|
|
|
360
719
|
let file = null;
|
|
361
720
|
|
|
362
|
-
let { blob, extension } = this.#decodeBase64(source);
|
|
721
|
+
let { blob, extension, size } = this.#decodeBase64(source);
|
|
363
722
|
if (blob && extension) {
|
|
364
|
-
file = new File([blob],
|
|
723
|
+
file = new File([blob], `${container.name}${extension}`, {
|
|
365
724
|
type: blob.type,
|
|
366
725
|
});
|
|
726
|
+
if (!container.changed) {
|
|
727
|
+
if (container.timeStamp === null && container.size === size) {
|
|
728
|
+
return false;
|
|
729
|
+
} else {
|
|
730
|
+
container.changed = { timeStamp: null, size: size, success: false };
|
|
731
|
+
}
|
|
732
|
+
}
|
|
367
733
|
} else {
|
|
368
734
|
const extMatch = source.match(/\.(gltf|glb)(\?|#|$)/i);
|
|
369
735
|
extension = extMatch ? `.${extMatch[1].toLowerCase()}` : ".gltf";
|
|
736
|
+
const [fileSize, fileTimeStamp] = await this.#getServerFileDataHeader(source);
|
|
737
|
+
if (container.size === fileSize && container.timeStamp === fileTimeStamp) {
|
|
738
|
+
return false;
|
|
739
|
+
} else {
|
|
740
|
+
container.changed = { timeStamp: fileTimeStamp, size: fileSize, success: false };
|
|
741
|
+
}
|
|
370
742
|
}
|
|
371
743
|
|
|
372
744
|
let options = {
|
|
373
745
|
pluginExtension: extension,
|
|
374
746
|
pluginOptions: {
|
|
375
747
|
gltf: {
|
|
748
|
+
loadAllMaterials: true,
|
|
376
749
|
preprocessUrlAsync: this.#transformUrl,
|
|
377
750
|
},
|
|
378
751
|
},
|
|
@@ -381,58 +754,112 @@ class PrefViewer extends HTMLElement {
|
|
|
381
754
|
return LoadAssetContainerAsync(file || source, this.#scene, options);
|
|
382
755
|
}
|
|
383
756
|
|
|
384
|
-
async #loadContainers(loadModel = true, loadEnvironment = true) {
|
|
757
|
+
async #loadContainers(loadModel = true, loadEnvironment = true, loadMaterials = true) {
|
|
385
758
|
const promiseArray = [];
|
|
759
|
+
promiseArray.push(loadModel ? this.#loadAssetContainer(this.#data.containers.model) : false);
|
|
760
|
+
promiseArray.push(loadEnvironment ? this.#loadAssetContainer(this.#data.containers.environment) : false);
|
|
761
|
+
promiseArray.push(loadMaterials ? this.#loadAssetContainer(this.#data.containers.materials) : false);
|
|
386
762
|
|
|
387
|
-
|
|
388
|
-
promiseArray.push(loadEnvironment ? this.#loadAssetContainer(this.#environment.storage) : false);
|
|
763
|
+
this.#setStatusSceneLoading();
|
|
389
764
|
|
|
390
765
|
Promise.allSettled(promiseArray)
|
|
391
766
|
.then(async (values) => {
|
|
392
767
|
const modelContainer = values[0];
|
|
393
768
|
const environmentContainer = values[1];
|
|
769
|
+
const materialsContainer = values[2];
|
|
394
770
|
|
|
395
771
|
if (modelContainer.status === "fulfilled" && modelContainer.value) {
|
|
396
|
-
this.#
|
|
772
|
+
this.#stripImportedLights(modelContainer.value);
|
|
773
|
+
this.#replaceContainer(this.#data.containers.model, modelContainer.value);
|
|
774
|
+
this.#storeChangedFlagsForContainer(this.#data.containers.model);
|
|
775
|
+
} else {
|
|
776
|
+
this.#data.containers.model.show ? this.#addContainer(this.#data.containers.model) : this.#removeContainer(this.#data.containers.model);
|
|
397
777
|
}
|
|
398
778
|
|
|
399
779
|
if (environmentContainer.status === "fulfilled" && environmentContainer.value) {
|
|
400
|
-
this.#
|
|
780
|
+
this.#stripImportedLights(environmentContainer.value);
|
|
781
|
+
this.#replaceContainer(this.#data.containers.environment, environmentContainer.value);
|
|
782
|
+
this.#storeChangedFlagsForContainer(this.#data.containers.environment);
|
|
783
|
+
} else {
|
|
784
|
+
this.#data.containers.environment.show ? this.#addContainer(this.#data.containers.environment) : this.#removeContainer(this.#data.containers.environment);
|
|
401
785
|
}
|
|
402
786
|
|
|
403
|
-
|
|
787
|
+
if (materialsContainer.status === "fulfilled" && materialsContainer.value) {
|
|
788
|
+
this.#stripImportedLights(materialsContainer.value);
|
|
789
|
+
this.#replaceContainer(this.#data.containers.materials, materialsContainer.value);
|
|
790
|
+
this.#storeChangedFlagsForContainer(this.#data.containers.materials);
|
|
791
|
+
}
|
|
404
792
|
|
|
405
|
-
this
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
})
|
|
411
|
-
);
|
|
793
|
+
this.#setOptionsMaterials();
|
|
794
|
+
this.#setOptionsCamera();
|
|
795
|
+
this.#setVisibilityOfWallAndFloorInModel();
|
|
796
|
+
this.#setStatusSceneLoaded();
|
|
797
|
+
this.#resetChangedFlags();
|
|
412
798
|
})
|
|
413
799
|
.catch((error) => {
|
|
800
|
+
this.loaded = true;
|
|
414
801
|
console.error("PrefViewer: failed to load model", error);
|
|
415
802
|
this.dispatchEvent(
|
|
416
|
-
new CustomEvent("
|
|
417
|
-
detail: { error: error },
|
|
803
|
+
new CustomEvent("scene-error", {
|
|
418
804
|
bubbles: true,
|
|
805
|
+
cancelable: false,
|
|
419
806
|
composed: true,
|
|
807
|
+
detail: { error: error },
|
|
420
808
|
})
|
|
421
809
|
);
|
|
422
810
|
});
|
|
423
811
|
}
|
|
424
812
|
|
|
813
|
+
#stripImportedLights(container) {
|
|
814
|
+
// El glTF puede traer KHR_lights_punctual: bórralas antes de añadir a la escena
|
|
815
|
+
if (container?.lights?.length) {
|
|
816
|
+
// Clonar para no mutar mientras iteras
|
|
817
|
+
container.lights.slice().forEach(l => l.dispose());
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
425
821
|
// Public Methods
|
|
426
822
|
loadConfig(config) {
|
|
427
823
|
config = typeof config === "string" ? JSON.parse(config) : config;
|
|
428
824
|
if (!config) {
|
|
429
825
|
return false;
|
|
430
826
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
this.#
|
|
434
|
-
this.#
|
|
435
|
-
this.#
|
|
827
|
+
|
|
828
|
+
// Containers
|
|
829
|
+
this.#data.containers.model.storage = config.model?.storage || null;
|
|
830
|
+
this.#data.containers.model.show = config.model?.visible !== undefined ? config.model.visible : this.#data.containers.model.show;
|
|
831
|
+
this.#data.containers.environment.storage = config.scene?.storage || null;
|
|
832
|
+
this.#data.containers.environment.show = config.scene?.visible !== undefined ? config.scene.visible : this.#data.containers.environment.show;
|
|
833
|
+
this.#data.containers.materials.storage = config.materials?.storage || null;
|
|
834
|
+
|
|
835
|
+
// Options
|
|
836
|
+
if (config.options) {
|
|
837
|
+
this.#checkCameraChanged(config.options);
|
|
838
|
+
this.#checkMaterialsChanged(config.options);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
this.initialized && this.#loadContainers(true, true, true);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
setOptions(options) {
|
|
845
|
+
if (!options) {
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
this.#setStatusOptionsLoading();
|
|
850
|
+
|
|
851
|
+
let someSetted = false;
|
|
852
|
+
if (this.#checkCameraChanged(options)) {
|
|
853
|
+
someSetted = someSetted || this.#setOptionsCamera();
|
|
854
|
+
}
|
|
855
|
+
if (this.#checkMaterialsChanged(options)) {
|
|
856
|
+
someSetted = someSetted || this.#setOptionsMaterials();
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
this.#setStatusOptionsLoaded();
|
|
860
|
+
this.#resetChangedFlags();
|
|
861
|
+
|
|
862
|
+
return someSetted;
|
|
436
863
|
}
|
|
437
864
|
|
|
438
865
|
loadModel(model) {
|
|
@@ -440,9 +867,9 @@ class PrefViewer extends HTMLElement {
|
|
|
440
867
|
if (!model) {
|
|
441
868
|
return false;
|
|
442
869
|
}
|
|
443
|
-
this.#model.storage = model.storage || null;
|
|
444
|
-
this.#model.show = model.visible !== undefined ? model.visible : this.#model.show;
|
|
445
|
-
this
|
|
870
|
+
this.#data.containers.model.storage = model.storage || null;
|
|
871
|
+
this.#data.containers.model.show = model.visible !== undefined ? model.visible : this.#data.containers.model.show;
|
|
872
|
+
this.initialized && this.#loadContainers(true, false, false);
|
|
446
873
|
}
|
|
447
874
|
|
|
448
875
|
loadScene(scene) {
|
|
@@ -450,43 +877,41 @@ class PrefViewer extends HTMLElement {
|
|
|
450
877
|
if (!scene) {
|
|
451
878
|
return false;
|
|
452
879
|
}
|
|
453
|
-
this.#environment.storage = scene.storage || null;
|
|
454
|
-
this.#environment.show = scene.visible !== undefined ? scene.visible : this.#environment.show;
|
|
455
|
-
this
|
|
880
|
+
this.#data.containers.environment.storage = scene.storage || null;
|
|
881
|
+
this.#data.containers.environment.show = scene.visible !== undefined ? scene.visible : this.#data.containers.environment.show;
|
|
882
|
+
this.initialized && this.#loadContainers(false, true, false);
|
|
456
883
|
}
|
|
457
884
|
|
|
458
885
|
showModel() {
|
|
459
|
-
this.#model.show = true;
|
|
460
|
-
this.#addContainer(this.#model);
|
|
886
|
+
this.#data.containers.model.show = true;
|
|
887
|
+
this.#addContainer(this.#data.containers.model);
|
|
461
888
|
}
|
|
462
889
|
|
|
463
890
|
hideModel() {
|
|
464
|
-
this.#model.show = false;
|
|
465
|
-
this.#removeContainer(this.#model);
|
|
891
|
+
this.#data.containers.model.show = false;
|
|
892
|
+
this.#removeContainer(this.#data.containers.model);
|
|
466
893
|
}
|
|
467
894
|
|
|
468
895
|
showScene() {
|
|
469
|
-
this.#environment.show = true;
|
|
470
|
-
this.#addContainer(this.#environment);
|
|
896
|
+
this.#data.containers.environment.show = true;
|
|
897
|
+
this.#addContainer(this.#data.containers.environment);
|
|
471
898
|
this.#setVisibilityOfWallAndFloorInModel();
|
|
472
899
|
}
|
|
473
900
|
|
|
474
901
|
hideScene() {
|
|
475
|
-
this.#environment.show = false;
|
|
476
|
-
this.#removeContainer(this.#environment);
|
|
902
|
+
this.#data.containers.environment.show = false;
|
|
903
|
+
this.#removeContainer(this.#data.containers.environment);
|
|
477
904
|
this.#setVisibilityOfWallAndFloorInModel();
|
|
478
905
|
}
|
|
479
906
|
|
|
480
907
|
downloadModelGLB() {
|
|
481
908
|
const fileName = "model";
|
|
482
|
-
GLTF2Export.GLBAsync(this.#model.
|
|
483
|
-
glb.downloadFiles();
|
|
484
|
-
});
|
|
909
|
+
GLTF2Export.GLBAsync(this.#data.containers.model.assetContainer, fileName, { exportWithoutWaitingForScene: true }).then((glb) => glb.downloadFiles());
|
|
485
910
|
}
|
|
486
911
|
|
|
487
912
|
downloadModelUSDZ() {
|
|
488
913
|
const fileName = "model";
|
|
489
|
-
USDZExportAsync(this.#model.
|
|
914
|
+
USDZExportAsync(this.#data.containers.model.assetContainer).then((response) => {
|
|
490
915
|
if (response) {
|
|
491
916
|
Tools.Download(new Blob([response], { type: "model/vnd.usdz+zip" }), `${fileName}.usdz`);
|
|
492
917
|
}
|
|
@@ -504,9 +929,7 @@ class PrefViewer extends HTMLElement {
|
|
|
504
929
|
|
|
505
930
|
downloadModelAndSceneGLB() {
|
|
506
931
|
const fileName = "scene";
|
|
507
|
-
GLTF2Export.GLBAsync(this.#scene, fileName, { exportWithoutWaitingForScene: true }).then((glb) =>
|
|
508
|
-
glb.downloadFiles();
|
|
509
|
-
});
|
|
932
|
+
GLTF2Export.GLBAsync(this.#scene, fileName, { exportWithoutWaitingForScene: true }).then((glb) => glb.downloadFiles());
|
|
510
933
|
}
|
|
511
934
|
}
|
|
512
935
|
|