@preference-sl/pref-viewer 2.11.0-beta.19 → 2.11.0-beta.20

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preference-sl/pref-viewer",
3
- "version": "2.11.0-beta.19",
3
+ "version": "2.11.0-beta.20",
4
4
  "description": "Web Component to preview GLTF models with Babylon.js",
5
5
  "author": "Alex Moreno Palacio <amoreno@preference.es>",
6
6
  "scripts": {
@@ -150,11 +150,8 @@ export default class BabylonJSAnimationController {
150
150
  }
151
151
  this.hideMenu();
152
152
  this.#animatedNodes = [];
153
+ this.#openingAnimations.forEach((openingAnimation) => openingAnimation.dispose());
153
154
  this.#openingAnimations = [];
154
- const observer = this.#scene.onPointerObservable._observers.find((observer) => observer.callback.name.includes("#onAnimationPointerObservable"));
155
- if (observer) {
156
- this.#scene.onPointerObservable.remove(observer);
157
- }
158
155
  }
159
156
 
160
157
  /**
@@ -78,6 +78,10 @@ export default class OpeningAnimationMenu {
78
78
  repeatOff: `<svg id="repeat-off" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M2,5.27L3.28,4L20,20.72L18.73,22L15.73,19H7V22L3,18L7,14V17H13.73L7,10.27V11H5V8.27L2,5.27M17,13H19V17.18L17,15.18V13M17,5V2L21,6L17,10V7H8.82L6.82,5H17Z"/></svg>`,
79
79
  };
80
80
 
81
+ #handler = {
82
+ onSliderInput: null,
83
+ };
84
+
81
85
  /**
82
86
  * Constructs the OpeningAnimationMenu and initializes the menu UI.
83
87
  * @param {string} name - The name of the animation.
@@ -154,7 +158,8 @@ export default class OpeningAnimationMenu {
154
158
  this.#slider.setAttribute("step", "0.01");
155
159
  this.#slider.setAttribute("value", this.#animationProgress.toString());
156
160
  this.#slider.classList.add("animation-menu-slider");
157
- this.#slider.addEventListener("input", this.#onSliderInput.bind(this));
161
+ this.#handler.onSliderInput = this.#onSliderInput.bind(this);
162
+ this.#slider.addEventListener("input", this.#handler.onSliderInput);
158
163
 
159
164
  this.#containerMain.appendChild(this.#slider);
160
165
  }
@@ -327,6 +332,7 @@ export default class OpeningAnimationMenu {
327
332
  * @returns {void}
328
333
  */
329
334
  dispose() {
335
+ this.#slider.removeEventListener("input", this.#handler.onSliderInput);
330
336
  if (this.isVisible) {
331
337
  this.#containerMain.remove();
332
338
  }
@@ -12,6 +12,7 @@ import OpeningAnimationMenu from "./babylonjs-animation-opening-menu.js";
12
12
  * - Manages the animation control menu (OpeningAnimationMenu) and its callbacks.
13
13
  *
14
14
  * Public Methods:
15
+ * - dispose(): Disposes the OpeningAnimation instance and releases all associated resources.
15
16
  * - isAnimationForNode(node): Checks if the animation affects the given node.
16
17
  * - playOpen(): Starts the opening animation.
17
18
  * - playClose(): Starts the closing animation.
@@ -48,20 +49,26 @@ export default class OpeningAnimation {
48
49
  closing: 4,
49
50
  };
50
51
 
52
+ name = "";
51
53
  #openAnimation = null;
52
54
  #closeAnimation = null;
53
-
54
55
  #nodes = [];
56
+ #menu = null;
57
+
55
58
  #state = OpeningAnimation.states.closed;
56
59
  #lastPausedFrame = 0;
57
60
  #startFrame = 0;
58
61
  #endFrame = 0;
59
62
  #speedRatio = 1.0;
60
63
  #loop = false;
61
-
62
- #menu = null;
63
64
  #progressThreshold = 0.025;
64
65
 
66
+ #handlers = {
67
+ onOpened: null,
68
+ onClosed: null,
69
+ updateControlsSlider: null,
70
+ };
71
+
65
72
  /**
66
73
  * Creates a new OpeningAnimation instance for managing open/close animations of a model part.
67
74
  * @param {string} name - The identifier for this animation (e.g., door name).
@@ -85,8 +92,15 @@ export default class OpeningAnimation {
85
92
  this.#speedRatio = this.#openAnimation.speedRatio || 1.0;
86
93
 
87
94
  this.#getNodesFromAnimationGroups();
88
- this.#openAnimation.onAnimationGroupEndObservable.add(this.#onOpened.bind(this));
89
- this.#closeAnimation.onAnimationGroupEndObservable.add(this.#onClosed.bind(this));
95
+ this.#bindHandlers();
96
+ this.#openAnimation.onAnimationGroupEndObservable.add(this.#handlers.onOpened);
97
+ this.#closeAnimation.onAnimationGroupEndObservable.add(this.#handlers.onClosed);
98
+ }
99
+
100
+ #bindHandlers() {
101
+ this.#handlers.onOpened = this.#onOpened.bind(this);
102
+ this.#handlers.onClosed = this.#onClosed.bind(this);
103
+ this.#handlers.updateControlsSlider = this.#updateControlsSlider.bind(this);
90
104
  }
91
105
 
92
106
  /**
@@ -303,6 +317,25 @@ export default class OpeningAnimation {
303
317
  * ---------------------------
304
318
  */
