@preference-sl/pref-viewer 2.5.2 → 2.5.4
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 +107 -85
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();
|
|
@@ -132,10 +128,7 @@ class PrefViewer extends HTMLElement {
|
|
|
132
128
|
"$1"
|
|
133
129
|
);
|
|
134
130
|
const fixedSlashes = stripped.replace(/\\/g, "/");
|
|
135
|
-
|
|
136
|
-
return fixedSlashes;
|
|
137
|
-
}
|
|
138
|
-
return fixedSlashes;
|
|
131
|
+
return /^https?:\/\//i.test(fixedSlashes) ? fixedSlashes : fixedSlashes;
|
|
139
132
|
};
|
|
140
133
|
|
|
141
134
|
SceneLoader.OnPluginActivatedObservable.add((plugin) => {
|
|
@@ -176,17 +169,8 @@ class PrefViewer extends HTMLElement {
|
|
|
176
169
|
this._createLights();
|
|
177
170
|
this._setupEventListeners();
|
|
178
171
|
|
|
179
|
-
this.engine.runRenderLoop(() =>
|
|
180
|
-
|
|
181
|
-
this.scene.render();
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
this._onWindowResize = () => {
|
|
186
|
-
if (this.engine) {
|
|
187
|
-
this.engine.resize();
|
|
188
|
-
}
|
|
189
|
-
};
|
|
172
|
+
this.engine.runRenderLoop(() => this.scene && this.scene.render());
|
|
173
|
+
this._onWindowResize = () => this.engine && this.engine.resize();
|
|
190
174
|
window.addEventListener("resize", this._onWindowResize);
|
|
191
175
|
}
|
|
192
176
|
|
|
@@ -209,7 +193,6 @@ class PrefViewer extends HTMLElement {
|
|
|
209
193
|
this.scene
|
|
210
194
|
);
|
|
211
195
|
this.hemiLight.intensity = 0.6;
|
|
212
|
-
|
|
213
196
|
this.dirLight = new DirectionalLight(
|
|
214
197
|
"dirLight",
|
|
215
198
|
new Vector3(-0.5, -1, -0.5),
|
|
@@ -222,39 +205,50 @@ class PrefViewer extends HTMLElement {
|
|
|
222
205
|
_setupEventListeners() {
|
|
223
206
|
this.canvas.addEventListener("wheel", (evt) => {
|
|
224
207
|
if (!this.scene || !this.camera) return;
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
? pickResult.pickedPoint.clone()
|
|
231
|
-
: this.camera.target.clone();
|
|
232
|
-
this.camera.target = pivotPoint;
|
|
233
|
-
this.camera.inertialRadiusOffset +=
|
|
234
|
-
evt.deltaY * this.camera.wheelPrecision * 0.01;
|
|
208
|
+
const pick = this.scene.pick(this.scene.pointerX, this.scene.pointerY);
|
|
209
|
+
this.camera.target = pick.hit
|
|
210
|
+
? pick.pickedPoint.clone()
|
|
211
|
+
: this.camera.target;
|
|
212
|
+
this.camera.inertialRadiusOffset += evt.deltaY * this.camera.wheelPrecision * 0.01;
|
|
235
213
|
evt.preventDefault();
|
|
236
214
|
});
|
|
237
215
|
}
|
|
238
216
|
|
|
239
217
|
// ====== Model Management ======
|
|
240
218
|
async _reloadModel() {
|
|
241
|
-
if (!this.scene || !this.modelUrl) {
|
|
242
|
-
console.warn("PrefViewer: _reloadModel aborted (scene or
|
|
219
|
+
if (!this.scene || (!this.modelUrl && !this.modelBase64)) {
|
|
220
|
+
console.warn("PrefViewer: _reloadModel aborted (no scene or no model)");
|
|
243
221
|
return;
|
|
244
222
|
}
|
|
245
|
-
|
|
246
223
|
this._disposePreviousMeshes();
|
|
247
224
|
|
|
248
225
|
try {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
this.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
226
|
+
let result;
|
|
227
|
+
if (this.modelBase64) {
|
|
228
|
+
const blob = this._createBlobFromBase64(this.modelBase64);
|
|
229
|
+
const ext = this._getExtensionFromMimeType(blob.type);
|
|
230
|
+
const fileName = `model${ext}`;
|
|
231
|
+
const file = new File([blob], fileName, { type: blob.type });
|
|
232
|
+
console.log('[PrefViewer] Importing from File:', fileName, blob);
|
|
233
|
+
result = await SceneLoader.ImportMeshAsync(
|
|
234
|
+
null,
|
|
235
|
+
"",
|
|
236
|
+
file,
|
|
237
|
+
this.scene,
|
|
238
|
+
undefined,
|
|
239
|
+
ext
|
|
240
|
+
);
|
|
241
|
+
} else {
|
|
242
|
+
const ext = this._getExtensionFromUrl(this.modelUrl);
|
|
243
|
+
result = await SceneLoader.ImportMeshAsync(
|
|
244
|
+
null,
|
|
245
|
+
"",
|
|
246
|
+
this.modelUrl,
|
|
247
|
+
this.scene,
|
|
248
|
+
undefined,
|
|
249
|
+
ext
|
|
250
|
+
);
|
|
251
|
+
}
|
|
258
252
|
this.scene.createDefaultCameraOrLight(true, true, true);
|
|
259
253
|
this.dispatchEvent(
|
|
260
254
|
new CustomEvent("model-loaded", {
|
|
@@ -277,24 +271,52 @@ class PrefViewer extends HTMLElement {
|
|
|
277
271
|
|
|
278
272
|
_disposePreviousMeshes() {
|
|
279
273
|
if (!this.scene) return;
|
|
280
|
-
this.scene.meshes.slice().forEach((
|
|
281
|
-
if (mesh.getClassName() === "Mesh") {
|
|
282
|
-
mesh.dispose();
|
|
283
|
-
}
|
|
284
|
-
});
|
|
274
|
+
this.scene.meshes.slice().forEach((m) => m.getClassName() === "Mesh" && m.dispose());
|
|
285
275
|
}
|
|
286
276
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
277
|
+
_createBlobFromBase64(base64) {
|
|
278
|
+
console.log('[PrefViewer] _createBlobFromBase64 called');
|
|
279
|
+
const [prefix, data] = base64.split(',');
|
|
280
|
+
console.log('[PrefViewer] prefix:', prefix);
|
|
281
|
+
let mimeType = 'application/octet-stream';
|
|
282
|
+
if (prefix && prefix.startsWith('data:')) {
|
|
283
|
+
const end = prefix.indexOf(';');
|
|
284
|
+
mimeType = prefix.substring(5, end >= 0 ? end : prefix.length) || mimeType;
|
|
285
|
+
}
|
|
286
|
+
console.log('[PrefViewer] inferred mimeType:', mimeType);
|
|
287
|
+
const raw = data ?? base64;
|
|
288
|
+
try {
|
|
289
|
+
const binary = atob(raw);
|
|
290
|
+
const array = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
291
|
+
const blob = new Blob([array], { type: mimeType });
|
|
292
|
+
console.log('[PrefViewer] Blob created:', blob);
|
|
293
|
+
return blob;
|
|
294
|
+
} catch (err) {
|
|
295
|
+
console.error('[PrefViewer] Failed to decode Base64 or create Blob:', err);
|
|
296
|
+
this.dispatchEvent(
|
|
297
|
+
new CustomEvent('model-error', {
|
|
298
|
+
detail: { error: err },
|
|
299
|
+
bubbles: true,
|
|
300
|
+
composed: true
|
|
301
|
+
})
|
|
302
|
+
);
|
|
303
|
+
throw err;
|
|
296
304
|
}
|
|
297
305
|
}
|
|
306
|
+
|
|
307
|
+
_getExtensionFromMimeType(mimeType) {
|
|
308
|
+
if (mimeType.includes("json")) return ".gltf";
|
|
309
|
+
if (mimeType.includes("glb") || mimeType === "application/octet-stream") return ".glb";
|
|
310
|
+
return ".gltf";
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
_getExtensionFromUrl(url) {
|
|
314
|
+
const match = url.match(/\.(gltf|glb)(\?|#|$)/i);
|
|
315
|
+
return match ? `.${match[1].toLowerCase()}` : ".gltf";
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ====== Cleanup ======
|
|
319
|
+
_disposeEngine() { if (this.engine) { this.engine.dispose(); this.engine = null; this.scene = null; this.camera = null; this.hemiLight = null; this.dirLight = null; } }
|
|
298
320
|
}
|
|
299
321
|
|
|
300
322
|
customElements.define("pref-viewer", PrefViewer);
|