circuit-to-canvas 0.0.43 → 0.0.45

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.js CHANGED
@@ -816,6 +816,12 @@ function drawPcbVia(params) {
816
816
  }
817
817
 
818
818
  // lib/drawer/elements/pcb-hole.ts
819
+ function getRotation(hole) {
820
+ if ("ccw_rotation" in hole && typeof hole.ccw_rotation === "number") {
821
+ return hole.ccw_rotation;
822
+ }
823
+ return 0;
824
+ }
819
825
  function drawPcbHole(params) {
820
826
  const { ctx, hole, realToCanvasMat, colorMap } = params;
821
827
  const hasSoldermask = hole.is_covered_with_solder_mask === true && hole.soldermask_margin !== void 0 && hole.soldermask_margin > 0;
@@ -841,6 +847,7 @@ function drawPcbHole(params) {
841
847
  return;
842
848
  }
843
849
  if (hole.hole_shape === "square") {
850
+ const rotation = getRotation(hole);
844
851
  if (hasSoldermask && margin > 0) {
845
852
  drawRect({
846
853
  ctx,
@@ -848,7 +855,8 @@ function drawPcbHole(params) {
848
855
  width: hole.hole_diameter + margin * 2,
849
856
  height: hole.hole_diameter + margin * 2,
850
857
  fill: positiveMarginColor,
851
- realToCanvasMat
858
+ realToCanvasMat,
859
+ rotation
852
860
  });
853
861
  }
854
862
  drawRect({
@@ -857,11 +865,13 @@ function drawPcbHole(params) {
857
865
  width: hole.hole_diameter,
858
866
  height: hole.hole_diameter,
859
867
  fill: colorMap.drill,
860
- realToCanvasMat
868
+ realToCanvasMat,
869
+ rotation
861
870
  });
862
871
  return;
863
872
  }
864
873
  if (hole.hole_shape === "oval") {
874
+ const rotation = getRotation(hole);
865
875
  if (hasSoldermask && margin > 0) {
866
876
  drawOval({
867
877
  ctx,
@@ -869,7 +879,8 @@ function drawPcbHole(params) {
869
879
  radius_x: hole.hole_width / 2 + margin,
870
880
  radius_y: hole.hole_height / 2 + margin,
871
881
  fill: positiveMarginColor,
872
- realToCanvasMat
882
+ realToCanvasMat,
883
+ rotation
873
884
  });
874
885
  }
875
886
  drawOval({
@@ -878,11 +889,13 @@ function drawPcbHole(params) {
878
889
  radius_x: hole.hole_width / 2,
879
890
  radius_y: hole.hole_height / 2,
880
891
  fill: colorMap.drill,
881
- realToCanvasMat
892
+ realToCanvasMat,
893
+ rotation
882
894
  });
883
895
  return;
884
896
  }
885
897
  if (hole.hole_shape === "rect") {
898
+ const rotation = getRotation(hole);
886
899
  if (hasSoldermask && margin > 0) {
887
900
  drawRect({
888
901
  ctx,
@@ -890,7 +903,8 @@ function drawPcbHole(params) {
890
903
  width: hole.hole_width + margin * 2,
891
904
  height: hole.hole_height + margin * 2,
892
905
  fill: positiveMarginColor,
893
- realToCanvasMat
906
+ realToCanvasMat,
907
+ rotation
894
908
  });
895
909
  }
896
910
  drawRect({
@@ -899,11 +913,13 @@ function drawPcbHole(params) {
899
913
  width: hole.hole_width,
900
914
  height: hole.hole_height,
901
915
  fill: colorMap.drill,
902
- realToCanvasMat
916
+ realToCanvasMat,
917
+ rotation
903
918
  });
904
919
  return;
905
920
  }
906
921
  if (hole.hole_shape === "pill") {
922
+ const rotation = getRotation(hole);
907
923
  if (hasSoldermask && margin > 0) {
908
924
  drawPill({
909
925
  ctx,
@@ -911,7 +927,8 @@ function drawPcbHole(params) {
911
927
  width: hole.hole_width + margin * 2,
912
928
  height: hole.hole_height + margin * 2,
913
929
  fill: positiveMarginColor,
914
- realToCanvasMat
930
+ realToCanvasMat,
931
+ rotation
915
932
  });
916
933
  }
917
934
  drawPill({
@@ -920,12 +937,13 @@ function drawPcbHole(params) {
920
937
  width: hole.hole_width,
921
938
  height: hole.hole_height,
922
939
  fill: colorMap.drill,
923
- realToCanvasMat
940
+ realToCanvasMat,
941
+ rotation
924
942
  });
925
943
  return;
926
944
  }
927
945
  if (hole.hole_shape === "rotated_pill") {
928
- const rotation = hole.ccw_rotation ?? 0;
946
+ const rotation = getRotation(hole);
929
947
  if (hasSoldermask && margin > 0) {
930
948
  drawPill({
931
949
  ctx,
@@ -1825,9 +1843,6 @@ function drawPcbCopperPour(params) {
1825
1843
  // lib/drawer/elements/pcb-copper-text.ts
1826
1844
  import { applyToPoint as applyToPoint13 } from "transformation-matrix";
1827
1845
  var DEFAULT_PADDING = { left: 0.2, right: 0.2, top: 0.2, bottom: 0.2 };
1828
- function layerToCopperColor(layer, colorMap) {
1829
- return colorMap.copper[layer] ?? colorMap.copper.top;
1830
- }
1831
1846
  function mapAnchorAlignment2(alignment) {
1832
1847
  if (!alignment) return "center";
1833
1848
  if (alignment.includes("left")) return "center_left";
@@ -1849,13 +1864,11 @@ function drawPcbCopperText(params) {
1849
1864
  ...DEFAULT_PADDING,
1850
1865
  ...text.knockout_padding
1851
1866
  };
1852
- const textColor = layerToCopperColor(text.layer, colorMap);
1867
+ const textColor = colorMap.copper[text.layer] ?? colorMap.copper.top;
1853
1868
  const layout = getAlphabetLayout(content, fontSize);
1854
1869
  const totalWidth = layout.width + layout.strokeWidth;
1855
1870
  const alignment = mapAnchorAlignment2(text.anchor_alignment);
1856
1871
  const startPos = getTextStartPosition(alignment, layout);
1857
- const startX = startPos.x;
1858
- const startY = startPos.y;
1859
1872
  ctx.save();
1860
1873
  ctx.translate(x, y);
1861
1874
  if (text.is_mirrored) ctx.scale(-1, 1);
@@ -1868,29 +1881,22 @@ function drawPcbCopperText(params) {
1868
1881
  const paddingRight = padding.right * scale2;
1869
1882
  const paddingTop = padding.top * scale2;
1870
1883
  const paddingBottom = padding.bottom * scale2;
1871
- const textBoxTop = startY - layout.strokeWidth / 2;
1872
- const textBoxBottom = startY + layout.height + layout.strokeWidth / 2;
1873
- const textBoxHeight = textBoxBottom - textBoxTop;
1874
- const xOffset = startX - paddingLeft;
1875
- const yOffset = textBoxTop - paddingTop;
1876
- const knockoutWidth = totalWidth + paddingLeft + paddingRight;
1877
- const knockoutHeight = textBoxHeight + paddingTop + paddingBottom;
1884
+ const rectX = startPos.x - paddingLeft * 4;
1885
+ const rectY = startPos.y - paddingTop * 4;
1886
+ const rectWidth = totalWidth + paddingLeft * 2 + paddingRight * 2;
1887
+ const rectHeight = layout.height + layout.strokeWidth + paddingTop * 2 + paddingBottom * 2;
1878
1888
  ctx.fillStyle = textColor;
1879
- ctx.fillRect(xOffset, yOffset, knockoutWidth, knockoutHeight);
1880
- const previousCompositeOperation = ctx.globalCompositeOperation;
1881
- ctx.globalCompositeOperation = "destination-out";
1882
- ctx.fillStyle = "rgba(0,0,0,1)";
1883
- ctx.strokeStyle = "rgba(0,0,0,1)";
1884
- strokeAlphabetText({ ctx, text: content, fontSize, startX, startY });
1885
- if (previousCompositeOperation) {
1886
- ctx.globalCompositeOperation = previousCompositeOperation;
1887
- } else {
1888
- ctx.globalCompositeOperation = "source-over";
1889
- }
1889
+ ctx.fillRect(rectX, rectY, rectWidth, rectHeight);
1890
1890
  } else {
1891
1891
  ctx.strokeStyle = textColor;
1892
- strokeAlphabetText({ ctx, text: content, fontSize, startX, startY });
1893
1892
  }
1893
+ strokeAlphabetText({
1894
+ ctx,
1895
+ text: content,
1896
+ fontSize,
1897
+ startX: startPos.x,
1898
+ startY: startPos.y
1899
+ });
1894
1900
  ctx.restore();
1895
1901
  }
1896
1902
 
@@ -1,13 +1,12 @@
1
- import type { PcbCopperText } from "circuit-json"
1
+ import type { NinePointAnchor, PcbCopperText } from "circuit-json"
2
2
  import type { Matrix } from "transformation-matrix"
3
3
  import { applyToPoint } from "transformation-matrix"
4
- import type { PcbColorMap, CanvasContext } from "../types"
5
4
  import {
6
5
  getAlphabetLayout,
7
- strokeAlphabetText,
8
6
  getTextStartPosition,
9
- type AnchorAlignment,
7
+ strokeAlphabetText,
10
8
  } from "../shapes/text"
9
+ import type { CanvasContext, PcbColorMap } from "../types"
11
10
 
12
11
  export interface DrawPcbCopperTextParams {
13
12
  ctx: CanvasContext
@@ -18,14 +17,8 @@ export interface DrawPcbCopperTextParams {
18
17
 
19
18
  const DEFAULT_PADDING = { left: 0.2, right: 0.2, top: 0.2, bottom: 0.2 }
20
19
 
21
- function layerToCopperColor(layer: string, colorMap: PcbColorMap): string {
22
- return (
23
- colorMap.copper[layer as keyof typeof colorMap.copper] ??
24
- colorMap.copper.top
25
- )
26
- }
27
-
28
- function mapAnchorAlignment(alignment?: string): AnchorAlignment {
20
+ function mapAnchorAlignment(alignment?: string): NinePointAnchor {
21
+ // Vertical component is intentionally collapsed to center; callers only care about left/center/right.
29
22
  if (!alignment) return "center"
30
23
  if (alignment.includes("left")) return "center_left"
31
24
  if (alignment.includes("right")) return "center_right"
@@ -49,13 +42,13 @@ export function drawPcbCopperText(params: DrawPcbCopperTextParams): void {
49
42
  ...DEFAULT_PADDING,
50
43
  ...text.knockout_padding,
51
44
  }
52
- const textColor = layerToCopperColor(text.layer, colorMap)
45
+ const textColor =
46
+ colorMap.copper[text.layer as keyof typeof colorMap.copper] ??
47
+ colorMap.copper.top
53
48
  const layout = getAlphabetLayout(content, fontSize)
54
49
  const totalWidth = layout.width + layout.strokeWidth
55
50
  const alignment = mapAnchorAlignment(text.anchor_alignment)
56
51
  const startPos = getTextStartPosition(alignment, layout)
57
- const startX = startPos.x
58
- const startY = startPos.y
59
52
 
60
53
  ctx.save()
61
54
  ctx.translate(x, y)
@@ -71,32 +64,25 @@ export function drawPcbCopperText(params: DrawPcbCopperTextParams): void {
71
64
  const paddingRight = padding.right * scale
72
65
  const paddingTop = padding.top * scale
73
66
  const paddingBottom = padding.bottom * scale
74
- // Calculate knockout rectangle to cover the text box
75
- const textBoxTop = startY - layout.strokeWidth / 2
76
- const textBoxBottom = startY + layout.height + layout.strokeWidth / 2
77
- const textBoxHeight = textBoxBottom - textBoxTop
78
-
79
- const xOffset = startX - paddingLeft
80
- const yOffset = textBoxTop - paddingTop
81
- const knockoutWidth = totalWidth + paddingLeft + paddingRight
82
- const knockoutHeight = textBoxHeight + paddingTop + paddingBottom
67
+ const rectX = startPos.x - paddingLeft * 4
68
+ const rectY = startPos.y - paddingTop * 4
69
+ const rectWidth = totalWidth + paddingLeft * 2 + paddingRight * 2
70
+ const rectHeight =
71
+ layout.height + layout.strokeWidth + paddingTop * 2 + paddingBottom * 2
83
72
 
73
+ // Draw knockout rectangle
84
74
  ctx.fillStyle = textColor
85
- ctx.fillRect(xOffset, yOffset, knockoutWidth, knockoutHeight)
86
-
87
- const previousCompositeOperation = ctx.globalCompositeOperation
88
- ctx.globalCompositeOperation = "destination-out"
89
- ctx.fillStyle = "rgba(0,0,0,1)"
90
- ctx.strokeStyle = "rgba(0,0,0,1)"
91
- strokeAlphabetText({ ctx, text: content, fontSize, startX, startY })
92
- if (previousCompositeOperation) {
93
- ctx.globalCompositeOperation = previousCompositeOperation
94
- } else {
95
- ctx.globalCompositeOperation = "source-over"
96
- }
75
+ ctx.fillRect(rectX, rectY, rectWidth, rectHeight)
97
76
  } else {
98
77
  ctx.strokeStyle = textColor
99
- strokeAlphabetText({ ctx, text: content, fontSize, startX, startY })
100
78
  }
79
+
80
+ strokeAlphabetText({
81
+ ctx,
82
+ text: content,
83
+ fontSize,
84
+ startX: startPos.x,
85
+ startY: startPos.y,
86
+ })
101
87
  ctx.restore()
102
88
  }
@@ -13,6 +13,14 @@ export interface DrawPcbHoleParams {
13
13
  colorMap: PcbColorMap
14
14
  }
15
15
 
16
+ // Helper function to safely access ccw_rotation property
17
+ function getRotation(hole: PCBHole): number {
18
+ if ("ccw_rotation" in hole && typeof hole.ccw_rotation === "number") {
19
+ return hole.ccw_rotation
20
+ }
21
+ return 0
22
+ }
23
+
16
24
  export function drawPcbHole(params: DrawPcbHoleParams): void {
17
25
  const { ctx, hole, realToCanvasMat, colorMap } = params
18
26
 
@@ -47,6 +55,7 @@ export function drawPcbHole(params: DrawPcbHoleParams): void {
47
55
  }
48
56
 
49
57
  if (hole.hole_shape === "square") {
58
+ const rotation = getRotation(hole)
50
59
  // For positive margins, draw extended mask area first
51
60
  if (hasSoldermask && margin > 0) {
52
61
  drawRect({
@@ -56,6 +65,7 @@ export function drawPcbHole(params: DrawPcbHoleParams): void {
56
65
  height: hole.hole_diameter + margin * 2,
57
66
  fill: positiveMarginColor,
58
67
  realToCanvasMat,
68
+ rotation,
59
69
  })
60
70
  }
61
71
 
@@ -67,11 +77,13 @@ export function drawPcbHole(params: DrawPcbHoleParams): void {
67
77
  height: hole.hole_diameter,
68
78
  fill: colorMap.drill,
69
79
  realToCanvasMat,
80
+ rotation,
70
81
  })
71
82
  return
72
83
  }
73
84
 
74
85
  if (hole.hole_shape === "oval") {
86
+ const rotation = getRotation(hole)
75
87
  // For positive margins, draw extended mask area first
76
88
  if (hasSoldermask && margin > 0) {
77
89
  drawOval({
@@ -81,6 +93,7 @@ export function drawPcbHole(params: DrawPcbHoleParams): void {
81
93
  radius_y: hole.hole_height / 2 + margin,
82
94
  fill: positiveMarginColor,
83
95
  realToCanvasMat,
96
+ rotation,
84
97
  })
85
98
  }
86
99
 
@@ -92,11 +105,13 @@ export function drawPcbHole(params: DrawPcbHoleParams): void {
92
105
  radius_y: hole.hole_height / 2,
93
106
  fill: colorMap.drill,
94
107
  realToCanvasMat,
108
+ rotation,
95
109
  })
96
110
  return
97
111
  }
98
112
 
99
113
  if (hole.hole_shape === "rect") {
114
+ const rotation = getRotation(hole)
100
115
  // For positive margins, draw extended mask area first
101
116
  if (hasSoldermask && margin > 0) {
102
117
  drawRect({
@@ -106,6 +121,7 @@ export function drawPcbHole(params: DrawPcbHoleParams): void {
106
121
  height: hole.hole_height + margin * 2,
107
122
  fill: positiveMarginColor,
108
123
  realToCanvasMat,
124
+ rotation,
109
125
  })
110
126
  }
111
127
 
@@ -117,11 +133,13 @@ export function drawPcbHole(params: DrawPcbHoleParams): void {
117
133
  height: hole.hole_height,
118
134
  fill: colorMap.drill,
119
135
  realToCanvasMat,
136
+ rotation,
120
137
  })
121
138
  return
122
139
  }
123
140
 
124
141
  if (hole.hole_shape === "pill") {
142
+ const rotation = getRotation(hole)
125
143
  // For positive margins, draw extended mask area first
126
144
  if (hasSoldermask && margin > 0) {
127
145
  drawPill({
@@ -131,6 +149,7 @@ export function drawPcbHole(params: DrawPcbHoleParams): void {
131
149
  height: hole.hole_height + margin * 2,
132
150
  fill: positiveMarginColor,
133
151
  realToCanvasMat,
152
+ rotation,
134
153
  })
135
154
  }
136
155
 
@@ -142,12 +161,13 @@ export function drawPcbHole(params: DrawPcbHoleParams): void {
142
161
  height: hole.hole_height,
143
162
  fill: colorMap.drill,
144
163
  realToCanvasMat,
164
+ rotation,
145
165
  })
146
166
  return
147
167
  }
148
168
 
149
169
  if (hole.hole_shape === "rotated_pill") {
150
- const rotation = (hole as any).ccw_rotation ?? 0
170
+ const rotation = getRotation(hole)
151
171
 
152
172
  // For positive margins, draw extended mask area first
153
173
  if (hasSoldermask && margin > 0) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "circuit-to-canvas",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.43",
4
+ "version": "0.0.45",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "build": "tsup-node ./lib/index.ts --format esm --dts",
@@ -103,3 +103,110 @@ test("draw pill hole", async () => {
103
103
  "pill-hole",
104
104
  )
105
105
  })
106
+
107
+ test("draw rotated oval hole", async () => {
108
+ const canvas = createCanvas(100, 100)
109
+ const ctx = canvas.getContext("2d")
110
+ const drawer = new CircuitToCanvasDrawer(ctx)
111
+
112
+ ctx.fillStyle = "#1a1a1a"
113
+ ctx.fillRect(0, 0, 100, 100)
114
+
115
+ const hole: PCBHole & { ccw_rotation?: number } = {
116
+ type: "pcb_hole",
117
+ pcb_hole_id: "hole1",
118
+ hole_shape: "oval",
119
+ hole_width: 50,
120
+ hole_height: 30,
121
+ x: 50,
122
+ y: 50,
123
+ ccw_rotation: 45,
124
+ }
125
+
126
+ drawer.drawElements([hole])
127
+
128
+ await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
129
+ import.meta.path,
130
+ "rotated-oval-hole",
131
+ )
132
+ })
133
+
134
+ test("draw rotated rect hole", async () => {
135
+ const canvas = createCanvas(100, 100)
136
+ const ctx = canvas.getContext("2d")
137
+ const drawer = new CircuitToCanvasDrawer(ctx)
138
+
139
+ ctx.fillStyle = "#1a1a1a"
140
+ ctx.fillRect(0, 0, 100, 100)
141
+
142
+ const hole: PCBHole & { ccw_rotation?: number } = {
143
+ type: "pcb_hole",
144
+ pcb_hole_id: "hole1",
145
+ hole_shape: "rect",
146
+ hole_width: 50,
147
+ hole_height: 30,
148
+ x: 50,
149
+ y: 50,
150
+ ccw_rotation: 45,
151
+ }
152
+
153
+ drawer.drawElements([hole])
154
+
155
+ await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
156
+ import.meta.path,
157
+ "rotated-rect-hole",
158
+ )
159
+ })
160
+
161
+ test("draw rotated square hole", async () => {
162
+ const canvas = createCanvas(100, 100)
163
+ const ctx = canvas.getContext("2d")
164
+ const drawer = new CircuitToCanvasDrawer(ctx)
165
+
166
+ ctx.fillStyle = "#1a1a1a"
167
+ ctx.fillRect(0, 0, 100, 100)
168
+
169
+ const hole: PCBHole & { ccw_rotation?: number } = {
170
+ type: "pcb_hole",
171
+ pcb_hole_id: "hole1",
172
+ hole_shape: "square",
173
+ hole_diameter: 30,
174
+ x: 50,
175
+ y: 50,
176
+ ccw_rotation: 45,
177
+ }
178
+
179
+ drawer.drawElements([hole])
180
+
181
+ await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
182
+ import.meta.path,
183
+ "rotated-square-hole",
184
+ )
185
+ })
186
+
187
+ test("draw rotated pill hole", async () => {
188
+ const canvas = createCanvas(100, 100)
189
+ const ctx = canvas.getContext("2d")
190
+ const drawer = new CircuitToCanvasDrawer(ctx)
191
+
192
+ ctx.fillStyle = "#1a1a1a"
193
+ ctx.fillRect(0, 0, 100, 100)
194
+
195
+ const hole: PCBHole & { ccw_rotation?: number } = {
196
+ type: "pcb_hole",
197
+ pcb_hole_id: "hole1",
198
+ hole_shape: "pill",
199
+ hole_width: 60,
200
+ hole_height: 30,
201
+ x: 50,
202
+ y: 50,
203
+ ccw_rotation: 45,
204
+ }
205
+
206
+ drawer.drawElements([hole])
207
+
208
+ await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
209
+ import.meta.path,
210
+ "rotated-pill-hole",
211
+ )
212
+ })