305
319
 
320
+ /**
321
+ * Disposes the OpeningAnimation instance and releases all associated resources.
322
+ * @public
323
+ * @returns {void}
324
+ */
325
+ dispose() {
326
+ this.hideControls();
327
+ if (this.#openAnimation) {
328
+ this.#openAnimation.onAnimationGroupEndObservable.removeCallback(this.#handlers.onOpened);
329
+ this.#openAnimation = null;
330
+ }
331
+ if (this.#closeAnimation) {
332
+ this.#closeAnimation.onAnimationGroupEndObservable.removeCallback(this.#handlers.onClosed);
333
+ this.#closeAnimation = null;
334
+ }
335
+ this.#nodes = [];
336
+ this.name = "";
337
+ }
338
+
306
339
  /**
307
340
  * Checks if the animation affects the given node.
308
341
  * @param {string} node - Node identifier.
@@ -452,7 +485,7 @@ export default class OpeningAnimation {
452
485
  this.#menu = new OpeningAnimationMenu(this.name, canvas, this.#state, this.#getProgress(), this.#loop, controlCallbacks);
453
486
 
454
487
  // Attach to Babylon.js scene render loop for real-time updates
455
- this.#openAnimation._scene.onBeforeRenderObservable.add(this.#updateControlsSlider.bind(this));
488
+ this.#openAnimation._scene.onBeforeRenderObservable.add(this.#handlers.updateControlsSlider);
456
489
  }
457
490
 
458
491
  /**
@@ -463,7 +496,7 @@ export default class OpeningAnimation {
463
496
  if (!this.isControlsVisible()) {
464
497
  return;
465
498
  }
466
- this.#openAnimation._scene.onBeforeRenderObservable.removeCallback(this.#updateControlsSlider.bind(this));
499
+ this.#openAnimation?._scene?.onBeforeRenderObservable.removeCallback(this.#handlers.updateControlsSlider);
467
500
  this.#menu.dispose();
468
501
  this.#menu = null;
469
502
  }
@@ -48,6 +48,7 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
48
48
  * - downloadUSDZ(content): Downloads the current scene, model, or environment as a USDZ file.
49
49
  *
50
50
  * Private Methods (using ECMAScript private fields):
51
+ * - #bindHandlers(): Pre-binds reusable event handlers to preserve stable references.
51
52
  * - #configureDracoCompression(): Sets up Draco mesh compression.
52
53
  * - #renderLoop(): Babylon.js render loop callback.
53
54
  * - #addStylesToARButton(): Styles AR button.
@@ -112,6 +113,12 @@ export default class BabylonJSController {
112
113
  #gltfResolver = null; // GLTFResolver instance
113
114
  #babylonJSAnimationController = null; // AnimationController instance
114
115
 
116
+ #handlers = {
117
+ onKeyUp: null,
118
+ onPointerObservable: null,
119
+ renderLoop: null,
120
+ };
121
+
115
122
  /**
116
123
  * Constructs a new BabylonJSController instance.
117
124
  * Initializes the canvas, asset containers, and options for the Babylon.js scene.
@@ -134,6 +141,18 @@ export default class BabylonJSController {
134
141
  };
135
142
  });
136
143
  this.#options = options;
144
+ this.#bindHandlers();
145
+ }
146
+
147
+ /**
148
+ * Pre-binds reusable event handlers to preserve stable references.
149
+ * @private
150
+ * @returns {void}
151
+ */
152
+ #bindHandlers() {
153
+ this.#handlers.onKeyUp = this.#onKeyUp.bind(this);
154
+ this.#handlers.onPointerObservable = this.#onPointerObservable.bind(this);
155
+ this.#handlers.renderLoop = this.#renderLoop.bind(this);
137
156
  }
