@cadview/core 0.1.0 → 0.2.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/dist/index.d.cts CHANGED
@@ -359,6 +359,24 @@ declare class EventEmitter<T extends Record<string, any> = Record<string, any>>
359
359
  }
360
360
 
361
361
  type Tool = 'pan' | 'select' | 'measure';
362
+ /**
363
+ * Interface for registering custom file format converters.
364
+ * Converters are checked in order during `loadFile()` and `loadBuffer()`.
365
+ * The first converter whose `detect()` returns true will be used.
366
+ */
367
+ interface FormatConverter {
368
+ /**
369
+ * Return true if the buffer is in this format.
370
+ * Implementations should check magic bytes, not file extensions.
371
+ * Must not throw — return false on any error.
372
+ */
373
+ detect(buffer: ArrayBuffer): boolean;
374
+ /**
375
+ * Convert the buffer to a DXF string.
376
+ * The returned string must be valid DXF parseable by `parseDxf()`.
377
+ */
378
+ convert(buffer: ArrayBuffer): Promise<string>;
379
+ }
362
380
  interface CadViewerOptions {
363
381
  theme?: Theme;
364
382
  backgroundColor?: string;
@@ -367,6 +385,8 @@ interface CadViewerOptions {
367
385
  maxZoom?: number;
368
386
  zoomSpeed?: number;
369
387
  initialTool?: Tool;
388
+ /** Format converters for non-DXF file formats (e.g. DWG via @cadview/dwg). */
389
+ formatConverters?: FormatConverter[];
370
390
  }
371
391
  declare class CadViewer {
372
392
  private canvas;
@@ -381,14 +401,50 @@ declare class CadViewer {
381
401
  private currentTool;
382
402
  private inputHandler;
383
403
  private resizeObserver;
404
+ private formatConverters;
384
405
  private selectedEntityIndex;
385
406
  private renderPending;
386
407
  private destroyed;
408
+ private loadGeneration;
387
409
  private mouseScreenX;
388
410
  private mouseScreenY;
389
411
  constructor(canvas: HTMLCanvasElement, options?: CadViewerOptions);
412
+ /**
413
+ * Throws if the viewer has been destroyed.
414
+ * Call at the start of any public method that mutates state.
415
+ */
416
+ private guardDestroyed;
417
+ /**
418
+ * Run registered format converters on a buffer.
419
+ * Returns the converted DXF string if a converter matched, or null otherwise.
420
+ * Each converter's detect() is wrapped in try/catch — a throwing detect() is skipped.
421
+ */
422
+ private runConverters;
423
+ /**
424
+ * Load a CAD file from a browser File object.
425
+ * Automatically detects the format using registered converters (e.g. DWG).
426
+ * Falls back to DXF parsing if no converter matches.
427
+ */
390
428
  loadFile(file: File): Promise<void>;
429
+ /**
430
+ * Load a CAD file from an ArrayBuffer with format converter support.
431
+ * Unlike `loadArrayBuffer()` (sync, DXF-only), this method is async and
432
+ * checks registered FormatConverters for non-DXF formats.
433
+ */
434
+ loadBuffer(buffer: ArrayBuffer): Promise<void>;
435
+ /**
436
+ * Load a pre-parsed DxfDocument directly, bypassing the parser.
437
+ * Useful for custom parsers or pre-processed documents.
438
+ */
439
+ loadDocument(doc: DxfDocument): void;
440
+ /**
441
+ * Load a DXF string directly (synchronous, no format conversion).
442
+ */
391
443
  loadString(dxf: string): void;
444
+ /**
445
+ * Load a DXF file from an ArrayBuffer (synchronous, no format conversion).
446
+ * For format conversion support (e.g. DWG), use `loadFile()` or `loadBuffer()` instead.
447
+ */
392
448
  loadArrayBuffer(buffer: ArrayBuffer): void;
393
449
  /**
394
450
  * Clear the current document and reset all state without destroying the viewer.
@@ -507,4 +563,4 @@ interface BBox {
507
563
  declare function computeEntitiesBounds(entities: DxfEntity[]): BBox | null;
508
564
  declare function computeEntityBBox(entity: DxfEntity): BBox | null;
509
565
 
510
- export { type BBox, CadViewer, type CadViewerEventMap, type CadViewerOptions, Camera, CanvasRenderer, type DxfArcEntity, type DxfAttrib, type DxfBlock, type DxfCircleEntity, type DxfDimensionEntity, type DxfDocument, type DxfEllipseEntity, type DxfEntity, type DxfEntityBase, type DxfHatchBoundaryPath, type DxfHatchEdge, type DxfHatchEntity, type DxfHeader, type DxfInsertEntity, type DxfLayer, type DxfLineEntity, type DxfLineType, type DxfLwPolylineEntity, type DxfLwPolylineVertex, type DxfMTextEntity, DxfParseError, type DxfPointEntity, type DxfPolylineEntity, type DxfSplineEntity, type DxfStyle, type DxfTextEntity, EventEmitter, LayerManager, type MeasureEvent, type MeasureState, MeasureTool, type Point2D, type Point3D, type SelectEvent, type SnapResult, type SnapType, SpatialIndex, THEMES, type Theme, type ThemeConfig, type Tool, type ViewTransform, aciToDisplayColor, aciToHex, applyTransform, computeEntitiesBounds, computeEntityBBox, drawEntity, findSnaps, fitToView, hitTest, parseDxf, renderMeasureOverlay, resolveEntityColor, screenToWorld, trueColorToHex, worldToScreen, zoomAtPoint };
566
+ export { type BBox, CadViewer, type CadViewerEventMap, type CadViewerOptions, Camera, CanvasRenderer, type DxfArcEntity, type DxfAttrib, type DxfBlock, type DxfCircleEntity, type DxfDimensionEntity, type DxfDocument, type DxfEllipseEntity, type DxfEntity, type DxfEntityBase, type DxfHatchBoundaryPath, type DxfHatchEdge, type DxfHatchEntity, type DxfHeader, type DxfInsertEntity, type DxfLayer, type DxfLineEntity, type DxfLineType, type DxfLwPolylineEntity, type DxfLwPolylineVertex, type DxfMTextEntity, DxfParseError, type DxfPointEntity, type DxfPolylineEntity, type DxfSplineEntity, type DxfStyle, type DxfTextEntity, EventEmitter, type FormatConverter, LayerManager, type MeasureEvent, type MeasureState, MeasureTool, type Point2D, type Point3D, type SelectEvent, type SnapResult, type SnapType, SpatialIndex, THEMES, type Theme, type ThemeConfig, type Tool, type ViewTransform, aciToDisplayColor, aciToHex, applyTransform, computeEntitiesBounds, computeEntityBBox, drawEntity, findSnaps, fitToView, hitTest, parseDxf, renderMeasureOverlay, resolveEntityColor, screenToWorld, trueColorToHex, worldToScreen, zoomAtPoint };
package/dist/index.d.ts CHANGED
@@ -359,6 +359,24 @@ declare class EventEmitter<T extends Record<string, any> = Record<string, any>>
359
359
  }
360
360
 
361
361
  type Tool = 'pan' | 'select' | 'measure';
362
+ /**
363
+ * Interface for registering custom file format converters.
364
+ * Converters are checked in order during `loadFile()` and `loadBuffer()`.
365
+ * The first converter whose `detect()` returns true will be used.
366
+ */
367
+ interface FormatConverter {
368
+ /**
369
+ * Return true if the buffer is in this format.
370
+ * Implementations should check magic bytes, not file extensions.
371
+ * Must not throw — return false on any error.
372
+ */
373
+ detect(buffer: ArrayBuffer): boolean;
374
+ /**
375
+ * Convert the buffer to a DXF string.
376
+ * The returned string must be valid DXF parseable by `parseDxf()`.
377
+ */
378
+ convert(buffer: ArrayBuffer): Promise<string>;
379
+ }
362
380
  interface CadViewerOptions {
363
381
  theme?: Theme;
364
382
  backgroundColor?: string;
@@ -367,6 +385,8 @@ interface CadViewerOptions {
367
385
  maxZoom?: number;
368
386
  zoomSpeed?: number;
369
387
  initialTool?: Tool;
388
+ /** Format converters for non-DXF file formats (e.g. DWG via @cadview/dwg). */
389
+ formatConverters?: FormatConverter[];
370
390
  }
371
391
  declare class CadViewer {
372
392
  private canvas;
@@ -381,14 +401,50 @@ declare class CadViewer {
381
401
  private currentTool;
382
402
  private inputHandler;
383
403
  private resizeObserver;
404
+ private formatConverters;
384
405
  private selectedEntityIndex;
385
406
  private renderPending;
386
407
  private destroyed;
408
+ private loadGeneration;
387
409
  private mouseScreenX;
388
410
  private mouseScreenY;
389
411
  constructor(canvas: HTMLCanvasElement, options?: CadViewerOptions);
412
+ /**
413
+ * Throws if the viewer has been destroyed.
414
+ * Call at the start of any public method that mutates state.
415
+ */
416
+ private guardDestroyed;
417
+ /**
418
+ * Run registered format converters on a buffer.
419
+ * Returns the converted DXF string if a converter matched, or null otherwise.
420
+ * Each converter's detect() is wrapped in try/catch — a throwing detect() is skipped.
421
+ */
422
+ private runConverters;
423
+ /**
424
+ * Load a CAD file from a browser File object.
425
+ * Automatically detects the format using registered converters (e.g. DWG).
426
+ * Falls back to DXF parsing if no converter matches.
427
+ */
390
428
  loadFile(file: File): Promise<void>;
429
+ /**
430
+ * Load a CAD file from an ArrayBuffer with format converter support.
431
+ * Unlike `loadArrayBuffer()` (sync, DXF-only), this method is async and
432
+ * checks registered FormatConverters for non-DXF formats.
433
+ */
434
+ loadBuffer(buffer: ArrayBuffer): Promise<void>;
435
+ /**
436
+ * Load a pre-parsed DxfDocument directly, bypassing the parser.
437
+ * Useful for custom parsers or pre-processed documents.
438
+ */
439
+ loadDocument(doc: DxfDocument): void;
440
+ /**
441
+ * Load a DXF string directly (synchronous, no format conversion).
442
+ */
391
443
  loadString(dxf: string): void;
444
+ /**
445
+ * Load a DXF file from an ArrayBuffer (synchronous, no format conversion).
446
+ * For format conversion support (e.g. DWG), use `loadFile()` or `loadBuffer()` instead.
447
+ */
392
448
  loadArrayBuffer(buffer: ArrayBuffer): void;
393
449
  /**
394
450
  * Clear the current document and reset all state without destroying the viewer.
@@ -507,4 +563,4 @@ interface BBox {
507
563
  declare function computeEntitiesBounds(entities: DxfEntity[]): BBox | null;
508
564
  declare function computeEntityBBox(entity: DxfEntity): BBox | null;
509
565
 
510
- export { type BBox, CadViewer, type CadViewerEventMap, type CadViewerOptions, Camera, CanvasRenderer, type DxfArcEntity, type DxfAttrib, type DxfBlock, type DxfCircleEntity, type DxfDimensionEntity, type DxfDocument, type DxfEllipseEntity, type DxfEntity, type DxfEntityBase, type DxfHatchBoundaryPath, type DxfHatchEdge, type DxfHatchEntity, type DxfHeader, type DxfInsertEntity, type DxfLayer, type DxfLineEntity, type DxfLineType, type DxfLwPolylineEntity, type DxfLwPolylineVertex, type DxfMTextEntity, DxfParseError, type DxfPointEntity, type DxfPolylineEntity, type DxfSplineEntity, type DxfStyle, type DxfTextEntity, EventEmitter, LayerManager, type MeasureEvent, type MeasureState, MeasureTool, type Point2D, type Point3D, type SelectEvent, type SnapResult, type SnapType, SpatialIndex, THEMES, type Theme, type ThemeConfig, type Tool, type ViewTransform, aciToDisplayColor, aciToHex, applyTransform, computeEntitiesBounds, computeEntityBBox, drawEntity, findSnaps, fitToView, hitTest, parseDxf, renderMeasureOverlay, resolveEntityColor, screenToWorld, trueColorToHex, worldToScreen, zoomAtPoint };
566
+ export { type BBox, CadViewer, type CadViewerEventMap, type CadViewerOptions, Camera, CanvasRenderer, type DxfArcEntity, type DxfAttrib, type DxfBlock, type DxfCircleEntity, type DxfDimensionEntity, type DxfDocument, type DxfEllipseEntity, type DxfEntity, type DxfEntityBase, type DxfHatchBoundaryPath, type DxfHatchEdge, type DxfHatchEntity, type DxfHeader, type DxfInsertEntity, type DxfLayer, type DxfLineEntity, type DxfLineType, type DxfLwPolylineEntity, type DxfLwPolylineVertex, type DxfMTextEntity, DxfParseError, type DxfPointEntity, type DxfPolylineEntity, type DxfSplineEntity, type DxfStyle, type DxfTextEntity, EventEmitter, type FormatConverter, LayerManager, type MeasureEvent, type MeasureState, MeasureTool, type Point2D, type Point3D, type SelectEvent, type SnapResult, type SnapType, SpatialIndex, THEMES, type Theme, type ThemeConfig, type Tool, type ViewTransform, aciToDisplayColor, aciToHex, applyTransform, computeEntitiesBounds, computeEntityBBox, drawEntity, findSnaps, fitToView, hitTest, parseDxf, renderMeasureOverlay, resolveEntityColor, screenToWorld, trueColorToHex, worldToScreen, zoomAtPoint };
package/dist/index.js CHANGED
@@ -1545,7 +1545,7 @@ function decodeInput(input) {
1545
1545
  const bytes = new Uint8Array(input);
1546
1546
  const sentinelBytes = new TextDecoder("ascii").decode(bytes.slice(0, BINARY_DXF_SENTINEL.length));
1547
1547
  if (sentinelBytes === BINARY_DXF_SENTINEL) {
1548
- throw new Error("Binary DXF format is not supported. Please export as ASCII DXF.");
1548
+ throw new DxfParseError("Binary DXF format is not supported. Please export as ASCII DXF.");
1549
1549
  }
1550
1550
  let text = new TextDecoder("utf-8").decode(input);
1551
1551
  const versionMatch = text.match(/\$ACADVER[\s\S]*?\n\s*1\s*\n\s*(\S+)/);
@@ -1616,7 +1616,7 @@ function parseDxf(input) {
1616
1616
  try {
1617
1617
  text = decodeInput(input);
1618
1618
  } catch (err) {
1619
- if (err instanceof Error && err.message.includes("Binary DXF")) {
1619
+ if (err instanceof DxfParseError) {
1620
1620
  throw err;
1621
1621
  }
1622
1622
  throw new DxfParseError("Failed to decode DXF input.", err);
@@ -2479,23 +2479,25 @@ function deBoor(degree, controlPoints, knots, t, weights) {
2479
2479
  const denom = knots[i + degree - r + 1] - knots[i];
2480
2480
  if (Math.abs(denom) < 1e-10) continue;
2481
2481
  const alpha = (t - knots[i]) / denom;
2482
+ const dj = d[j];
2483
+ const djPrev = d[j - 1];
2482
2484
  if (weights) {
2483
2485
  const w0 = w[j - 1] * (1 - alpha);
2484
2486
  const w1 = w[j] * alpha;
2485
2487
  const wSum = w0 + w1;
2486
2488
  if (Math.abs(wSum) < 1e-10) continue;
2487
- d[j].x = (d[j - 1].x * w0 + d[j].x * w1) / wSum;
2488
- d[j].y = (d[j - 1].y * w0 + d[j].y * w1) / wSum;
2489
- d[j].z = (d[j - 1].z * w0 + d[j].z * w1) / wSum;
2489
+ dj.x = (djPrev.x * w0 + dj.x * w1) / wSum;
2490
+ dj.y = (djPrev.y * w0 + dj.y * w1) / wSum;
2491
+ dj.z = (djPrev.z * w0 + dj.z * w1) / wSum;
2490
2492
  w[j] = wSum;
2491
2493
  } else {
2492
- d[j].x = (1 - alpha) * d[j - 1].x + alpha * d[j].x;
2493
- d[j].y = (1 - alpha) * d[j - 1].y + alpha * d[j].y;
2494
- d[j].z = (1 - alpha) * d[j - 1].z + alpha * d[j].z;
2494
+ dj.x = (1 - alpha) * djPrev.x + alpha * dj.x;
2495
+ dj.y = (1 - alpha) * djPrev.y + alpha * dj.y;
2496
+ dj.z = (1 - alpha) * djPrev.z + alpha * dj.z;
2495
2497
  }
2496
2498
  }
2497
2499
  }
2498
- return d[degree];
2500
+ return d[degree] ?? { x: 0, y: 0, z: 0 };
2499
2501
  }
2500
2502
  function fitPointsToPolyline(fitPoints) {
2501
2503
  if (fitPoints.length < 2) return fitPoints.map((p) => ({ x: p.x, y: p.y }));
@@ -3711,9 +3713,11 @@ var CadViewer = class {
3711
3713
  currentTool;
3712
3714
  inputHandler;
3713
3715
  resizeObserver;
3716
+ formatConverters;
3714
3717
  selectedEntityIndex = -1;
3715
3718
  renderPending = false;
3716
3719
  destroyed = false;
3720
+ loadGeneration = 0;
3717
3721
  mouseScreenX = 0;
3718
3722
  mouseScreenY = 0;
3719
3723
  constructor(canvas, options) {
@@ -3727,6 +3731,7 @@ var CadViewer = class {
3727
3731
  zoomSpeed: options?.zoomSpeed ?? 1.1,
3728
3732
  initialTool: options?.initialTool ?? "pan"
3729
3733
  };
3734
+ this.formatConverters = options?.formatConverters ?? [];
3730
3735
  this.renderer = new CanvasRenderer(canvas);
3731
3736
  this.camera = new Camera(this.options);
3732
3737
  this.layerManager = new LayerManager();
@@ -3741,15 +3746,91 @@ var CadViewer = class {
3741
3746
  this.requestRender();
3742
3747
  }
3743
3748
  // === Loading ===
3749
+ /**
3750
+ * Throws if the viewer has been destroyed.
3751
+ * Call at the start of any public method that mutates state.
3752
+ */
3753
+ guardDestroyed() {
3754
+ if (this.destroyed) {
3755
+ throw new Error("CadViewer: cannot call methods on a destroyed instance.");
3756
+ }
3757
+ }
3758
+ /**
3759
+ * Run registered format converters on a buffer.
3760
+ * Returns the converted DXF string if a converter matched, or null otherwise.
3761
+ * Each converter's detect() is wrapped in try/catch — a throwing detect() is skipped.
3762
+ */
3763
+ async runConverters(buffer) {
3764
+ for (const converter of this.formatConverters) {
3765
+ let detected = false;
3766
+ try {
3767
+ detected = converter.detect(buffer);
3768
+ } catch {
3769
+ continue;
3770
+ }
3771
+ if (detected) {
3772
+ return converter.convert(buffer);
3773
+ }
3774
+ }
3775
+ return null;
3776
+ }
3777
+ /**
3778
+ * Load a CAD file from a browser File object.
3779
+ * Automatically detects the format using registered converters (e.g. DWG).
3780
+ * Falls back to DXF parsing if no converter matches.
3781
+ */
3744
3782
  async loadFile(file) {
3783
+ this.guardDestroyed();
3784
+ const generation = ++this.loadGeneration;
3745
3785
  const buffer = await file.arrayBuffer();
3746
- this.loadArrayBuffer(buffer);
3786
+ if (this.destroyed || generation !== this.loadGeneration) return;
3787
+ const dxfString = await this.runConverters(buffer);
3788
+ if (this.destroyed || generation !== this.loadGeneration) return;
3789
+ this.doc = dxfString != null ? parseDxf(dxfString) : parseDxf(buffer);
3790
+ this.onDocumentLoaded();
3747
3791
  }
3792
+ /**
3793
+ * Load a CAD file from an ArrayBuffer with format converter support.
3794
+ * Unlike `loadArrayBuffer()` (sync, DXF-only), this method is async and
3795
+ * checks registered FormatConverters for non-DXF formats.
3796
+ */
3797
+ async loadBuffer(buffer) {
3798
+ this.guardDestroyed();
3799
+ const generation = ++this.loadGeneration;
3800
+ const dxfString = await this.runConverters(buffer);
3801
+ if (this.destroyed || generation !== this.loadGeneration) return;
3802
+ this.doc = dxfString != null ? parseDxf(dxfString) : parseDxf(buffer);
3803
+ this.onDocumentLoaded();
3804
+ }
3805
+ /**
3806
+ * Load a pre-parsed DxfDocument directly, bypassing the parser.
3807
+ * Useful for custom parsers or pre-processed documents.
3808
+ */
3809
+ loadDocument(doc) {
3810
+ this.guardDestroyed();
3811
+ ++this.loadGeneration;
3812
+ if (!doc || !Array.isArray(doc.entities) || !(doc.layers instanceof Map)) {
3813
+ throw new Error("CadViewer: invalid DxfDocument \u2014 expected entities array and layers Map.");
3814
+ }
3815
+ this.doc = doc;
3816
+ this.onDocumentLoaded();
3817
+ }
3818
+ /**
3819
+ * Load a DXF string directly (synchronous, no format conversion).
3820
+ */
3748
3821
  loadString(dxf) {
3822
+ this.guardDestroyed();
3823
+ ++this.loadGeneration;
3749
3824
  this.doc = parseDxf(dxf);
3750
3825
  this.onDocumentLoaded();
3751
3826
  }
3827
+ /**
3828
+ * Load a DXF file from an ArrayBuffer (synchronous, no format conversion).
3829
+ * For format conversion support (e.g. DWG), use `loadFile()` or `loadBuffer()` instead.
3830
+ */
3752
3831
  loadArrayBuffer(buffer) {
3832
+ this.guardDestroyed();
3833
+ ++this.loadGeneration;
3753
3834
  this.doc = parseDxf(buffer);
3754
3835
  this.onDocumentLoaded();
3755
3836
  }
@@ -3757,6 +3838,8 @@ var CadViewer = class {
3757
3838
  * Clear the current document and reset all state without destroying the viewer.
3758
3839
  */
3759
3840
  clearDocument() {
3841
+ this.guardDestroyed();
3842
+ ++this.loadGeneration;
3760
3843
  this.doc = null;
3761
3844
  this.selectedEntityIndex = -1;
3762
3845
  this.spatialIndex.clear();
@@ -3777,6 +3860,7 @@ var CadViewer = class {
3777
3860
  }
3778
3861
  // === Camera Controls ===
3779
3862
  fitToView() {
3863
+ this.guardDestroyed();
3780
3864
  if (!this.doc) return;
3781
3865
  const bounds = this.computeDocumentBounds();
3782
3866
  if (!bounds) return;
@@ -3788,6 +3872,7 @@ var CadViewer = class {
3788
3872
  this.emitter.emit("viewchange", this.camera.getTransform());
3789
3873
  }
3790
3874
  zoomTo(scale) {
3875
+ this.guardDestroyed();
3791
3876
  const rect = this.canvas.getBoundingClientRect();
3792
3877
  const centerX = rect.width / 2;
3793
3878
  const centerY = rect.height / 2;
@@ -3797,6 +3882,7 @@ var CadViewer = class {
3797
3882
  this.emitter.emit("viewchange", this.camera.getTransform());
3798
3883
  }
3799
3884
  panTo(worldX, worldY) {
3885
+ this.guardDestroyed();
3800
3886
  const rect = this.canvas.getBoundingClientRect();
3801
3887
  const vt = this.camera.getTransform();
3802
3888
  const currentSX = worldX * vt.scale + vt.offsetX;
@@ -3819,15 +3905,18 @@ var CadViewer = class {
3819
3905
  return this.layerManager.getAllLayers();
3820
3906
  }
3821
3907
  setLayerVisible(name, visible) {
3908
+ this.guardDestroyed();
3822
3909
  this.layerManager.setVisible(name, visible);
3823
3910
  this.requestRender();
3824
3911
  }
3825
3912
  setLayerColor(name, color) {
3913
+ this.guardDestroyed();
3826
3914
  this.layerManager.setColorOverride(name, color);
3827
3915
  this.requestRender();
3828
3916
  }
3829
3917
  // === Theme ===
3830
3918
  setTheme(theme) {
3919
+ this.guardDestroyed();
3831
3920
  this.options.theme = theme;
3832
3921
  this.requestRender();
3833
3922
  }
@@ -3835,11 +3924,13 @@ var CadViewer = class {
3835
3924
  return this.options.theme;
3836
3925
  }
3837
3926
  setBackgroundColor(color) {
3927
+ this.guardDestroyed();
3838
3928
  this.options.backgroundColor = color;
3839
3929
  this.requestRender();
3840
3930
  }
3841
3931
  // === Tools ===
3842
3932
  setTool(tool) {
3933
+ this.guardDestroyed();
3843
3934
  if (this.currentTool === "measure" && tool !== "measure") {
3844
3935
  this.measureTool.deactivate();
3845
3936
  }