@preference-sl/pref-viewer 2.1.7 → 2.1.9

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 +99 -222
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preference-sl/pref-viewer",
3
- "version": "2.1.7",
3
+ "version": "2.1.9",
4
4
  "description": "Web Component to preview GLTF models with Babylon.js",
5
5
  "author": "Alex Moreno Palacio <amoreno@preference.es>",
6
6
  "license": "MIT",
package/src/index.js CHANGED
@@ -1,288 +1,165 @@
1
- /**
2
- * =============================================================================
3
- * PrefViewer Web Component (JavaScript)
4
- * =============================================================================
5
- *
6
- * Overview
7
- * --------
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 a `model` attribute to load different glTF files (defaults to "./models/patata.gltf").
12
- * • Automatically disposes previous meshes when switching models.
13
- * • Dispatches “model-loaded” and “model-error” CustomEvents so host pages can react.
14
- *
15
- * Usage
16
- * -----
17
- * 1. **Import the script (module)**
18
- * <script type="module" src="path/to/pref-viewer.js"></script>
19
- *
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>
25
- *
26
- * 3. **Listen for loading events (optional)**
27
- * const viewer = document.querySelector("pref-viewer");
28
- * viewer.addEventListener("model-loaded", (evt) => {
29
- * console.log("Loaded meshes:", evt.detail.meshes);
30
- * });
31
- * viewer.addEventListener("model-error", (evt) => {
32
- * console.error("Failed to load model:", evt.detail.error);
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
- * -----------------------------------------------------------------------------
41
- */
42
-
43
1
  import { Engine, Scene, ArcRotateCamera, Vector3, SceneLoader, Color4, HemisphericLight, DirectionalLight } from "@babylonjs/core";
44
2
  import "@babylonjs/loaders";
45
3
 