138
157
 
139
158
  /**
@@ -446,10 +465,10 @@ export default class BabylonJSController {
446
465
  */
447
466
  #enableInteraction() {
448
467
  if (this.#canvas) {
449
- this.#canvas.addEventListener("keyup", this.#onKeyUp.bind(this));
468
+ this.#canvas.addEventListener("keyup", this.#handlers.onKeyUp);
450
469
  }
451
470
  if (this.#scene) {
452
- this.#scene.onPointerObservable.add(this.#onPointerObservable.bind(this));
471
+ this.#scene.onPointerObservable.add(this.#handlers.onPointerObservable);
453
472
  }
454
473
  }
455
474
 
@@ -460,10 +479,10 @@ export default class BabylonJSController {
460
479
  */
461
480
  #disableInteraction() {
462
481
  if (this.#canvas) {
463
- this.#canvas.removeEventListener("keyup", this.#onKeyUp.bind(this));
482
+ this.#canvas.removeEventListener("keyup", this.#handlers.onKeyUp);
464
483
  }
465
484
  if (this.#scene !== null) {
466
- this.#scene.onPointerObservable.removeCallback(this.#onPointerObservable.bind(this));
485
+ this.#scene.onPointerObservable.removeCallback(this.#handlers.onPointerObservable);
467
486
  }
468
487
  }
469
488
 
@@ -855,7 +874,7 @@ export default class BabylonJSController {
855
874
  * @returns {void}
856
875
  */
857
876
  #stopRender() {
858
- this.#engine.stopRenderLoop(this.#renderLoop.bind(this));
877
+ this.#engine.stopRenderLoop(this.#handlers.renderLoop);
859
878
  }
860
879
  /**
861
880
  * Starts the Babylon.js render loop for the current scene.
@@ -865,7 +884,7 @@ export default class BabylonJSController {
865
884
  */
866
885
  async #startRender() {
867
886
  await this.#scene.whenReadyAsync();
868
- this.#engine.runRenderLoop(this.#renderLoop.bind(this));
887
+ this.#engine.runRenderLoop(this.#handlers.renderLoop);
869
888
  }
870
889
 
