@preference-sl/pref-viewer 2.1.8 → 2.2.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.
- package/package.json +1 -1
- package/src/index.js +328 -0
- package/src/index.ts +0 -174
- package/src/models/patata.gltf +0 -10059
- package/src/test.global.html +0 -240
- package/src/test.html +0 -62
package/package.json
CHANGED
package/src/index.js
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
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/window.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 {
|
|
44
|
+
Engine,
|
|
45
|
+
Scene,
|
|
46
|
+
ArcRotateCamera,
|
|
47
|
+
Vector3,
|
|
48
|
+
SceneLoader,
|
|
49
|
+
Color4,
|
|
50
|
+
HemisphericLight,
|
|
51
|
+
DirectionalLight
|
|
52
|
+
} from "@babylonjs/core";
|
|
53
|
+
import "@babylonjs/loaders";
|
|
54
|
+
|
|
55
|
+
class PrefViewer extends HTMLElement {
|
|
56
|
+
constructor() {
|
|
57
|
+
super();
|
|
58
|
+
console.log("PrefViewer: constructor");
|
|
59
|
+
this.attachShadow({ mode: "open" });
|
|
60
|
+
this._createCanvas();
|
|
61
|
+
this._wrapCanvas();
|
|
62
|
+
|
|
63
|
+
// These will be set in _initializeBabylon()
|
|
64
|
+
this.engine = null;
|
|
65
|
+
this.scene = null;
|
|
66
|
+
this.camera = null;
|
|
67
|
+
this.hemiLight = null;
|
|
68
|
+
this.dirLight = null;
|
|
69
|
+
this._onWindowResize = null;
|
|
70
|
+
|
|
71
|
+
// modelUrl might be provided via attribute before connectedCallback
|
|
72
|
+
this.modelUrl = null;
|
|
73
|
+
this._hasInitialized = false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static get observedAttributes() {
|
|
77
|
+
return ["model"];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
attributeChangedCallback(name, _oldValue, newValue) {
|
|
81
|
+
console.log(`PrefViewer: attributeChangedCallback - ${name} -> ${newValue}`);
|
|
82
|
+
if (name === "model" && newValue) {
|
|
83
|
+
this.modelUrl = newValue;
|
|
84
|
+
console.log(`PrefViewer: modelUrl set to ${this.modelUrl}`);
|
|
85
|
+
// Only reload if initialization has already happened
|
|
86
|
+
if (this._hasInitialized) {
|
|
87
|
+
this._reloadModel();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
connectedCallback() {
|
|
93
|
+
console.log("PrefViewer: connectedCallback");
|
|
94
|
+
// 1) Determine modelUrl now that element is connected
|
|
95
|
+
if (!this.hasAttribute("model")) {
|
|
96
|
+
this.modelUrl = new URL("./models/window.gltf", import.meta.url).href;
|
|
97
|
+
console.log(`PrefViewer: no model attribute, defaulting to ${this.modelUrl}`);
|
|
98
|
+
} else {
|
|
99
|
+
this.modelUrl = this.getAttribute("model");
|
|
100
|
+
console.log(`PrefViewer: model attribute present, using ${this.modelUrl}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 2) Initialize Babylon (engine + scene + camera + lights + hooks)
|
|
104
|
+
this._initializeBabylon();
|
|
105
|
+
|
|
106
|
+
// 3) Mark that initialization is done
|
|
107
|
+
this._hasInitialized = true;
|
|
108
|
+
|
|
109
|
+
// 4) Load whatever modelUrl we have
|
|
110
|
+
this._reloadModel();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
disconnectedCallback() {
|
|
114
|
+
console.log("PrefViewer: disconnectedCallback - disposing engine");
|
|
115
|
+
this._disposeEngine();
|
|
116
|
+
window.removeEventListener("resize", this._onWindowResize);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ====== Private setup methods ======
|
|
120
|
+
_createCanvas() {
|
|
121
|
+
console.log("PrefViewer: _createCanvas");
|
|
122
|
+
this.canvas = document.createElement("canvas");
|
|
123
|
+
Object.assign(this.canvas.style, {
|
|
124
|
+
width: "100%",
|
|
125
|
+
height: "100%",
|
|
126
|
+
display: "block"
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
_wrapCanvas() {
|
|
131
|
+
console.log("PrefViewer: _wrapCanvas");
|
|
132
|
+
const wrapper = document.createElement("div");
|
|
133
|
+
Object.assign(wrapper.style, {
|
|
134
|
+
width: "100%",
|
|
135
|
+
height: "100%",
|
|
136
|
+
position: "relative"
|
|
137
|
+
});
|
|
138
|
+
wrapper.appendChild(this.canvas);
|
|
139
|
+
this.shadowRoot.append(wrapper);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
_initializeBabylon() {
|
|
143
|
+
console.log("PrefViewer: _initializeBabylon - creating engine and scene");
|
|
144
|
+
|
|
145
|
+
// 1) Create the Babylon engine & scene
|
|
146
|
+
this.engine = new Engine(this.canvas, true, { alpha: true });
|
|
147
|
+
this.scene = new Scene(this.engine);
|
|
148
|
+
this.scene.clearColor = new Color4(1, 1, 1, 1);
|
|
149
|
+
|
|
150
|
+
// 2) Hook into Babylon’s GLTF loader so that any URIs starting with "blob:…"
|
|
151
|
+
// get stripped off before we check for an absolute "https://".
|
|
152
|
+
console.log("PrefViewer: Adding preprocessUrl hook");
|
|
153
|
+
SceneLoader.OnPluginActivatedObservable.add((plugin) => {
|
|
154
|
+
console.log(`PrefViewer: Plugin activated - ${plugin.name}`);
|
|
155
|
+
if (plugin.name === "gltf" || plugin.name === "gltf2") {
|
|
156
|
+
plugin.preprocessUrl = (url) => {
|
|
157
|
+
// a) If the loader already prepended "blob:…", strip it out.
|
|
158
|
+
// Regex explanation: ^blob:(?:file|https?|ftp):\/\/[^\/]+\/(.*)
|
|
159
|
+
// basically removes the entire "blob:http://localhost:3000/" prefix.
|
|
160
|
+
const stripped = url.replace(
|
|
161
|
+
/^blob:(?:http|https|file):\/\/[^\/]+\/(.+)/i,
|
|
162
|
+
"$1"
|
|
163
|
+
);
|
|
164
|
+
// b) Normalize backslashes "\" → forward slashes "/"
|
|
165
|
+
const fixedSlashes = stripped.replace(/\\/g, "/");
|
|
166
|
+
console.log(
|
|
167
|
+
`PrefViewer: preprocessUrl received "${url}", stripped to "${stripped}", normalized to "${fixedSlashes}"`
|
|
168
|
+
);
|
|
169
|
+
// c) If it now starts with "http://" or "https://", return it as an absolute URL:
|
|
170
|
+
if (/^https?:\/\//i.test(fixedSlashes)) {
|
|
171
|
+
console.log(`PrefViewer: preprocessUrl returning absolute URL "${fixedSlashes}"`);
|
|
172
|
+
return fixedSlashes;
|
|
173
|
+
}
|
|
174
|
+
// d) Otherwise, return the relative path (Babylon will resolve it relative to the blob if needed)
|
|
175
|
+
console.log(`PrefViewer: preprocessUrl returning relative URL "${fixedSlashes}"`);
|
|
176
|
+
return fixedSlashes;
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// 3) Create camera and lights
|
|
182
|
+
console.log("PrefViewer: _createCamera and _createLights");
|
|
183
|
+
this._createCamera();
|
|
184
|
+
this._createLights();
|
|
185
|
+
|
|
186
|
+
// 4) Hook up input/event handlers (e.g. wheel-to-zoom)
|
|
187
|
+
console.log("PrefViewer: _setupEventListeners");
|
|
188
|
+
this._setupEventListeners();
|
|
189
|
+
|
|
190
|
+
// 5) Start Babylon’s render loop
|
|
191
|
+
console.log("PrefViewer: Starting render loop");
|
|
192
|
+
this.engine.runRenderLoop(() => {
|
|
193
|
+
if (this.scene) {
|
|
194
|
+
this.scene.render();
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
this._onWindowResize = () => {
|
|
198
|
+
console.log("PrefViewer: Window resized - calling engine.resize()");
|
|
199
|
+
this.engine.resize();
|
|
200
|
+
};
|
|
201
|
+
window.addEventListener("resize", this._onWindowResize);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
_createCamera() {
|
|
205
|
+
console.log("PrefViewer: _createCamera");
|
|
206
|
+
// ArcRotateCamera that orbits around origin
|
|
207
|
+
this.camera = new ArcRotateCamera(
|
|
208
|
+
"camera",
|
|
209
|
+
Math.PI / 2,
|
|
210
|
+
Math.PI / 3,
|
|
211
|
+
10,
|
|
212
|
+
Vector3.Zero(),
|
|
213
|
+
this.scene
|
|
214
|
+
);
|
|
215
|
+
this.camera.attachControl(this.canvas, true);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
_createLights() {
|
|
219
|
+
console.log("PrefViewer: _createLights");
|
|
220
|
+
this.hemiLight = new HemisphericLight(
|
|
221
|
+
"hemiLight",
|
|
222
|
+
new Vector3(0, 1, 0),
|
|
223
|
+
this.scene
|
|
224
|
+
);
|
|
225
|
+
this.hemiLight.intensity = 0.6;
|
|
226
|
+
|
|
227
|
+
this.dirLight = new DirectionalLight(
|
|
228
|
+
"dirLight",
|
|
229
|
+
new Vector3(-0.5, -1, -0.5),
|
|
230
|
+
this.scene
|
|
231
|
+
);
|
|
232
|
+
this.dirLight.position = new Vector3(0, 5, 0);
|
|
233
|
+
this.dirLight.intensity = 0.8;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
_setupEventListeners() {
|
|
237
|
+
console.log("PrefViewer: _setupEventListeners");
|
|
238
|
+
// Zoom toward point-of-interest on wheel scroll
|
|
239
|
+
this.canvas.addEventListener("wheel", (evt) => {
|
|
240
|
+
if (!this.scene || !this.camera) return;
|
|
241
|
+
const pickResult = this.scene.pick(
|
|
242
|
+
this.scene.pointerX,
|
|
243
|
+
this.scene.pointerY
|
|
244
|
+
);
|
|
245
|
+
const pivotPoint = pickResult.hit
|
|
246
|
+
? pickResult.pickedPoint.clone()
|
|
247
|
+
: this.camera.target.clone();
|
|
248
|
+
this.camera.target = pivotPoint;
|
|
249
|
+
this.camera.inertialRadiusOffset +=
|
|
250
|
+
evt.deltaY * this.camera.wheelPrecision * 0.01;
|
|
251
|
+
evt.preventDefault();
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ====== Model loading / management ======
|
|
256
|
+
async _reloadModel() {
|
|
257
|
+
console.log(`PrefViewer: _reloadModel - loading ${this.modelUrl}`);
|
|
258
|
+
if (!this.scene || !this.modelUrl) {
|
|
259
|
+
console.warn("PrefViewer: _reloadModel aborted (scene or modelUrl missing)");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Dispose previous meshes so we don’t accumulate them
|
|
264
|
+
this._disposePreviousMeshes();
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
console.log(`PrefViewer: ImportMeshAsync("${this.modelUrl}")`);
|
|
268
|
+
const result = await SceneLoader.ImportMeshAsync(
|
|
269
|
+
null,
|
|
270
|
+
"",
|
|
271
|
+
this.modelUrl,
|
|
272
|
+
this.scene,
|
|
273
|
+
undefined,
|
|
274
|
+
".gltf"
|
|
275
|
+
);
|
|
276
|
+
console.log("PrefViewer: Model loaded, creating default camera/light if needed");
|
|
277
|
+
this.scene.createDefaultCameraOrLight(true, true, true);
|
|
278
|
+
|
|
279
|
+
console.log("PrefViewer: Dispatching model-loaded event");
|
|
280
|
+
this.dispatchEvent(
|
|
281
|
+
new CustomEvent("model-loaded", {
|
|
282
|
+
detail: {
|
|
283
|
+
meshes: result.meshes,
|
|
284
|
+
particleSystems: result.particleSystems
|
|
285
|
+
},
|
|
286
|
+
bubbles: true,
|
|
287
|
+
composed: true
|
|
288
|
+
})
|
|
289
|
+
);
|
|
290
|
+
} catch (err) {
|
|
291
|
+
console.error("PrefViewer: Error loading model:", err);
|
|
292
|
+
console.log("PrefViewer: Dispatching model-error event");
|
|
293
|
+
this.dispatchEvent(
|
|
294
|
+
new CustomEvent("model-error", {
|
|
295
|
+
detail: { error: err },
|
|
296
|
+
bubbles: true,
|
|
297
|
+
composed: true
|
|
298
|
+
})
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
_disposePreviousMeshes() {
|
|
304
|
+
console.log("PrefViewer: _disposePreviousMeshes");
|
|
305
|
+
if (!this.scene) return;
|
|
306
|
+
this.scene.meshes.slice().forEach((mesh) => {
|
|
307
|
+
if (mesh.getClassName() === "Mesh") {
|
|
308
|
+
console.log(`PrefViewer: Disposing mesh ${mesh.name}`);
|
|
309
|
+
mesh.dispose();
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ====== Cleanup ======
|
|
315
|
+
_disposeEngine() {
|
|
316
|
+
console.log("PrefViewer: _disposeEngine");
|
|
317
|
+
if (this.engine) {
|
|
318
|
+
this.engine.dispose();
|
|
319
|
+
this.engine = null;
|
|
320
|
+
this.scene = null;
|
|
321
|
+
this.camera = null;
|
|
322
|
+
this.hemiLight = null;
|
|
323
|
+
this.dirLight = null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
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);
|