@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +107 -85
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preference-sl/pref-viewer",
3
- "version": "2.5.2",
3
+ "version": "2.5.4",
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();
@@ -132,10 +128,7 @@ class PrefViewer extends HTMLElement {
132
128
  "$1"
133
129
  );
134
130
  const fixedSlashes = stripped.replace(/\\/g, "/");
135
- if (/^https?:\/\//i.test(fixedSlashes)) {
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
- if (this.scene) {
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 pickResult = this.scene.pick(
226
- this.scene.pointerX,
227
- this.scene.pointerY
228
- );
229
- const pivotPoint = pickResult.hit
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 modelUrl missing)");
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
- const result = await SceneLoader.ImportMeshAsync(
250
- null,
251
- "",
252
- this.modelUrl,
253
- this.scene,
254
- undefined,
255
- ".gltf"
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((mesh) => {
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
- // ====== Cleanup ======
288
- _disposeEngine() {
289
- if (this.engine) {
290
- this.engine.dispose();
291
- this.engine = null;
292
- this.scene = null;
293
- this.camera = null;
294
- this.hemiLight = null;
295
- this.dirLight = null;
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);