@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preference-sl/pref-viewer",
3
- "version": "2.5.1",
3
+ "version": "2.5.3",
4
4
  "description": "Web Component to preview GLTF models with Babylon.js",
5
5
  "author": "Alex Moreno Palacio <amoreno@preference.es>",
6
6
  "scripts": {
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 a `model` attribute to load different glTF files (defaults to "./models/window.gltf").
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
- * 1. **Import the script (module)**
18
- * <script type="module" src="path/to/pref-viewer.js"></script>
17
+ * <pref-viewer
18
+ * model="https://example.com/models/myModel.gltf"
19
+ * style="width:800px; height:600px;">
20
+ * </pref-viewer>
19
21
  *
20
- * 2. **Place the custom element in your HTML**
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
- * 3. **Listen for loading events (optional)**
27
- * const viewer = document.querySelector("pref-viewer");
28
- * viewer.addEventListener("model-loaded", (evt) => {
29
- * xx
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
- // modelUrl will be provided via attribute
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
- if (this._hasInitialized) {
83
- this._reloadModel();
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 attribute; fail early if missing
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
- // Initialize modelUrl from attribute
110
- this.modelUrl = this.getAttribute("model");
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 modelUrl missing)");
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
- const result = await SceneLoader.ImportMeshAsync(
250
- null,
251
- "",
252
- this.modelUrl,
253
- this.scene,
254
- undefined,
255
- ".gltf"
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: { meshes: result.meshes, particleSystems: result.particleSystems },
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) {