circuit-to-canvas 0.0.50 → 0.0.52

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 (53) hide show
  1. package/dist/index.d.ts +10 -4
  2. package/dist/index.js +2911 -2719
  3. package/lib/drawer/CircuitToCanvasDrawer.ts +317 -366
  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 +28 -13
  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 +3 -292
  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/package.json +1 -1
  20. package/tests/board-snapshot/__snapshots__/usb-c-flashlight-board.snap.png +0 -0
  21. package/tests/board-snapshot/usb-c-flashlight-board.test.ts +1 -0
  22. package/tests/elements/__snapshots__/board-with-elements.snap.png +0 -0
  23. package/tests/elements/__snapshots__/brep-copper-pours.snap.png +0 -0
  24. package/tests/elements/__snapshots__/custom-outline-board.snap.png +0 -0
  25. package/tests/elements/__snapshots__/oval-plated-hole.snap.png +0 -0
  26. package/tests/elements/__snapshots__/pcb-board.snap.png +0 -0
  27. package/tests/elements/__snapshots__/pcb-comprehensive-soldermask-margin.snap.png +0 -0
  28. package/tests/elements/__snapshots__/pcb-fabrication-note-dimension.snap.png +0 -0
  29. package/tests/elements/__snapshots__/pcb-hole-soldermask-margin.snap.png +0 -0
  30. package/tests/elements/__snapshots__/pcb-keepout-layer-filter.snap.png +0 -0
  31. package/tests/elements/__snapshots__/pcb-keepout-multiple-layers.snap.png +0 -0
  32. package/tests/elements/__snapshots__/pcb-keepout-rect-and-circle.snap.png +0 -0
  33. package/tests/elements/__snapshots__/pcb-keepout-with-group-id.snap.png +0 -0
  34. package/tests/elements/__snapshots__/pcb-no-soldermask.snap.png +0 -0
  35. package/tests/elements/__snapshots__/pcb-plated-hole-soldermask-margin.snap.png +0 -0
  36. package/tests/elements/__snapshots__/pcb-plated-hole.snap.png +0 -0
  37. package/tests/elements/__snapshots__/pcb-silkscreen-on-component.snap.png +0 -0
  38. package/tests/elements/__snapshots__/pcb-silkscreen-oval.snap.png +0 -0
  39. package/tests/elements/__snapshots__/pcb-smtpad-asymmetric-soldermask-margin.snap.png +0 -0
  40. package/tests/elements/__snapshots__/pcb-smtpad-soldermask-coverage.snap.png +0 -0
  41. package/tests/elements/__snapshots__/pcb-smtpad-soldermask-margin.snap.png +0 -0
  42. package/tests/elements/__snapshots__/pill-plated-hole.snap.png +0 -0
  43. package/tests/elements/pcb-board.test.ts +2 -2
  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-keepout-with-group-id.test.ts +1 -1
  47. package/tests/elements/pcb-no-soldermask.test.ts +1281 -0
  48. package/tests/elements/pcb-plated-hole-soldermask-margin.test.ts +1 -1
  49. package/tests/elements/pcb-plated-hole.test.ts +40 -4
  50. package/tests/elements/pcb-smtpad-asymmetric-soldermask-margin.test.ts +1 -1
  51. package/tests/elements/pcb-smtpad-soldermask-coverage.test.ts +1 -1
  52. package/tests/elements/pcb-smtpad-soldermask-margin.test.ts +1 -1
  53. package/tests/fixtures/getStackedPngSvgComparison.ts +10 -4
