@preference-sl/pref-viewer 2.5.5 → 2.6.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 +62 -68
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,
|
|
@@ -53,38 +56,38 @@ class PrefViewer extends HTMLElement {
|
|
|
53
56
|
this.dirLight = null;
|
|
54
57
|
this._onWindowResize = null;
|
|
55
58
|
|
|
56
|
-
// Model
|
|
59
|
+
// Model sources
|
|
57
60
|
this.modelUrl = null;
|
|
58
61
|
this.modelBase64 = null;
|
|
59
62
|
|
|
60
|
-
this.
|
|
61
|
-
this.
|
|
63
|
+
this._initialized = false;
|
|
64
|
+
this._urlHooked = false;
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
static get observedAttributes() {
|
|
65
68
|
return ["model", "model-data"];
|
|
66
69
|
}
|
|
67
70
|
|
|
68
|
-
attributeChangedCallback(name,
|
|
71
|
+
attributeChangedCallback(name, _old, value) {
|
|
69
72
|
if (name === "model") {
|
|
70
|
-
this.modelUrl =
|
|
73
|
+
this.modelUrl = value;
|
|
71
74
|
this.modelBase64 = null;
|
|
72
|
-
|
|
75
|
+
this._initialized && this._reloadModel();
|
|
73
76
|
} else if (name === "model-data") {
|
|
74
|
-
this.modelBase64 =
|
|
77
|
+
this.modelBase64 = value;
|
|
75
78
|
this.modelUrl = null;
|
|
76
|
-
|
|
79
|
+
this._initialized && this._reloadModel();
|
|
77
80
|
}
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
connectedCallback() {
|
|
81
|
-
if (!this.
|
|
84
|
+
if (!this._urlHooked) {
|
|
82
85
|
this._setupUrlPreprocessing();
|
|
83
|
-
this.
|
|
86
|
+
this._urlHooked = true;
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
if (!this.hasAttribute("model") && !this.hasAttribute("model-data")) {
|
|
87
|
-
const err = 'PrefViewer:
|
|
90
|
+
const err = 'PrefViewer: provide either "model" or "model-data" attribute.';
|
|
88
91
|
console.error(err);
|
|
89
92
|
this.dispatchEvent(
|
|
90
93
|
new CustomEvent("model-error", { detail: { error: new Error(err) }, bubbles: true, composed: true })
|
|
@@ -92,28 +95,21 @@ class PrefViewer extends HTMLElement {
|
|
|
92
95
|
return;
|
|
93
96
|
}
|
|
94
97
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
if (this.hasAttribute("model-data")) {
|
|
99
|
-
this.modelBase64 = this.getAttribute("model-data");
|
|
100
|
-
}
|
|
98
|
+
this.modelUrl = this.getAttribute("model");
|
|
99
|
+
this.modelBase64 = this.getAttribute("model-data");
|
|
101
100
|
|
|
102
101
|
this._initializeBabylon();
|
|
103
|
-
this.
|
|
102
|
+
this._initialized = true;
|
|
104
103
|
this._reloadModel();
|
|
105
104
|
}
|
|
106
105
|
|
|
107
106
|
disconnectedCallback() {
|
|
108
107
|
this._disposeEngine();
|
|
109
|
-
|
|
108
|
+
this._onWindowResize && window.removeEventListener("resize", this._onWindowResize);
|
|
110
109
|
}
|
|
111
110
|
|
|
112
111
|
_setupUrlPreprocessing() {
|
|
113
|
-
const transformUrl = (url) =>
|
|
114
|
-
const stripped = url.replace(/^blob:(?:http|https|file):\/\/[^\/]+\/(.+)/i, "$1");
|
|
115
|
-
return stripped.replace(/\\/g, "/");
|
|
116
|
-
};
|
|
112
|
+
const transformUrl = (url) => url.replace(/^blob:[^/]+\//i, "").replace(/\\/g, "/");
|
|
117
113
|
SceneLoader.OnPluginActivatedObservable.add((plugin) => {
|
|
118
114
|
if (plugin.name === "gltf" || plugin.name === "gltf2") {
|
|
119
115
|
plugin.preprocessUrl = transformUrl;
|
|
@@ -140,7 +136,8 @@ class PrefViewer extends HTMLElement {
|
|
|
140
136
|
this.scene.clearColor = new Color4(1, 1, 1, 1);
|
|
141
137
|
this._createCamera();
|
|
142
138
|
this._createLights();
|
|
143
|
-
this.
|
|
139
|
+
this._setupInteraction();
|
|
140
|
+
|
|
144
141
|
this.engine.runRenderLoop(() => this.scene && this.scene.render());
|
|
145
142
|
this._onWindowResize = () => this.engine && this.engine.resize();
|
|
146
143
|
window.addEventListener("resize", this._onWindowResize);
|
|
@@ -148,20 +145,25 @@ class PrefViewer extends HTMLElement {
|
|
|
148
145
|
|
|
149
146
|
_createCamera() {
|
|
150
147
|
this.camera = new ArcRotateCamera(
|
|
151
|
-
"camera",
|
|
148
|
+
"camera",
|
|
149
|
+
Math.PI / 2,
|
|
150
|
+
Math.PI / 3,
|
|
151
|
+
10,
|
|
152
|
+
Vector3.Zero(),
|
|
153
|
+
this.scene
|
|
152
154
|
);
|
|
153
155
|
this.camera.attachControl(this.canvas, true);
|
|
154
156
|
}
|
|
155
157
|
|
|
156
158
|
_createLights() {
|
|
157
|
-
this.hemiLight = new HemisphericLight("hemiLight", new Vector3(0,1,0), this.scene);
|
|
159
|
+
this.hemiLight = new HemisphericLight("hemiLight", new Vector3(0, 1, 0), this.scene);
|
|
158
160
|
this.hemiLight.intensity = 0.6;
|
|
159
|
-
this.dirLight = new DirectionalLight("dirLight", new Vector3(-0.5
|
|
160
|
-
this.dirLight.position = new Vector3(0,5,0);
|
|
161
|
+
this.dirLight = new DirectionalLight("dirLight", new Vector3(-0.5, -1, -0.5), this.scene);
|
|
162
|
+
this.dirLight.position = new Vector3(0, 5, 0);
|
|
161
163
|
this.dirLight.intensity = 0.8;
|
|
162
164
|
}
|
|
163
165
|
|
|
164
|
-
|
|
166
|
+
_setupInteraction() {
|
|
165
167
|
this.canvas.addEventListener("wheel", (evt) => {
|
|
166
168
|
if (!this.scene || !this.camera) return;
|
|
167
169
|
const pick = this.scene.pick(this.scene.pointerX, this.scene.pointerY);
|
|
@@ -174,54 +176,46 @@ class PrefViewer extends HTMLElement {
|
|
|
174
176
|
async _reloadModel() {
|
|
175
177
|
if (!this.scene || (!this.modelUrl && !this.modelBase64)) return;
|
|
176
178
|
this._disposePreviousMeshes();
|
|
179
|
+
|
|
177
180
|
try {
|
|
178
181
|
let result;
|
|
179
182
|
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);
|
|
183
|
+
const { blob, extension } = this._decodeBase64(this.modelBase64);
|
|
184
|
+
const file = new File([blob], `model${extension}`, { type: blob.type });
|
|
184
185
|
result = await SceneLoader.ImportMeshAsync(null, "", file, this.scene, undefined, extension);
|
|
185
186
|
} else {
|
|
186
|
-
const
|
|
187
|
-
|
|
187
|
+
const extMatch = this.modelUrl.match(/\.(gltf|glb)(\?|#|$)/i);
|
|
188
|
+
const extension = extMatch ? `.${extMatch[1].toLowerCase()}` : ".gltf";
|
|
189
|
+
result = await SceneLoader.ImportMeshAsync(null, "", this.modelUrl, this.scene, undefined, extension);
|
|
188
190
|
}
|
|
191
|
+
|
|
189
192
|
this.scene.createDefaultCameraOrLight(true, true, true);
|
|
190
|
-
this.dispatchEvent(
|
|
193
|
+
this.dispatchEvent(
|
|
194
|
+
new CustomEvent("model-loaded", { detail: result, bubbles: true, composed: true })
|
|
195
|
+
);
|
|
191
196
|
} catch (err) {
|
|
192
|
-
console.error("PrefViewer:
|
|
193
|
-
this.dispatchEvent(
|
|
197
|
+
console.error("PrefViewer: failed to load model", err);
|
|
198
|
+
this.dispatchEvent(
|
|
199
|
+
new CustomEvent("model-error", { detail: { error: err }, bubbles: true, composed: true })
|
|
200
|
+
);
|
|
194
201
|
}
|
|
195
202
|
}
|
|
196
203
|
|
|
197
204
|
_disposePreviousMeshes() {
|
|
198
205
|
if (!this.scene) return;
|
|
199
|
-
this.scene.meshes.slice().forEach(m => m.getClassName()==="Mesh" && m.dispose());
|
|
206
|
+
this.scene.meshes.slice().forEach((m) => m.getClassName() === "Mesh" && m.dispose());
|
|
200
207
|
}
|
|
201
208
|
|
|
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
|
|
209
|
+
_decodeBase64(base64) {
|
|
210
|
+
const [, payload] = base64.split(",");
|
|
211
|
+
const raw = payload || base64;
|
|
212
|
+
const decoded = atob(raw);
|
|
214
213
|
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));
|
|
214
|
+
try { JSON.parse(decoded); isJson = true; } catch {}
|
|
215
|
+
const extension = isJson ? ".gltf" : ".glb";
|
|
216
|
+
const type = isJson ? "model/gltf+json" : "model/gltf-binary";
|
|
217
|
+
const array = Uint8Array.from(decoded, (c) => c.charCodeAt(0));
|
|
223
218
|
const blob = new Blob([array], { type });
|
|
224
|
-
console.log('[PrefViewer] Created Blob of size', blob.size);
|
|
225
219
|
return { blob, extension };
|
|
226
220
|
}
|
|
227
221
|
|