871
890
  /**
@@ -174,7 +174,12 @@ export class FileStorage {
174
174
  xhr.onload = () => {
175
175
  if (xhr.status === 200) {
176
176
  const blob = xhr.response;
177
- const timeStamp = new Date(xhr.getResponseHeader("Last-Modified")).toISOString();
177
+ const lastModified = xhr.getResponseHeader("Last-Modified");
178
+ let timeStamp = null;
179
+ if (lastModified) {
180
+ const parsed = new Date(lastModified);
181
+ timeStamp = Number.isNaN(parsed.valueOf()) ? null : parsed.toISOString();
182
+ }
178
183
  file = { blob: blob, timeStamp: timeStamp };
179
184
  resolve(file);
180
185
  } else {
@@ -204,7 +209,11 @@ export class FileStorage {
204
209
  xhr.responseType = "blob";
205
210
  xhr.onload = () => {
206
211
  if (xhr.status === 200) {
207
- timeStamp = new Date(xhr.getResponseHeader("Last-Modified")).toISOString();
212
+ const lastModified = xhr.getResponseHeader("Last-Modified");
213
+ if (lastModified) {
214
+ const parsed = new Date(lastModified);
215
+ timeStamp = Number.isNaN(parsed.valueOf()) ? null : parsed.toISOString();
216
+ }
208
217
  resolve(timeStamp);
209
218
  } else {
210
219
  resolve(timeStamp);
@@ -2,59 +2,67 @@
2
2
  import Panzoom from "@panzoom/panzoom/dist/panzoom.es.js";
3
3
 
4
4
  /**
5
- * PanzoomController - Encapsulates the logic for managing pan and zoom interactions on a given DOM element.
5
+ * PanzoomController - Encapsulates logic for managing pan and zoom interactions on a DOM element using the Panzoom library.
6
6
  *
7
- * Responsibilities:
8
- * - Provides an interface to enable, disable, and control pan/zoom interactions.
9
- * - Manages event listeners for mouse, keyboard, and touch interactions.
10
- * - Tracks the current pan/zoom state and invokes a callback when the state changes.
11
- * - Supports zooming in/out, panning, resetting, and mouse wheel zoom.
7
+ * Summary:
8
+ * Provides a high-level API to enable, disable, and control pan/zoom interactions for SVG or other DOM content.
9
+ * Manages event listeners for mouse, keyboard, and touch interactions, and tracks the current pan/zoom state.
10
+ * Supports zooming in/out, panning, resetting, and mouse wheel zoom, with customizable options and callbacks.
11
+ *
12
+ * Key Responsibilities:
13
+ * - Initializes and manages a Panzoom instance for a wrapper element.
14
+ * - Handles enabling/disabling pan/zoom and related event listeners.
15
+ * - Tracks and exposes the current pan/zoom state.
16
+ * - Supports keyboard shortcuts, mouse wheel zoom, and context menu prevention.
17
+ * - Provides methods for panning, zooming, and resetting the view.
18
+ * - Invokes a callback when the pan/zoom state changes.
12
19
  *
13
20
  * Usage:
14
- * - Instantiate the controller with a DOM element and optional configuration:
15
- * const controller = new PanzoomController(wrapperElement, options, stateChangeCallback);
16
- * - Use public methods like `enable()`, `zoomIn()`, `pan(x, y)`, etc., to control interactions.
17
- * - Call `disable()` to remove all event listeners and destroy the Panzoom instance.
21
+ * - Instantiate: const controller = new PanzoomController(wrapperElement, options, stateChangeCallback);
22
+ * - Enable pan/zoom: controller.enable();
23
+ * - Enable events: controller.enableEvents();
24
+ * - Control view: controller.pan(x, y), controller.zoomIn(), controller.zoomOut(), controller.reset();
25
+ * - Disable pan/zoom and events: controller.disable(), controller.disableEvents();
18
26
  *
19
27
  * Constructor Parameters:
20
- * - `wrapper` (HTMLElement): The DOM element to apply pan/zoom interactions to.
21
- * - `options` (object): Configuration options for Panzoom (e.g., minScale, maxScale, step).
22
- * - `changedCallback` (function): Optional callback invoked when the pan/zoom state changes.
28
+ * - wrapper (HTMLElement): The DOM element to apply pan/zoom interactions to.
29
+ * - options (object): Configuration options for Panzoom (e.g., minScale, maxScale, step).
30
+ * - changedCallback (function): Optional callback invoked when the pan/zoom state changes.
23
31
  *
24
32
  * Public Methods:
25
- * - `enable()`: Enables the Panzoom instance and initializes it if not already enabled.
26
- * - `disable()`: Disables the Panzoom instance and removes all event listeners.
27
- * - `enableEvents()`: Attaches all required event listeners for interactions.
28
- * - `disableEvents()`: Removes all previously attached event listeners.
29
- * - `pan(x, y)`: Pans the view to the specified coordinates.
30
- * - `reset()`: Resets pan and zoom to fit the entire content within the available space.
31
- * - `zoomIn(focal?)`: Zooms in by one step, optionally centered on a focal point.
32
- * - `zoomOut(focal?)`: Zooms out by one step, optionally centered on a focal point.
33
+ * - enable(): Enables the Panzoom instance and initializes it if not already enabled.
34
+ * - disable(): Disables the Panzoom instance and removes all event listeners.
35
+ * - enableEvents(): Attaches all required event listeners for interactions.
36
+ * - disableEvents(): Removes all previously attached event listeners.
37
+ * - pan(x, y): Pans the view to the specified coordinates.
38
+ * - reset(): Resets pan and zoom to fit the entire content within the available space.
39
+ * - zoomIn(focal?): Zooms in by one step, optionally centered on a focal point.
40
+ * - zoomOut(focal?): Zooms out by one step, optionally centered on a focal point.
33
41
  *
34
42
  * Getters:
35
- * - `panzoom`: Returns the current Panzoom instance or `null` if not initialized.
36
- * - `state`: Returns the current pan/zoom state (e.g., moved, scaled, maximized, minimized).
43
+ * - panzoom: Returns the current Panzoom instance or null if not initialized.
44
+ * - state: Returns the current pan/zoom state (moved, scaled, maximized, minimized).
37
45
  *
38
46
  * Private Methods:
39
- * - `#checkPanzoomState(transform)`: Updates the internal state and invokes the state change callback.
40
- * - `#resetTransform()`: Resets the transform applied to the wrapper element.
41
- * - `#setFocus()`: Sets focus on the parent element to enable keyboard interactions.
42
- * - `#onPanzoomStart(e)`: Handles the start of a pan/zoom interaction.
43
- * - `#onPanzoomChange(e)`: Handles changes in pan/zoom state.
44
- * - `#onPanzoomEnd(e)`: Handles the end of a pan/zoom interaction.
45
- * - `#onZoomWithWheel(e)`: Handles zoom interactions via the mouse wheel.
46
- * - `#enableMouseWheelZoom()`: Enables zooming with the mouse wheel.
47
- * - `#disableMouseWheelZoom()`: Disables zooming with the mouse wheel.
48
- * - `#getDistanceBetweenPoints(point1, point2)`: Calculates the Euclidean distance between two points.
49
- * - `#getPointFromEvent(e)`: Extracts client coordinates from a mouse or touch event.
47
+ * - #bindHandlers(): Pre-binds reusable event handlers to preserve stable references.
48
+ * - #checkPanzoomState(transform): Updates the internal state and invokes the state change callback.
49
+ * - #resetTransform(): Resets the transform applied to the wrapper element.
50
+ * - #setFocus(): Sets focus on the parent element to enable keyboard interactions.
51
+ * - #onPanzoomStart(e): Handles the start of a pan/zoom interaction.
52
+ * - #onPanzoomChange(e): Handles changes in pan/zoom state.
53
+ * - #onPanzoomEnd(e): Handles the end of a pan/zoom interaction.
54
+ * - #onZoomWithWheel(e): Handles zoom interactions via the mouse wheel.
55
+ * - #enableMouseWheelZoom(): Enables zooming with the mouse wheel.
56
+ * - #disableMouseWheelZoom(): Disables zooming with the mouse wheel.
57
+ * - #getDistanceBetweenPoints(point1, point2): Calculates the Euclidean distance between two points.
58
+ * - #getPointFromEvent(e): Extracts client coordinates from a mouse or touch event.
50
59
  *
51
60
  * Notes:
52
61
  * - Designed to work with the Panzoom library for managing pan/zoom interactions.
53
- * - Provides a clean separation of concerns by encapsulating all pan/zoom logic in a single class.
62
+ * - Encapsulates all pan/zoom logic in a single class for clean separation of concerns.
54
63
  * - Can be extended or customized by overriding methods or providing additional options.
55
64
  */
