@preference-sl/pref-viewer 2.5.5 → 2.6.0

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 +62 -68
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preference-sl/pref-viewer",
3
- "version": "2.5.5",
3
+ "version": "2.6.0",
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
@@ -6,25 +6,28 @@
6
6
  * Overview
7
7
  * --------
8
8
  * `PrefViewer` is a self-contained Web Component built with Babylon.js that:
9
- * • Inserts a <canvas> into its shadow DOM to render a glTF model.
10
- * • Creates and manages a Babylon Engine, Scene, ArcRotateCamera, and basic lighting.
11
- * • Listens for `model` (URL) or `model-data` (Base64) attributes to load different glTF files.
12
- * • Automatically disposes previous meshes when switching models.
13
- * • Dispatches “model-loaded” and “model-error” CustomEvents so host pages can react.
9
+ * • Renders glTF or GLB models in a <canvas> inside its shadow DOM.
10
+ * • Supports loading via remote URLs (`model` attribute) or Base64 data URIs (`model-data` attribute).
11
+ * • Automatically handles scene creation (engine, camera, lighting) and resource cleanup.
12
+ * • Emits `model-loaded` and `model-error` events for integration.
14
13
  *
15
14
  * Usage
16
15
  * -----
16
+ * Load from a URL:
17
+ * ```html
17
18
  * <pref-viewer
18
- * model="https://example.com/models/myModel.gltf"
19
+ * model="https://example.com/models/myModel.glb"
19
20
  * style="width:800px; height:600px;">
20
21
  * </pref-viewer>
22
+ * ```
21
23
  *
22
- * or:
23
- *
24
+ * Load from Base64 data:
25
+ * ```html
24
26
  * <pref-viewer
25
27
  * model-data="data:application/gltf+json;base64,UEsDB..."
26
28
  * style="width:800px; height:600px;">
27
29
  * </pref-viewer>
30
+ * ```
28
31
  */
