@preference-sl/pref-viewer 2.5.3 → 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 +52 -166
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,35 +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
|
-
if (/^https?:\/\//i.test(fixedSlashes)) {
|
|
132
|
-
return fixedSlashes;
|
|
133
|
-
}
|
|
134
|
-
return fixedSlashes;
|
|
114
|
+
const stripped = url.replace(/^blob:(?:http|https|file):\/\/[^\/]+\/(.+)/i, "$1");
|
|
115
|
+
return stripped.replace(/\\/g, "/");
|
|
135
116
|
};
|
|
136
|
-
|
|
137
117
|
SceneLoader.OnPluginActivatedObservable.add((plugin) => {
|
|
138
118
|
if (plugin.name === "gltf" || plugin.name === "gltf2") {
|
|
139
119
|
plugin.preprocessUrl = transformUrl;
|
|
@@ -142,23 +122,14 @@ class PrefViewer extends HTMLElement {
|
|
|
142
122
|
});
|
|
143
123
|
}
|
|
144
124
|
|
|
145
|
-
// ====== Setup Helpers ======
|
|
146
125
|
_createCanvas() {
|
|
147
126
|
this.canvas = document.createElement("canvas");
|
|
148
|
-
Object.assign(this.canvas.style, {
|
|
149
|
-
width: "100%",
|
|
150
|
-
height: "100%",
|
|
151
|
-
display: "block"
|
|
152
|
-
});
|
|
127
|
+
Object.assign(this.canvas.style, { width: "100%", height: "100%", display: "block" });
|
|
153
128
|
}
|
|
154
129
|
|
|
155
130
|
_wrapCanvas() {
|
|
156
131
|
const wrapper = document.createElement("div");
|
|
157
|
-
Object.assign(wrapper.style, {
|
|
158
|
-
width: "100%",
|
|
159
|
-
height: "100%",
|
|
160
|
-
position: "relative"
|
|
161
|
-
});
|
|
132
|
+
Object.assign(wrapper.style, { width: "100%", height: "100%", position: "relative" });
|
|
162
133
|
wrapper.appendChild(this.canvas);
|
|
163
134
|
this.shadowRoot.append(wrapper);
|
|
164
135
|
}
|
|
@@ -167,182 +138,97 @@ class PrefViewer extends HTMLElement {
|
|
|
167
138
|
this.engine = new Engine(this.canvas, true, { alpha: true });
|
|
168
139
|
this.scene = new Scene(this.engine);
|
|
169
140
|
this.scene.clearColor = new Color4(1, 1, 1, 1);
|
|
170
|
-
|
|
171
141
|
this._createCamera();
|
|
172
142
|
this._createLights();
|
|
173
143
|
this._setupEventListeners();
|
|
174
|
-
|
|
175
|
-
this.
|
|
176
|
-
if (this.scene) this.scene.render();
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
this._onWindowResize = () => {
|
|
180
|
-
if (this.engine) this.engine.resize();
|
|
181
|
-
};
|
|
144
|
+
this.engine.runRenderLoop(() => this.scene && this.scene.render());
|
|
145
|
+
this._onWindowResize = () => this.engine && this.engine.resize();
|
|
182
146
|
window.addEventListener("resize", this._onWindowResize);
|
|
183
147
|
}
|
|
184
148
|
|
|
185
149
|
_createCamera() {
|
|
186
150
|
this.camera = new ArcRotateCamera(
|
|
187
|
-
"camera",
|
|
188
|
-
Math.PI / 2,
|
|
189
|
-
Math.PI / 3,
|
|
190
|
-
10,
|
|
191
|
-
Vector3.Zero(),
|
|
192
|
-
this.scene
|
|
151
|
+
"camera", Math.PI/2, Math.PI/3, 10, Vector3.Zero(), this.scene
|
|
193
152
|
);
|
|
194
153
|
this.camera.attachControl(this.canvas, true);
|
|
195
154
|
}
|
|
196
155
|
|
|
197
156
|
_createLights() {
|
|
198
|
-
this.hemiLight = new HemisphericLight(
|
|
199
|
-
"hemiLight",
|
|
200
|
-
new Vector3(0, 1, 0),
|
|
201
|
-
this.scene
|
|
202
|
-
);
|
|
157
|
+
this.hemiLight = new HemisphericLight("hemiLight", new Vector3(0,1,0), this.scene);
|
|
203
158
|
this.hemiLight.intensity = 0.6;
|
|
204
|
-
|
|
205
|
-
this.dirLight = new
|
|
206
|
-
"dirLight",
|
|
207
|
-
new Vector3(-0.5, -1, -0.5),
|
|
208
|
-
this.scene
|
|
209
|
-
);
|
|
210
|
-
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);
|
|
211
161
|
this.dirLight.intensity = 0.8;
|
|
212
162
|
}
|
|
213
163
|
|
|
214
164
|
_setupEventListeners() {
|
|
215
165
|
this.canvas.addEventListener("wheel", (evt) => {
|
|
216
166
|
if (!this.scene || !this.camera) return;
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
);
|
|
221
|
-
const pivotPoint = pickResult.hit
|
|
222
|
-
? pickResult.pickedPoint.clone()
|
|
223
|
-
: this.camera.target.clone();
|
|
224
|
-
this.camera.target = pivotPoint;
|
|
225
|
-
this.camera.inertialRadiusOffset +=
|
|
226
|
-
evt.deltaY * this.camera.wheelPrecision * 0.01;
|
|
167
|
+
const pick = this.scene.pick(this.scene.pointerX, this.scene.pointerY);
|
|
168
|
+
this.camera.target = pick.hit ? pick.pickedPoint.clone() : this.camera.target;
|
|
169
|
+
this.camera.inertialRadiusOffset += evt.deltaY * this.camera.wheelPrecision * 0.01;
|
|
227
170
|
evt.preventDefault();
|
|
228
171
|
});
|
|
229
172
|
}
|
|
230
173
|
|
|
231
|
-
// ====== Model Management ======
|
|
232
174
|
async _reloadModel() {
|
|
233
|
-
if (!this.scene || (!this.modelUrl && !this.modelBase64))
|
|
234
|
-
console.warn("PrefViewer: _reloadModel aborted (no scene or no model)");
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
|
|
175
|
+
if (!this.scene || (!this.modelUrl && !this.modelBase64)) return;
|
|
238
176
|
this._disposePreviousMeshes();
|
|
239
|
-
|
|
240
177
|
try {
|
|
241
178
|
let result;
|
|
242
179
|
if (this.modelBase64) {
|
|
243
|
-
const blob = this._createBlobFromBase64(this.modelBase64);
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
blob,
|
|
249
|
-
this.scene,
|
|
250
|
-
undefined,
|
|
251
|
-
ext
|
|
252
|
-
);
|
|
180
|
+
const { blob, extension } = this._createBlobFromBase64(this.modelBase64);
|
|
181
|
+
const fileName = `model${extension}`;
|
|
182
|
+
const file = new File([blob], fileName, { type: blob.type });
|
|
183
|
+
console.log('[PrefViewer] Loading from Base64 as File:', fileName);
|
|
184
|
+
result = await SceneLoader.ImportMeshAsync(null, "", file, this.scene, undefined, extension);
|
|
253
185
|
} else {
|
|
254
|
-
const ext =
|
|
255
|
-
result = await SceneLoader.ImportMeshAsync(
|
|
256
|
-
null,
|
|
257
|
-
"",
|
|
258
|
-
this.modelUrl,
|
|
259
|
-
this.scene,
|
|
260
|
-
undefined,
|
|
261
|
-
ext
|
|
262
|
-
);
|
|
186
|
+
const ext = (this.modelUrl.match(/\.(gltf|glb)(\?|#|$)/i) || [])[1]?.toLowerCase() || 'gltf';
|
|
187
|
+
result = await SceneLoader.ImportMeshAsync(null, "", this.modelUrl, this.scene, undefined, `.${ext}`);
|
|
263
188
|
}
|
|
264
|
-
|
|
265
189
|
this.scene.createDefaultCameraOrLight(true, true, true);
|
|
266
|
-
this.dispatchEvent(
|
|
267
|
-
new CustomEvent("model-loaded", {
|
|
268
|
-
detail: {
|
|
269
|
-
meshes: result.meshes,
|
|
270
|
-
particleSystems: result.particleSystems
|
|
271
|
-
},
|
|
272
|
-
bubbles: true,
|
|
273
|
-
composed: true
|
|
274
|
-
})
|
|
275
|
-
);
|
|
190
|
+
this.dispatchEvent(new CustomEvent("model-loaded", { detail: result, bubbles: true, composed: true }));
|
|
276
191
|
} catch (err) {
|
|
277
192
|
console.error("PrefViewer: Error loading model:", err);
|
|
278
|
-
this.dispatchEvent(
|
|
279
|
-
new CustomEvent("model-error", {
|
|
280
|
-
detail: { error: err },
|
|
281
|
-
bubbles: true,
|
|
282
|
-
composed: true
|
|
283
|
-
})
|
|
284
|
-
);
|
|
193
|
+
this.dispatchEvent(new CustomEvent("model-error", { detail: { error: err }, bubbles: true, composed: true }));
|
|
285
194
|
}
|
|
286
195
|
}
|
|
287
196
|
|
|
288
197
|
_disposePreviousMeshes() {
|
|
289
198
|
if (!this.scene) return;
|
|
290
|
-
this.scene.meshes.slice().forEach((
|
|
291
|
-
if (mesh.getClassName() === "Mesh") mesh.dispose();
|
|
292
|
-
});
|
|
199
|
+
this.scene.meshes.slice().forEach(m => m.getClassName()==="Mesh" && m.dispose());
|
|
293
200
|
}
|
|
294
201
|
|
|
295
202
|
_createBlobFromBase64(base64) {
|
|
296
|
-
console.log('[PrefViewer]
|
|
297
|
-
const [prefix,
|
|
298
|
-
|
|
299
|
-
let
|
|
300
|
-
if (prefix && prefix.startsWith('data:')) {
|
|
301
|
-
const end = prefix.indexOf(';');
|
|
302
|
-
mimeType = prefix.substring(5, end >= 0 ? end : prefix.length) || mimeType;
|
|
303
|
-
}
|
|
304
|
-
console.log('[PrefViewer] inferred mimeType:', mimeType);
|
|
305
|
-
const raw = data ?? base64;
|
|
203
|
+
console.log('[PrefViewer] Decoding Base64...');
|
|
204
|
+
const [prefix, payload] = base64.split(',');
|
|
205
|
+
const raw = payload ?? base64;
|
|
206
|
+
let decoded;
|
|
306
207
|
try {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
return blob;
|
|
312
|
-
} catch (err) {
|
|
313
|
-
console.error('[PrefViewer] Failed to decode Base64 or create Blob:', err);
|
|
314
|
-
this.dispatchEvent(
|
|
315
|
-
new CustomEvent('model-error', {
|
|
316
|
-
detail: { error: err },
|
|
317
|
-
bubbles: true,
|
|
318
|
-
composed: true
|
|
319
|
-
})
|
|
320
|
-
);
|
|
321
|
-
throw err;
|
|
208
|
+
decoded = atob(raw);
|
|
209
|
+
} catch (e) {
|
|
210
|
+
console.error('[PrefViewer] atob failed:', e);
|
|
211
|
+
throw e;
|
|
322
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 };
|
|
323
226
|
}
|
|
324
227
|
|
|
325
|
-
_getExtensionFromMimeType(mimeType) {
|
|
326
|
-
if (mimeType.includes("json")) return ".gltf";
|
|
327
|
-
if (mimeType.includes("glb") || mimeType === "application/octet-stream") return ".glb";
|
|
328
|
-
return ".gltf";
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
_getExtensionFromUrl(url) {
|
|
332
|
-
const match = url.match(/\.(gltf|glb)(\?|#|$)/i);
|
|
333
|
-
return match ? `.${match[1].toLowerCase()}` : ".gltf";
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// ====== Cleanup ======
|
|
337
228
|
_disposeEngine() {
|
|
338
|
-
if (this.engine)
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
this.scene = null;
|
|
342
|
-
this.camera = null;
|
|
343
|
-
this.hemiLight = null;
|
|
344
|
-
this.dirLight = null;
|
|
345
|
-
}
|
|
229
|
+
if (!this.engine) return;
|
|
230
|
+
this.engine.dispose();
|
|
231
|
+
this.engine = this.scene = this.camera = this.hemiLight = this.dirLight = null;
|
|
346
232
|
}
|
|
347
233
|
}
|
|
348
234
|
|