@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/README.md CHANGED
@@ -76,14 +76,18 @@ new CadViewer(canvas: HTMLCanvasElement, options?: CadViewerOptions)
76
76
  | `maxZoom` | `number` | `100000` | Maximum zoom scale |
77
77
  | `zoomSpeed` | `number` | `1.1` | Zoom factor per wheel tick |
78
78
  | `initialTool` | `Tool` | `'pan'` | Active tool on init |
79
+ | `formatConverters` | `FormatConverter[]` | `[]` | Format converters for non-DXF files (e.g. DWG) |
79
80
 
80
81
  #### Methods
81
82
 
82
83
  | Method | Description |
83
84
  |--------|-------------|
84
- | `loadFile(file: File)` | Load DXF from a File object |
85
- | `loadString(dxf: string)` | Load DXF from a string |
86
- | `loadArrayBuffer(buffer: ArrayBuffer)` | Load DXF from an ArrayBuffer |
85
+ | `loadFile(file: File)` | Load from a File object (async, runs format converters) |
86
+ | `loadBuffer(buffer: ArrayBuffer)` | Load from an ArrayBuffer (async, runs format converters) |
87
+ | `loadString(dxf: string)` | Load DXF from a string (sync, no conversion) |
88
+ | `loadArrayBuffer(buffer: ArrayBuffer)` | Load DXF from an ArrayBuffer (sync, no conversion) |
89
+ | `loadDocument(doc: DxfDocument)` | Load a pre-parsed DxfDocument directly |
90
+ | `clearDocument()` | Clear the current document without destroying the viewer |
87
91
  | `fitToView()` | Fit drawing to canvas bounds |
88
92
  | `setTheme(theme)` | Set color theme |
89
93
  | `setTool(tool)` | Set active tool (`pan`, `select`, `measure`) |
@@ -93,6 +97,35 @@ new CadViewer(canvas: HTMLCanvasElement, options?: CadViewerOptions)
93
97
  | `off(event, callback)` | Unsubscribe from events |
94
98
  | `destroy()` | Clean up all resources |
95
99
 
100
+ ### `FormatConverter`
101
+
102
+ Interface for registering custom file format converters. Converters are checked in order during `loadFile()` and `loadBuffer()` — the first match wins.
103
+
104
+ ```ts
105
+ interface FormatConverter {
106
+ /** Return true if the buffer is in this format (check magic bytes, not extensions). */
107
+ detect(buffer: ArrayBuffer): boolean;
108
+ /** Convert the buffer to a DXF string. */
109
+ convert(buffer: ArrayBuffer): Promise<string>;
110
+ }
111
+ ```
112
+
113
+ **Example — DWG support via `@cadview/dwg`:**
114
+
115
+ ```ts
116
+ import { CadViewer } from '@cadview/core';
117
+ import { dwgConverter } from '@cadview/dwg';
118
+
119
+ const viewer = new CadViewer(canvas, {
120
+ formatConverters: [dwgConverter],
121
+ });
122
+
123
+ // loadFile and loadBuffer now handle both DXF and DWG
124
+ await viewer.loadFile(file);
125
+ ```
126
+
127
+ > **Note:** `@cadview/dwg` is licensed under GPL-3.0 (due to LibreDWG). `@cadview/core` remains MIT. See the [@cadview/dwg README](../dwg/README.md) for details.
128
+
96
129
  ### `parseDxf`
97
130
 
98
131
  Standalone DXF parser — use this if you only need parsing without the viewer.
