@preference-sl/pref-viewer 2.5.5 → 2.7.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 +81 -71
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -6,25 +6,28 @@
|
|
|
6
6
|
* Overview
|
|
7
7
|
* --------
|
|
8
8
|
* `PrefViewer` is a self-contained Web Component built with Babylon.js that:
|
|
9
|
-
* •
|
|
10
|
-
* •
|
|
11
|
-
* •
|
|
12
|
-
* •
|
|
13
|
-
* • Dispatches “model-loaded” and “model-error” CustomEvents so host pages can react.
|
|
9
|
+
* • Renders glTF or GLB models in a <canvas> inside its shadow DOM.
|
|
10
|
+
* • Supports loading via remote URLs (`model` attribute) or Base64 data URIs (`model-data` attribute).
|
|
11
|
+
* • Automatically handles scene creation (engine, camera, lighting) and resource cleanup.
|
|
12
|
+
* • Emits `model-loaded` and `model-error` events for integration.
|
|
14
13
|
*
|
|
15
14
|
* Usage
|
|
16
15
|
* -----
|
|
16
|
+
* Load from a URL:
|
|
17
|
+
* ```html
|
|
17
18
|
* <pref-viewer
|
|
18
|
-
* model="https://example.com/models/myModel.
|
|
19
|
+
* model="https://example.com/models/myModel.glb"
|
|
19
20
|
* style="width:800px; height:600px;">
|
|
20
21
|
* </pref-viewer>
|
|
22
|
+
* ```
|
|
21
23
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
+
* Load from Base64 data:
|
|
25
|
+
* ```html
|
|
24
26
|
* <pref-viewer
|
|
25
27
|
* model-data="data:application/gltf+json;base64,UEsDB..."
|
|
26
28
|
* style="width:800px; height:600px;">
|
|
27
29
|
* </pref-viewer>
|
|
30
|
+
* ```
|
|
28
31
|
*/
|
|
29
32
|
import {
|
|
30
33
|
Engine,
|
|
@@ -34,7 +37,9 @@ import {
|
|
|
34
37
|
SceneLoader,
|
|
35
38
|
Color4,
|
|
36
39
|
HemisphericLight,
|
|
37
|
-
DirectionalLight
|
|
40
|
+
DirectionalLight,
|
|
41
|
+
PointLight,
|
|
42
|
+
ShadowGenerator
|
|
38
43
|
} from "@babylonjs/core";
|
|
39
44
|
import "@babylonjs/loaders";
|
|
40
45
|
|
|
@@ -53,38 +58,38 @@ class PrefViewer extends HTMLElement {
|
|
|
53
58
|
this.dirLight = null;
|
|
54
59
|
this._onWindowResize = null;
|
|
55
60
|
|
|
56
|
-
// Model
|
|
61
|
+
// Model sources
|
|
57
62
|
this.modelUrl = null;
|
|
58
63
|
this.modelBase64 = null;
|
|
59
64
|
|
|
60
|
-
this.
|
|
61
|
-
this.
|
|
65
|
+
this._initialized = false;
|
|
66
|
+
this._urlHooked = false;
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
static get observedAttributes() {
|
|
65
70
|
return ["model", "model-data"];
|
|
66
71
|
}
|
|
67
72
|
|
|
68
|
-
attributeChangedCallback(name,
|
|
73
|
+
attributeChangedCallback(name, _old, value) {
|
|
69
74
|
if (name === "model") {
|
|
70
|
-
this.modelUrl =
|
|
75
|
+
this.modelUrl = value;
|
|
71
76
|
this.modelBase64 = null;
|
|
72
|
-
|
|
77
|
+
this._initialized && this._reloadModel();
|
|
73
78
|
} else if (name === "model-data") {
|
|
74
|
-
this.modelBase64 =
|
|
79
|
+
this.modelBase64 = value;
|
|
75
80
|
this.modelUrl = null;
|
|
76
|
-
|
|
81
|
+
this._initialized && this._reloadModel();
|
|
77
82
|
}
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
connectedCallback() {
|
|
81
|
-
if (!this.
|
|
86
|
+
if (!this._urlHooked) {
|
|
82
87
|
this._setupUrlPreprocessing();
|
|
83
|
-
this.
|
|
88
|
+
this._urlHooked = true;
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
if (!this.hasAttribute("model") && !this.hasAttribute("model-data")) {
|
|
87
|
-
const err = 'PrefViewer:
|
|
92
|
+
const err = 'PrefViewer: provide either "model" or "model-data" attribute.';
|
|
88
93
|
console.error(err);
|
|
89
94
|
this.dispatchEvent(
|
|
90
95
|
new CustomEvent("model-error", { detail: { error: new Error(err) }, bubbles: true, composed: true })
|
|
@@ -92,28 +97,21 @@ class PrefViewer extends HTMLElement {
|
|
|
92
97
|
return;
|
|
93
98
|
}
|
|
94
99
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
if (this.hasAttribute("model-data")) {
|
|
99
|
-
this.modelBase64 = this.getAttribute("model-data");
|
|
100
|
-
}
|
|
100
|
+
this.modelUrl = this.getAttribute("model");
|
|
101
|
+
this.modelBase64 = this.getAttribute("model-data");
|
|
101
102
|
|
|
102
103
|
this._initializeBabylon();
|
|
103
|
-
this.
|
|
104
|
+
this._initialized = true;
|
|
104
105
|
this._reloadModel();
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
disconnectedCallback() {
|
|
108
109
|
this._disposeEngine();
|
|
109
|
-
|
|
110
|
+
this._onWindowResize && window.removeEventListener("resize", this._onWindowResize);
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
_setupUrlPreprocessing() {
|
|
113
|
-
const transformUrl = (url) =>
|
|
114
|
-
const stripped = url.replace(/^blob:(?:http|https|file):\/\/[^\/]+\/(.+)/i, "$1");
|
|
115
|
-
return stripped.replace(/\\/g, "/");
|
|
116
|
-
};
|
|
114
|
+
const transformUrl = (url) => url.replace(/^blob:[^/]+\//i, "").replace(/\\/g, "/");
|
|
117
115
|
SceneLoader.OnPluginActivatedObservable.add((plugin) => {
|
|
118
116
|
if (plugin.name === "gltf" || plugin.name === "gltf2") {
|
|
119
117
|
plugin.preprocessUrl = transformUrl;
|
|
@@ -140,7 +138,8 @@ class PrefViewer extends HTMLElement {
|
|
|
140
138
|
this.scene.clearColor = new Color4(1, 1, 1, 1);
|
|
141
139
|
this._createCamera();
|
|
142
140
|
this._createLights();
|
|
143
|
-
this.
|
|
141
|
+
this._setupInteraction();
|
|
142
|
+
|
|
144
143
|
this.engine.runRenderLoop(() => this.scene && this.scene.render());
|
|
145
144
|
this._onWindowResize = () => this.engine && this.engine.resize();
|
|
146
145
|
window.addEventListener("resize", this._onWindowResize);
|
|
@@ -148,20 +147,39 @@ class PrefViewer extends HTMLElement {
|
|
|
148
147
|
|
|
149
148
|
_createCamera() {
|
|
150
149
|
this.camera = new ArcRotateCamera(
|
|
151
|
-
"camera",
|
|
150
|
+
"camera",
|
|
151
|
+
Math.PI / 2,
|
|
152
|
+
Math.PI / 3,
|
|
153
|
+
10,
|
|
154
|
+
Vector3.Zero(),
|
|
155
|
+
this.scene
|
|
152
156
|
);
|
|
153
157
|
this.camera.attachControl(this.canvas, true);
|
|
154
158
|
}
|
|
155
159
|
|
|
156
160
|
_createLights() {
|
|
157
|
-
|
|
158
|
-
this.hemiLight
|
|
159
|
-
this.
|
|
160
|
-
|
|
161
|
-
|
|
161
|
+
// Ambient hemisphere for general fill
|
|
162
|
+
this.hemiLight = new HemisphericLight("hemiLight", new Vector3(0, 1, 0), this.scene);
|
|
163
|
+
this.hemiLight.intensity = 0.8; // stronger ambient
|
|
164
|
+
|
|
165
|
+
// Main “sun” light, but softer
|
|
166
|
+
this.dirLight = new DirectionalLight("dirLight", new Vector3(-0.5, -1, -0.5), this.scene);
|
|
167
|
+
this.dirLight.position = new Vector3(0, 5, 0);
|
|
168
|
+
this.dirLight.intensity = 0.3; // much lower than before
|
|
169
|
+
|
|
170
|
+
// Optional: very light, blurred shadows just for form (darkness: 0 → no shadows, 1 → full)
|
|
171
|
+
const shadowGen = new ShadowGenerator(1024, this.dirLight);
|
|
172
|
+
shadowGen.useBlurExponentialShadowMap = true; // soften edges
|
|
173
|
+
shadowGen.blurKernel = 16;
|
|
174
|
+
shadowGen.darkness = 0.2; // practically “skimmed milk” shadows
|
|
175
|
+
|
|
176
|
+
// Headlight: ensure the camera always shines on what you’re looking at
|
|
177
|
+
this.cameraLight = new PointLight("cameraLight", this.camera.position, this.scene);
|
|
178
|
+
this.cameraLight.parent = this.camera;
|
|
179
|
+
this.cameraLight.intensity = 0.5;
|
|
162
180
|
}
|
|
163
181
|
|
|
164
|
-
|
|
182
|
+
_setupInteraction() {
|
|
165
183
|
this.canvas.addEventListener("wheel", (evt) => {
|
|
166
184
|
if (!this.scene || !this.camera) return;
|
|
167
185
|
const pick = this.scene.pick(this.scene.pointerX, this.scene.pointerY);
|
|
@@ -174,54 +192,46 @@ class PrefViewer extends HTMLElement {
|
|
|
174
192
|
async _reloadModel() {
|
|
175
193
|
if (!this.scene || (!this.modelUrl && !this.modelBase64)) return;
|
|
176
194
|
this._disposePreviousMeshes();
|
|
195
|
+
|
|
177
196
|
try {
|
|
178
197
|
let result;
|
|
179
198
|
if (this.modelBase64) {
|
|
180
|
-
const { blob, extension } = this.
|
|
181
|
-
const
|
|
182
|
-
const file = new File([blob], fileName, { type: blob.type });
|
|
183
|
-
console.log('[PrefViewer] Loading from Base64 as File:', fileName);
|
|
199
|
+
const { blob, extension } = this._decodeBase64(this.modelBase64);
|
|
200
|
+
const file = new File([blob], `model${extension}`, { type: blob.type });
|
|
184
201
|
result = await SceneLoader.ImportMeshAsync(null, "", file, this.scene, undefined, extension);
|
|
185
202
|
} else {
|
|
186
|
-
const
|
|
187
|
-
|
|
203
|
+
const extMatch = this.modelUrl.match(/\.(gltf|glb)(\?|#|$)/i);
|
|
204
|
+
const extension = extMatch ? `.${extMatch[1].toLowerCase()}` : ".gltf";
|
|
205
|
+
result = await SceneLoader.ImportMeshAsync(null, "", this.modelUrl, this.scene, undefined, extension);
|
|
188
206
|
}
|
|
207
|
+
|
|
189
208
|
this.scene.createDefaultCameraOrLight(true, true, true);
|
|
190
|
-
this.dispatchEvent(
|
|
209
|
+
this.dispatchEvent(
|
|
210
|
+
new CustomEvent("model-loaded", { detail: result, bubbles: true, composed: true })
|
|
211
|
+
);
|
|
191
212
|
} catch (err) {
|
|
192
|
-
console.error("PrefViewer:
|
|
193
|
-
this.dispatchEvent(
|
|
213
|
+
console.error("PrefViewer: failed to load model", err);
|
|
214
|
+
this.dispatchEvent(
|
|
215
|
+
new CustomEvent("model-error", { detail: { error: err }, bubbles: true, composed: true })
|
|
216
|
+
);
|
|
194
217
|
}
|
|
195
218
|
}
|
|
196
219
|
|
|
197
220
|
_disposePreviousMeshes() {
|
|
198
221
|
if (!this.scene) return;
|
|
199
|
-
this.scene.meshes.slice().forEach(m => m.getClassName()==="Mesh" && m.dispose());
|
|
222
|
+
this.scene.meshes.slice().forEach((m) => m.getClassName() === "Mesh" && m.dispose());
|
|
200
223
|
}
|
|
201
224
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
let decoded;
|
|
207
|
-
try {
|
|
208
|
-
decoded = atob(raw);
|
|
209
|
-
} catch (e) {
|
|
210
|
-
console.error('[PrefViewer] atob failed:', e);
|
|
211
|
-
throw e;
|
|
212
|
-
}
|
|
213
|
-
// detect JSON vs binary glb
|
|
225
|
+
_decodeBase64(base64) {
|
|
226
|
+
const [, payload] = base64.split(",");
|
|
227
|
+
const raw = payload || base64;
|
|
228
|
+
const decoded = atob(raw);
|
|
214
229
|
let isJson = false;
|
|
215
|
-
try {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const extension = isJson ? '.gltf' : '.glb';
|
|
220
|
-
const type = isJson ? 'model/gltf+json' : 'model/gltf-binary';
|
|
221
|
-
console.log('[PrefViewer] Detected format:', extension, type);
|
|
222
|
-
const array = Uint8Array.from(decoded, c => c.charCodeAt(0));
|
|
230
|
+
try { JSON.parse(decoded); isJson = true; } catch { }
|
|
231
|
+
const extension = isJson ? ".gltf" : ".glb";
|
|
232
|
+
const type = isJson ? "model/gltf+json" : "model/gltf-binary";
|
|
233
|
+
const array = Uint8Array.from(decoded, (c) => c.charCodeAt(0));
|
|
223
234
|
const blob = new Blob([array], { type });
|
|
224
|
-
console.log('[PrefViewer] Created Blob of size', blob.size);
|
|
225
235
|
return { blob, extension };
|
|
226
236
|
}
|
|
227
237
|
|