56
65
  export default class PanzoomController {
57
- #changedCallback = null;
58
66
  #interactionOptions = {
59
67
  clickThreshold: 6, // Threshold to avoid false clicks on elements when panning or zooming
60
68
  };
@@ -80,11 +88,40 @@ export default class PanzoomController {
80
88
  minimized: false,
81
89
  };
82
90
  #wrapper = null;
83
-
91
+
92
+ #changedCallback = null;
93
+ #handlers = {
94
+ panzoomStart: null,
95
+ panzoomChange: null,
96
+ panzoomEnd: null,
97
+ keyUp: null,
98
+ focus: null,
99
+ blur: null,
100
+ contextMenu: null,
101
+ wheel: null,
102
+ };
103
+
84
104
  constructor(wrapper = null, options = {}, changedCallback = null) {
85
105
  this.#wrapper = wrapper;
86
106
  Object.assign(this.#options, options);
87
107
  this.#changedCallback = changedCallback;
108
+ this.#bindHandlers();
109
+ }
110
+
111
+ /**
112
+ * Pre-binds reusable event handlers to preserve stable references.
113
+ * @private
114
+ * @returns {void}
115
+ */
116
+ #bindHandlers() {
117
+ this.#handlers.panzoomStart = this.#onPanzoomStart.bind(this);
118
+ this.#handlers.panzoomChange = this.#onPanzoomChange.bind(this);
119
+ this.#handlers.panzoomEnd = this.#onPanzoomEnd.bind(this);
120
+ this.#handlers.keyUp = this.#onKeyUp.bind(this);
121
+ this.#handlers.focus = this.#enableMouseWheelZoom.bind(this);
122
+ this.#handlers.blur = this.#disableMouseWheelZoom.bind(this);
123
+ this.#handlers.contextMenu = this.#onContextMenu.bind(this);
124
+ this.#handlers.wheel = this.#onZoomWithWheel.bind(this);
88
125
  }
