@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.
- package/package.json +1 -1
- package/src/index.js +165 -0
- package/src/index.ts +0 -174
package/package.json
CHANGED
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);
|