@preference-sl/pref-viewer 2.5.3 → 2.5.5

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 +52 -166
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preference-sl/pref-viewer",
3
- "version": "2.5.3",
3
+ "version": "2.5.5",
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
@@ -78,22 +78,16 @@ class PrefViewer extends HTMLElement {
78
78
  }
79
79
 
80
80
  connectedCallback() {
81
- // Set up URL preprocessing once
82
81
  if (!this._pluginHookSetup) {
83
82
  this._setupUrlPreprocessing();
84
83
  this._pluginHookSetup = true;
85
84
  }
86
85
 
87
- // Require either a model URL or Base64 data
88
86
  if (!this.hasAttribute("model") && !this.hasAttribute("model-data")) {
89
- const errorMsg = 'PrefViewer: No "model" or "model-data" attribute provided.';
90
- console.error(errorMsg);
87
+ const err = 'PrefViewer: No "model" or "model-data" attribute provided.';
88
+ console.error(err);
91
89
  this.dispatchEvent(
92
- new CustomEvent("model-error", {
93
- detail: { error: new Error(errorMsg) },
94
- bubbles: true,
95
- composed: true
96
- })
90
+ new CustomEvent("model-error", { detail: { error: new Error(err) }, bubbles: true, composed: true })
97
91
  );
98
92
  return;
99
93
  }
@@ -105,35 +99,21 @@ class PrefViewer extends HTMLElement {
105
99
  this.modelBase64 = this.getAttribute("model-data");
106
100
  }
107
101
 
108
- // Initialize Babylon (engine + scene + camera + lights + hooks)
109
102
  this._initializeBabylon();
110
103
  this._hasInitialized = true;
111
-
112
- // Load the specified model
113
104
  this._reloadModel();
114
105
  }
115
106
 
116
107
  disconnectedCallback() {
117
108
  this._disposeEngine();
118
- if (this._onWindowResize) {
119
- window.removeEventListener("resize", this._onWindowResize);
120
- }
109
+ if (this._onWindowResize) window.removeEventListener("resize", this._onWindowResize);
121
110
  }
122
111
 
