@preference-sl/pref-viewer 2.1.8 → 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 (3) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +165 -0
  3. package/src/index.ts +0 -174
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preference-sl/pref-viewer",
3
- "version": "2.1.8",
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 ADDED
@@ -0,0 +1,165 @@
1
+ import { Engine, Scene, ArcRotateCamera, Vector3, SceneLoader, Color4, HemisphericLight, DirectionalLight } from "@babylonjs/core";
2
+ import "@babylonjs/loaders";
3
+
4
+ class PrefViewer extends HTMLElement {
5
+ constructor() {
6
+ super();
7
+ this.attachShadow({ mode: 'open' });
8
+
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
26
+ this.engine = null;
27
+ this.scene = null;
28
+ this.camera = null;
29
+ this.hemiLight = null;
30
+ this.dirLight = null;
31
+ this._onWindowResize = null;
32
+ this.modelFile = null;
33
+ this._hasInitialized = false;
34
+ }
35
+
36
+ static get observedAttributes() {
37
+ return ['model'];
38
+ }
39
+
40
+ attributeChangedCallback(name, _oldValue, newValue) {
41
+ if (name === 'model' && newValue && this._hasInitialized) {
42
+ this._reloadModel(newValue);
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
+ /**
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);
83
+ }
84
+
85
+ _initializeBabylon() {
86
+ this.engine = new Engine(this.canvas, true, { alpha: true });
87
+ this.scene = new Scene(this.engine);
88
+ this.scene.clearColor = new Color4(1, 1, 1, 1);
89
+
90
+ // Cámara orbitadora
91
+ this.camera = new ArcRotateCamera(
92
+ 'camera', Math.PI / 2, Math.PI / 3, 10,
93
+ Vector3.Zero(), this.scene
94
+ );
95
+ this.camera.attachControl(this.canvas, true);
96
+
97
+ // Iluminación básica
98
+ this.hemiLight = new HemisphericLight('hemi', new Vector3(0, 1, 0), this.scene);
99
+ this.hemiLight.intensity = 0.6;
100
+ this.dirLight = new DirectionalLight('dir', new Vector3(-0.5, -1, -0.5), this.scene);
101
+ this.dirLight.position = new Vector3(0, 5, 0);
102
+ this.dirLight.intensity = 0.8;
103
+
104
+ // Zoom con rueda apuntando al punto
105
+ this.canvas.addEventListener('wheel', evt => {
106
+ if (!this.scene || !this.camera) return;
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;
111
+ evt.preventDefault();
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);
120
+ }
121
+
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;
128
+
129
+ // Dispose de mallas anteriores
130
+ this.scene.meshes.slice().forEach(m => {
131
+ if (m.getClassName() === 'Mesh') m.dispose();
132
+ });
133
+
134
+ try {
135
+ const result = await SceneLoader.ImportMeshAsync(
136
+ null, '', source, this.scene
137
+ );
138
+ this.scene.createDefaultCameraOrLight(true, true, true);
139
+ this.dispatchEvent(new CustomEvent('model-loaded', {
140
+ detail: { meshes: result.meshes, particleSystems: result.particleSystems },
141
+ bubbles: true,
142
+ composed: true
143
+ }));
144
+ } catch (err) {
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
+ }));
151
+ }
152
+ }
153
+
154
+ _disposeEngine() {
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;
162
+ }
163
+ }
164
+
165
+ customElements.define('pref-viewer', PrefViewer);
package/src/index.ts DELETED
@@ -1,174 +0,0 @@
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);