circuit-to-canvas 0.0.49 → 0.0.51

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.
Files changed (52) hide show
  1. package/dist/index.d.ts +13 -5
  2. package/dist/index.js +1450 -1226
  3. package/lib/drawer/CircuitToCanvasDrawer.ts +262 -312
  4. package/lib/drawer/elements/helper-functions/draw-pill.ts +39 -0
  5. package/lib/drawer/elements/helper-functions/draw-polygon.ts +25 -0
  6. package/lib/drawer/elements/helper-functions/draw-rounded-rect.ts +34 -0
  7. package/lib/drawer/elements/helper-functions/index.ts +3 -0
  8. package/lib/drawer/elements/pcb-board.ts +13 -3
  9. package/lib/drawer/elements/pcb-hole.ts +56 -338
  10. package/lib/drawer/elements/pcb-plated-hole.ts +154 -442
  11. package/lib/drawer/elements/pcb-smtpad.ts +5 -271
  12. package/lib/drawer/elements/pcb-soldermask/board.ts +44 -0
  13. package/lib/drawer/elements/pcb-soldermask/cutout.ts +74 -0
  14. package/lib/drawer/elements/pcb-soldermask/hole.ts +288 -0
  15. package/lib/drawer/elements/pcb-soldermask/index.ts +140 -0
  16. package/lib/drawer/elements/pcb-soldermask/plated-hole.ts +365 -0
  17. package/lib/drawer/elements/pcb-soldermask/smt-pad.ts +354 -0
  18. package/lib/drawer/elements/pcb-soldermask/via.ts +27 -0
  19. package/lib/drawer/elements/soldermask-margin.ts +39 -8
  20. package/package.json +2 -2
  21. package/tests/board-snapshot/__snapshots__/usb-c-flashlight-board.snap.png +0 -0
  22. package/tests/board-snapshot/usb-c-flashlight-board.test.ts +1 -0
  23. package/tests/elements/__snapshots__/board-with-elements.snap.png +0 -0
  24. package/tests/elements/__snapshots__/brep-copper-pours.snap.png +0 -0
  25. package/tests/elements/__snapshots__/custom-outline-board.snap.png +0 -0
  26. package/tests/elements/__snapshots__/oval-plated-hole.snap.png +0 -0
  27. package/tests/elements/__snapshots__/pcb-board.snap.png +0 -0
  28. package/tests/elements/__snapshots__/pcb-comprehensive-soldermask-margin.snap.png +0 -0
  29. package/tests/elements/__snapshots__/pcb-fabrication-note-dimension.snap.png +0 -0
  30. package/tests/elements/__snapshots__/pcb-hole-soldermask-margin.snap.png +0 -0
  31. package/tests/elements/__snapshots__/pcb-keepout-layer-filter.snap.png +0 -0
  32. package/tests/elements/__snapshots__/pcb-keepout-multiple-layers.snap.png +0 -0
  33. package/tests/elements/__snapshots__/pcb-keepout-rect-and-circle.snap.png +0 -0
  34. package/tests/elements/__snapshots__/pcb-keepout-with-group-id.snap.png +0 -0
  35. package/tests/elements/__snapshots__/pcb-no-soldermask.snap.png +0 -0
  36. package/tests/elements/__snapshots__/pcb-plated-hole-soldermask-margin.snap.png +0 -0
  37. package/tests/elements/__snapshots__/pcb-plated-hole.snap.png +0 -0
  38. package/tests/elements/__snapshots__/pcb-silkscreen-on-component.snap.png +0 -0
  39. package/tests/elements/__snapshots__/pcb-silkscreen-oval.snap.png +0 -0
  40. package/tests/elements/__snapshots__/pcb-smtpad-asymmetric-soldermask-margin.snap.png +0 -0
  41. package/tests/elements/__snapshots__/pcb-smtpad-soldermask-coverage.snap.png +0 -0
  42. package/tests/elements/__snapshots__/pcb-smtpad-soldermask-margin.snap.png +0 -0
  43. package/tests/elements/__snapshots__/pill-plated-hole.snap.png +0 -0
  44. package/tests/elements/pcb-comprehensive-soldermask-margin.test.ts +2 -2
  45. package/tests/elements/pcb-hole-soldermask-margin.test.ts +155 -2
  46. package/tests/elements/pcb-no-soldermask.test.ts +1281 -0
  47. package/tests/elements/pcb-plated-hole-soldermask-margin.test.ts +1 -1
  48. package/tests/elements/pcb-plated-hole.test.ts +40 -4
  49. package/tests/elements/pcb-smtpad-asymmetric-soldermask-margin.test.ts +140 -0
  50. package/tests/elements/pcb-smtpad-soldermask-coverage.test.ts +1 -1
  51. package/tests/elements/pcb-smtpad-soldermask-margin.test.ts +18 -2
  52. package/tests/fixtures/getStackedPngSvgComparison.ts +8 -2