46
4
  class PrefViewer extends HTMLElement {
47
5
  constructor() {
48
6
  super();
49
- console.log("PrefViewer: constructor");
50
- this.attachShadow({ mode: "open" });
51
- this._createCanvas();
52
- this._wrapCanvas();
7
+ this.attachShadow({ mode: 'open' });
53
8
 
54
- // These will be set in _initializeBabylon()
9
+ // Crear y envolver canvas
10
+ this.canvas = document.createElement('canvas');
11
+ Object.assign(this.canvas.style, {
12
+ width: '100%',
13
+ height: '100%',
14
+ display: 'block',
15
+ });
16
+ const wrapper = document.createElement('div');
17
+ Object.assign(wrapper.style, {
18
+ width: '100%',
19
+ height: '100%',
20
+ position: 'relative',
21
+ });
22
+ wrapper.appendChild(this.canvas);
23
+ this.shadowRoot.append(wrapper);
24
+
25
+ // Estado inicial
55
26
  this.engine = null;
56
27
  this.scene = null;
57
28
  this.camera = null;
58
29
  this.hemiLight = null;
59
30
  this.dirLight = null;
60
31
  this._onWindowResize = null;
61
-
62
- // modelUrl might be provided via attribute before connectedCallback
63
- this.modelUrl = null;
32
+ this.modelFile = null;
64
33
  this._hasInitialized = false;
65
34
  }
66
35
 
67
36
  static get observedAttributes() {
68
- return ["model"];
37
+ return ['model'];
69
38
  }
70
39
 
71
40
  attributeChangedCallback(name, _oldValue, newValue) {
72
- console.log(`PrefViewer: attributeChangedCallback - ${name} -> ${newValue}`);
73
- if (name === "model" && newValue) {
74
- this.modelUrl = newValue;
75
- console.log(`PrefViewer: modelUrl set to ${this.modelUrl}`);
76
- // Only reload if initialization has already happened
77
- if (this._hasInitialized) {
78
- this._reloadModel();
79
- }
41
+ if (name === 'model' && newValue && this._hasInitialized) {
42
+ this._reloadModel(newValue);
80
43
  }
81
44
  }
82
45
 
83
46
  connectedCallback() {
84
- console.log("PrefViewer: connectedCallback");
85
- // 1) Determine modelUrl now that element is connected
86
- if (!this.hasAttribute("model")) {
87
- this.modelUrl = new URL("./models/patata.gltf", import.meta.url).href;
88
- console.log(`PrefViewer: no model attribute, defaulting to ${this.modelUrl}`);
89
- } else {
90
- this.modelUrl = this.getAttribute("model");
91
- console.log(`PrefViewer: model attribute present, using ${this.modelUrl}`);
92
- }
93
-
94
- // 2) Initialize Babylon (engine + scene + camera + lights + hooks)
95
47
  this._initializeBabylon();
96
-
97
- // 3) Mark that initialization is done
98
48
  this._hasInitialized = true;
99
-
100
- // 4) Load whatever modelUrl we have
101
- this._reloadModel();
102
49
  }
103
50
 
104
51
  disconnectedCallback() {
105
- console.log("PrefViewer: disconnectedCallback - disposing engine");
106
52
  this._disposeEngine();
107
- window.removeEventListener("resize", this._onWindowResize);
53
+ if (this._onWindowResize) {
54
+ window.removeEventListener('resize', this._onWindowResize);
55
+ }
108
56
  }
109
57
 
110
- // ====== Private setup methods ======
111
- _createCanvas() {
112
- console.log("PrefViewer: _createCanvas");
113
- this.canvas = document.createElement("canvas");
114
- Object.assign(this.canvas.style, {
115
- width: "100%",
116
- height: "100%",
117
- display: "block"
118
- });
119
- }
120
-
121
- _wrapCanvas() {
122
- console.log("PrefViewer: _wrapCanvas");
123
- const wrapper = document.createElement("div");
124
- Object.assign(wrapper.style, {
125
- width: "100%",
126
- height: "100%",
127
- position: "relative"
128
- });
129
- wrapper.appendChild(this.canvas);
130
- this.shadowRoot.append(wrapper);
58
+ /**
59
+ * API pública: cargar bytes como modelo (.gltf/.glb)
60
+ * @param {Uint8Array} bytes
61
+ * @param {boolean} isJson - true si el contenido es glTF JSON
62
+ */
63
+ loadFromBytes(bytes, isJson) {
64
+ // Detectar GLB por magic number
65
+ const isGlb = bytes.length > 4
66
+ && bytes[0] === 0x67
67
+ && bytes[1] === 0x6c
68
+ && bytes[2] === 0x54
69
+ && bytes[3] === 0x46;
70
+ const mimeType = isGlb
71
+ ? 'model/gltf-binary'
72
+ : isJson
73
+ ? 'model/gltf+json'
74
+ : 'application/octet-stream';
75
+ const extension = isGlb ? 'glb' : 'gltf';
76
+
77
+ // Empaquetar en File con nombre
78
+ const file = new File([bytes], `model.${extension}`, { type: mimeType });
79
+ this.modelFile = file;
80
+
81
+ // Recargar modelo usando el File
82
+ this._reloadModel(file);
131
83
  }
132
84
 
133
85
  _initializeBabylon() {
134
- console.log("PrefViewer: _initializeBabylon - creating engine and scene");
135
-
136
- // 1) Create the Babylon engine & scene
137
86
  this.engine = new Engine(this.canvas, true, { alpha: true });
138
87
  this.scene = new Scene(this.engine);
139
88
  this.scene.clearColor = new Color4(1, 1, 1, 1);
140
89
 
141
- // 2) Create camera and lights
142
- console.log("PrefViewer: _createCamera and _createLights");
143
- this._createCamera();
144
- this._createLights();
145
-
146
- // 3) Hook up input/event handlers (e.g. wheel-to-zoom)
147
- console.log("PrefViewer: _setupEventListeners");
148
- this._setupEventListeners();
149
-
150
- // 4) Start Babylon’s render loop
151
- console.log("PrefViewer: Starting render loop");
152
- this.engine.runRenderLoop(() => {
153
- if (this.scene) {
154
- this.scene.render();
155
- }
156
- });
157
- this._onWindowResize = () => {
158
- console.log("PrefViewer: Window resized - calling engine.resize()");
159
- this.engine.resize();
160
- };
161
- window.addEventListener("resize", this._onWindowResize);
162
- }
163
-
164
- _createCamera() {
165
- console.log("PrefViewer: _createCamera");
166
- // ArcRotateCamera that orbits around origin
90
+ // Cámara orbitadora
167
91
  this.camera = new ArcRotateCamera(
168
- "camera",
169
- Math.PI / 2,
170
- Math.PI / 3,
171
- 10,
172
- Vector3.Zero(),
173
- this.scene
92
+ 'camera', Math.PI / 2, Math.PI / 3, 10,
93
+ Vector3.Zero(), this.scene
174
94
  );
175
95
  this.camera.attachControl(this.canvas, true);
176
- }
177
96
 
178
- _createLights() {
179
- console.log("PrefViewer: _createLights");
180
- this.hemiLight = new HemisphericLight(
181
- "hemiLight",
182
- new Vector3(0, 1, 0),
183
- this.scene
184
- );
97
+ // Iluminación básica
98
+ this.hemiLight = new HemisphericLight('hemi', new Vector3(0, 1, 0), this.scene);
185
99
  this.hemiLight.intensity = 0.6;
186
-
187
- this.dirLight = new DirectionalLight(
188
- "dirLight",
189
- new Vector3(-0.5, -1, -0.5),
190
- this.scene
191
- );
100
+ this.dirLight = new DirectionalLight('dir', new Vector3(-0.5, -1, -0.5), this.scene);
192
101
  this.dirLight.position = new Vector3(0, 5, 0);
193
102
  this.dirLight.intensity = 0.8;
194
- }
195
103
 
196
- _setupEventListeners() {
197
- console.log("PrefViewer: _setupEventListeners");
198
- // Zoom toward point-of-interest on wheel scroll
199
- this.canvas.addEventListener("wheel", (evt) => {
104
+ // Zoom con rueda apuntando al punto
105
+ this.canvas.addEventListener('wheel', evt => {
200
106
  if (!this.scene || !this.camera) return;
201
- const pickResult = this.scene.pick(
202
- this.scene.pointerX,
203
- this.scene.pointerY
204
- );
205
- const pivotPoint = pickResult.hit
206
- ? pickResult.pickedPoint.clone()
207
- : this.camera.target.clone();
208
- this.camera.target = pivotPoint;
209
- this.camera.inertialRadiusOffset +=
210
- evt.deltaY * this.camera.wheelPrecision * 0.01;
107
+ const pick = this.scene.pick(this.scene.pointerX, this.scene.pointerY);
108
+ const pivot = pick.hit ? pick.pickedPoint.clone() : this.camera.target.clone();
109
+ this.camera.target = pivot;
110
+ this.camera.inertialRadiusOffset += evt.deltaY * this.camera.wheelPrecision * 0.01;
211
111
  evt.preventDefault();
212
112
  });
113
+
114
+ // Render loop
115
+ this.engine.runRenderLoop(() => this.scene && this.scene.render());
116
+
117
+ // Ajuste en resize
118
+ this._onWindowResize = () => this.engine.resize();
119
+ window.addEventListener('resize', this._onWindowResize);
213
120
  }
214
121
 
215
- // ====== Model loading / management ======
216
- async _reloadModel() {
217
- console.log(`PrefViewer: _reloadModel - loading ${this.modelUrl}`);
218
- if (!this.scene || !this.modelUrl) {
219
- console.warn("PrefViewer: _reloadModel aborted (scene or modelUrl missing)");
220
- return;
221
- }
122
+ /**
123
+ * Recarga el modelo desde URL string o File
124
+ * @param {string|File} source
125
+ */
126
+ async _reloadModel(source) {
127
+ if (!this.scene) return;
222
128
 
223
- // Dispose previous meshes so we don’t accumulate them
224
- this._disposePreviousMeshes();
129
+ // Dispose de mallas anteriores
130
+ this.scene.meshes.slice().forEach(m => {
131
+ if (m.getClassName() === 'Mesh') m.dispose();
132
+ });
225
133
 
226
134
  try {
227
- console.log(`PrefViewer: ImportMeshAsync("${this.modelUrl}")`);
228
135
  const result = await SceneLoader.ImportMeshAsync(
229
- null,
230
- "",
231
- this.modelUrl,
232
- this.scene,
233
- undefined,
234
- // ".gltf"
136
+ null, '', source, this.scene
235
137
  );
236
- console.log("PrefViewer: Model loaded, creating default camera/light if needed");
237
138
  this.scene.createDefaultCameraOrLight(true, true, true);
238
-
239
- console.log("PrefViewer: Dispatching model-loaded event");
240
- this.dispatchEvent(
241
- new CustomEvent("model-loaded", {
242
- detail: {
243
- meshes: result.meshes,
244
- particleSystems: result.particleSystems
245
- },
246
- bubbles: true,
247
- composed: true
248
- })
249
- );
139
+ this.dispatchEvent(new CustomEvent('model-loaded', {
140
+ detail: { meshes: result.meshes, particleSystems: result.particleSystems },
141
+ bubbles: true,
142
+ composed: true
143
+ }));
250
144
  } catch (err) {
251
- console.error("PrefViewer: Error loading model:", err);
252
- console.log("PrefViewer: Dispatching model-error event");
253
- this.dispatchEvent(
254
- new CustomEvent("model-error", {
255
- detail: { error: err },
256
- bubbles: true,
257
- composed: true
258
- })
259
- );
145
+ console.error('PrefViewer: Error loading model:', err);
146
+ this.dispatchEvent(new CustomEvent('model-error', {
147
+ detail: { error: err },
148
+ bubbles: true,
149
+ composed: true
150
+ }));
260
151
  }
261
152
  }
262
153
 
263
- _disposePreviousMeshes() {
264
- console.log("PrefViewer: _disposePreviousMeshes");
265
- if (!this.scene) return;
266
- this.scene.meshes.slice().forEach((mesh) => {
267
- if (mesh.getClassName() === "Mesh") {
268
- console.log(`PrefViewer: Disposing mesh ${mesh.name}`);
269
- mesh.dispose();
270
- }
271
- });
272
- }
273
-
274
- // ====== Cleanup ======
275
154
  _disposeEngine() {
276
- console.log("PrefViewer: _disposeEngine");
277
- if (this.engine) {
278
- this.engine.dispose();
279
- this.engine = null;
280
- this.scene = null;
281
- this.camera = null;
282
- this.hemiLight = null;
283
- this.dirLight = null;
284
- }
155
+ if (!this.engine) return;
156
+ this.engine.dispose();
157
+ this.engine = null;
158
+ this.scene = null;
159
+ this.camera = null;
160
+ this.hemiLight = null;
161
+ this.dirLight = null;
285
162
  }
286
163
  }
287
164
 
288
- customElements.define("pref-viewer", PrefViewer);
165
+ customElements.define('pref-viewer', PrefViewer);