@preference-sl/pref-viewer 2.5.1 → 2.5.3
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/index.js +102 -53
- package/src/models/window.gltf +0 -21424
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -8,36 +8,23 @@
|
|
|
8
8
|
* `PrefViewer` is a self-contained Web Component built with Babylon.js that:
|
|
9
9
|
* • Inserts a <canvas> into its shadow DOM to render a glTF model.
|
|
10
10
|
* • Creates and manages a Babylon Engine, Scene, ArcRotateCamera, and basic lighting.
|
|
11
|
-
* • Listens for
|
|
11
|
+
* • Listens for `model` (URL) or `model-data` (Base64) attributes to load different glTF files.
|
|
12
12
|
* • Automatically disposes previous meshes when switching models.
|
|
13
13
|
* • Dispatches “model-loaded” and “model-error” CustomEvents so host pages can react.
|
|
14
14
|
*
|
|
15
15
|
* Usage
|
|
16
16
|
* -----
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* <pref-viewer
|
|
18
|
+
* model="https://example.com/models/myModel.gltf"
|
|
19
|
+
* style="width:800px; height:600px;">
|
|
20
|
+
* </pref-viewer>
|
|
19
21
|
*
|
|
20
|
-
*
|
|
21
|
-
* <pref-viewer
|
|
22
|
-
* model="https://example.com/models/myModel.gltf"
|
|
23
|
-
* style="width:800px; height:600px;">
|
|
24
|
-
* </pref-viewer>
|
|
22
|
+
* or:
|
|
25
23
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* });
|
|
31
|
-
* viewer.addEventListener("model-error", (evt) => {
|
|
32
|
-
* xx
|
|
33
|
-
* });
|
|
34
|
-
*
|
|
35
|
-
* 4. **Change models at runtime**
|
|
36
|
-
* viewer.setAttribute("model", "https://example.com/models/anotherModel.glb");
|
|
37
|
-
*
|
|
38
|
-
* -----------------------------------------------------------------------------
|
|
39
|
-
* Implementation code below with updated preprocessUrl
|
|
40
|
-
* -----------------------------------------------------------------------------
|
|
24
|
+
* <pref-viewer
|
|
25
|
+
* model-data="data:application/gltf+json;base64,UEsDB..."
|
|
26
|
+
* style="width:800px; height:600px;">
|
|
27
|
+
* </pref-viewer>
|
|
41
28
|
*/
|
|
42
29
|
import {
|
|
43
30
|
Engine,
|
|
@@ -66,22 +53,27 @@ class PrefViewer extends HTMLElement {
|
|
|
66
53
|
this.dirLight = null;
|
|
67
54
|
this._onWindowResize = null;
|
|
68
55
|
|
|
69
|
-
//
|
|
56
|
+
// Model data sources
|
|
70
57
|
this.modelUrl = null;
|
|
58
|
+
this.modelBase64 = null;
|
|
59
|
+
|
|
71
60
|
this._hasInitialized = false;
|
|
72
61
|
this._pluginHookSetup = false;
|
|
73
62
|
}
|
|
74
63
|
|
|
75
64
|
static get observedAttributes() {
|
|
76
|
-
return ["model"];
|
|
65
|
+
return ["model", "model-data"];
|
|
77
66
|
}
|
|
78
67
|
|
|
79
68
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
80
69
|
if (name === "model") {
|
|
81
70
|
this.modelUrl = newValue;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
71
|
+
this.modelBase64 = null;
|
|
72
|
+
if (this._hasInitialized) this._reloadModel();
|
|
73
|
+
} else if (name === "model-data") {
|
|
74
|
+
this.modelBase64 = newValue;
|
|
75
|
+
this.modelUrl = null;
|
|
76
|
+
if (this._hasInitialized) this._reloadModel();
|
|
85
77
|
}
|
|
86
78
|
}
|
|
87
79
|
|
|
@@ -92,9 +84,9 @@ class PrefViewer extends HTMLElement {
|
|
|
92
84
|
this._pluginHookSetup = true;
|
|
93
85
|
}
|
|
94
86
|
|
|
95
|
-
// Require a model
|
|
96
|
-
if (!this.hasAttribute("model")) {
|
|
97
|
-
const errorMsg = 'PrefViewer: No "model" attribute provided.';
|
|
87
|
+
// Require either a model URL or Base64 data
|
|
88
|
+
if (!this.hasAttribute("model") && !this.hasAttribute("model-data")) {
|
|
89
|
+
const errorMsg = 'PrefViewer: No "model" or "model-data" attribute provided.';
|
|
98
90
|
console.error(errorMsg);
|
|
99
91
|
this.dispatchEvent(
|
|
100
92
|
new CustomEvent("model-error", {
|
|
@@ -106,8 +98,12 @@ class PrefViewer extends HTMLElement {
|
|
|
106
98
|
return;
|
|
107
99
|
}
|
|
108
100
|
|
|
109
|
-
|
|
110
|
-
|
|
101
|
+
if (this.hasAttribute("model")) {
|
|
102
|
+
this.modelUrl = this.getAttribute("model");
|
|
103
|
+
}
|
|
104
|
+
if (this.hasAttribute("model-data")) {
|
|
105
|
+
this.modelBase64 = this.getAttribute("model-data");
|
|
106
|
+
}
|
|
111
107
|
|
|
112
108
|
// Initialize Babylon (engine + scene + camera + lights + hooks)
|
|
113
109
|
this._initializeBabylon();
|
|
@@ -177,15 +173,11 @@ class PrefViewer extends HTMLElement {
|
|
|
177
173
|
this._setupEventListeners();
|
|
178
174
|
|
|
179
175
|
this.engine.runRenderLoop(() => {
|
|
180
|
-
if (this.scene)
|
|
181
|
-
this.scene.render();
|
|
182
|
-
}
|
|
176
|
+
if (this.scene) this.scene.render();
|
|
183
177
|
});
|
|
184
178
|
|
|
185
179
|
this._onWindowResize = () => {
|
|
186
|
-
if (this.engine)
|
|
187
|
-
this.engine.resize();
|
|
188
|
-
}
|
|
180
|
+
if (this.engine) this.engine.resize();
|
|
189
181
|
};
|
|
190
182
|
window.addEventListener("resize", this._onWindowResize);
|
|
191
183
|
}
|
|
@@ -238,27 +230,45 @@ class PrefViewer extends HTMLElement {
|
|
|
238
230
|
|
|
239
231
|
// ====== Model Management ======
|
|
240
232
|
async _reloadModel() {
|
|
241
|
-
if (!this.scene || !this.modelUrl) {
|
|
242
|
-
console.warn("PrefViewer: _reloadModel aborted (scene or
|
|
233
|
+
if (!this.scene || (!this.modelUrl && !this.modelBase64)) {
|
|
234
|
+
console.warn("PrefViewer: _reloadModel aborted (no scene or no model)");
|
|
243
235
|
return;
|
|
244
236
|
}
|
|
245
237
|
|
|
246
238
|
this._disposePreviousMeshes();
|
|
247
239
|
|
|
248
240
|
try {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
this.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
241
|
+
let result;
|
|
242
|
+
if (this.modelBase64) {
|
|
243
|
+
const blob = this._createBlobFromBase64(this.modelBase64);
|
|
244
|
+
const ext = this._getExtensionFromMimeType(blob.type);
|
|
245
|
+
result = await SceneLoader.ImportMeshAsync(
|
|
246
|
+
null,
|
|
247
|
+
"",
|
|
248
|
+
blob,
|
|
249
|
+
this.scene,
|
|
250
|
+
undefined,
|
|
251
|
+
ext
|
|
252
|
+
);
|
|
253
|
+
} else {
|
|
254
|
+
const ext = this._getExtensionFromUrl(this.modelUrl);
|
|
255
|
+
result = await SceneLoader.ImportMeshAsync(
|
|
256
|
+
null,
|
|
257
|
+
"",
|
|
258
|
+
this.modelUrl,
|
|
259
|
+
this.scene,
|
|
260
|
+
undefined,
|
|
261
|
+
ext
|
|
262
|
+
);
|
|
263
|
+
}
|
|
257
264
|
|
|
258
265
|
this.scene.createDefaultCameraOrLight(true, true, true);
|
|
259
266
|
this.dispatchEvent(
|
|
260
267
|
new CustomEvent("model-loaded", {
|
|
261
|
-
detail: {
|
|
268
|
+
detail: {
|
|
269
|
+
meshes: result.meshes,
|
|
270
|
+
particleSystems: result.particleSystems
|
|
271
|
+
},
|
|
262
272
|
bubbles: true,
|
|
263
273
|
composed: true
|
|
264
274
|
})
|
|
@@ -278,12 +288,51 @@ class PrefViewer extends HTMLElement {
|
|
|
278
288
|
_disposePreviousMeshes() {
|
|
279
289
|
if (!this.scene) return;
|
|
280
290
|
this.scene.meshes.slice().forEach((mesh) => {
|
|
281
|
-
if (mesh.getClassName() === "Mesh")
|
|
282
|
-
mesh.dispose();
|
|
283
|
-
}
|
|
291
|
+
if (mesh.getClassName() === "Mesh") mesh.dispose();
|
|
284
292
|
});
|
|
285
293
|
}
|
|
286
294
|
|
|
295
|
+
_createBlobFromBase64(base64) {
|
|
296
|
+
console.log('[PrefViewer] _createBlobFromBase64 called');
|
|
297
|
+
const [prefix, data] = base64.split(',');
|
|
298
|
+
console.log('[PrefViewer] prefix:', prefix);
|
|
299
|
+
let mimeType = 'application/octet-stream';
|
|
300
|
+
if (prefix && prefix.startsWith('data:')) {
|
|
301
|
+
const end = prefix.indexOf(';');
|
|
302
|
+
mimeType = prefix.substring(5, end >= 0 ? end : prefix.length) || mimeType;
|
|
303
|
+
}
|
|
304
|
+
console.log('[PrefViewer] inferred mimeType:', mimeType);
|
|
305
|
+
const raw = data ?? base64;
|
|
306
|
+
try {
|
|
307
|
+
const binary = atob(raw);
|
|
308
|
+
const array = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
309
|
+
const blob = new Blob([array], { type: mimeType });
|
|
310
|
+
console.log('[PrefViewer] Blob created:', blob);
|
|
311
|
+
return blob;
|
|
312
|
+
} catch (err) {
|
|
313
|
+
console.error('[PrefViewer] Failed to decode Base64 or create Blob:', err);
|
|
314
|
+
this.dispatchEvent(
|
|
315
|
+
new CustomEvent('model-error', {
|
|
316
|
+
detail: { error: err },
|
|
317
|
+
bubbles: true,
|
|
318
|
+
composed: true
|
|
319
|
+
})
|
|
320
|
+
);
|
|
321
|
+
throw err;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
_getExtensionFromMimeType(mimeType) {
|
|
326
|
+
if (mimeType.includes("json")) return ".gltf";
|
|
327
|
+
if (mimeType.includes("glb") || mimeType === "application/octet-stream") return ".glb";
|
|
328
|
+
return ".gltf";
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
_getExtensionFromUrl(url) {
|
|
332
|
+
const match = url.match(/\.(gltf|glb)(\?|#|$)/i);
|
|
333
|
+
return match ? `.${match[1].toLowerCase()}` : ".gltf";
|
|
334
|
+
}
|
|
335
|
+
|
|
287
336
|
// ====== Cleanup ======
|
|
288
337
|
_disposeEngine() {
|
|
289
338
|
if (this.engine) {
|