123
- // ====== URL Preprocessing ======
124
112
  _setupUrlPreprocessing() {
125
113
  const transformUrl = (url) => {
126
- const stripped = url.replace(
127
- /^blob:(?:http|https|file):\/\/[^\/]+\/(.+)/i,
128
- "$1"
129
- );
130
- const fixedSlashes = stripped.replace(/\\/g, "/");
131
- if (/^https?:\/\//i.test(fixedSlashes)) {
132
- return fixedSlashes;
133
- }
134
- return fixedSlashes;
114
+ const stripped = url.replace(/^blob:(?:http|https|file):\/\/[^\/]+\/(.+)/i, "$1");
115
+ return stripped.replace(/\\/g, "/");
135
116
  };
136
-
137
117
  SceneLoader.OnPluginActivatedObservable.add((plugin) => {
138
118
  if (plugin.name === "gltf" || plugin.name === "gltf2") {
139
119
  plugin.preprocessUrl = transformUrl;
@@ -142,23 +122,14 @@ class PrefViewer extends HTMLElement {
142
122
  });
143
123
  }
144
124
 
145
- // ====== Setup Helpers ======
146
125
  _createCanvas() {
147
126
  this.canvas = document.createElement("canvas");
148
- Object.assign(this.canvas.style, {
149
- width: "100%",
150
- height: "100%",
151
- display: "block"
152
- });
127
+ Object.assign(this.canvas.style, { width: "100%", height: "100%", display: "block" });
153
128
  }
154
129
 
155
130
  _wrapCanvas() {
156
131
  const wrapper = document.createElement("div");
157
- Object.assign(wrapper.style, {
158
- width: "100%",
159
- height: "100%",
160
- position: "relative"
161
- });
132
+ Object.assign(wrapper.style, { width: "100%", height: "100%", position: "relative" });
162
133
  wrapper.appendChild(this.canvas);
163
134
  this.shadowRoot.append(wrapper);
164
135
  }
@@ -167,182 +138,97 @@ class PrefViewer extends HTMLElement {
167
138
  this.engine = new Engine(this.canvas, true, { alpha: true });
168
139
  this.scene = new Scene(this.engine);
169
140
  this.scene.clearColor = new Color4(1, 1, 1, 1);
170
-
171
141
  this._createCamera();
172
142
  this._createLights();
173
143
  this._setupEventListeners();
174
-
175
- this.engine.runRenderLoop(() => {
176
- if (this.scene) this.scene.render();
177
- });
178
-
179
- this._onWindowResize = () => {
180
- if (this.engine) this.engine.resize();
181
- };
144
+ this.engine.runRenderLoop(() => this.scene && this.scene.render());
145
+ this._onWindowResize = () => this.engine && this.engine.resize();
182
146
  window.addEventListener("resize", this._onWindowResize);
183
147
  }
184
148
 
185
149
  _createCamera() {
186
150
  this.camera = new ArcRotateCamera(
187
- "camera",
188
- Math.PI / 2,
189
- Math.PI / 3,
190
- 10,
191
- Vector3.Zero(),
192
- this.scene
151
+ "camera", Math.PI/2, Math.PI/3, 10, Vector3.Zero(), this.scene
193
152
  );
194
153
  this.camera.attachControl(this.canvas, true);
195
154
  }
196
155
 
197
156
  _createLights() {
198
- this.hemiLight = new HemisphericLight(
199
- "hemiLight",
200
- new Vector3(0, 1, 0),
201
- this.scene
202
- );
157
+ this.hemiLight = new HemisphericLight("hemiLight", new Vector3(0,1,0), this.scene);
203
158
  this.hemiLight.intensity = 0.6;
204
-
205
- this.dirLight = new DirectionalLight(
206
- "dirLight",
207
- new Vector3(-0.5, -1, -0.5),
208
- this.scene
209
- );
210
- this.dirLight.position = new Vector3(0, 5, 0);
159
+ this.dirLight = new DirectionalLight("dirLight", new Vector3(-0.5,-1,-0.5), this.scene);
160
+ this.dirLight.position = new Vector3(0,5,0);
211
161
  this.dirLight.intensity = 0.8;
212
162
  }
213
163
 
214
164
  _setupEventListeners() {
215
165
  this.canvas.addEventListener("wheel", (evt) => {
216
166
  if (!this.scene || !this.camera) return;
217
- const pickResult = this.scene.pick(
218
- this.scene.pointerX,
219
- this.scene.pointerY
220
- );
221
- const pivotPoint = pickResult.hit
222
- ? pickResult.pickedPoint.clone()
223
- : this.camera.target.clone();
224
- this.camera.target = pivotPoint;
225
- this.camera.inertialRadiusOffset +=
226
- evt.deltaY * this.camera.wheelPrecision * 0.01;
167
+ const pick = this.scene.pick(this.scene.pointerX, this.scene.pointerY);
168
+ this.camera.target = pick.hit ? pick.pickedPoint.clone() : this.camera.target;
169
+ this.camera.inertialRadiusOffset += evt.deltaY * this.camera.wheelPrecision * 0.01;
227
170
  evt.preventDefault();
228
171
  });
229
172
  }
230
173
 
231
- // ====== Model Management ======
232
174
  async _reloadModel() {
233
- if (!this.scene || (!this.modelUrl && !this.modelBase64)) {
234
- console.warn("PrefViewer: _reloadModel aborted (no scene or no model)");
235
- return;
236
- }
237
-
175
+ if (!this.scene || (!this.modelUrl && !this.modelBase64)) return;
238
176
  this._disposePreviousMeshes();
239
-
240
177
  try {
241
178
  let result;
242
179
  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
- );
180
+ const { blob, extension } = this._createBlobFromBase64(this.modelBase64);
181
+ const fileName = `model${extension}`;
182
+ const file = new File([blob], fileName, { type: blob.type });
183
+ console.log('[PrefViewer] Loading from Base64 as File:', fileName);
184
+ result = await SceneLoader.ImportMeshAsync(null, "", file, this.scene, undefined, extension);
253
185
  } 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
- );
186
+ const ext = (this.modelUrl.match(/\.(gltf|glb)(\?|#|$)/i) || [])[1]?.toLowerCase() || 'gltf';
187
+ result = await SceneLoader.ImportMeshAsync(null, "", this.modelUrl, this.scene, undefined, `.${ext}`);
263
188
  }
264
-
265
189
  this.scene.createDefaultCameraOrLight(true, true, true);
266
- this.dispatchEvent(
267
- new CustomEvent("model-loaded", {
268
- detail: {
269
- meshes: result.meshes,
270
- particleSystems: result.particleSystems
271
- },
272
- bubbles: true,
273
- composed: true
274
- })
275
- );
190
+ this.dispatchEvent(new CustomEvent("model-loaded", { detail: result, bubbles: true, composed: true }));
276
191
  } catch (err) {
277
192
  console.error("PrefViewer: Error loading model:", err);
278
- this.dispatchEvent(
279
- new CustomEvent("model-error", {
280
- detail: { error: err },
281
- bubbles: true,
282
- composed: true
283
- })
284
- );
193
+ this.dispatchEvent(new CustomEvent("model-error", { detail: { error: err }, bubbles: true, composed: true }));
285
194
  }
286
195
  }
287
196
 
288
197
  _disposePreviousMeshes() {
289
198
  if (!this.scene) return;
290
- this.scene.meshes.slice().forEach((mesh) => {
291
- if (mesh.getClassName() === "Mesh") mesh.dispose();
292
- });
199
+ this.scene.meshes.slice().forEach(m => m.getClassName()==="Mesh" && m.dispose());
293
200
  }
294
201
 
295
202
  _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;
203
+ console.log('[PrefViewer] Decoding Base64...');
204
+ const [prefix, payload] = base64.split(',');
205
+ const raw = payload ?? base64;
206
+ let decoded;
306
207
  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;
208
+ decoded = atob(raw);
209
+ } catch (e) {
210
+ console.error('[PrefViewer] atob failed:', e);
211
+ throw e;
322
212
  }
213
+ // detect JSON vs binary glb
214
+ let isJson = false;
215
+ try {
216
+ JSON.parse(decoded);
217
+ isJson = true;
218
+ } catch {}
219
+ const extension = isJson ? '.gltf' : '.glb';
220
+ const type = isJson ? 'model/gltf+json' : 'model/gltf-binary';
221
+ console.log('[PrefViewer] Detected format:', extension, type);
222
+ const array = Uint8Array.from(decoded, c => c.charCodeAt(0));
223
+ const blob = new Blob([array], { type });
224
+ console.log('[PrefViewer] Created Blob of size', blob.size);
225
+ return { blob, extension };
323
226
  }
324
227
 
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
-
336
- // ====== Cleanup ======
337
228
  _disposeEngine() {
338
- if (this.engine) {
339
- this.engine.dispose();
340
- this.engine = null;
341
- this.scene = null;
342
- this.camera = null;
343
- this.hemiLight = null;
344
- this.dirLight = null;
345
- }
229
+ if (!this.engine) return;
230
+ this.engine.dispose();
231
+ this.engine = this.scene = this.camera = this.hemiLight = this.dirLight = null;
346
232
  }
347
233
  }
348
234