@preference-sl/pref-viewer 2.3.0 → 2.3.1

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/src/index.js CHANGED
@@ -1,348 +1,348 @@
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/window.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 with updated preprocessUrl
40
- * -----------------------------------------------------------------------------
41
- */
42
-
43
- import {
44
- Engine,
45
- Scene,
46
- ArcRotateCamera,
47
- Vector3,
48
- SceneLoader,
49
- Color4,
50
- HemisphericLight,
51
- DirectionalLight
52
- } from "@babylonjs/core";
53
- import "@babylonjs/loaders";
54
-
55
- class PrefViewer extends HTMLElement {
56
- constructor() {
57
- super();
58
- console.log("PrefViewer: constructor");
59
- this.attachShadow({ mode: "open" });
60
- this._createCanvas();
61
- this._wrapCanvas();
62
-
63
- // These will be set in _initializeBabylon()
64
- this.engine = null;
65
- this.scene = null;
66
- this.camera = null;
67
- this.hemiLight = null;
68
- this.dirLight = null;
69
- this._onWindowResize = null;
70
-
71
- // modelUrl might be provided via attribute before connectedCallback
72
- this.modelUrl = null;
73
- this._hasInitialized = false;
74
- this._pluginHookSetup = false; // Track if we've already set up the plugin hook
75
- }
76
-
77
- static get observedAttributes() {
78
- return ["model"];
79
- }
80
-
81
- attributeChangedCallback(name, _oldValue, newValue) {
82
- console.log(`PrefViewer: attributeChangedCallback - ${name} -> ${newValue}`);
83
- if (name === "model" && newValue) {
84
- this.modelUrl = newValue;
85
- console.log(`PrefViewer: modelUrl set to ${this.modelUrl}`);
86
- // Only reload if initialization has already happened
87
- if (this._hasInitialized) {
88
- this._reloadModel();
89
- }
90
- }
91
- }
92
-
93
- connectedCallback() {
94
- console.log("PrefViewer: connectedCallback");
95
-
96
- // Set up the plugin hook ONCE globally before any other initialization
97
- if (!this._pluginHookSetup) {
98
- this._setupUrlPreprocessing();
99
- this._pluginHookSetup = true;
100
- }
101
-
102
- // 1) Determine modelUrl now that element is connected
103
- if (!this.hasAttribute("model")) {
104
- this.modelUrl = new URL("./models/window.gltf", import.meta.url).href;
105
- console.log(`PrefViewer: no model attribute, defaulting to ${this.modelUrl}`);
106
- } else {
107
- this.modelUrl = this.getAttribute("model");
108
- console.log(`PrefViewer: model attribute present, using ${this.modelUrl}`);
109
- }
110
-
111
- // 2) Initialize Babylon (engine + scene + camera + lights + hooks)
112
- this._initializeBabylon();
113
-
114
- // 3) Mark that initialization is done
115
- this._hasInitialized = true;
116
-
117
- // 4) Load whatever modelUrl we have
118
- this._reloadModel();
119
- }
120
-
121
- disconnectedCallback() {
122
- console.log("PrefViewer: disconnectedCallback - disposing engine");
123
- this._disposeEngine();
124
- if (this._onWindowResize) {
125
- window.removeEventListener("resize", this._onWindowResize);
126
- }
127
- }
128
-
129
- // ====== URL Preprocessing Setup ======
130
- _setupUrlPreprocessing() {
131
- console.log("PrefViewer: Setting up URL preprocessing hook");
132
-
133
- const transformUrl = (url) => {
134
- // a) If the loader already prepended "blob:…", strip it out.
135
- const stripped = url.replace(
136
- /^blob:(?:http|https|file):\/\/[^\/]+\/(.+)/i,
137
- "$1"
138
- );
139
- // b) Normalize backslashes "\" → forward slashes "/"
140
- const fixedSlashes = stripped.replace(/\\/g, "/");
141
- console.log(
142
- `PrefViewer: preprocessUrl received "${url}", stripped to "${stripped}", normalized to "${fixedSlashes}"`
143
- );
144
- // c) If it now starts with "http://" or "https://", return it as an absolute URL:
145
- if (/^https?:\/\//i.test(fixedSlashes)) {
146
- console.log(`PrefViewer: preprocessUrl returning absolute URL "${fixedSlashes}"`);
147
- return fixedSlashes;
148
- }
149
- // d) Otherwise, return the relative path
150
- console.log(`PrefViewer: preprocessUrl returning relative URL "${fixedSlashes}"`);
151
- return fixedSlashes;
152
- };
153
-
154
- SceneLoader.OnPluginActivatedObservable.add((plugin) => {
155
- console.log(`PrefViewer: Plugin activated - ${plugin.name}`);
156
- if (plugin.name === "gltf" || plugin.name === "gltf2") {
157
- // Support both sync and async preprocessing
158
- plugin.preprocessUrl = transformUrl;
159
- if (typeof plugin.preprocessUrlAsync === 'function' || plugin.preprocessUrlAsync === undefined) {
160
- plugin.preprocessUrlAsync = (url) => Promise.resolve(transformUrl(url));
161
- }
162
- console.log("PrefViewer: URL preprocessing hooks set up for GLTF plugin");
163
- }
164
- });
165
- }
166
-
167
- // ====== Private setup methods ======
168
- _createCanvas() {
169
- console.log("PrefViewer: _createCanvas");
170
- this.canvas = document.createElement("canvas");
171
- Object.assign(this.canvas.style, {
172
- width: "100%",
173
- height: "100%",
174
- display: "block"
175
- });
176
- }
177
-
178
- _wrapCanvas() {
179
- console.log("PrefViewer: _wrapCanvas");
180
- const wrapper = document.createElement("div");
181
- Object.assign(wrapper.style, {
182
- width: "100%",
183
- height: "100%",
184
- position: "relative"
185
- });
186
- wrapper.appendChild(this.canvas);
187
- this.shadowRoot.append(wrapper);
188
- }
189
-
190
- _initializeBabylon() {
191
- console.log("PrefViewer: _initializeBabylon - creating engine and scene");
192
-
193
- // 1) Create the Babylon engine & scene
194
- this.engine = new Engine(this.canvas, true, { alpha: true });
195
- this.scene = new Scene(this.engine);
196
- this.scene.clearColor = new Color4(1, 1, 1, 1);
197
-
198
- // 2) Create camera and lights
199
- console.log("PrefViewer: _createCamera and _createLights");
200
- this._createCamera();
201
- this._createLights();
202
-
203
- // 3) Hook up input/event handlers (e.g. wheel-to-zoom)
204
- console.log("PrefViewer: _setupEventListeners");
205
- this._setupEventListeners();
206
-
207
- // 4) Start Babylon's render loop
208
- console.log("PrefViewer: Starting render loop");
209
- this.engine.runRenderLoop(() => {
210
- if (this.scene) {
211
- this.scene.render();
212
- }
213
- });
214
-
215
- this._onWindowResize = () => {
216
- console.log("PrefViewer: Window resized - calling engine.resize()");
217
- if (this.engine) {
218
- this.engine.resize();
219
- }
220
- };
221
- window.addEventListener("resize", this._onWindowResize);
222
- }
223
-
224
- _createCamera() {
225
- console.log("PrefViewer: _createCamera");
226
- // ArcRotateCamera that orbits around origin
227
- this.camera = new ArcRotateCamera(
228
- "camera",
229
- Math.PI / 2,
230
- Math.PI / 3,
231
- 10,
232
- Vector3.Zero(),
233
- this.scene
234
- );
235
- this.camera.attachControl(this.canvas, true);
236
- }
237
-
238
- _createLights() {
239
- console.log("PrefViewer: _createLights");
240
- this.hemiLight = new HemisphericLight(
241
- "hemiLight",
242
- new Vector3(0, 1, 0),
243
- this.scene
244
- );
245
- this.hemiLight.intensity = 0.6;
246
-
247
- this.dirLight = new DirectionalLight(
248
- "dirLight",
249
- new Vector3(-0.5, -1, -0.5),
250
- this.scene
251
- );
252
- this.dirLight.position = new Vector3(0, 5, 0);
253
- this.dirLight.intensity = 0.8;
254
- }
255
-
256
- _setupEventListeners() {
257
- console.log("PrefViewer: _setupEventListeners");
258
- // Zoom toward point-of-interest on wheel scroll
259
- this.canvas.addEventListener("wheel", (evt) => {
260
- if (!this.scene || !this.camera) return;
261
- const pickResult = this.scene.pick(
262
- this.scene.pointerX,
263
- this.scene.pointerY
264
- );
265
- const pivotPoint = pickResult.hit
266
- ? pickResult.pickedPoint.clone()
267
- : this.camera.target.clone();
268
- this.camera.target = pivotPoint;
269
- this.camera.inertialRadiusOffset +=
270
- evt.deltaY * this.camera.wheelPrecision * 0.01;
271
- evt.preventDefault();
272
- });
273
- }
274
-
275
- // ====== Model loading / management ======
276
- async _reloadModel() {
277
- console.log(`PrefViewer: _reloadModel - loading ${this.modelUrl}`);
278
- if (!this.scene || !this.modelUrl) {
279
- console.warn("PrefViewer: _reloadModel aborted (scene or modelUrl missing)");
280
- return;
281
- }
282
-
283
- // Dispose previous meshes so we don't accumulate them
284
- this._disposePreviousMeshes();
285
-
286
- try {
287
- console.log(`PrefViewer: ImportMeshAsync("${this.modelUrl}")`);
288
- const result = await SceneLoader.ImportMeshAsync(
289
- null,
290
- "",
291
- this.modelUrl,
292
- this.scene,
293
- undefined,
294
- ".gltf"
295
- );
296
- console.log("PrefViewer: Model loaded, creating default camera/light if needed");
297
- this.scene.createDefaultCameraOrLight(true, true, true);
298
-
299
- console.log("PrefViewer: Dispatching model-loaded event");
300
- this.dispatchEvent(
301
- new CustomEvent("model-loaded", {
302
- detail: {
303
- meshes: result.meshes,
304
- particleSystems: result.particleSystems
305
- },
306
- bubbles: true,
307
- composed: true
308
- })
309
- );
310
- } catch (err) {
311
- console.error("PrefViewer: Error loading model:", err);
312
- console.log("PrefViewer: Dispatching model-error event");
313
- this.dispatchEvent(
314
- new CustomEvent("model-error", {
315
- detail: { error: err },
316
- bubbles: true,
317
- composed: true
318
- })
319
- );
320
- }
321
- }
322
-
323
- _disposePreviousMeshes() {
324
- console.log("PrefViewer: _disposePreviousMeshes");
325
- if (!this.scene) return;
326
- this.scene.meshes.slice().forEach((mesh) => {
327
- if (mesh.getClassName() === "Mesh") {
328
- console.log(`PrefViewer: Disposing mesh ${mesh.name}`);
329
- mesh.dispose();
330
- }
331
- });
332
- }
333
-
334
- // ====== Cleanup ======
335
- _disposeEngine() {
336
- console.log("PrefViewer: _disposeEngine");
337
- if (this.engine) {
338
- this.engine.dispose();
339
- this.engine = null;
340
- this.scene = null;
341
- this.camera = null;
342
- this.hemiLight = null;
343
- this.dirLight = null;
344
- }
345
- }
346
- }
347
-
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/window.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 with updated preprocessUrl
40
+ * -----------------------------------------------------------------------------
41
+ */
42
+
43
+ import {
44
+ Engine,
45
+ Scene,
46
+ ArcRotateCamera,
47
+ Vector3,
48
+ SceneLoader,
49
+ Color4,
50
+ HemisphericLight,
51
+ DirectionalLight
52
+ } from "@babylonjs/core";
53
+ import "@babylonjs/loaders";
54
+
55
+ class PrefViewer extends HTMLElement {
56
+ constructor() {
57
+ super();
58
+ console.log("PrefViewer: constructor");
59
+ this.attachShadow({ mode: "open" });
60
+ this._createCanvas();
61
+ this._wrapCanvas();
62
+
63
+ // These will be set in _initializeBabylon()
64
+ this.engine = null;
65
+ this.scene = null;
66
+ this.camera = null;
67
+ this.hemiLight = null;
68
+ this.dirLight = null;
69
+ this._onWindowResize = null;
70
+
71
+ // modelUrl might be provided via attribute before connectedCallback
72
+ this.modelUrl = null;
73
+ this._hasInitialized = false;
74
+ this._pluginHookSetup = false; // Track if we've already set up the plugin hook
75
+ }
76
+
77
+ static get observedAttributes() {
78
+ return ["model"];
79
+ }
80
+
81
+ attributeChangedCallback(name, _oldValue, newValue) {
82
+ console.log(`PrefViewer: attributeChangedCallback - ${name} -> ${newValue}`);
83
+ if (name === "model" && newValue) {
84
+ this.modelUrl = newValue;
85
+ console.log(`PrefViewer: modelUrl set to ${this.modelUrl}`);
86
+ // Only reload if initialization has already happened
87
+ if (this._hasInitialized) {
88
+ this._reloadModel();
89
+ }
90
+ }
91
+ }
92
+
93
+ connectedCallback() {
94
+ console.log("PrefViewer: connectedCallback");
95
+
96
+ // Set up the plugin hook ONCE globally before any other initialization
97
+ if (!this._pluginHookSetup) {
98
+ this._setupUrlPreprocessing();
99
+ this._pluginHookSetup = true;
100
+ }
101
+
102
+ // 1) Determine modelUrl now that element is connected
103
+ if (!this.hasAttribute("model")) {
104
+ this.modelUrl = new URL("./models/window.gltf", import.meta.url).href;
105
+ console.log(`PrefViewer: no model attribute, defaulting to ${this.modelUrl}`);
106
+ } else {
107
+ this.modelUrl = this.getAttribute("model");
108
+ console.log(`PrefViewer: model attribute present, using ${this.modelUrl}`);
109
+ }
110
+
111
+ // 2) Initialize Babylon (engine + scene + camera + lights + hooks)
112
+ this._initializeBabylon();
113
+
114
+ // 3) Mark that initialization is done
115
+ this._hasInitialized = true;
116
+
117
+ // 4) Load whatever modelUrl we have
118
+ this._reloadModel();
119
+ }
120
+
121
+ disconnectedCallback() {
122
+ console.log("PrefViewer: disconnectedCallback - disposing engine");
123
+ this._disposeEngine();
124
+ if (this._onWindowResize) {
125
+ window.removeEventListener("resize", this._onWindowResize);
126
+ }
127
+ }
128
+
129
+ // ====== URL Preprocessing Setup ======
130
+ _setupUrlPreprocessing() {
131
+ console.log("PrefViewer: Setting up URL preprocessing hook");
132
+
133
+ const transformUrl = (url) => {
134
+ // a) If the loader already prepended "blob:…", strip it out.
135
+ const stripped = url.replace(
136
+ /^blob:(?:http|https|file):\/\/[^\/]+\/(.+)/i,
137
+ "$1"
138
+ );
139
+ // b) Normalize backslashes "\" → forward slashes "/"
140
+ const fixedSlashes = stripped.replace(/\\/g, "/");
141
+ console.log(
142
+ `PrefViewer: preprocessUrl received "${url}", stripped to "${stripped}", normalized to "${fixedSlashes}"`
143
+ );
144
+ // c) If it now starts with "http://" or "https://", return it as an absolute URL:
145
+ if (/^https?:\/\//i.test(fixedSlashes)) {
146
+ console.log(`PrefViewer: preprocessUrl returning absolute URL "${fixedSlashes}"`);
147
+ return fixedSlashes;
148
+ }
149
+ // d) Otherwise, return the relative path
150
+ console.log(`PrefViewer: preprocessUrl returning relative URL "${fixedSlashes}"`);
151
+ return fixedSlashes;
152
+ };
153
+
154
+ SceneLoader.OnPluginActivatedObservable.add((plugin) => {
155
+ console.log(`PrefViewer: Plugin activated - ${plugin.name}`);
156
+ if (plugin.name === "gltf" || plugin.name === "gltf2") {
157
+ // Support both sync and async preprocessing
158
+ plugin.preprocessUrl = transformUrl;
159
+ if (typeof plugin.preprocessUrlAsync === 'function' || plugin.preprocessUrlAsync === undefined) {
160
+ plugin.preprocessUrlAsync = (url) => Promise.resolve(transformUrl(url));
161
+ }
162
+ console.log("PrefViewer: URL preprocessing hooks set up for GLTF plugin");
163
+ }
164
+ });
165
+ }
166
+
167
+ // ====== Private setup methods ======
168
+ _createCanvas() {
169
+ console.log("PrefViewer: _createCanvas");
170
+ this.canvas = document.createElement("canvas");
171
+ Object.assign(this.canvas.style, {
172
+ width: "100%",
173
+ height: "100%",
174
+ display: "block"
175
+ });
176
+ }
177
+
178
+ _wrapCanvas() {
179
+ console.log("PrefViewer: _wrapCanvas");
180
+ const wrapper = document.createElement("div");
181
+ Object.assign(wrapper.style, {
182
+ width: "100%",
183
+ height: "100%",
184
+ position: "relative"
185
+ });
186
+ wrapper.appendChild(this.canvas);
187
+ this.shadowRoot.append(wrapper);
188
+ }
189
+
190
+ _initializeBabylon() {
191
+ console.log("PrefViewer: _initializeBabylon - creating engine and scene");
192
+
193
+ // 1) Create the Babylon engine & scene
194
+ this.engine = new Engine(this.canvas, true, { alpha: true });
195
+ this.scene = new Scene(this.engine);
196
+ this.scene.clearColor = new Color4(1, 1, 1, 1);
197
+
198
+ // 2) Create camera and lights
199
+ console.log("PrefViewer: _createCamera and _createLights");
200
+ this._createCamera();
201
+ this._createLights();
202
+
203
+ // 3) Hook up input/event handlers (e.g. wheel-to-zoom)
204
+ console.log("PrefViewer: _setupEventListeners");
205
+ this._setupEventListeners();
206
+
207
+ // 4) Start Babylon's render loop
208
+ console.log("PrefViewer: Starting render loop");
209
+ this.engine.runRenderLoop(() => {
210
+ if (this.scene) {
211
+ this.scene.render();
212
+ }
213
+ });
214
+
215
+ this._onWindowResize = () => {
216
+ console.log("PrefViewer: Window resized - calling engine.resize()");
217
+ if (this.engine) {
218
+ this.engine.resize();
219
+ }
220
+ };
221
+ window.addEventListener("resize", this._onWindowResize);
222
+ }
223
+
224
+ _createCamera() {
225
+ console.log("PrefViewer: _createCamera");
226
+ // ArcRotateCamera that orbits around origin
227
+ this.camera = new ArcRotateCamera(
228
+ "camera",
229
+ Math.PI / 2,
230
+ Math.PI / 3,
231
+ 10,
232
+ Vector3.Zero(),
233
+ this.scene
234
+ );
235
+ this.camera.attachControl(this.canvas, true);
236
+ }
237
+
238
+ _createLights() {
239
+ console.log("PrefViewer: _createLights");
240
+ this.hemiLight = new HemisphericLight(
241
+ "hemiLight",
242
+ new Vector3(0, 1, 0),
243
+ this.scene
244
+ );
245
+ this.hemiLight.intensity = 0.6;
246
+
247
+ this.dirLight = new DirectionalLight(
248
+ "dirLight",
249
+ new Vector3(-0.5, -1, -0.5),
250
+ this.scene
251
+ );
252
+ this.dirLight.position = new Vector3(0, 5, 0);
253
+ this.dirLight.intensity = 0.8;
254
+ }
255
+
256
+ _setupEventListeners() {
257
+ console.log("PrefViewer: _setupEventListeners");
258
+ // Zoom toward point-of-interest on wheel scroll
259
+ this.canvas.addEventListener("wheel", (evt) => {
260
+ if (!this.scene || !this.camera) return;
261
+ const pickResult = this.scene.pick(
262
+ this.scene.pointerX,
263
+ this.scene.pointerY
264
+ );
265
+ const pivotPoint = pickResult.hit
266
+ ? pickResult.pickedPoint.clone()
267
+ : this.camera.target.clone();
268
+ this.camera.target = pivotPoint;
269
+ this.camera.inertialRadiusOffset +=
270
+ evt.deltaY * this.camera.wheelPrecision * 0.01;
271
+ evt.preventDefault();
272
+ });
273
+ }
274
+
275
+ // ====== Model loading / management ======
276
+ async _reloadModel() {
277
+ console.log(`PrefViewer: _reloadModel - loading ${this.modelUrl}`);
278
+ if (!this.scene || !this.modelUrl) {
279
+ console.warn("PrefViewer: _reloadModel aborted (scene or modelUrl missing)");
280
+ return;
281
+ }
282
+
283
+ // Dispose previous meshes so we don't accumulate them
284
+ this._disposePreviousMeshes();
285
+
286
+ try {
287
+ console.log(`PrefViewer: ImportMeshAsync("${this.modelUrl}")`);
288
+ const result = await SceneLoader.ImportMeshAsync(
289
+ null,
290
+ "",
291
+ this.modelUrl,
292
+ this.scene,
293
+ undefined,
294
+ ".gltf"
295
+ );
296
+ console.log("PrefViewer: Model loaded, creating default camera/light if needed");
297
+ this.scene.createDefaultCameraOrLight(true, true, true);
298
+
299
+ console.log("PrefViewer: Dispatching model-loaded event");
300
+ this.dispatchEvent(
301
+ new CustomEvent("model-loaded", {
302
+ detail: {
303
+ meshes: result.meshes,
304
+ particleSystems: result.particleSystems
305
+ },
306
+ bubbles: true,
307
+ composed: true
308
+ })
309
+ );
310
+ } catch (err) {
311
+ console.error("PrefViewer: Error loading model:", err);
312
+ console.log("PrefViewer: Dispatching model-error event");
313
+ this.dispatchEvent(
314
+ new CustomEvent("model-error", {
315
+ detail: { error: err },
316
+ bubbles: true,
317
+ composed: true
318
+ })
319
+ );
320
+ }
321
+ }
322
+
323
+ _disposePreviousMeshes() {
324
+ console.log("PrefViewer: _disposePreviousMeshes");
325
+ if (!this.scene) return;
326
+ this.scene.meshes.slice().forEach((mesh) => {
327
+ if (mesh.getClassName() === "Mesh") {
328
+ console.log(`PrefViewer: Disposing mesh ${mesh.name}`);
329
+ mesh.dispose();
330
+ }
331
+ });
332
+ }
333
+
334
+ // ====== Cleanup ======
335
+ _disposeEngine() {
336
+ console.log("PrefViewer: _disposeEngine");
337
+ if (this.engine) {
338
+ this.engine.dispose();
339
+ this.engine = null;
340
+ this.scene = null;
341
+ this.camera = null;
342
+ this.hemiLight = null;
343
+ this.dirLight = null;
344
+ }
345
+ }
346
+ }
347
+
348
348
  customElements.define("pref-viewer", PrefViewer);