29
32
  import {
30
33
  Engine,
@@ -53,38 +56,38 @@ class PrefViewer extends HTMLElement {
53
56
  this.dirLight = null;
54
57
  this._onWindowResize = null;
55
58
 
56
- // Model data sources
59
+ // Model sources
57
60
  this.modelUrl = null;
58
61
  this.modelBase64 = null;
59
62
 
60
- this._hasInitialized = false;
61
- this._pluginHookSetup = false;
63
+ this._initialized = false;
64
+ this._urlHooked = false;
62
65
  }
63
66
 
64
67
  static get observedAttributes() {
65
68
  return ["model", "model-data"];
66
69
  }
67
70
 
68
- attributeChangedCallback(name, _oldValue, newValue) {
71
+ attributeChangedCallback(name, _old, value) {
69
72
  if (name === "model") {
70
- this.modelUrl = newValue;
73
+ this.modelUrl = value;
71
74
  this.modelBase64 = null;
72
- if (this._hasInitialized) this._reloadModel();
75
+ this._initialized && this._reloadModel();
73
76
  } else if (name === "model-data") {
74
- this.modelBase64 = newValue;
77
+ this.modelBase64 = value;
75
78
  this.modelUrl = null;
76
- if (this._hasInitialized) this._reloadModel();
79
+ this._initialized && this._reloadModel();
77
80
  }
78
81
  }
79
82
 
80
83
  connectedCallback() {
81
- if (!this._pluginHookSetup) {
84
+ if (!this._urlHooked) {
82
85
  this._setupUrlPreprocessing();
83
- this._pluginHookSetup = true;
86
+ this._urlHooked = true;
84
87
  }
85
88
 
86
89
  if (!this.hasAttribute("model") && !this.hasAttribute("model-data")) {
87
- const err = 'PrefViewer: No "model" or "model-data" attribute provided.';
90
+ const err = 'PrefViewer: provide either "model" or "model-data" attribute.';
88
91
  console.error(err);
89
92
  this.dispatchEvent(
90
93
  new CustomEvent("model-error", { detail: { error: new Error(err) }, bubbles: true, composed: true })
@@ -92,28 +95,21 @@ class PrefViewer extends HTMLElement {
92
95
  return;
93
96
  }
94
97
 
95
- if (this.hasAttribute("model")) {
96
- this.modelUrl = this.getAttribute("model");
97
- }
98
- if (this.hasAttribute("model-data")) {
99
- this.modelBase64 = this.getAttribute("model-data");
100
- }
98
+ this.modelUrl = this.getAttribute("model");
99
+ this.modelBase64 = this.getAttribute("model-data");
101
100
 
102
101
  this._initializeBabylon();
103
- this._hasInitialized = true;
102
+ this._initialized = true;
104
103
  this._reloadModel();
105
104
  }
106
105
 
107
106
  disconnectedCallback() {
108
107
  this._disposeEngine();
109
- if (this._onWindowResize) window.removeEventListener("resize", this._onWindowResize);
108
+ this._onWindowResize && window.removeEventListener("resize", this._onWindowResize);
110
109
  }
111
110
 
112
111
  _setupUrlPreprocessing() {
113
- const transformUrl = (url) => {
114
- const stripped = url.replace(/^blob:(?:http|https|file):\/\/[^\/]+\/(.+)/i, "$1");
115
- return stripped.replace(/\\/g, "/");
116
- };
112
+ const transformUrl = (url) => url.replace(/^blob:[^/]+\//i, "").replace(/\\/g, "/");
117
113
  SceneLoader.OnPluginActivatedObservable.add((plugin) => {
118
114
  if (plugin.name === "gltf" || plugin.name === "gltf2") {
119
115
  plugin.preprocessUrl = transformUrl;
@@ -140,7 +136,8 @@ class PrefViewer extends HTMLElement {
140
136
  this.scene.clearColor = new Color4(1, 1, 1, 1);
141
137
  this._createCamera();
142
138
  this._createLights();
143
- this._setupEventListeners();
139
+ this._setupInteraction();
140
+
144
141
  this.engine.runRenderLoop(() => this.scene && this.scene.render());
145
142
  this._onWindowResize = () => this.engine && this.engine.resize();
146
143
  window.addEventListener("resize", this._onWindowResize);
@@ -148,20 +145,25 @@ class PrefViewer extends HTMLElement {
148
145
 
149
146
  _createCamera() {
150
147
  this.camera = new ArcRotateCamera(
151
- "camera", Math.PI/2, Math.PI/3, 10, Vector3.Zero(), this.scene
148
+ "camera",
149
+ Math.PI / 2,
150
+ Math.PI / 3,
151
+ 10,
152
+ Vector3.Zero(),
153
+ this.scene
152
154
  );
153
155
  this.camera.attachControl(this.canvas, true);
154
156
  }
155
157
 
156
158
  _createLights() {
157
- this.hemiLight = new HemisphericLight("hemiLight", new Vector3(0,1,0), this.scene);
159
+ this.hemiLight = new HemisphericLight("hemiLight", new Vector3(0, 1, 0), this.scene);
158
160
  this.hemiLight.intensity = 0.6;
159
- this.dirLight = new DirectionalLight("dirLight", new Vector3(-0.5,-1,-0.5), this.scene);
160
- this.dirLight.position = new Vector3(0,5,0);
161
+ this.dirLight = new DirectionalLight("dirLight", new Vector3(-0.5, -1, -0.5), this.scene);
162
+ this.dirLight.position = new Vector3(0, 5, 0);
161
163
  this.dirLight.intensity = 0.8;
162
164
  }
163
165
 
164
- _setupEventListeners() {
166
+ _setupInteraction() {
165
167
  this.canvas.addEventListener("wheel", (evt) => {
166
168
  if (!this.scene || !this.camera) return;
167
169
  const pick = this.scene.pick(this.scene.pointerX, this.scene.pointerY);
@@ -174,54 +176,46 @@ class PrefViewer extends HTMLElement {
174
176
  async _reloadModel() {
175
177
  if (!this.scene || (!this.modelUrl && !this.modelBase64)) return;
176
178
  this._disposePreviousMeshes();
179
+
177
180
  try {
178
181
  let result;
179
182
  if (this.modelBase64) {
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);
183
+ const { blob, extension } = this._decodeBase64(this.modelBase64);
184
+ const file = new File([blob], `model${extension}`, { type: blob.type });
184
185
  result = await SceneLoader.ImportMeshAsync(null, "", file, this.scene, undefined, extension);
185
186
  } else {
186
- const ext = (this.modelUrl.match(/\.(gltf|glb)(\?|#|$)/i) || [])[1]?.toLowerCase() || 'gltf';
187
- result = await SceneLoader.ImportMeshAsync(null, "", this.modelUrl, this.scene, undefined, `.${ext}`);
187
+ const extMatch = this.modelUrl.match(/\.(gltf|glb)(\?|#|$)/i);
188
+ const extension = extMatch ? `.${extMatch[1].toLowerCase()}` : ".gltf";
189
+ result = await SceneLoader.ImportMeshAsync(null, "", this.modelUrl, this.scene, undefined, extension);
188
190
  }
191
+
189
192
  this.scene.createDefaultCameraOrLight(true, true, true);
190
- this.dispatchEvent(new CustomEvent("model-loaded", { detail: result, bubbles: true, composed: true }));
193
+ this.dispatchEvent(
194
+ new CustomEvent("model-loaded", { detail: result, bubbles: true, composed: true })
195
+ );
191
196
  } catch (err) {
192
- console.error("PrefViewer: Error loading model:", err);
193
- this.dispatchEvent(new CustomEvent("model-error", { detail: { error: err }, bubbles: true, composed: true }));
197
+ console.error("PrefViewer: failed to load model", err);
198
+ this.dispatchEvent(
199
+ new CustomEvent("model-error", { detail: { error: err }, bubbles: true, composed: true })
200
+ );
194
201
  }
195
202
  }
196
203
 
197
204
  _disposePreviousMeshes() {
198
205
  if (!this.scene) return;
199
- this.scene.meshes.slice().forEach(m => m.getClassName()==="Mesh" && m.dispose());
206
+ this.scene.meshes.slice().forEach((m) => m.getClassName() === "Mesh" && m.dispose());
200
207
  }
201
208
 
202
- _createBlobFromBase64(base64) {
203
- console.log('[PrefViewer] Decoding Base64...');
204
- const [prefix, payload] = base64.split(',');
205
- const raw = payload ?? base64;
206
- let decoded;
207
- try {
208
- decoded = atob(raw);
209
- } catch (e) {
210
- console.error('[PrefViewer] atob failed:', e);
211
- throw e;
212
- }
213
- // detect JSON vs binary glb
209
+ _decodeBase64(base64) {
210
+ const [, payload] = base64.split(",");
211
+ const raw = payload || base64;
212
+ const decoded = atob(raw);
214
213
  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));
214
+ try { JSON.parse(decoded); isJson = true; } catch {}
215
+ const extension = isJson ? ".gltf" : ".glb";
216
+ const type = isJson ? "model/gltf+json" : "model/gltf-binary";
217
+ const array = Uint8Array.from(decoded, (c) => c.charCodeAt(0));
223
218
  const blob = new Blob([array], { type });
224
- console.log('[PrefViewer] Created Blob of size', blob.size);
225
219
  return { blob, extension };
226
220
  }
227
221