89
126
 
90
127
  /**
@@ -295,7 +332,7 @@ export default class PanzoomController {
295
332
  if (!this.#panzoom) {
296
333
  return;
297
334
  }
298
- this.#wrapper.parentElement.removeEventListener("wheel", this.#onZoomWithWheel.bind(this));
335
+ this.#wrapper.parentElement.removeEventListener("wheel", this.#handlers.wheel);
299
336
  }
300
337
 
301
338
  /**
@@ -310,7 +347,7 @@ export default class PanzoomController {
310
347
  return;
311
348
  }
312
349
  this.#disableMouseWheelZoom();
313
- this.#wrapper.parentElement.addEventListener("wheel", this.#onZoomWithWheel.bind(this));
350
+ this.#wrapper.parentElement.addEventListener("wheel", this.#handlers.wheel);
314
351
  }
315
352
 
316
353
  /**
@@ -364,13 +401,13 @@ export default class PanzoomController {
364
401
 
365
402
  this.disableEvents();
366
403
 
367
- this.#wrapper.addEventListener("panzoomstart", this.#onPanzoomStart.bind(this));
368
- this.#wrapper.addEventListener("panzoomchange", this.#onPanzoomChange.bind(this));
369
- this.#wrapper.addEventListener("panzoomend", this.#onPanzoomEnd.bind(this));
370
- this.#wrapper.parentElement.addEventListener("keyup", this.#onKeyUp.bind(this));
371
- this.#wrapper.parentElement.addEventListener("focus", this.#enableMouseWheelZoom.bind(this), true);
372
- this.#wrapper.parentElement.addEventListener("blur", this.#disableMouseWheelZoom.bind(this), true);
373
- this.#wrapper.parentElement.addEventListener("contextmenu", this.#onContextMenu);
404
+ this.#wrapper.addEventListener("panzoomstart", this.#handlers.panzoomStart);
405
+ this.#wrapper.addEventListener("panzoomchange", this.#handlers.panzoomChange);
406
+ this.#wrapper.addEventListener("panzoomend", this.#handlers.panzoomEnd);
407
+ this.#wrapper.parentElement.addEventListener("keyup", this.#handlers.keyUp);
408
+ this.#wrapper.parentElement.addEventListener("focus", this.#handlers.focus, true);
409
+ this.#wrapper.parentElement.addEventListener("blur", this.#handlers.blur, true);
410
+ this.#wrapper.parentElement.addEventListener("contextmenu", this.#handlers.contextMenu);
374
411
  this.#wrapper.parentElement.setAttribute("tabindex", 0);
375
412
  this.#setFocus();
376
413
  }
@@ -386,13 +423,13 @@ export default class PanzoomController {
386
423
  return;
387
424
  }
388
425
  this.#wrapper.parentElement.removeAttribute("tabindex");
389
- this.#wrapper.removeEventListener("panzoomstart", this.#onPanzoomStart.bind(this));
390
- this.#wrapper.removeEventListener("panzoomchange", this.#onPanzoomChange.bind(this));
391
- this.#wrapper.removeEventListener("panzoomend", this.#onPanzoomEnd.bind(this));
392
- this.#wrapper.parentElement.removeEventListener("keyup", this.#onKeyUp.bind(this));
393
- this.#wrapper.parentElement.removeEventListener("focus", this.#enableMouseWheelZoom.bind(this));
394
- this.#wrapper.parentElement.removeEventListener("blur", this.#disableMouseWheelZoom.bind(this));
395
- this.#wrapper.parentElement.removeEventListener("contextmenu", this.#onContextMenu);
426
+ this.#wrapper.removeEventListener("panzoomstart", this.#handlers.panzoomStart);
427
+ this.#wrapper.removeEventListener("panzoomchange", this.#handlers.panzoomChange);
428
+ this.#wrapper.removeEventListener("panzoomend", this.#handlers.panzoomEnd);
429
+ this.#wrapper.parentElement.removeEventListener("keyup", this.#handlers.keyUp);
430
+ this.#wrapper.parentElement.removeEventListener("focus", this.#handlers.focus);
431
+ this.#wrapper.parentElement.removeEventListener("blur", this.#handlers.blur);
432
+ this.#wrapper.parentElement.removeEventListener("contextmenu", this.#handlers.contextMenu);
396
433
  }
397
434
 
398
435
  /**
@@ -69,6 +69,10 @@ export default class PrefViewer2D extends HTMLElement {
69
69
  #wrapper = null; // Panzoom element
70
70
  #panzoomController = null; // PanzoomController instance
71
71
 
72
+ #handlers = {
73
+ onPanzoomChanged: null,
74
+ };
75
+
72
76
  /**
73
77
  * Create a new instance of the 2D viewer component.
74
78
  * The constructor only calls super(); heavy initialization happens in connectedCallback.
@@ -115,6 +119,7 @@ export default class PrefViewer2D extends HTMLElement {
115
119
  /**
116
120
  * Called when the element is inserted into the DOM.
117
121
  * Sets up DOM nodes, initial visibility and starts loading the SVG if provided.
122
+ * @public
118
123
  * @returns {void}
119
124
  */