package/dist/index.cjs CHANGED
@@ -1551,7 +1551,7 @@ function decodeInput(input) {
1551
1551
  const bytes = new Uint8Array(input);
1552
1552
  const sentinelBytes = new TextDecoder("ascii").decode(bytes.slice(0, BINARY_DXF_SENTINEL.length));
1553
1553
  if (sentinelBytes === BINARY_DXF_SENTINEL) {
1554
- throw new Error("Binary DXF format is not supported. Please export as ASCII DXF.");
1554
+ throw new DxfParseError("Binary DXF format is not supported. Please export as ASCII DXF.");
1555
1555
  }
1556
1556
  let text = new TextDecoder("utf-8").decode(input);
1557
1557
  const versionMatch = text.match(/\$ACADVER[\s\S]*?\n\s*1\s*\n\s*(\S+)/);
@@ -1622,7 +1622,7 @@ function parseDxf(input) {
1622
1622
  try {
1623
1623
  text = decodeInput(input);
1624
1624
  } catch (err) {
1625
- if (err instanceof Error && err.message.includes("Binary DXF")) {
1625
+ if (err instanceof DxfParseError) {
1626
1626
  throw err;
1627
1627
  }
1628
1628
  throw new DxfParseError("Failed to decode DXF input.", err);
@@ -2485,23 +2485,25 @@ function deBoor(degree, controlPoints, knots, t, weights) {
2485
2485
  const denom = knots[i + degree - r + 1] - knots[i];
2486
2486
  if (Math.abs(denom) < 1e-10) continue;
2487
2487
  const alpha = (t - knots[i]) / denom;
2488
+ const dj = d[j];
2489
+ const djPrev = d[j - 1];
2488
2490
  if (weights) {
2489
2491
  const w0 = w[j - 1] * (1 - alpha);
2490
2492
  const w1 = w[j] * alpha;
2491
2493
  const wSum = w0 + w1;
2492
2494
  if (Math.abs(wSum) < 1e-10) continue;
2493
- d[j].x = (d[j - 1].x * w0 + d[j].x * w1) / wSum;
2494
- d[j].y = (d[j - 1].y * w0 + d[j].y * w1) / wSum;
2495
- d[j].z = (d[j - 1].z * w0 + d[j].z * w1) / wSum;
2495
+ dj.x = (djPrev.x * w0 + dj.x * w1) / wSum;
2496
+ dj.y = (djPrev.y * w0 + dj.y * w1) / wSum;
2497
+ dj.z = (djPrev.z * w0 + dj.z * w1) / wSum;
2496
2498
  w[j] = wSum;
2497
2499
  } else {
2498
- d[j].x = (1 - alpha) * d[j - 1].x + alpha * d[j].x;
2499
- d[j].y = (1 - alpha) * d[j - 1].y + alpha * d[j].y;
2500
- d[j].z = (1 - alpha) * d[j - 1].z + alpha * d[j].z;
2500
+ dj.x = (1 - alpha) * djPrev.x + alpha * dj.x;
2501
+ dj.y = (1 - alpha) * djPrev.y + alpha * dj.y;
2502
+ dj.z = (1 - alpha) * djPrev.z + alpha * dj.z;
2501
2503
  }
2502
2504
  }
2503
2505
  }
2504
- return d[degree];
2506
+ return d[degree] ?? { x: 0, y: 0, z: 0 };
2505
2507
  }
2506
2508
  function fitPointsToPolyline(fitPoints) {
2507
2509
  if (fitPoints.length < 2) return fitPoints.map((p) => ({ x: p.x, y: p.y }));
@@ -3717,9 +3719,11 @@ var CadViewer = class {
3717
3719
  currentTool;
3718
3720
  inputHandler;
3719
3721
  resizeObserver;
3722
+ formatConverters;
3720
3723
  selectedEntityIndex = -1;
3721
3724
  renderPending = false;
3722
3725
  destroyed = false;
3726
+ loadGeneration = 0;
3723
3727
  mouseScreenX = 0;
3724
3728
  mouseScreenY = 0;
3725
3729
  constructor(canvas, options) {
@@ -3733,6 +3737,7 @@ var CadViewer = class {
3733
3737
  zoomSpeed: options?.zoomSpeed ?? 1.1,
3734
3738
  initialTool: options?.initialTool ?? "pan"
3735
3739
  };
3740
+ this.formatConverters = options?.formatConverters ?? [];
3736
3741
  this.renderer = new CanvasRenderer(canvas);
3737
3742
  this.camera = new Camera(this.options);
3738
3743
  this.layerManager = new LayerManager();
@@ -3747,15 +3752,91 @@ var CadViewer = class {
3747
3752
  this.requestRender();
3748
3753
  }
3749
3754
  // === Loading ===
3755
+ /**
3756
+ * Throws if the viewer has been destroyed.
3757
+ * Call at the start of any public method that mutates state.
3758
+ */
3759
+ guardDestroyed() {
3760
+ if (this.destroyed) {
3761
+ throw new Error("CadViewer: cannot call methods on a destroyed instance.");
3762
+ }
3763
+ }
3764
+ /**
3765
+ * Run registered format converters on a buffer.
3766
+ * Returns the converted DXF string if a converter matched, or null otherwise.
3767
+ * Each converter's detect() is wrapped in try/catch — a throwing detect() is skipped.
3768
+ */
3769
+ async runConverters(buffer) {
3770
+ for (const converter of this.formatConverters) {
3771
+ let detected = false;
3772
+ try {
3773
+ detected = converter.detect(buffer);
3774
+ } catch {
3775
+ continue;
3776
+ }
3777
+ if (detected) {
3778
+ return converter.convert(buffer);
3779
+ }
3780
+ }
3781
+ return null;
3782
+ }
3783
+ /**
3784
+ * Load a CAD file from a browser File object.
3785
+ * Automatically detects the format using registered converters (e.g. DWG).
3786
+ * Falls back to DXF parsing if no converter matches.
3787
+ */
3750
3788
  async loadFile(file) {
3789
+ this.guardDestroyed();
3790
+ const generation = ++this.loadGeneration;
3751
3791
  const buffer = await file.arrayBuffer();
3752
- this.loadArrayBuffer(buffer);
3792
+ if (this.destroyed || generation !== this.loadGeneration) return;
3793
+ const dxfString = await this.runConverters(buffer);
3794
+ if (this.destroyed || generation !== this.loadGeneration) return;
3795
+ this.doc = dxfString != null ? parseDxf(dxfString) : parseDxf(buffer);
3796
+ this.onDocumentLoaded();
3753
3797
  }
3798
+ /**
3799
+ * Load a CAD file from an ArrayBuffer with format converter support.
3800
+ * Unlike `loadArrayBuffer()` (sync, DXF-only), this method is async and
3801
+ * checks registered FormatConverters for non-DXF formats.
3802
+ */
3803
+ async loadBuffer(buffer) {
3804
+ this.guardDestroyed();
3805
+ const generation = ++this.loadGeneration;
3806
+ const dxfString = await this.runConverters(buffer);
3807
+ if (this.destroyed || generation !== this.loadGeneration) return;
3808
+ this.doc = dxfString != null ? parseDxf(dxfString) : parseDxf(buffer);
3809
+ this.onDocumentLoaded();
3810
+ }
3811
+ /**
3812
+ * Load a pre-parsed DxfDocument directly, bypassing the parser.
3813
+ * Useful for custom parsers or pre-processed documents.
3814
+ */
3815
+ loadDocument(doc) {
3816
+ this.guardDestroyed();
3817
+ ++this.loadGeneration;
3818
+ if (!doc || !Array.isArray(doc.entities) || !(doc.layers instanceof Map)) {
3819
+ throw new Error("CadViewer: invalid DxfDocument \u2014 expected entities array and layers Map.");
3820
+ }
3821
+ this.doc = doc;
3822
+ this.onDocumentLoaded();
3823
+ }
3824
+ /**
3825
+ * Load a DXF string directly (synchronous, no format conversion).
3826
+ */
3754
3827
  loadString(dxf) {
3828
+ this.guardDestroyed();
3829
+ ++this.loadGeneration;
3755
3830
  this.doc = parseDxf(dxf);
3756
3831
  this.onDocumentLoaded();
3757
3832
  }
3833
+ /**
3834
+ * Load a DXF file from an ArrayBuffer (synchronous, no format conversion).
3835
+ * For format conversion support (e.g. DWG), use `loadFile()` or `loadBuffer()` instead.
3836
+ */
3758
3837
  loadArrayBuffer(buffer) {
3838
+ this.guardDestroyed();
3839
+ ++this.loadGeneration;
3759
3840
  this.doc = parseDxf(buffer);
3760
3841
  this.onDocumentLoaded();
3761
3842
  }
@@ -3763,6 +3844,8 @@ var CadViewer = class {
3763
3844
  * Clear the current document and reset all state without destroying the viewer.
3764
3845
  */
3765
3846
  clearDocument() {
3847
+ this.guardDestroyed();
3848
+ ++this.loadGeneration;
3766
3849
  this.doc = null;
3767
3850
  this.selectedEntityIndex = -1;
3768
3851
  this.spatialIndex.clear();
@@ -3783,6 +3866,7 @@ var CadViewer = class {
3783
3866
  }
3784
3867
  // === Camera Controls ===
3785
3868
  fitToView() {
3869
+ this.guardDestroyed();
3786
3870
  if (!this.doc) return;
3787
3871
  const bounds = this.computeDocumentBounds();
3788
3872
  if (!bounds) return;
@@ -3794,6 +3878,7 @@ var CadViewer = class {
3794
3878
  this.emitter.emit("viewchange", this.camera.getTransform());
3795
3879
  }
3796
3880
  zoomTo(scale) {
3881
+ this.guardDestroyed();
3797
3882
  const rect = this.canvas.getBoundingClientRect();
3798
3883
  const centerX = rect.width / 2;
3799
3884
  const centerY = rect.height / 2;
@@ -3803,6 +3888,7 @@ var CadViewer = class {
3803
3888
  this.emitter.emit("viewchange", this.camera.getTransform());
3804
3889
  }
3805
3890
  panTo(worldX, worldY) {
3891
+ this.guardDestroyed();
3806
3892
  const rect = this.canvas.getBoundingClientRect();
3807
3893
  const vt = this.camera.getTransform();
3808
3894
  const currentSX = worldX * vt.scale + vt.offsetX;
@@ -3825,15 +3911,18 @@ var CadViewer = class {
3825
3911
  return this.layerManager.getAllLayers();
3826
3912
  }
3827
3913
  setLayerVisible(name, visible) {
3914
+ this.guardDestroyed();
3828
3915
  this.layerManager.setVisible(name, visible);
3829
3916
  this.requestRender();
3830
3917
  }
3831
3918
  setLayerColor(name, color) {
3919
+ this.guardDestroyed();
3832
3920
  this.layerManager.setColorOverride(name, color);
3833
3921
  this.requestRender();
3834
3922
  }
3835
3923
  // === Theme ===
3836
3924
  setTheme(theme) {
3925
+ this.guardDestroyed();
3837
3926
  this.options.theme = theme;
3838
3927
  this.requestRender();
3839
3928
  }
@@ -3841,11 +3930,13 @@ var CadViewer = class {
3841
3930
  return this.options.theme;
3842
3931
  }
3843
3932
  setBackgroundColor(color) {
3933
+ this.guardDestroyed();
3844
3934
  this.options.backgroundColor = color;
3845
3935
  this.requestRender();
3846
3936
  }
3847
3937
  // === Tools ===
3848
3938
  setTool(tool) {
3939
+ this.guardDestroyed();
3849
3940
  if (this.currentTool === "measure" && tool !== "measure") {
3850
3941
  this.measureTool.deactivate();
3851
3942
  }