@preference-sl/pref-viewer 2.1.7 → 2.1.8

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 (3) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +174 -0
  3. package/src/index.js +0 -288
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.8",
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.ts ADDED
@@ -0,0 +1,174 @@
1
+ import { Engine, Scene, ArcRotateCamera, Vector3, SceneLoader, Color4, HemisphericLight, DirectionalLight,} from "@babylonjs/core";
2
+ import "@babylonjs/loaders";
3
+
4
+ class PrefViewer extends HTMLElement {
5
+ private canvas!: HTMLCanvasElement;
6
+ private engine: Engine | null = null;
7
+ private scene: Scene | null = null;
8
+ private camera: ArcRotateCamera | null = null;
9
+ private hemiLight: HemisphericLight | null = null;
10
+ private dirLight: DirectionalLight | null = null;
11
+ private _onWindowResize: (() => void) | null = null;
12
+ private modelFile: File | null = null;
13
+ private _hasInitialized = false;
14
+
15
+ constructor() {
16
+ super();
17
+ this.attachShadow({ mode: "open" });
18
+ // Canvas + wrapper
19
+ this.canvas = document.createElement("canvas");
20
+ Object.assign(this.canvas.style, {
21
+ width: "100%",
22
+ height: "100%",
23
+ display: "block",
24
+ });
25
+ const wrapper = document.createElement("div");
26
+ Object.assign(wrapper.style, {
27
+ width: "100%",
28
+ height: "100%",
29
+ position: "relative",
30
+ });
31
+ wrapper.appendChild(this.canvas);
32
+ this.shadowRoot!.append(wrapper);
33
+ }
34
+
35
+ static get observedAttributes() {
36
+ return ["model"];
37
+ }
38
+
39
+ attributeChangedCallback(name: string, _old: string|null, newVal: string|null) {
40
+ if (name === "model" && newVal && this._hasInitialized) {
41
+ // si alguien cambia el atributo model, recargamos desde URL
42
+ this._reloadModel(newVal);
43
+ }
44
+ }
45
+
46
+ connectedCallback() {
47
+ this._initializeBabylon();
48
+ this._hasInitialized = true;
49
+ }
50
+
51
+ disconnectedCallback() {
52
+ this._disposeEngine();
53
+ if (this._onWindowResize) {
54
+ window.removeEventListener("resize", this._onWindowResize);
55
+ }
56
+ }
57
+
58
+ /** API pública: recibe bytes + flag isJson, empaqueta y carga */
59
+ public loadFromBytes(bytes: Uint8Array, isJson: boolean) {
60
+ // 1) crear Blob + URL
61
+ const isGlb =
62
+ bytes.length > 4 &&
63
+ bytes[0] === 0x67 &&
64
+ bytes[1] === 0x6c &&
65
+ bytes[2] === 0x54 &&
66
+ bytes[3] === 0x46;
67
+ const mimeType = isGlb
68
+ ? "model/gltf-binary"
69
+ : isJson
70
+ ? "model/gltf+json"
71
+ : "application/octet-stream";
72
+
73
+ const file = new File([bytes], isGlb ? "model.glb" : "model.gltf", { type: mimeType });
74
+ this.modelFile = file;
75
+
76
+ // 2) Llamar internamente a _reloadModel con el File
77
+ this._reloadModel(file);
78
+ }
79
+
80
+ private _initializeBabylon() {
81
+ this.engine = new Engine(this.canvas, true, { alpha: true });
82
+ this.scene = new Scene(this.engine);
83
+ this.scene.clearColor = new Color4(1, 1, 1, 1);
84
+
85
+ this.camera = new ArcRotateCamera(
86
+ "camera",
87
+ Math.PI / 2,
88
+ Math.PI / 3,
89
+ 10,
90
+ Vector3.Zero(),
91
+ this.scene
92
+ );
93
+ this.camera.attachControl(this.canvas, true);
94
+
95
+ this.hemiLight = new HemisphericLight("hemi", new Vector3(0, 1, 0), this.scene);
96
+ this.hemiLight.intensity = 0.6;
97
+ this.dirLight = new DirectionalLight("dir", new Vector3(-0.5, -1, -0.5), this.scene);
98
+ this.dirLight.position = new Vector3(0, 5, 0);
99
+ this.dirLight.intensity = 0.8;
100
+
101
+ this.canvas.addEventListener("wheel", (evt) => {
102
+ if (!this.scene || !this.camera) return;
103
+ const pick = this.scene.pick(this.scene.pointerX, this.scene.pointerY);
104
+ const pivot = pick.hit ? pick.pickedPoint.clone() : this.camera.target.clone();
105
+ this.camera.target = pivot;
106
+ this.camera.inertialRadiusOffset += evt.deltaY * this.camera.wheelPrecision * 0.01;
107
+ evt.preventDefault();
108
+ });
109
+
110
+ this.engine.runRenderLoop(() => {
111
+ if (this.scene) {
112
+ this.scene.render();
113
+ }
114
+ });
115
+ this._onWindowResize = () => this.engine!.resize();
116
+ window.addEventListener("resize", this._onWindowResize);
117
+ }
118
+
119
+ /**
120
+ * Carga desde URL (string) o File
121
+ */
122
+ private async _reloadModel(source: string|File) {
123
+ if (!this.scene) return;
124
+ // Dispose anteriores
125
+ this.scene.meshes.slice().forEach((m) => {
126
+ if (m.getClassName() === "Mesh") m.dispose();
127
+ });
128
+
129
+ try {
130
+ const result = await SceneLoader.ImportMeshAsync(
131
+ null,
132
+ "",
133
+ source,
134
+ this.scene
135
+ );
136
+ this.scene.createDefaultCameraOrLight(true, true, true);
137
+ this.dispatchEvent(
138
+ new CustomEvent("model-loaded", {
139
+ detail: { meshes: result.meshes, particleSystems: result.particleSystems },
140
+ bubbles: true,
141
+ composed: true,
142
+ })
143
+ );
144
+ } catch (err) {
145
+ console.error("PrefViewer: Error loading model:", err);
146
+ this.dispatchEvent(
147
+ new CustomEvent("model-error", {
148
+ detail: { error: err },
149
+ bubbles: true,
150
+ composed: true,
151
+ })
152
+ );
153
+ } finally {
154
+ // revoke URL si venía de File
155
+ if (source instanceof File) {
156
+ // no tenemos la URL directa, pero Babylon la creó internamente
157
+ // y la revocará al dispose del engine
158
+ }
159
+ }
160
+ }
161
+
162
+ private _disposeEngine() {
163
+ if (this.engine) {
164
+ this.engine.dispose();
165
+ this.engine = null;
166
+ this.scene = null;
167
+ this.camera = null;
168
+ this.hemiLight = null;
169
+ this.dirLight = null;
170
+ }
171
+ }
172
+ }
173
+
174
+ customElements.define("pref-viewer", PrefViewer);
package/src/index.js DELETED
@@ -1,288 +0,0 @@
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
- import { Engine, Scene, ArcRotateCamera, Vector3, SceneLoader, Color4, HemisphericLight, DirectionalLight } from "@babylonjs/core";
44
- import "@babylonjs/loaders";
45
-
46
- class PrefViewer extends HTMLElement {
47
- constructor() {
48
- super();
49
- console.log("PrefViewer: constructor");
50
- this.attachShadow({ mode: "open" });
51
- this._createCanvas();
52
- this._wrapCanvas();
53
-
54
- // These will be set in _initializeBabylon()
55
- this.engine = null;
56
- this.scene = null;
57
- this.camera = null;
58
- this.hemiLight = null;
59
- this.dirLight = null;
60
- this._onWindowResize = null;
61
-
62
- // modelUrl might be provided via attribute before connectedCallback
63
- this.modelUrl = null;
64
- this._hasInitialized = false;
65
- }
66
-
67
- static get observedAttributes() {
68
- return ["model"];
69
- }
70
-
71
- 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
- }
80
- }
81
- }
82
-
83
- 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
- this._initializeBabylon();
96
-
97
- // 3) Mark that initialization is done
98
- this._hasInitialized = true;
99
-
100
- // 4) Load whatever modelUrl we have
101
- this._reloadModel();
102
- }
103
-
104
- disconnectedCallback() {
105
- console.log("PrefViewer: disconnectedCallback - disposing engine");
106
- this._disposeEngine();
107
- window.removeEventListener("resize", this._onWindowResize);
108
- }
109
-
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);
131
- }
132
-
133
- _initializeBabylon() {
134
- console.log("PrefViewer: _initializeBabylon - creating engine and scene");
135
-
136
- // 1) Create the Babylon engine & scene
137
- this.engine = new Engine(this.canvas, true, { alpha: true });
138
- this.scene = new Scene(this.engine);
139
- this.scene.clearColor = new Color4(1, 1, 1, 1);
140
-
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
167
- this.camera = new ArcRotateCamera(
168
- "camera",
169
- Math.PI / 2,
170
- Math.PI / 3,
171
- 10,
172
- Vector3.Zero(),
173
- this.scene
174
- );
175
- this.camera.attachControl(this.canvas, true);
176
- }
177
-
178
- _createLights() {
179
- console.log("PrefViewer: _createLights");
180
- this.hemiLight = new HemisphericLight(
181
- "hemiLight",
182
- new Vector3(0, 1, 0),
183
- this.scene
184
- );
185
- 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
- );
192
- this.dirLight.position = new Vector3(0, 5, 0);
193
- this.dirLight.intensity = 0.8;
194
- }
195
-
196
- _setupEventListeners() {
197
- console.log("PrefViewer: _setupEventListeners");
198
- // Zoom toward point-of-interest on wheel scroll
199
- this.canvas.addEventListener("wheel", (evt) => {
200
- 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;
211
- evt.preventDefault();
212
- });
213
- }
214
-
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
- }
222
-
223
- // Dispose previous meshes so we don’t accumulate them
224
- this._disposePreviousMeshes();
225
-
226
- try {
227
- console.log(`PrefViewer: ImportMeshAsync("${this.modelUrl}")`);
228
- const result = await SceneLoader.ImportMeshAsync(
229
- null,
230
- "",
231
- this.modelUrl,
232
- this.scene,
233
- undefined,
234
- // ".gltf"
235
- );
236
- console.log("PrefViewer: Model loaded, creating default camera/light if needed");
237
- 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
- );
250
- } 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
- );
260
- }
261
- }
262
-
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
- _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
- }
285
- }
286
- }
287
-
288
- customElements.define("pref-viewer", PrefViewer);