@@ -0,0 +1,27 @@
1
+ import type { PcbVia } from "circuit-json"
2
+ import type { Matrix } from "transformation-matrix"
3
+ import { applyToPoint } from "transformation-matrix"
4
+ import type { CanvasContext, PcbColorMap } from "../../types"
5
+
6
+ /**
7
+ * Process soldermask for a via.
8
+ * Vias typically have soldermask openings to expose the copper ring.
9
+ */
10
+ export function processViaSoldermask(params: {
11
+ ctx: CanvasContext
12
+ via: PcbVia
13
+ realToCanvasMat: Matrix
14
+ colorMap: PcbColorMap
15
+ }): void {
16
+ const { ctx, via, realToCanvasMat, colorMap } = params
17
+ // Vias typically have soldermask openings to expose the copper ring
18
+ // Draw substrate color to simulate the cutout
19
+ const [cx, cy] = applyToPoint(realToCanvasMat, [via.x, via.y])
20
+ const scaledRadius = (via.outer_diameter / 2) * Math.abs(realToCanvasMat.a)
21
+
22
+ ctx.fillStyle = colorMap.substrate
23
+ ctx.beginPath()
24
+ ctx.arc(cx, cy, scaledRadius, 0, Math.PI * 2)
25
+ ctx.closePath()
26
+ ctx.fill()
27
+ }
@@ -17,13 +17,29 @@ export function drawSoldermaskRingForRect(
17
17
  realToCanvasMat: Matrix,
18
18
  soldermaskColor: string,
19
19
  padColor: string,
20
+ asymmetricMargins?: {
21
+ left: number
22
+ right: number
23
+ top: number
24
+ bottom: number
25
+ },
20
26
  ): void {
21
27
  const [cx, cy] = applyToPoint(realToCanvasMat, [center.x, center.y])
22
28
  const scaledWidth = width * Math.abs(realToCanvasMat.a)
23
29
  const scaledHeight = height * Math.abs(realToCanvasMat.a)
24
- const scaledMargin = Math.abs(margin) * Math.abs(realToCanvasMat.a)
25
30
  const scaledRadius = borderRadius * Math.abs(realToCanvasMat.a)
26
31
 
32
+ const ml = asymmetricMargins?.left ?? margin
33
+ const mr = asymmetricMargins?.right ?? margin
34
+ const mt = asymmetricMargins?.top ?? margin
35
+ const mb = asymmetricMargins?.bottom ?? margin
36
+
37
+ // Thickness of the soldermask ring (only if negative margin)
38
+ const scaledThicknessL = Math.max(0, -ml) * Math.abs(realToCanvasMat.a)
39
+ const scaledThicknessR = Math.max(0, -mr) * Math.abs(realToCanvasMat.a)
40
+ const scaledThicknessT = Math.max(0, -mt) * Math.abs(realToCanvasMat.a)
41
+ const scaledThicknessB = Math.max(0, -mb) * Math.abs(realToCanvasMat.a)
42
+
27
43
  ctx.save()
28
44
  ctx.translate(cx, cy)
29
45
 
@@ -76,16 +92,31 @@ export function drawSoldermaskRingForRect(
76
92
  ctx.globalCompositeOperation = prevCompositeOp || "source-over"
77
93
  }
78
94
 
79
- // Restore pad color in inner rectangle (reduced by margin)
80
- const innerWidth = scaledWidth - scaledMargin * 2
81
- const innerHeight = scaledHeight - scaledMargin * 2
82
- const innerRadius = Math.max(0, scaledRadius - scaledMargin)
95
+ // Restore pad color in inner rectangle (reduced by margins)
96
+ const innerWidth = Math.max(
97
+ 0,
98
+ scaledWidth - (scaledThicknessL + scaledThicknessR),
99
+ )
100
+ const innerHeight = Math.max(
101
+ 0,
102
+ scaledHeight - (scaledThicknessT + scaledThicknessB),
103
+ )
104
+ const innerRadius = Math.max(
105
+ 0,
106
+ scaledRadius -
107
+ (scaledThicknessL +
108
+ scaledThicknessR +
109
+ scaledThicknessT +
110
+ scaledThicknessB) /
111
+ 4,
112
+ )
83
113
 
84
114
  if (innerWidth > 0 && innerHeight > 0) {
85
115
  ctx.beginPath()
116
+ const x = -scaledWidth / 2 + scaledThicknessL
117
+ const y = -scaledHeight / 2 + scaledThicknessT
118
+
86
119
  if (innerRadius > 0) {
87
- const x = -innerWidth / 2
88
- const y = -innerHeight / 2
89
120
  const r = Math.min(innerRadius, innerWidth / 2, innerHeight / 2)
90
121
 
91
122
  ctx.moveTo(x + r, y)
@@ -104,7 +135,7 @@ export function drawSoldermaskRingForRect(
104
135
  ctx.lineTo(x, y + r)
105
136
  ctx.arcTo(x, y, x + r, y, r)
106
137
  } else {
107
- ctx.rect(-innerWidth / 2, -innerHeight / 2, innerWidth, innerHeight)
138
+ ctx.rect(x, y, innerWidth, innerHeight)
108
139
  }
109
140
 
110
141
  ctx.fillStyle = padColor
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.49",
4
+ "version": "0.0.51",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "build": "tsup-node ./lib/index.ts --format esm --dts",
@@ -17,7 +17,7 @@
17
17
  "@tscircuit/math-utils": "^0.0.29",
18
18
  "@types/bun": "latest",
19
19
  "bun-match-svg": "^0.0.14",
20
- "circuit-json": "^0.0.348",
20
+ "circuit-json": "^0.0.356",
21
21
  "circuit-json-to-connectivity-map": "^0.0.23",
22
22
  "circuit-to-svg": "^0.0.303",
23
23
  "looks-same": "^10.0.1",
@@ -9,6 +9,7 @@ test("USB-C flashlight - comprehensive comparison (circuit-to-canvas vs circuit-
9
9
  const stackedPng = await getStackedPngSvgComparison(circuitElements, {
10
10
  width: 400,
11
11
  height: 800,
12
+ showSoldermask: true,
12
13
  })
13
14
 
14
15
  await expect(stackedPng).toMatchPngSnapshot(import.meta.path)
@@ -1,7 +1,7 @@
1
1
  import { expect, test } from "bun:test"
2
2
  import { createCanvas } from "@napi-rs/canvas"
3
- import { CircuitToCanvasDrawer } from "../../lib/drawer"
4
3
  import type { AnyCircuitElement } from "circuit-json"
4
+ import { CircuitToCanvasDrawer } from "../../lib/drawer"
5
5
 
6
6
  /**
7
7
  * Comprehensive test for soldermask margin functionality:
@@ -1273,7 +1273,7 @@ test("comprehensive soldermask margin test", async () => {
1273
1273
  ]
1274
1274
 
1275
1275
  drawer.setCameraBounds({ minX: -95, maxX: 90, minY: -22, maxY: 22 })
1276
- drawer.drawElements(circuit)
1276
+ drawer.drawElements(circuit, { showSoldermask: true })
1277
1277
 
1278
1278
  await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
1279
1279
  import.meta.path,
@@ -2,7 +2,7 @@ import { expect, test } from "bun:test"
2
2
  import { createCanvas } from "@napi-rs/canvas"
3
3
  import { CircuitToCanvasDrawer } from "../../lib/drawer"
4
4
 
5
- test("draw holes with positive soldermask margins", async () => {
5
+ test("draw holes with positive and negative soldermask margins", async () => {
6
6
  const canvas = createCanvas(800, 600)
7
7
  const ctx = canvas.getContext("2d")
8
8
  const drawer = new CircuitToCanvasDrawer(ctx)
@@ -29,6 +29,122 @@ test("draw holes with positive soldermask margins", async () => {
29
29
 
30
30
  soldermask_margin: 0.2,
31
31
  },
32
+ // Circle with negative margin
33
+ {
34
+ type: "pcb_hole",
35
+ pcb_hole_id: "hole_circle_negative",
36
+ hole_shape: "circle",
37
+ x: -4,
38
+ y: -2,
39
+ hole_diameter: 1.0,
40
+
41
+ soldermask_margin: -0.15,
42
+ },
43
+ // Square with positive margin
44
+ {
45
+ type: "pcb_hole",
46
+ pcb_hole_id: "hole_square_positive",
47
+ hole_shape: "square",
48
+ x: -1,
49
+ y: 2,
50
+ hole_diameter: 1.0,
51
+
52
+ soldermask_margin: 0.15,
53
+ },
54
+ // Square with negative margin
55
+ {
56
+ type: "pcb_hole",
57
+ pcb_hole_id: "hole_square_negative",
58
+ hole_shape: "square",
59
+ x: -1,
60
+ y: -2,
61
+ hole_diameter: 1.0,
62
+
63
+ soldermask_margin: -0.1,
64
+ },
65
+ // Oval with positive margin
66
+ {
67
+ type: "pcb_hole",
68
+ pcb_hole_id: "hole_oval_positive",
69
+ hole_shape: "oval",
70
+ x: 2,
71
+ y: 2,
72
+ hole_width: 1.5,
73
+ hole_height: 0.8,
74
+
75
+ soldermask_margin: 0.1,
76
+ },
77
+ // Oval with negative margin
78
+ {
79
+ type: "pcb_hole",
80
+ pcb_hole_id: "hole_oval_negative",
81
+ hole_shape: "oval",
82
+ x: 2,
83
+ y: -2,
84
+ hole_width: 1.5,
85
+ hole_height: 0.8,
86
+
87
+ soldermask_margin: -0.12,
88
+ },
89
+ // Rect with positive margin
90
+ {
91
+ type: "pcb_hole",
92
+ pcb_hole_id: "hole_rect_positive",
93
+ hole_shape: "rect",
94
+ x: 5,
95
+ y: 2,
96
+ hole_width: 1.5,
97
+ hole_height: 0.8,
98
+
99
+ soldermask_margin: 0.15,
100
+ },
101
+ // Pill with positive margin
102
+ {
103
+ type: "pcb_hole",
104
+ pcb_hole_id: "hole_pill_positive",
105
+ hole_shape: "pill",
106
+ x: -2.5,
107
+ y: 0,
108
+ hole_width: 1.5,
109
+ hole_height: 0.8,
110
+
111
+ soldermask_margin: 0.1,
112
+ },
113
+ // Pill with negative margin
114
+ {
115
+ type: "pcb_hole",
116
+ pcb_hole_id: "hole_pill_negative",
117
+ hole_shape: "pill",
118
+ x: -2.5,
119
+ y: -4,
120
+ hole_width: 1.5,
121
+ hole_height: 0.8,
122
+
123
+ soldermask_margin: -0.08,
124
+ },
125
+ // Pill with negative margin
126
+ {
127
+ type: "pcb_hole",
128
+ pcb_hole_id: "hole_pill_negative",
129
+ hole_shape: "pill",
130
+ x: -2.5,
131
+ y: -4,
132
+ hole_width: 1.5,
133
+ hole_height: 0.8,
134
+
135
+ soldermask_margin: -0.08,
136
+ },
137
+ // Circle with positive margin (mask extends beyond hole)
138
+ {
139
+ type: "pcb_hole",
140
+ pcb_hole_id: "hole_circle_positive",
141
+ hole_shape: "circle",
142
+ x: -4,
143
+ y: 2,
144
+ hole_diameter: 1.0,
145
+
146
+ soldermask_margin: 0.2,
147
+ },
32
148
  // Square with positive margin
33
149
  {
34
150
  type: "pcb_hole",
@@ -122,10 +238,47 @@ test("draw holes with positive soldermask margins", async () => {
122
238
  text: "+0.1mm",
123
239
  font_size: 0.4,
124
240
  },
241
+ // Labels for negative margin holes (bottom row)
242
+ {
243
+ type: "pcb_silkscreen_text",
244
+ pcb_silkscreen_text_id: "text_circle_neg",
245
+ layer: "top",
246
+ anchor_position: { x: -4, y: -0.8 },
247
+ anchor_alignment: "center",
248
+ text: "-0.15mm",
249
+ font_size: 0.4,
250
+ },
251
+ {
252
+ type: "pcb_silkscreen_text",
253
+ pcb_silkscreen_text_id: "text_square_neg",
254
+ layer: "top",
255
+ anchor_position: { x: -1, y: -0.8 },
256
+ anchor_alignment: "center",
257
+ text: "-0.1mm",
258
+ font_size: 0.4,
259
+ },
260
+ {
261
+ type: "pcb_silkscreen_text",
262
+ pcb_silkscreen_text_id: "text_oval_neg",
263
+ layer: "top",
264
+ anchor_position: { x: 2, y: -0.8 },
265
+ anchor_alignment: "center",
266
+ text: "-0.12mm",
267
+ font_size: 0.4,
268
+ },
269
+ {
270
+ type: "pcb_silkscreen_text",
271
+ pcb_silkscreen_text_id: "text_pill_neg",
272
+ layer: "top",
273
+ anchor_position: { x: -2.5, y: -2.8 },
274
+ anchor_alignment: "center",
275
+ text: "-0.08mm",
276
+ font_size: 0.4,
277
+ },
125
278
  ]
126
279
 
127
280
  drawer.setCameraBounds({ minX: -7, maxX: 7, minY: -5, maxY: 5 })
128
- drawer.drawElements(circuit)
281
+ drawer.drawElements(circuit, { showSoldermask: true })
129
282
 
130
283
  await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
131
284
  import.meta.path,