@@ -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,68 +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
- let r = 0
23
+ function getBorderRadius(pad: PcbSmtPad): number {
40
24
  if (pad.shape === "rect" || pad.shape === "rotated_rect") {
41
- r = pad.corner_radius ?? pad.rect_border_radius ?? 0
25
+ return pad.corner_radius ?? pad.rect_border_radius ?? 0
42
26
  }
43
- return r + margin
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
- const margin = isCoveredWithSoldermask ? 0 : (pad.soldermask_margin ?? 0)
52
-
53
- const soldermaskRingColor = getSoldermaskColor(pad.layer, colorMap)
54
- const positiveMarginColor = colorMap.substrate
55
- const soldermaskOverlayColor = getSoldermaskColor(pad.layer, colorMap)
56
-
57
- const hasSoldermask = !isCoveredWithSoldermask && margin !== 0
58
-
59
- let ml = margin
60
- let mr = margin
61
- let mt = margin
62
- let mb = margin
63
- let hasAnySoldermask = hasSoldermask
64
-
65
- if (
66
- !isCoveredWithSoldermask &&
67
- (pad.shape === "rect" || pad.shape === "rotated_rect")
68
- ) {
69
- ml = pad.soldermask_margin_left ?? pad.soldermask_margin ?? 0
70
- mr = pad.soldermask_margin_right ?? pad.soldermask_margin ?? 0
71
- mt = pad.soldermask_margin_top ?? pad.soldermask_margin ?? 0
72
- mb = pad.soldermask_margin_bottom ?? pad.soldermask_margin ?? 0
73
- hasAnySoldermask = ml !== 0 || mr !== 0 || mt !== 0 || mb !== 0
74
- }
75
34
 
76
35
  // Draw the copper pad
77
36
  if (pad.shape === "rect") {
78
- // For positive margins, draw extended mask area first
79
- if (hasAnySoldermask && (ml > 0 || mr > 0 || mt > 0 || mb > 0)) {
80
- drawRect({
81
- ctx,
82
- center: { x: pad.x + (mr - ml) / 2, y: pad.y + (mt - mb) / 2 },
83
- width: pad.width + ml + mr,
84
- height: pad.height + mt + mb,
85
- fill: positiveMarginColor,
86
- realToCanvasMat,
87
- borderRadius: getBorderRadius(pad),
88
- })
89
- }
90
-
91
- // Draw the pad on top
92
37
  drawRect({
93
38
  ctx,
94
39
  center: { x: pad.x, y: pad.y },
@@ -98,61 +43,10 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
98
43
  realToCanvasMat,
99
44
  borderRadius: getBorderRadius(pad),
100
45
  })
101
-
102
- // For negative margins, draw soldermask ring on top of the pad
103
- if (hasAnySoldermask && (ml < 0 || mr < 0 || mt < 0 || mb < 0)) {
104
- drawSoldermaskRingForRect(
105
- ctx,
106
- { x: pad.x, y: pad.y },
107
- pad.width,
108
- pad.height,
109
- pad.soldermask_margin ?? 0,
110
- getBorderRadius(pad),
111
- 0,
112
- realToCanvasMat,
113
- soldermaskRingColor,
114
- color,
115
- { left: ml, right: mr, top: mt, bottom: mb },
116
- )
117
- }
118
-
119
- // If covered with soldermask, draw soldermaskOverCopper overlay
120
- if (isCoveredWithSoldermask) {
121
- drawRect({
122
- ctx,
123
- center: { x: pad.x, y: pad.y },
124
- width: pad.width,
125
- height: pad.height,
126
- fill: soldermaskOverlayColor,
127
- realToCanvasMat,
128
- borderRadius: getBorderRadius(pad),
129
- })
130
- }
131
46
  return
132
47
  }
133
48
 
134
49
  if (pad.shape === "rotated_rect") {
135
- const radians = ((pad.ccw_rotation ?? 0) * Math.PI) / 180
136
- const dxLocal = (mr - ml) / 2
137
- const dyLocal = (mt - mb) / 2
138
- const dxGlobal = dxLocal * Math.cos(radians) - dyLocal * Math.sin(radians)
139
- const dyGlobal = dxLocal * Math.sin(radians) + dyLocal * Math.cos(radians)
140
-
141
- // For positive margins, draw extended mask area first
142
- if (hasAnySoldermask && (ml > 0 || mr > 0 || mt > 0 || mb > 0)) {
143
- drawRect({
144
- ctx,
145
- center: { x: pad.x + dxGlobal, y: pad.y + dyGlobal },
146
- width: pad.width + ml + mr,
147
- height: pad.height + mt + mb,
148
- fill: positiveMarginColor,
149
- realToCanvasMat,
150
- borderRadius: getBorderRadius(pad),
151
- rotation: pad.ccw_rotation ?? 0,
152
- })
153
- }
154
-
155
- // Draw the pad on top
156
50
  drawRect({
157
51
  ctx,
158
52
  center: { x: pad.x, y: pad.y },
@@ -163,53 +57,10 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
163
57
  borderRadius: getBorderRadius(pad),
164
58
  rotation: pad.ccw_rotation ?? 0,
165
59
  })
166
-
167
- // For negative margins, draw soldermask ring on top of the pad
168
- if (hasAnySoldermask && (ml < 0 || mr < 0 || mt < 0 || mb < 0)) {
169
- drawSoldermaskRingForRect(
170
- ctx,
171
- { x: pad.x, y: pad.y },
172
- pad.width,
173
- pad.height,
174
- pad.soldermask_margin ?? 0,
175
- getBorderRadius(pad),
176
- pad.ccw_rotation ?? 0,
177
- realToCanvasMat,
178
- soldermaskRingColor,
179
- color,
180
- { left: ml, right: mr, top: mt, bottom: mb },
181
- )
182
- }
183
-
184
- // If covered with soldermask, draw soldermaskOverCopper overlay
185
- if (isCoveredWithSoldermask) {
186
- drawRect({
187
- ctx,
188
- center: { x: pad.x, y: pad.y },
189
- width: pad.width,
190
- height: pad.height,
191
- fill: soldermaskOverlayColor,
192
- realToCanvasMat,
193
- borderRadius: getBorderRadius(pad),
194
- rotation: pad.ccw_rotation ?? 0,
195
- })
196
- }
197
60
  return
198
61
  }
199
62
 
200
63
  if (pad.shape === "circle") {
201
- // For positive margins, draw extended mask area first
202
- if (hasSoldermask && margin > 0) {
203
- drawCircle({
204
- ctx,
205
- center: { x: pad.x, y: pad.y },
206
- radius: pad.radius + margin,
207
- fill: positiveMarginColor,
208
- realToCanvasMat,
209
- })
210
- }
211
-
212
- // Draw the pad on top
213
64
  drawCircle({
214
65
  ctx,
215
66
  center: { x: pad.x, y: pad.y },
@@ -217,47 +68,10 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
217
68
  fill: color,
218
69
  realToCanvasMat,
219
70
  })
220
-
221
- // For negative margins, draw soldermask ring on top of the pad
222
- if (hasSoldermask && margin < 0) {
223
- drawSoldermaskRingForCircle(
224
- ctx,
225
- { x: pad.x, y: pad.y },
226
- pad.radius,
227
- margin,
228
- realToCanvasMat,
229
- soldermaskRingColor,
230
- color,
231
- )
232
- }
233
-
234
- // If covered with soldermask and margin == 0 (treat as 0 positive margin), draw soldermaskOverCopper overlay
235
- if (isCoveredWithSoldermask && margin === 0) {
236
- drawCircle({
237
- ctx,
238
- center: { x: pad.x, y: pad.y },
239
- radius: pad.radius,
240
- fill: soldermaskOverlayColor,
241
- realToCanvasMat,
242
- })
243
- }
244
71
  return
245
72
  }
246
73
 
247
74
  if (pad.shape === "pill") {
248
- // For positive margins, draw extended mask area first
249
- if (hasSoldermask && margin > 0) {
250
- drawPill({
251
- ctx,
252
- center: { x: pad.x, y: pad.y },
253
- width: pad.width + margin * 2,
254
- height: pad.height + margin * 2,
255
- fill: positiveMarginColor,
256
- realToCanvasMat,
257
- })
258
- }
259
-
260
- // Draw the pad on top
261
75
  drawPill({
262
76
  ctx,
263
77
  center: { x: pad.x, y: pad.y },
@@ -266,51 +80,10 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
266
80
  fill: color,
267
81
  realToCanvasMat,
268
82
  })
269
-
270
- // For negative margins, draw soldermask ring on top of the pad
271
- if (hasSoldermask && margin < 0) {
272
- drawSoldermaskRingForPill(
273
- ctx,
274
- { x: pad.x, y: pad.y },
275
- pad.width,
276
- pad.height,
277
- margin,
278
- 0,
279
- realToCanvasMat,
280
- soldermaskRingColor,
281
- color,
282
- )
283
- }
284
-
285
- // If covered with soldermask and margin == 0 (treat as 0 positive margin), draw soldermaskOverCopper overlay
286
- if (isCoveredWithSoldermask && margin === 0) {
287
- drawPill({
288
- ctx,
289
- center: { x: pad.x, y: pad.y },
290
- width: pad.width,
291
- height: pad.height,
292
- fill: soldermaskOverlayColor,
293
- realToCanvasMat,
294
- })
295
- }
296
83
  return
297
84
  }
298
85
 
299
86
  if (pad.shape === "rotated_pill") {
300
- // For positive margins, draw extended mask area first
301
- if (hasSoldermask && margin > 0) {
302
- drawPill({
303
- ctx,
304
- center: { x: pad.x, y: pad.y },
305
- width: pad.width + margin * 2,
306
- height: pad.height + margin * 2,
307
- fill: positiveMarginColor,
308
- realToCanvasMat,
309
- rotation: pad.ccw_rotation ?? 0,
310
- })
311
- }
312
-
313
- // Draw the pad on top
314
87
  drawPill({
315
88
  ctx,
316
89
  center: { x: pad.x, y: pad.y },
@@ -320,79 +93,17 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
320
93
  realToCanvasMat,
321
94
  rotation: pad.ccw_rotation ?? 0,
322
95
  })
323
-
324
- // For negative margins, draw soldermask ring on top of the pad
325
- if (hasSoldermask && margin < 0) {
326
- drawSoldermaskRingForPill(
327
- ctx,
328
- { x: pad.x, y: pad.y },
329
- pad.width,
330
- pad.height,
331
- margin,
332
- pad.ccw_rotation ?? 0,
333
- realToCanvasMat,
334
- soldermaskRingColor,
335
- color,
336
- )
337
- }
338
-
339
- // If covered with soldermask and margin == 0 (treat as 0 positive margin), draw soldermaskOverCopper overlay
340
- if (isCoveredWithSoldermask && margin === 0) {
341
- drawPill({
342
- ctx,
343
- center: { x: pad.x, y: pad.y },
344
- width: pad.width,
345
- height: pad.height,
346
- fill: soldermaskOverlayColor,
347
- realToCanvasMat,
348
- rotation: pad.ccw_rotation ?? 0,
349
- })
350
- }
351
96
  return
352
97
  }
353
98
 
354
99
  if (pad.shape === "polygon") {
355
100
  if (pad.points && pad.points.length >= 3) {
356
- // For positive margins, draw extended mask area first
357
- if (hasSoldermask && margin > 0) {
358
- const expandedPoints = offsetPolygonPoints(pad.points, margin)
359
- drawPolygon({
360
- ctx,
361
- points: expandedPoints,
362
- fill: positiveMarginColor,
363
- realToCanvasMat,
364
- })
365
- }
366
-
367
- // Draw the copper pad
368
101
  drawPolygon({
369
102
  ctx,
370
103
  points: pad.points,
371
104
  fill: color,
372
105
  realToCanvasMat,
373
106
  })
374
-
375
- // For negative margins, draw soldermask ring on top of the pad
376
- if (hasSoldermask && margin < 0) {
377
- drawSoldermaskRingForPolygon(
378
- ctx,
379
- pad.points,
380
- margin,
381
- realToCanvasMat,
382
- soldermaskRingColor,
383
- color,
384
- )
385
- }
386
-
387
- // If covered with soldermask and margin == 0 (treat as 0 positive margin), draw soldermaskOverCopper overlay
388
- if (isCoveredWithSoldermask && margin === 0) {
389
- drawPolygon({
390
- ctx,
391
- points: pad.points,
392
- fill: soldermaskOverlayColor,
393
- realToCanvasMat,
394
- })
395
- }
396
107
  }
397
108
  return
398
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
+ }