120
125
  connectedCallback() {
@@ -128,6 +133,7 @@ export default class PrefViewer2D extends HTMLElement {
128
133
  /**
129
134
  * Called when the element is removed from the DOM.
130
135
  * Disables the PanzoomController to clean up event listeners and resources.
136
+ * @public
131
137
  * @returns {void}
132
138
  */
133
139
  disconnectedCallback() {
@@ -168,7 +174,8 @@ export default class PrefViewer2D extends HTMLElement {
168
174
  * @returns {void}
169
175
  */
170
176
  #initializePanzoom() {
171
- this.#panzoomController = new PanzoomController(this.#wrapper, undefined, this.#onPanzoomChanged.bind(this));
177
+ this.#handlers.onPanzoomChanged = this.#onPanzoomChanged.bind(this);
178
+ this.#panzoomController = new PanzoomController(this.#wrapper, undefined, this.#handlers.onPanzoomChanged);
172
179
  }
173
180
 
174
181
  /**
@@ -351,6 +358,9 @@ export default class PrefViewer2D extends HTMLElement {
351
358
  * - Resolves to { success: true, error: null } when the SVG was accepted and the component has started rendering.
352
359
  */
353
360
  async load(svgConfig) {
361
+ if (!this.#svgResolver) {
362
+ this.#svgResolver = new SVGResolver(this.#svg);
363
+ }
354
364
  if (!svgConfig || !(await this.#svgResolver.getSVG(svgConfig))) {
355
365
  const errorDetail = this.#onError();
356
366
  return { success: false, ...errorDetail };
@@ -130,6 +130,7 @@ export default class PrefViewer3D extends HTMLElement {
130
130
  * Called when the element is added to the DOM.
131
131
  * Creates the DOM structure, sets initial visibility, initializes component data, and sets up the BabylonJSController.
132
132
  * Performs heavy initialization tasks required for the 3D viewer.
133
+ * @public
133
134
  * @returns {void}
134
135
  */
135
136
  connectedCallback() {
@@ -137,11 +138,13 @@ export default class PrefViewer3D extends HTMLElement {
137
138
  this.#setInitialVisibility();
138
139
  this.#initializeData();
139
140
  this.#initializeBabylonJS();
141
+ this.#isInitialized = true;
140
142
  }
141
143
 
142
144
  /**
143
145
  * Called when the element is removed from the DOM.
144
146
  * Disables the BabylonJSController to clean up resources and event listeners associated with the 3D viewer.
147
+ * @public
145
148
  * @returns {void}
146
149
  */
147
150
  disconnectedCallback() {
@@ -28,6 +28,10 @@ export default class PrefViewerDialog extends HTMLElement {
28
28
  #content = null;
29
29
  #footer = null;
30
30
 
31
+ #handles = {
32
+ onBackdropClick: null,
33
+ };
34
+
31
35
  /**
32
36
  * Initializes the custom dialog element.
33
37
  * Only calls super(); heavy initialization is deferred to connectedCallback.
@@ -35,13 +39,14 @@ export default class PrefViewerDialog extends HTMLElement {
35
39
  constructor() {
36
40
  super();
37
41
  }
38
-
42
+
39
43
  /**
40
44
  * Called when the element is inserted into the DOM.
41
45
  * Sets up the dialog's DOM structure and styles.
42
46
  * @returns {void}
43
- */
44
- connectedCallback() {
47
+ */
48
+ connectedCallback() {
49
+ this.#handles.onBackdropClick = this.#onBackdropClick.bind(this);
45
50
  this.#createDOMElements();
46
51
  }
47
52
 
@@ -51,7 +56,7 @@ export default class PrefViewerDialog extends HTMLElement {
51
56
  * @returns {void}
52
57
  */
53
58
  disconnectedCallback() {
54
- this.removeEventListener("click", this.#closeOnBackdropClick.bind(this));
59
+ this.removeEventListener("click", this.#handles.onBackdropClick);
55
60
  }
56
61
 
57
62
  /**
@@ -73,14 +78,14 @@ export default class PrefViewerDialog extends HTMLElement {
73
78
  style.textContent = PrefViewerDialogStyles;
74
79
  this.append(style);
75
80
 
76
- this.addEventListener("click", this.#closeOnBackdropClick.bind(this));
81
+ this.addEventListener("click", this.#handles.onBackdropClick);
77
82
 
78
83
  this.#header = this.#wrapper.querySelector(".dialog-header");
79
84
  this.#content = this.#wrapper.querySelector(".dialog-content");
80
85
  this.#footer = this.#wrapper.querySelector(".dialog-footer");
81
86
  }
82
87
 
83
- #closeOnBackdropClick(event) {
88
+ #onBackdropClick(event) {
84
89
  if (event.target === this) {
85
90
  this.close();
86
91
  }
@@ -120,6 +125,7 @@ export default class PrefViewerDialog extends HTMLElement {
120
125
  * @returns {void}
121
126
  */
122
127
  close() {
128
+ this.removeEventListener("click", this.#handles.onBackdropClick);
123
129
  this.removeAttribute("open");
124
130
  const parent = this.getRootNode().host;
125
131
  if (parent) {
@@ -87,6 +87,10 @@ export default class PrefViewer extends HTMLElement {
87
87
  #component3D = null;
88
88
  #dialog = null;
89
89
 
90
+ #handlers = {
91
+ on2DZoomChanged: null,
92
+ };
93
+
90
94
  /**
91
95
  * Creates a new PrefViewer instance and attaches a shadow DOM.
92
96
  * Initializes internal state and component references.
@@ -185,6 +189,24 @@ export default class PrefViewer extends HTMLElement {
185
189
  this.#processNextTask();
186
190
  }
187
191
 
192
+ /**
193
+ * Called when the element is removed from the DOM.
194
+ * @public
195
+ * @returns {void}
196
+ */
197
+ disconnectedCallback() {
198
+ if (this.#dialog) {
199
+ this.#dialog?.remove();
200
+ }
201
+ if (this.#component2D) {
202
+ this.#component2D.removeEventListener("drawing-zoom-changed", this.#handlers.on2DZoomChanged);
203
+ this.#component2D.remove();
204
+ }
205
+ if (this.#component3D) {
206
+ this.#component3D.remove();
207
+ }
208
+ }
209
+
188
210
  /**
189
211
  * Creates and appends the 2D viewer component to the shadow DOM.
190
212
  * Sets the "visible" attribute to true by default.
@@ -192,9 +214,10 @@ export default class PrefViewer extends HTMLElement {
192
214
  * @returns {void}
193
215
  */
194
216
  #createComponent2D() {
217
+ this.#handlers.on2DZoomChanged = this.#on2DZoomChanged.bind(this);
195
218
  this.#component2D = document.createElement("pref-viewer-2d");
196
219
  this.#component2D.setAttribute("visible", "false");
197
- this.#component2D.addEventListener("drawing-zoom-changed", this.#on2DZoomChanged.bind(this));
220
+ this.#component2D.addEventListener("drawing-zoom-changed", this.#handlers.on2DZoomChanged);
198
221
  this.#wrapper.appendChild(this.#component2D);
199
222
  }
200
223