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
@@ -5,13 +5,6 @@ import { drawCircle } from "../shapes/circle"
5
5
  import { drawRect } from "../shapes/rect"
6
6
  import { drawPill } from "../shapes/pill"
7
7
  import { drawPolygon } from "../shapes/polygon"
8
- import {
9
- drawSoldermaskRingForRect,
10
- drawSoldermaskRingForCircle,
11
- drawSoldermaskRingForPill,
12
- drawSoldermaskRingForPolygon,
13
- offsetPolygonPoints,
14
- } from "./soldermask-margin"
15
8
 
16
9
  export interface DrawPcbSmtPadParams {
17
10
  ctx: CanvasContext
@@ -27,53 +20,20 @@ function layerToColor(layer: string, colorMap: PcbColorMap): string {
27
20
  )
28
21
  }
29
22
 
30
- function getSoldermaskColor(layer: string, colorMap: PcbColorMap): string {
31
- return (
32
- colorMap.soldermaskOverCopper[
33
- layer as keyof typeof colorMap.soldermaskOverCopper
34
- ] ?? colorMap.soldermaskOverCopper.top
35
- )
36
- }
37
-
38
- function getBorderRadius(pad: PcbSmtPad, margin = 0): number {
39
- return (
40
- ((pad as { corner_radius?: number }).corner_radius ??
41
- (pad as { rect_border_radius?: number }).rect_border_radius ??
42
- 0) + margin
43
- )
23
+ function getBorderRadius(pad: PcbSmtPad): number {
24
+ if (pad.shape === "rect" || pad.shape === "rotated_rect") {
25
+ return pad.corner_radius ?? pad.rect_border_radius ?? 0
26
+ }
27
+ return 0
44
28
  }
45
29
 
46
30
  export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
47
31
  const { ctx, pad, realToCanvasMat, colorMap } = params
48
32
 
49
33
  const color = layerToColor(pad.layer, colorMap)
50
- const isCoveredWithSoldermask = pad.is_covered_with_solder_mask === true
51
- // If covered with soldermask, fully covered with no margin; otherwise use soldermask_margin if set
52
- const margin = isCoveredWithSoldermask ? 0 : (pad.soldermask_margin ?? 0)
53
- const hasSoldermask =
54
- !isCoveredWithSoldermask &&
55
- pad.soldermask_margin !== undefined &&
56
- pad.soldermask_margin !== 0
57
- const soldermaskRingColor = getSoldermaskColor(pad.layer, colorMap)
58
- const positiveMarginColor = colorMap.substrate
59
- const soldermaskOverlayColor = getSoldermaskColor(pad.layer, colorMap)
60
34
 
61
35
  // Draw the copper pad
62
36
  if (pad.shape === "rect") {
63
- // For positive margins, draw extended mask area first
64
- if (hasSoldermask && margin > 0) {
65
- drawRect({
66
- ctx,
67
- center: { x: pad.x, y: pad.y },
68
- width: pad.width + margin * 2,
69
- height: pad.height + margin * 2,
70
- fill: positiveMarginColor,
71
- realToCanvasMat,
72
- borderRadius: getBorderRadius(pad),
73
- })
74
- }
75
-
76
- // Draw the pad on top
77
37
  drawRect({
78
38
  ctx,
79
39
  center: { x: pad.x, y: pad.y },
@@ -83,54 +43,10 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
83
43
  realToCanvasMat,
84
44
  borderRadius: getBorderRadius(pad),
85
45
  })
86
-
87
- // For negative margins, draw soldermask ring on top of the pad
88
- if (hasSoldermask && margin < 0) {
89
- drawSoldermaskRingForRect(
90
- ctx,
91
- { x: pad.x, y: pad.y },
92
- pad.width,
93
- pad.height,
94
- margin,
95
- getBorderRadius(pad),
96
- 0,
97
- realToCanvasMat,
98
- soldermaskRingColor,
99
- color,
100
- )
101
- }
102
-
103
- // If covered with soldermask and margin == 0 (treat as 0 positive margin), draw soldermaskOverCopper overlay
104
- if (isCoveredWithSoldermask && margin === 0) {
105
- drawRect({
106
- ctx,
107
- center: { x: pad.x, y: pad.y },
108
- width: pad.width,
109
- height: pad.height,
110
- fill: soldermaskOverlayColor,
111
- realToCanvasMat,
112
- borderRadius: getBorderRadius(pad),
113
- })
114
- }
115
46
  return
116
47
  }
117
48
 
118
49
  if (pad.shape === "rotated_rect") {
119
- // For positive margins, draw extended mask area first
120
- if (hasSoldermask && margin > 0) {
121
- drawRect({
122
- ctx,
123
- center: { x: pad.x, y: pad.y },
124
- width: pad.width + margin * 2,
125
- height: pad.height + margin * 2,
126
- fill: positiveMarginColor,
127
- realToCanvasMat,
128
- borderRadius: getBorderRadius(pad),
129
- rotation: pad.ccw_rotation ?? 0,
130
- })
131
- }
132
-
133
- // Draw the pad on top
134
50
  drawRect({
135
51
  ctx,
136
52
  center: { x: pad.x, y: pad.y },
@@ -141,52 +57,10 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
141
57
  borderRadius: getBorderRadius(pad),
142
58
  rotation: pad.ccw_rotation ?? 0,
143
59
  })
144
-
145
- // For negative margins, draw soldermask ring on top of the pad
146
- if (hasSoldermask && margin < 0) {
147
- drawSoldermaskRingForRect(
148
- ctx,
149
- { x: pad.x, y: pad.y },
150
- pad.width,
151
- pad.height,
152
- margin,
153
- getBorderRadius(pad),
154
- pad.ccw_rotation ?? 0,
155
- realToCanvasMat,
156
- soldermaskRingColor,
157
- color,
158
- )
159
- }
160
-
161
- // If covered with soldermask and margin == 0 (treat as 0 positive margin), draw soldermaskOverCopper overlay
162
- if (isCoveredWithSoldermask && margin === 0) {
163
- drawRect({
164
- ctx,
165
- center: { x: pad.x, y: pad.y },
166
- width: pad.width,
167
- height: pad.height,
168
- fill: soldermaskOverlayColor,
169
- realToCanvasMat,
170
- borderRadius: getBorderRadius(pad),
171
- rotation: pad.ccw_rotation ?? 0,
172
- })
173
- }
174
60
  return
175
61
  }
176
62
 
177
63
  if (pad.shape === "circle") {
178
- // For positive margins, draw extended mask area first
179
- if (hasSoldermask && margin > 0) {
180
- drawCircle({
181
- ctx,
182
- center: { x: pad.x, y: pad.y },
183
- radius: pad.radius + margin,
184
- fill: positiveMarginColor,
185
- realToCanvasMat,
186
- })
187
- }
188
-
189
- // Draw the pad on top
190
64
  drawCircle({
191
65
  ctx,
192
66
  center: { x: pad.x, y: pad.y },
@@ -194,47 +68,10 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
194
68
  fill: color,
195
69
  realToCanvasMat,
196
70
  })
197
-
198
- // For negative margins, draw soldermask ring on top of the pad
199
- if (hasSoldermask && margin < 0) {
200
- drawSoldermaskRingForCircle(
201
- ctx,
202
- { x: pad.x, y: pad.y },
203
- pad.radius,
204
- margin,
205
- realToCanvasMat,
206
- soldermaskRingColor,
207
- color,
208
- )
209
- }
210
-
211
- // If covered with soldermask and margin == 0 (treat as 0 positive margin), draw soldermaskOverCopper overlay
212
- if (isCoveredWithSoldermask && margin === 0) {
213
- drawCircle({
214
- ctx,
215
- center: { x: pad.x, y: pad.y },
216
- radius: pad.radius,
217
- fill: soldermaskOverlayColor,
218
- realToCanvasMat,
219
- })
220
- }
221
71
  return
222
72
  }
223
73
 
224
74
  if (pad.shape === "pill") {
225
- // For positive margins, draw extended mask area first
226
- if (hasSoldermask && margin > 0) {
227
- drawPill({
228
- ctx,
229
- center: { x: pad.x, y: pad.y },
230
- width: pad.width + margin * 2,
231
- height: pad.height + margin * 2,
232
- fill: positiveMarginColor,
233
- realToCanvasMat,
234
- })
235
- }
236
-
237
- // Draw the pad on top
238
75
  drawPill({
239
76
  ctx,
240
77
  center: { x: pad.x, y: pad.y },
@@ -243,51 +80,10 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
243
80
  fill: color,
244
81
  realToCanvasMat,
245
82
  })
246
-
247
- // For negative margins, draw soldermask ring on top of the pad
248
- if (hasSoldermask && margin < 0) {
249
- drawSoldermaskRingForPill(
250
- ctx,
251
- { x: pad.x, y: pad.y },
252
- pad.width,
253
- pad.height,
254
- margin,
255
- 0,
256
- realToCanvasMat,
257
- soldermaskRingColor,
258
- color,
259
- )
260
- }
261
-
262
- // If covered with soldermask and margin == 0 (treat as 0 positive margin), draw soldermaskOverCopper overlay
263
- if (isCoveredWithSoldermask && margin === 0) {
264
- drawPill({
265
- ctx,
266
- center: { x: pad.x, y: pad.y },
267
- width: pad.width,
268
- height: pad.height,
269
- fill: soldermaskOverlayColor,
270
- realToCanvasMat,
271
- })
272
- }
273
83
  return
274
84
  }
275
85
 
276
86
  if (pad.shape === "rotated_pill") {
277
- // For positive margins, draw extended mask area first
278
- if (hasSoldermask && margin > 0) {
279
- drawPill({
280
- ctx,
281
- center: { x: pad.x, y: pad.y },
282
- width: pad.width + margin * 2,
283
- height: pad.height + margin * 2,
284
- fill: positiveMarginColor,
285
- realToCanvasMat,
286
- rotation: pad.ccw_rotation ?? 0,
287
- })
288
- }
289
-
290
- // Draw the pad on top
291
87
  drawPill({
292
88
  ctx,
293
89
  center: { x: pad.x, y: pad.y },
@@ -297,79 +93,17 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
297
93
  realToCanvasMat,
298
94
  rotation: pad.ccw_rotation ?? 0,
299
95
  })
300
-
301
- // For negative margins, draw soldermask ring on top of the pad
302
- if (hasSoldermask && margin < 0) {
303
- drawSoldermaskRingForPill(
304
- ctx,
305
- { x: pad.x, y: pad.y },
306
- pad.width,
307
- pad.height,
308
- margin,
309
- pad.ccw_rotation ?? 0,
310
- realToCanvasMat,
311
- soldermaskRingColor,
312
- color,
313
- )
314
- }
315
-
316
- // If covered with soldermask and margin == 0 (treat as 0 positive margin), draw soldermaskOverCopper overlay
317
- if (isCoveredWithSoldermask && margin === 0) {
318
- drawPill({
319
- ctx,
320
- center: { x: pad.x, y: pad.y },
321
- width: pad.width,
322
- height: pad.height,
323
- fill: soldermaskOverlayColor,
324
- realToCanvasMat,
325
- rotation: pad.ccw_rotation ?? 0,
326
- })
327
- }
328
96
  return
329
97
  }
330
98
 
331
99
  if (pad.shape === "polygon") {
332
100
  if (pad.points && pad.points.length >= 3) {
333
- // For positive margins, draw extended mask area first
334
- if (hasSoldermask && margin > 0) {
335
- const expandedPoints = offsetPolygonPoints(pad.points, margin)
336
- drawPolygon({
337
- ctx,
338
- points: expandedPoints,
339
- fill: positiveMarginColor,
340
- realToCanvasMat,
341
- })
342
- }
343
-
344
- // Draw the copper pad
345
101
  drawPolygon({
346
102
  ctx,
347
103
  points: pad.points,
348
104
  fill: color,
349
105
  realToCanvasMat,
350
106
  })
351
-
352
- // For negative margins, draw soldermask ring on top of the pad
353
- if (hasSoldermask && margin < 0) {
354
- drawSoldermaskRingForPolygon(
355
- ctx,
356
- pad.points,
357
- margin,
358
- realToCanvasMat,
359
- soldermaskRingColor,
360
- color,
361
- )
362
- }
363
-
364
- // If covered with soldermask and margin == 0 (treat as 0 positive margin), draw soldermaskOverCopper overlay
365
- if (isCoveredWithSoldermask && margin === 0) {
366
- drawPolygon({
367
- ctx,
368
- points: pad.points,
369
- fill: soldermaskOverlayColor,
370
- realToCanvasMat,
371
- })
372
- }
373
107
  }
374
108
  return
375
109
  }
@@ -0,0 +1,44 @@
1
+ import type { PcbBoard } from "circuit-json"
2
+ import type { Matrix } from "transformation-matrix"
3
+ import { applyToPoint } from "transformation-matrix"
4
+ import type { CanvasContext } from "../../types"
5
+ import { drawPolygonPath } from "../helper-functions/draw-polygon"
6
+
7
+ /**
8
+ * Draws the base soldermask layer covering the entire board.
9
+ */
10
+ export function drawBoardSoldermask(params: {
11
+ ctx: CanvasContext
12
+ board: PcbBoard
13
+ realToCanvasMat: Matrix
14
+ soldermaskColor: string
15
+ }): void {
16
+ const { ctx, board, realToCanvasMat, soldermaskColor } = params
17
+ const { width, height, center, outline } = board
18
+
19
+ if (outline && Array.isArray(outline) && outline.length >= 3) {
20
+ // Draw filled polygon for custom outline
21
+ const canvasPoints = outline.map((p) => {
22
+ const [x, y] = applyToPoint(realToCanvasMat, [p.x, p.y])
23
+ return { x, y }
24
+ })
25
+
26
+ ctx.beginPath()
27
+ drawPolygonPath({ ctx, points: canvasPoints })
28
+ ctx.fillStyle = soldermaskColor
29
+ ctx.fill()
30
+ } else if (width !== undefined && height !== undefined && center) {
31
+ // Draw filled rectangle
32
+ const [cx, cy] = applyToPoint(realToCanvasMat, [center.x, center.y])
33
+ const scaledWidth = width * Math.abs(realToCanvasMat.a)
34
+ const scaledHeight = height * Math.abs(realToCanvasMat.a)
35
+
36
+ ctx.fillStyle = soldermaskColor
37
+ ctx.fillRect(
38
+ cx - scaledWidth / 2,
39
+ cy - scaledHeight / 2,
40
+ scaledWidth,
41
+ scaledHeight,
42
+ )
43
+ }
44
+ }
@@ -0,0 +1,74 @@
1
+ import type { PcbCutout } from "circuit-json"
2
+ import type { Matrix } from "transformation-matrix"
3
+ import { applyToPoint } from "transformation-matrix"
4
+ import type { CanvasContext, PcbColorMap } from "../../types"
5
+ import { drawPolygonPath } from "../helper-functions/draw-polygon"
6
+ import { drawRoundedRectPath } from "../helper-functions/draw-rounded-rect"
7
+ /**
8
+ * Process soldermask for a board cutout.
9
+ * Cutouts go through the entire board, so they cut through soldermask too.
10
+ */
11
+ export function processCutoutSoldermask(params: {
12
+ ctx: CanvasContext
13
+ cutout: PcbCutout
14
+ realToCanvasMat: Matrix
15
+ colorMap: PcbColorMap
16
+ }): void {
17
+ const { ctx, cutout, realToCanvasMat, colorMap } = params
18
+ // Cutouts go through the entire board, so they cut through soldermask too
19
+ // Use drill color to indicate the cutout
20
+ ctx.fillStyle = colorMap.drill
21
+
22
+ if (cutout.shape === "rect") {
23
+ const [cx, cy] = applyToPoint(realToCanvasMat, [
24
+ cutout.center?.x ?? 0,
25
+ cutout.center?.y ?? 0,
26
+ ])
27
+ const scaledWidth = cutout.width * Math.abs(realToCanvasMat.a)
28
+ const scaledHeight = cutout.height * Math.abs(realToCanvasMat.a)
29
+ const scaledRadius =
30
+ (cutout.corner_radius ?? 0) * Math.abs(realToCanvasMat.a)
31
+
32
+ ctx.save()
33
+ ctx.translate(cx, cy)
34
+ if (cutout.rotation) {
35
+ ctx.rotate((-cutout.rotation * Math.PI) / 180)
36
+ }
37
+
38
+ ctx.beginPath()
39
+ drawRoundedRectPath({
40
+ ctx,
41
+ cx: 0,
42
+ cy: 0,
43
+ width: scaledWidth,
44
+ height: scaledHeight,
45
+ radius: scaledRadius,
46
+ })
47
+ ctx.restore()
48
+ ctx.fill()
49
+ } else if (cutout.shape === "circle") {
50
+ const [cx, cy] = applyToPoint(realToCanvasMat, [
51
+ cutout.center?.x ?? 0,
52
+ cutout.center?.y ?? 0,
53
+ ])
54
+ const scaledRadius = cutout.radius * Math.abs(realToCanvasMat.a)
55
+
56
+ ctx.beginPath()
57
+ ctx.arc(cx, cy, scaledRadius, 0, Math.PI * 2)
58
+ ctx.closePath()
59
+ ctx.fill()
60
+ } else if (
61
+ cutout.shape === "polygon" &&
62
+ cutout.points &&
63
+ cutout.points.length >= 3
64
+ ) {
65
+ const canvasPoints = cutout.points.map((p) => {
66
+ const [x, y] = applyToPoint(realToCanvasMat, [p.x, p.y])
67
+ return { x, y }
68
+ })
69
+
70
+ ctx.beginPath()
71
+ drawPolygonPath({ ctx, points: canvasPoints })
72
+ ctx.fill()
73
+ }
74
+ }