@preference-sl/pref-viewer 2.5.4 → 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 +50 -137
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preference-sl/pref-viewer",
3
- "version": "2.5.4",
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,32 +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
- return /^https?:\/\//i.test(fixedSlashes) ? fixedSlashes : fixedSlashes;
114
+ const stripped = url.replace(/^blob:(?:http|https|file):\/\/[^\/]+\/(.+)/i, "$1");
115
+ return stripped.replace(/\\/g, "/");
132
116
  };
133
-
134
117
  SceneLoader.OnPluginActivatedObservable.add((plugin) => {
135
118
  if (plugin.name === "gltf" || plugin.name === "gltf2") {
136
119
  plugin.preprocessUrl = transformUrl;
@@ -139,23 +122,14 @@ class PrefViewer extends HTMLElement {
139
122
  });
140
123
  }
141
124
 
142
- // ====== Setup Helpers ======
143
125
  _createCanvas() {
144
126
  this.canvas = document.createElement("canvas");
145
- Object.assign(this.canvas.style, {
146
- width: "100%",
147
- height: "100%",
148
- display: "block"
149
- });
127
+ Object.assign(this.canvas.style, { width: "100%", height: "100%", display: "block" });
150
128
  }
151
129
 
152
130
  _wrapCanvas() {
153
131
  const wrapper = document.createElement("div");
154
- Object.assign(wrapper.style, {
155
- width: "100%",
156
- height: "100%",
157
- position: "relative"
158
- });
132
+ Object.assign(wrapper.style, { width: "100%", height: "100%", position: "relative" });
159
133
  wrapper.appendChild(this.canvas);
160
134
  this.shadowRoot.append(wrapper);
161
135
  }
@@ -164,11 +138,9 @@ class PrefViewer extends HTMLElement {
164
138
  this.engine = new Engine(this.canvas, true, { alpha: true });
165
139
  this.scene = new Scene(this.engine);
166
140
  this.scene.clearColor = new Color4(1, 1, 1, 1);
167
-
168
141
  this._createCamera();
169
142
  this._createLights();
170
143
  this._setupEventListeners();
171
-
172
144
  this.engine.runRenderLoop(() => this.scene && this.scene.render());
173
145
  this._onWindowResize = () => this.engine && this.engine.resize();
174
146
  window.addEventListener("resize", this._onWindowResize);
@@ -176,29 +148,16 @@ class PrefViewer extends HTMLElement {
176
148
 
177
149
  _createCamera() {
178
150
  this.camera = new ArcRotateCamera(
179
- "camera",
180
- Math.PI / 2,
181
- Math.PI / 3,
182
- 10,
183
- Vector3.Zero(),
184
- this.scene
151
+ "camera", Math.PI/2, Math.PI/3, 10, Vector3.Zero(), this.scene
185
152
  );
186
153
  this.camera.attachControl(this.canvas, true);
187
154
  }
188
155
 
189
156
  _createLights() {
190
- this.hemiLight = new HemisphericLight(
191
- "hemiLight",
192
- new Vector3(0, 1, 0),
193
- this.scene
194
- );
157
+ this.hemiLight = new HemisphericLight("hemiLight", new Vector3(0,1,0), this.scene);
195
158
  this.hemiLight.intensity = 0.6;
196
- this.dirLight = new DirectionalLight(
197
- "dirLight",
198
- new Vector3(-0.5, -1, -0.5),
199
- this.scene
200
- );
201
- 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);
202
161
  this.dirLight.intensity = 0.8;
203
162
  }
204
163
 
@@ -206,117 +165,71 @@ class PrefViewer extends HTMLElement {
206
165
  this.canvas.addEventListener("wheel", (evt) => {
207
166
  if (!this.scene || !this.camera) return;
208
167
  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;
168
+ this.camera.target = pick.hit ? pick.pickedPoint.clone() : this.camera.target;
212
169
  this.camera.inertialRadiusOffset += evt.deltaY * this.camera.wheelPrecision * 0.01;
213
170
  evt.preventDefault();
214
171
  });
215
172
  }
216
173
 
217
- // ====== Model Management ======
218
174
  async _reloadModel() {
219
- if (!this.scene || (!this.modelUrl && !this.modelBase64)) {
220
- console.warn("PrefViewer: _reloadModel aborted (no scene or no model)");
221
- return;
222
- }
175
+ if (!this.scene || (!this.modelUrl && !this.modelBase64)) return;
223
176
  this._disposePreviousMeshes();
224
-
225
177
  try {
226
178
  let result;
227
179
  if (this.modelBase64) {
228
- const blob = this._createBlobFromBase64(this.modelBase64);
229
- const ext = this._getExtensionFromMimeType(blob.type);
230
- const fileName = `model${ext}`;
180
+ const { blob, extension } = this._createBlobFromBase64(this.modelBase64);
181
+ const fileName = `model${extension}`;
231
182
  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
- );
183
+ console.log('[PrefViewer] Loading from Base64 as File:', fileName);
184
+ result = await SceneLoader.ImportMeshAsync(null, "", file, this.scene, undefined, extension);
241
185
  } 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
- );
186
+ const ext = (this.modelUrl.match(/\.(gltf|glb)(\?|#|$)/i) || [])[1]?.toLowerCase() || 'gltf';
187
+ result = await SceneLoader.ImportMeshAsync(null, "", this.modelUrl, this.scene, undefined, `.${ext}`);
251
188
  }
252
189
  this.scene.createDefaultCameraOrLight(true, true, true);
253
- this.dispatchEvent(
254
- new CustomEvent("model-loaded", {
255
- detail: { meshes: result.meshes, particleSystems: result.particleSystems },
256
- bubbles: true,
257
- composed: true
258
- })
259
- );
190
+ this.dispatchEvent(new CustomEvent("model-loaded", { detail: result, bubbles: true, composed: true }));
260
191
  } catch (err) {
261
192
  console.error("PrefViewer: Error loading model:", err);
262
- this.dispatchEvent(
263
- new CustomEvent("model-error", {
264
- detail: { error: err },
265
- bubbles: true,
266
- composed: true
267
- })
268
- );
193
+ this.dispatchEvent(new CustomEvent("model-error", { detail: { error: err }, bubbles: true, composed: true }));
269
194
  }
270
195
  }
271
196
 
272
197
  _disposePreviousMeshes() {
273
198
  if (!this.scene) return;
274
- this.scene.meshes.slice().forEach((m) => m.getClassName() === "Mesh" && m.dispose());
199
+ this.scene.meshes.slice().forEach(m => m.getClassName()==="Mesh" && m.dispose());
275
200
  }
276
201
 
277
202
  _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;
203
+ console.log('[PrefViewer] Decoding Base64...');
204
+ const [prefix, payload] = base64.split(',');
205
+ const raw = payload ?? base64;
206
+ let decoded;
288
207
  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;
208
+ decoded = atob(raw);
209
+ } catch (e) {
210
+ console.error('[PrefViewer] atob failed:', e);
211
+ throw e;
304
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 };
226
+ }
227
+
228
+ _disposeEngine() {
229
+ if (!this.engine) return;
230
+ this.engine.dispose();
231
+ this.engine = this.scene = this.camera = this.hemiLight = this.dirLight = null;
305
232
  }
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; } }
320
233
  }
321
234
 
322
235
  customElements.define("pref-viewer", PrefViewer);