@preference-sl/pref-viewer 2.1.0 → 2.1.2
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 +54 -62
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,45 +1,3 @@
|
|
|
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
|
|
40
|
-
* -----------------------------------------------------------------------------
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
1
|
import {
|
|
44
2
|
Engine,
|
|
45
3
|
Scene,
|
|
@@ -55,16 +13,22 @@ import "@babylonjs/loaders";
|
|
|
55
13
|
class PrefViewer extends HTMLElement {
|
|
56
14
|
constructor() {
|
|
57
15
|
super();
|
|
16
|
+
console.log("PrefViewer: constructor");
|
|
58
17
|
this.attachShadow({ mode: "open" });
|
|
59
18
|
this._createCanvas();
|
|
60
19
|
this._wrapCanvas();
|
|
20
|
+
|
|
21
|
+
// These will be assigned in initBabylon():
|
|
61
22
|
this.engine = null;
|
|
62
23
|
this.scene = null;
|
|
63
24
|
this.camera = null;
|
|
64
25
|
this.hemiLight = null;
|
|
65
26
|
this.dirLight = null;
|
|
66
|
-
this.modelUrl = null;
|
|
67
27
|
this._onWindowResize = null;
|
|
28
|
+
|
|
29
|
+
// modelUrl may be set via attribute before connectedCallback
|
|
30
|
+
this.modelUrl = null;
|
|
31
|
+
this._hasInitialized = false;
|
|
68
32
|
}
|
|
69
33
|
|
|
70
34
|
static get observedAttributes() {
|
|
@@ -72,27 +36,45 @@ class PrefViewer extends HTMLElement {
|
|
|
72
36
|
}
|
|
73
37
|
|
|
74
38
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
39
|
+
console.log(`PrefViewer: attributeChangedCallback - ${name} -> ${newValue}`);
|
|
75
40
|
if (name === "model" && newValue) {
|
|
76
41
|
this.modelUrl = newValue;
|
|
77
|
-
this.
|
|
42
|
+
console.log(`PrefViewer: modelUrl set to ${this.modelUrl}`);
|
|
43
|
+
// Only reload if we've already initialized Babylon
|
|
44
|
+
if (this._hasInitialized) {
|
|
45
|
+
this._reloadModel();
|
|
46
|
+
}
|
|
78
47
|
}
|
|
79
48
|
}
|
|
80
49
|
|
|
81
50
|
connectedCallback() {
|
|
82
|
-
|
|
51
|
+
console.log("PrefViewer: connectedCallback");
|
|
52
|
+
// 1) Determine modelUrl (either from attribute or default)
|
|
83
53
|
if (!this.hasAttribute("model")) {
|
|
84
54
|
this.modelUrl = new URL("./models/patata.gltf", import.meta.url).href;
|
|
55
|
+
console.log(`PrefViewer: no model attribute, defaulting to ${this.modelUrl}`);
|
|
56
|
+
} else {
|
|
57
|
+
this.modelUrl = this.getAttribute("model");
|
|
58
|
+
console.log(`PrefViewer: model attribute present, using ${this.modelUrl}`);
|
|
85
59
|
}
|
|
60
|
+
|
|
61
|
+
// 2) Now initialize Babylon
|
|
86
62
|
this._initializeBabylon();
|
|
63
|
+
// 3) Mark that init is done
|
|
64
|
+
this._hasInitialized = true;
|
|
65
|
+
// 4) Finally, load whatever modelUrl we have
|
|
66
|
+
this._reloadModel();
|
|
87
67
|
}
|
|
88
68
|
|
|
89
69
|
disconnectedCallback() {
|
|
70
|
+
console.log("PrefViewer: disconnectedCallback - disposing engine");
|
|
90
71
|
this._disposeEngine();
|
|
91
72
|
window.removeEventListener("resize", this._onWindowResize);
|
|
92
73
|
}
|
|
93
74
|
|
|
94
75
|
// ====== Private setup methods ======
|
|
95
76
|
_createCanvas() {
|
|
77
|
+
console.log("PrefViewer: _createCanvas");
|
|
96
78
|
this.canvas = document.createElement("canvas");
|
|
97
79
|
Object.assign(this.canvas.style, {
|
|
98
80
|
width: "100%",
|
|
@@ -102,6 +84,7 @@ class PrefViewer extends HTMLElement {
|
|
|
102
84
|
}
|
|
103
85
|
|
|
104
86
|
_wrapCanvas() {
|
|
87
|
+
console.log("PrefViewer: _wrapCanvas");
|
|
105
88
|
const wrapper = document.createElement("div");
|
|
106
89
|
Object.assign(wrapper.style, {
|
|
107
90
|
width: "100%",
|
|
@@ -113,49 +96,55 @@ class PrefViewer extends HTMLElement {
|
|
|
113
96
|
}
|
|
114
97
|
|
|
115
98
|
_initializeBabylon() {
|
|
99
|
+
console.log("PrefViewer: _initializeBabylon - creating engine and scene");
|
|
116
100
|
// 1) Create engine and scene
|
|
117
101
|
this.engine = new Engine(this.canvas, true, { alpha: true });
|
|
118
102
|
this.scene = new Scene(this.engine);
|
|
119
103
|
this.scene.clearColor = new Color4(1, 1, 1, 1);
|
|
120
104
|
|
|
121
105
|
// 2) Hook into Babylon’s GLTF loader so "https://..." URIs aren't prefixed with blob:
|
|
106
|
+
console.log("PrefViewer: Adding preprocessUrl hook");
|
|
122
107
|
SceneLoader.OnPluginActivatedObservable.add((plugin) => {
|
|
108
|
+
console.log(`PrefViewer: Plugin activated - ${plugin.name}`);
|
|
123
109
|
if (plugin.name === "gltf" || plugin.name === "gltf2") {
|
|
124
110
|
plugin.preprocessUrl = (url) => {
|
|
125
|
-
// Normalize backslashes to forward slashes
|
|
126
111
|
const fixed = url.replace(/\\/g, "/");
|
|
127
|
-
|
|
112
|
+
console.log(`PrefViewer: preprocessUrl received "${url}", normalized to "${fixed}"`);
|
|
128
113
|
if (/^https?:\/\//i.test(fixed)) {
|
|
114
|
+
console.log(`PrefViewer: preprocessUrl returning absolute URL "${fixed}"`);
|
|
129
115
|
return fixed;
|
|
130
116
|
}
|
|
131
|
-
|
|
117
|
+
console.log(`PrefViewer: preprocessUrl returning relative URL "${fixed}"`);
|
|
132
118
|
return fixed;
|
|
133
119
|
};
|
|
134
120
|
}
|
|
135
121
|
});
|
|
136
122
|
|
|
137
123
|
// 3) Create camera and lights
|
|
124
|
+
console.log("PrefViewer: _createCamera and _createLights");
|
|
138
125
|
this._createCamera();
|
|
139
126
|
this._createLights();
|
|
140
127
|
|
|
141
128
|
// 4) Hook up input/event handlers
|
|
129
|
+
console.log("PrefViewer: _setupEventListeners");
|
|
142
130
|
this._setupEventListeners();
|
|
143
131
|
|
|
144
132
|
// 5) Start render loop
|
|
133
|
+
console.log("PrefViewer: Starting render loop");
|
|
145
134
|
this.engine.runRenderLoop(() => {
|
|
146
135
|
if (this.scene) {
|
|
147
136
|
this.scene.render();
|
|
148
137
|
}
|
|
149
138
|
});
|
|
150
|
-
this._onWindowResize = () =>
|
|
139
|
+
this._onWindowResize = () => {
|
|
140
|
+
console.log("PrefViewer: Window resized - calling engine.resize()");
|
|
141
|
+
this.engine.resize();
|
|
142
|
+
};
|
|
151
143
|
window.addEventListener("resize", this._onWindowResize);
|
|
152
|
-
|
|
153
|
-
// 6) Load the initial model (if modelUrl is already set)
|
|
154
|
-
this._reloadModel();
|
|
155
144
|
}
|
|
156
145
|
|
|
157
146
|
_createCamera() {
|
|
158
|
-
|
|
147
|
+
console.log("PrefViewer: _createCamera");
|
|
159
148
|
this.camera = new ArcRotateCamera(
|
|
160
149
|
"camera",
|
|
161
150
|
Math.PI / 2,
|
|
@@ -168,7 +157,7 @@ class PrefViewer extends HTMLElement {
|
|
|
168
157
|
}
|
|
169
158
|
|
|
170
159
|
_createLights() {
|
|
171
|
-
|
|
160
|
+
console.log("PrefViewer: _createLights");
|
|
172
161
|
this.hemiLight = new HemisphericLight(
|
|
173
162
|
"hemiLight",
|
|
174
163
|
new Vector3(0, 1, 0),
|
|
@@ -186,10 +175,9 @@ class PrefViewer extends HTMLElement {
|
|
|
186
175
|
}
|
|
187
176
|
|
|
188
177
|
_setupEventListeners() {
|
|
189
|
-
|
|
178
|
+
console.log("PrefViewer: _setupEventListeners");
|
|
190
179
|
this.canvas.addEventListener("wheel", (evt) => {
|
|
191
180
|
if (!this.scene || !this.camera) return;
|
|
192
|
-
|
|
193
181
|
const pickResult = this.scene.pick(
|
|
194
182
|
this.scene.pointerX,
|
|
195
183
|
this.scene.pointerY
|
|
@@ -197,7 +185,6 @@ class PrefViewer extends HTMLElement {
|
|
|
197
185
|
const pivotPoint = pickResult.hit
|
|
198
186
|
? pickResult.pickedPoint.clone()
|
|
199
187
|
: this.camera.target.clone();
|
|
200
|
-
|
|
201
188
|
this.camera.target = pivotPoint;
|
|
202
189
|
this.camera.inertialRadiusOffset +=
|
|
203
190
|
evt.deltaY * this.camera.wheelPrecision * 0.01;
|
|
@@ -207,7 +194,9 @@ class PrefViewer extends HTMLElement {
|
|
|
207
194
|
|
|
208
195
|
// ====== Model loading / management ======
|
|
209
196
|
async _reloadModel() {
|
|
197
|
+
console.log(`PrefViewer: _reloadModel - loading ${this.modelUrl}`);
|
|
210
198
|
if (!this.scene || !this.modelUrl) {
|
|
199
|
+
console.warn("PrefViewer: _reloadModel aborted (scene or modelUrl missing)");
|
|
211
200
|
return;
|
|
212
201
|
}
|
|
213
202
|
|
|
@@ -215,6 +204,7 @@ class PrefViewer extends HTMLElement {
|
|
|
215
204
|
this._disposePreviousMeshes();
|
|
216
205
|
|
|
217
206
|
try {
|
|
207
|
+
console.log(`PrefViewer: ImportMeshAsync("${this.modelUrl}")`);
|
|
218
208
|
const result = await SceneLoader.ImportMeshAsync(
|
|
219
209
|
null,
|
|
220
210
|
"",
|
|
@@ -223,10 +213,10 @@ class PrefViewer extends HTMLElement {
|
|
|
223
213
|
undefined,
|
|
224
214
|
".gltf"
|
|
225
215
|
);
|
|
226
|
-
|
|
216
|
+
console.log("PrefViewer: Model loaded, creating default camera/light if needed");
|
|
227
217
|
this.scene.createDefaultCameraOrLight(true, true, true);
|
|
228
218
|
|
|
229
|
-
|
|
219
|
+
console.log("PrefViewer: Dispatching model-loaded event");
|
|
230
220
|
this.dispatchEvent(
|
|
231
221
|
new CustomEvent("model-loaded", {
|
|
232
222
|
detail: {
|
|
@@ -238,8 +228,8 @@ class PrefViewer extends HTMLElement {
|
|
|
238
228
|
})
|
|
239
229
|
);
|
|
240
230
|
} catch (err) {
|
|
241
|
-
console.error("Error loading model:", err);
|
|
242
|
-
|
|
231
|
+
console.error("PrefViewer: Error loading model:", err);
|
|
232
|
+
console.log("PrefViewer: Dispatching model-error event");
|
|
243
233
|
this.dispatchEvent(
|
|
244
234
|
new CustomEvent("model-error", {
|
|
245
235
|
detail: { error: err },
|
|
@@ -251,16 +241,18 @@ class PrefViewer extends HTMLElement {
|
|
|
251
241
|
}
|
|
252
242
|
|
|
253
243
|
_disposePreviousMeshes() {
|
|
244
|
+
console.log("PrefViewer: _disposePreviousMeshes");
|
|
254
245
|
if (!this.scene) return;
|
|
255
246
|
this.scene.meshes.slice().forEach((mesh) => {
|
|
256
247
|
if (mesh.getClassName() === "Mesh") {
|
|
248
|
+
console.log(`PrefViewer: Disposing mesh ${mesh.name}`);
|
|
257
249
|
mesh.dispose();
|
|
258
250
|
}
|
|
259
251
|
});
|
|
260
252
|
}
|
|
261
253
|
|
|
262
|
-
// ====== Cleanup ======
|
|
263
254
|
_disposeEngine() {
|
|
255
|
+
console.log("PrefViewer: _disposeEngine");
|
|
264
256
|
if (this.engine) {
|
|
265
257
|
this.engine.dispose();
|
|
266
258
|
this.engine = null;
|