@preference-sl/pref-viewer 2.5.4 → 2.5.5
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 +50 -137
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -78,22 +78,16 @@ class PrefViewer extends HTMLElement {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
connectedCallback() {
|
|
81
|
-
// Set up URL preprocessing once
|
|
82
81
|
if (!this._pluginHookSetup) {
|
|
83
82
|
this._setupUrlPreprocessing();
|
|
84
83
|
this._pluginHookSetup = true;
|
|
85
84
|
}
|
|
86
85
|
|
|
87
|
-
// Require either a model URL or Base64 data
|
|
88
86
|
if (!this.hasAttribute("model") && !this.hasAttribute("model-data")) {
|
|
89
|
-
const
|
|
90
|
-
console.error(
|
|
87
|
+
const err = 'PrefViewer: No "model" or "model-data" attribute provided.';
|
|
88
|
+
console.error(err);
|
|
91
89
|
this.dispatchEvent(
|
|
92
|
-
new CustomEvent("model-error", {
|
|
93
|
-
detail: { error: new Error(errorMsg) },
|
|
94
|
-
bubbles: true,
|
|
95
|
-
composed: true
|
|
96
|
-
})
|
|
90
|
+
new CustomEvent("model-error", { detail: { error: new Error(err) }, bubbles: true, composed: true })
|
|
97
91
|
);
|
|
98
92
|
return;
|
|
99
93
|
}
|
|
@@ -105,32 +99,21 @@ class PrefViewer extends HTMLElement {
|
|
|
105
99
|
this.modelBase64 = this.getAttribute("model-data");
|
|
106
100
|
}
|
|
107
101
|
|
|
108
|
-
// Initialize Babylon (engine + scene + camera + lights + hooks)
|
|
109
102
|
this._initializeBabylon();
|
|
110
103
|
this._hasInitialized = true;
|
|
111
|
-
|
|
112
|
-
// Load the specified model
|
|
113
104
|
this._reloadModel();
|
|
114
105
|
}
|
|
115
106
|
|
|
116
107
|
disconnectedCallback() {
|
|
117
108
|
this._disposeEngine();
|
|
118
|
-
if (this._onWindowResize)
|
|
119
|
-
window.removeEventListener("resize", this._onWindowResize);
|
|
120
|
-
}
|
|
109
|
+
if (this._onWindowResize) window.removeEventListener("resize", this._onWindowResize);
|
|
121
110
|
}
|
|
122
111
|
|
|
123
|
-
// ====== URL Preprocessing ======
|
|
124
112
|
_setupUrlPreprocessing() {
|
|
125
113
|
const transformUrl = (url) => {
|
|
126
|
-
const stripped = url.replace(
|
|
127
|
-
|
|
128
|
-
"$1"
|
|
129
|
-
);
|
|
130
|
-
const fixedSlashes = stripped.replace(/\\/g, "/");
|
|
131
|
-
return /^https?:\/\//i.test(fixedSlashes) ? fixedSlashes : fixedSlashes;
|
|
114
|
+
const stripped = url.replace(/^blob:(?:http|https|file):\/\/[^\/]+\/(.+)/i, "$1");
|
|
115
|
+
return stripped.replace(/\\/g, "/");
|
|
132
116
|
};
|
|
133
|
-
|
|
134
117
|
SceneLoader.OnPluginActivatedObservable.add((plugin) => {
|
|
135
118
|
if (plugin.name === "gltf" || plugin.name === "gltf2") {
|
|
136
119
|
plugin.preprocessUrl = transformUrl;
|
|
@@ -139,23 +122,14 @@ class PrefViewer extends HTMLElement {
|
|
|
139
122
|
});
|
|
140
123
|
}
|
|
141
124
|
|
|
142
|
-
// ====== Setup Helpers ======
|
|
143
125
|
_createCanvas() {
|
|
144
126
|
this.canvas = document.createElement("canvas");
|
|
145
|
-
Object.assign(this.canvas.style, {
|
|
146
|
-
width: "100%",
|
|
147
|
-
height: "100%",
|
|
148
|
-
display: "block"
|
|
149
|
-
});
|
|
127
|
+
Object.assign(this.canvas.style, { width: "100%", height: "100%", display: "block" });
|
|
150
128
|
}
|
|
151
129
|
|
|
152
130
|
_wrapCanvas() {
|
|
153
131
|
const wrapper = document.createElement("div");
|
|
154
|
-
Object.assign(wrapper.style, {
|
|
155
|
-
width: "100%",
|
|
156
|
-
height: "100%",
|
|
157
|
-
position: "relative"
|
|
158
|
-
});
|
|
132
|
+
Object.assign(wrapper.style, { width: "100%", height: "100%", position: "relative" });
|
|
159
133
|
wrapper.appendChild(this.canvas);
|
|
160
134
|
this.shadowRoot.append(wrapper);
|
|
161
135
|
}
|
|
@@ -164,11 +138,9 @@ class PrefViewer extends HTMLElement {
|
|
|
164
138
|
this.engine = new Engine(this.canvas, true, { alpha: true });
|
|
165
139
|
this.scene = new Scene(this.engine);
|
|
166
140
|
this.scene.clearColor = new Color4(1, 1, 1, 1);
|
|
167
|
-
|
|
168
141
|
this._createCamera();
|
|
169
142
|
this._createLights();
|
|
170
143
|
this._setupEventListeners();
|
|
171
|
-
|
|
172
144
|
this.engine.runRenderLoop(() => this.scene && this.scene.render());
|
|
173
145
|
this._onWindowResize = () => this.engine && this.engine.resize();
|
|
174
146
|
window.addEventListener("resize", this._onWindowResize);
|
|
@@ -176,29 +148,16 @@ class PrefViewer extends HTMLElement {
|
|
|
176
148
|
|
|
177
149
|
_createCamera() {
|
|
178
150
|
this.camera = new ArcRotateCamera(
|
|
179
|
-
"camera",
|
|
180
|
-
Math.PI / 2,
|
|
181
|
-
Math.PI / 3,
|
|
182
|
-
10,
|
|
183
|
-
Vector3.Zero(),
|
|
184
|
-
this.scene
|
|
151
|
+
"camera", Math.PI/2, Math.PI/3, 10, Vector3.Zero(), this.scene
|
|
185
152
|
);
|
|
186
153
|
this.camera.attachControl(this.canvas, true);
|
|
187
154
|
}
|
|
188
155
|
|
|
189
156
|
_createLights() {
|
|
190
|
-
this.hemiLight = new HemisphericLight(
|
|
191
|
-
"hemiLight",
|
|
192
|
-
new Vector3(0, 1, 0),
|
|
193
|
-
this.scene
|
|
194
|
-
);
|
|
157
|
+
this.hemiLight = new HemisphericLight("hemiLight", new Vector3(0,1,0), this.scene);
|
|
195
158
|
this.hemiLight.intensity = 0.6;
|
|
196
|
-
this.dirLight = new DirectionalLight(
|
|
197
|
-
|
|
198
|
-
new Vector3(-0.5, -1, -0.5),
|
|
199
|
-
this.scene
|
|
200
|
-
);
|
|
201
|
-
this.dirLight.position = new Vector3(0, 5, 0);
|
|
159
|
+
this.dirLight = new DirectionalLight("dirLight", new Vector3(-0.5,-1,-0.5), this.scene);
|
|
160
|
+
this.dirLight.position = new Vector3(0,5,0);
|
|
202
161
|
this.dirLight.intensity = 0.8;
|
|
203
162
|
}
|
|
204
163
|
|
|
@@ -206,117 +165,71 @@ class PrefViewer extends HTMLElement {
|
|
|
206
165
|
this.canvas.addEventListener("wheel", (evt) => {
|
|
207
166
|
if (!this.scene || !this.camera) return;
|
|
208
167
|
const pick = this.scene.pick(this.scene.pointerX, this.scene.pointerY);
|
|
209
|
-
this.camera.target = pick.hit
|
|
210
|
-
? pick.pickedPoint.clone()
|
|
211
|
-
: this.camera.target;
|
|
168
|
+
this.camera.target = pick.hit ? pick.pickedPoint.clone() : this.camera.target;
|
|
212
169
|
this.camera.inertialRadiusOffset += evt.deltaY * this.camera.wheelPrecision * 0.01;
|
|
213
170
|
evt.preventDefault();
|
|
214
171
|
});
|
|
215
172
|
}
|
|
216
173
|
|
|
217
|
-
// ====== Model Management ======
|
|
218
174
|
async _reloadModel() {
|
|
219
|
-
if (!this.scene || (!this.modelUrl && !this.modelBase64))
|
|
220
|
-
console.warn("PrefViewer: _reloadModel aborted (no scene or no model)");
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
175
|
+
if (!this.scene || (!this.modelUrl && !this.modelBase64)) return;
|
|
223
176
|
this._disposePreviousMeshes();
|
|
224
|
-
|
|
225
177
|
try {
|
|
226
178
|
let result;
|
|
227
179
|
if (this.modelBase64) {
|
|
228
|
-
const blob = this._createBlobFromBase64(this.modelBase64);
|
|
229
|
-
const
|
|
230
|
-
const fileName = `model${ext}`;
|
|
180
|
+
const { blob, extension } = this._createBlobFromBase64(this.modelBase64);
|
|
181
|
+
const fileName = `model${extension}`;
|
|
231
182
|
const file = new File([blob], fileName, { type: blob.type });
|
|
232
|
-
console.log('[PrefViewer]
|
|
233
|
-
result = await SceneLoader.ImportMeshAsync(
|
|
234
|
-
null,
|
|
235
|
-
"",
|
|
236
|
-
file,
|
|
237
|
-
this.scene,
|
|
238
|
-
undefined,
|
|
239
|
-
ext
|
|
240
|
-
);
|
|
183
|
+
console.log('[PrefViewer] Loading from Base64 as File:', fileName);
|
|
184
|
+
result = await SceneLoader.ImportMeshAsync(null, "", file, this.scene, undefined, extension);
|
|
241
185
|
} else {
|
|
242
|
-
const ext =
|
|
243
|
-
result = await SceneLoader.ImportMeshAsync(
|
|
244
|
-
null,
|
|
245
|
-
"",
|
|
246
|
-
this.modelUrl,
|
|
247
|
-
this.scene,
|
|
248
|
-
undefined,
|
|
249
|
-
ext
|
|
250
|
-
);
|
|
186
|
+
const ext = (this.modelUrl.match(/\.(gltf|glb)(\?|#|$)/i) || [])[1]?.toLowerCase() || 'gltf';
|
|
187
|
+
result = await SceneLoader.ImportMeshAsync(null, "", this.modelUrl, this.scene, undefined, `.${ext}`);
|
|
251
188
|
}
|
|
252
189
|
this.scene.createDefaultCameraOrLight(true, true, true);
|
|
253
|
-
this.dispatchEvent(
|
|
254
|
-
new CustomEvent("model-loaded", {
|
|
255
|
-
detail: { meshes: result.meshes, particleSystems: result.particleSystems },
|
|
256
|
-
bubbles: true,
|
|
257
|
-
composed: true
|
|
258
|
-
})
|
|
259
|
-
);
|
|
190
|
+
this.dispatchEvent(new CustomEvent("model-loaded", { detail: result, bubbles: true, composed: true }));
|
|
260
191
|
} catch (err) {
|
|
261
192
|
console.error("PrefViewer: Error loading model:", err);
|
|
262
|
-
this.dispatchEvent(
|
|
263
|
-
new CustomEvent("model-error", {
|
|
264
|
-
detail: { error: err },
|
|
265
|
-
bubbles: true,
|
|
266
|
-
composed: true
|
|
267
|
-
})
|
|
268
|
-
);
|
|
193
|
+
this.dispatchEvent(new CustomEvent("model-error", { detail: { error: err }, bubbles: true, composed: true }));
|
|
269
194
|
}
|
|
270
195
|
}
|
|
271
196
|
|
|
272
197
|
_disposePreviousMeshes() {
|
|
273
198
|
if (!this.scene) return;
|
|
274
|
-
this.scene.meshes.slice().forEach(
|
|
199
|
+
this.scene.meshes.slice().forEach(m => m.getClassName()==="Mesh" && m.dispose());
|
|
275
200
|
}
|
|
276
201
|
|
|
277
202
|
_createBlobFromBase64(base64) {
|
|
278
|
-
console.log('[PrefViewer]
|
|
279
|
-
const [prefix,
|
|
280
|
-
|
|
281
|
-
let
|
|
282
|
-
if (prefix && prefix.startsWith('data:')) {
|
|
283
|
-
const end = prefix.indexOf(';');
|
|
284
|
-
mimeType = prefix.substring(5, end >= 0 ? end : prefix.length) || mimeType;
|
|
285
|
-
}
|
|
286
|
-
console.log('[PrefViewer] inferred mimeType:', mimeType);
|
|
287
|
-
const raw = data ?? base64;
|
|
203
|
+
console.log('[PrefViewer] Decoding Base64...');
|
|
204
|
+
const [prefix, payload] = base64.split(',');
|
|
205
|
+
const raw = payload ?? base64;
|
|
206
|
+
let decoded;
|
|
288
207
|
try {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
return blob;
|
|
294
|
-
} catch (err) {
|
|
295
|
-
console.error('[PrefViewer] Failed to decode Base64 or create Blob:', err);
|
|
296
|
-
this.dispatchEvent(
|
|
297
|
-
new CustomEvent('model-error', {
|
|
298
|
-
detail: { error: err },
|
|
299
|
-
bubbles: true,
|
|
300
|
-
composed: true
|
|
301
|
-
})
|
|
302
|
-
);
|
|
303
|
-
throw err;
|
|
208
|
+
decoded = atob(raw);
|
|
209
|
+
} catch (e) {
|
|
210
|
+
console.error('[PrefViewer] atob failed:', e);
|
|
211
|
+
throw e;
|
|
304
212
|
}
|
|
213
|
+
// detect JSON vs binary glb
|
|
214
|
+
let isJson = false;
|
|
215
|
+
try {
|
|
216
|
+
JSON.parse(decoded);
|
|
217
|
+
isJson = true;
|
|
218
|
+
} catch {}
|
|
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));
|
|
223
|
+
const blob = new Blob([array], { type });
|
|
224
|
+
console.log('[PrefViewer] Created Blob of size', blob.size);
|
|
225
|
+
return { blob, extension };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
_disposeEngine() {
|
|
229
|
+
if (!this.engine) return;
|
|
230
|
+
this.engine.dispose();
|
|
231
|
+
this.engine = this.scene = this.camera = this.hemiLight = this.dirLight = null;
|
|
305
232
|
}
|
|
306
|
-
|
|
307
|
-
_getExtensionFromMimeType(mimeType) {
|
|
308
|
-
if (mimeType.includes("json")) return ".gltf";
|
|
309
|
-
if (mimeType.includes("glb") || mimeType === "application/octet-stream") return ".glb";
|
|
310
|
-
return ".gltf";
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
_getExtensionFromUrl(url) {
|
|
314
|
-
const match = url.match(/\.(gltf|glb)(\?|#|$)/i);
|
|
315
|
-
return match ? `.${match[1].toLowerCase()}` : ".gltf";
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// ====== Cleanup ======
|
|
319
|
-
_disposeEngine() { if (this.engine) { this.engine.dispose(); this.engine = null; this.scene = null; this.camera = null; this.hemiLight = null; this.dirLight = null; } }
|
|
320
233
|
}
|
|
321
234
|
|
|
322
235
|
customElements.define("pref-viewer", PrefViewer);
|