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,25 @@
1
+ import type { CanvasContext } from "../../types"
2
+
3
+ /**
4
+ * Draws a polygon path from an array of points.
5
+ * The path is not filled or stroked - call ctx.fill() or ctx.stroke() after.
6
+ */
7
+ export function drawPolygonPath(params: {
8
+ ctx: CanvasContext
9
+ points: Array<{ x: number; y: number }>
10
+ }): void {
11
+ const { ctx, points } = params
12
+ if (points.length < 3) return
13
+
14
+ const firstPoint = points[0]
15
+ if (firstPoint) {
16
+ ctx.moveTo(firstPoint.x, firstPoint.y)
17
+ for (let i = 1; i < points.length; i++) {
18
+ const point = points[i]
19
+ if (point) {
20
+ ctx.lineTo(point.x, point.y)
21
+ }
22
+ }
23
+ ctx.closePath()
24
+ }
25
+ }
@@ -0,0 +1,34 @@
1
+ import type { CanvasContext } from "../../types"
2
+
3
+ /**
4
+ * Draws a rounded rectangle path centered at (cx, cy).
5
+ * The path is not filled or stroked - call ctx.fill() or ctx.stroke() after.
6
+ */
7
+ export function drawRoundedRectPath(params: {
8
+ ctx: CanvasContext
9
+ cx: number
10
+ cy: number
11
+ width: number
12
+ height: number
13
+ radius: number
14
+ }): void {
15
+ const { ctx, cx, cy, width, height, radius } = params
16
+ const x = cx - width / 2
17
+ const y = cy - height / 2
18
+ const r = Math.min(radius, width / 2, height / 2)
19
+
20
+ if (r > 0) {
21
+ ctx.moveTo(x + r, y)
22
+ ctx.lineTo(x + width - r, y)
23
+ ctx.arcTo(x + width, y, x + width, y + r, r)
24
+ ctx.lineTo(x + width, y + height - r)
25
+ ctx.arcTo(x + width, y + height, x + width - r, y + height, r)
26
+ ctx.lineTo(x + r, y + height)
27
+ ctx.arcTo(x, y + height, x, y + height - r, r)
28
+ ctx.lineTo(x, y + r)
29
+ ctx.arcTo(x, y, x + r, y, r)
30
+ } else {
31
+ ctx.rect(x, y, width, height)
32
+ }
33
+ ctx.closePath()
34
+ }
@@ -0,0 +1,3 @@
1
+ export { drawPillPath } from "./draw-pill"
2
+ export { drawPolygonPath } from "./draw-polygon"
3
+ export { drawRoundedRectPath } from "./draw-rounded-rect"
@@ -15,8 +15,18 @@ export function drawPcbBoard(params: DrawPcbBoardParams): void {
15
15
  const { ctx, board, realToCanvasMat, colorMap } = params
16
16
  const { width, height, center, outline } = board
17
17
 
18
- // If the board has a custom outline, draw it as a path
18
+ // If the board has a custom outline, draw substrate and outline
19
19
  if (outline && Array.isArray(outline) && outline.length >= 3) {
20
+ // Draw substrate fill
21
+ drawPath({
22
+ ctx,
23
+ points: outline.map((p) => ({ x: p.x, y: p.y })),
24
+ fill: colorMap.substrate,
25
+ realToCanvasMat,
26
+ closePath: true,
27
+ })
28
+
29
+ // Draw outline stroke
20
30
  drawPath({
21
31
  ctx,
22
32
  points: outline.map((p) => ({ x: p.x, y: p.y })),
@@ -30,13 +40,13 @@ export function drawPcbBoard(params: DrawPcbBoardParams): void {
30
40
 
31
41
  // Otherwise draw a rectangle
32
42
  if (width !== undefined && height !== undefined && center) {
33
- // Draw the board outline as a rectangle stroke
43
+ // Draw substrate fill
34
44
  drawRect({
35
45
  ctx,
36
46
  center,
37
47
  width,
38
48
  height,
39
- fill: "transparent",
49
+ fill: colorMap.substrate,
40
50
  realToCanvasMat,
41
51
  })
42
52
 
@@ -1,26 +1,21 @@
1
- import type { PCBHole } from "circuit-json"
1
+ import type { PcbHole } from "circuit-json"
2
2
  import type { Matrix } from "transformation-matrix"
3
3
  import type { PcbColorMap, CanvasContext } from "../types"
4
4
  import { drawCircle } from "../shapes/circle"
5
5
  import { drawRect } from "../shapes/rect"
6
6
  import { drawOval } from "../shapes/oval"
7
7
  import { drawPill } from "../shapes/pill"
8
- import {
9
- drawSoldermaskRingForCircle,
10
- drawSoldermaskRingForOval,
11
- drawSoldermaskRingForPill,
12
- drawSoldermaskRingForRect,
13
- } from "./soldermask-margin"
14
8
 
15
9
  export interface DrawPcbHoleParams {
16
10
  ctx: CanvasContext
17
- hole: PCBHole
11
+ hole: PcbHole
18
12
  realToCanvasMat: Matrix
19
13
  colorMap: PcbColorMap
14
+ soldermaskMargin?: number
20
15
  }
21
16
 
22
17
  // Helper function to safely access ccw_rotation property
23
- function getRotation(hole: PCBHole): number {
18
+ function getRotation(hole: PcbHole): number {
24
19
  if ("ccw_rotation" in hole && typeof hole.ccw_rotation === "number") {
25
20
  return hole.ccw_rotation
26
21
  }
@@ -28,357 +23,80 @@ function getRotation(hole: PCBHole): number {
28
23
  }
29
24
 
30
25
  export function drawPcbHole(params: DrawPcbHoleParams): void {
31
- const { ctx, hole, realToCanvasMat, colorMap } = params
26
+ const { ctx, hole, realToCanvasMat, colorMap, soldermaskMargin = 0 } = params
32
27
 
33
- const isCoveredWithSoldermask = hole.is_covered_with_solder_mask === true
34
- const margin = isCoveredWithSoldermask ? 0 : (hole.soldermask_margin ?? 0)
35
- const hasSoldermask =
36
- !isCoveredWithSoldermask &&
37
- hole.soldermask_margin !== undefined &&
38
- hole.soldermask_margin !== 0
39
- const positiveMarginColor = colorMap.substrate
40
- const soldermaskOverlayColor = colorMap.soldermaskOverCopper.top
41
- const soldermaskRingColor = colorMap.soldermaskOverCopper.top
42
-
43
- if (hole.hole_shape === "circle") {
44
- // For positive margins, draw extended mask area first
45
- if (hasSoldermask && margin > 0) {
46
- drawCircle({
47
- ctx,
48
- center: { x: hole.x, y: hole.y },
49
- radius: hole.hole_diameter / 2 + margin,
50
- fill: positiveMarginColor,
51
- realToCanvasMat,
52
- })
53
- }
54
-
55
- // Draw the hole (only if not fully covered with soldermask)
56
- if (!isCoveredWithSoldermask) {
57
- drawCircle({
58
- ctx,
59
- center: { x: hole.x, y: hole.y },
60
- radius: hole.hole_diameter / 2,
61
- fill: colorMap.drill,
62
- realToCanvasMat,
63
- })
28
+ // Skip drawing if the hole is fully covered with soldermask
29
+ if (hole.is_covered_with_solder_mask === true) {
30
+ return
31
+ }
64
32
 
65
- // For negative margins, draw soldermask ring on top of the hole
66
- if (hasSoldermask && margin < 0) {
67
- drawSoldermaskRingForCircle(
68
- ctx,
69
- { x: hole.x, y: hole.y },
70
- hole.hole_diameter / 2,
71
- margin,
72
- realToCanvasMat,
73
- soldermaskRingColor,
74
- colorMap.drill,
75
- )
76
- }
77
- }
33
+ // For negative margins, draw smaller hole (inset by margin amount)
34
+ const holeInset = soldermaskMargin < 0 ? Math.abs(soldermaskMargin) : 0
78
35
 
79
- // If fully covered, draw soldermask overlay
80
- if (isCoveredWithSoldermask) {
81
- drawCircle({
82
- ctx,
83
- center: { x: hole.x, y: hole.y },
84
- radius: hole.hole_diameter / 2,
85
- fill: soldermaskOverlayColor,
86
- realToCanvasMat,
87
- })
88
- }
36
+ if (hole.hole_shape === "circle") {
37
+ drawCircle({
38
+ ctx,
39
+ center: { x: hole.x, y: hole.y },
40
+ radius: hole.hole_diameter / 2 - holeInset,
41
+ fill: colorMap.drill,
42
+ realToCanvasMat,
43
+ })
89
44
  return
90
45
  }
91
46
 
92
47
  if (hole.hole_shape === "square") {
93
48
  const rotation = getRotation(hole)
94
- // For positive margins, draw extended mask area first
95
- if (hasSoldermask && margin > 0) {
96
- drawRect({
97
- ctx,
98
- center: { x: hole.x, y: hole.y },
99
- width: hole.hole_diameter + margin * 2,
100
- height: hole.hole_diameter + margin * 2,
101
- fill: positiveMarginColor,
102
- realToCanvasMat,
103
- rotation,
104
- })
105
- }
106
-
107
- // Draw the hole (only if not fully covered with soldermask)
108
- if (!isCoveredWithSoldermask) {
109
- drawRect({
110
- ctx,
111
- center: { x: hole.x, y: hole.y },
112
- width: hole.hole_diameter,
113
- height: hole.hole_diameter,
114
- fill: colorMap.drill,
115
- realToCanvasMat,
116
- rotation,
117
- })
118
-
119
- // For negative margins, draw soldermask ring on top of the hole
120
- if (hasSoldermask && margin < 0) {
121
- drawSoldermaskRingForRect(
122
- ctx,
123
- { x: hole.x, y: hole.y },
124
- hole.hole_diameter,
125
- hole.hole_diameter,
126
- margin,
127
- 0,
128
- rotation,
129
- realToCanvasMat,
130
- soldermaskRingColor,
131
- colorMap.drill,
132
- )
133
- }
134
- }
135
-
136
- // If fully covered, draw soldermask overlay
137
- if (isCoveredWithSoldermask) {
138
- drawRect({
139
- ctx,
140
- center: { x: hole.x, y: hole.y },
141
- width: hole.hole_diameter,
142
- height: hole.hole_diameter,
143
- fill: soldermaskOverlayColor,
144
- realToCanvasMat,
145
- rotation,
146
- })
147
- }
49
+ drawRect({
50
+ ctx,
51
+ center: { x: hole.x, y: hole.y },
52
+ width: hole.hole_diameter - holeInset * 2,
53
+ height: hole.hole_diameter - holeInset * 2,
54
+ fill: colorMap.drill,
55
+ realToCanvasMat,
56
+ rotation,
57
+ })
148
58
  return
149
59
  }
150
60
 
151
61
  if (hole.hole_shape === "oval") {
152
62
  const rotation = getRotation(hole)
153
- // For positive margins, draw extended mask area first
154
- if (hasSoldermask && margin > 0) {
155
- drawOval({
156
- ctx,
157
- center: { x: hole.x, y: hole.y },
158
- radius_x: hole.hole_width / 2 + margin,
159
- radius_y: hole.hole_height / 2 + margin,
160
- fill: positiveMarginColor,
161
- realToCanvasMat,
162
- rotation,
163
- })
164
- }
165
-
166
- // Draw the hole (only if not fully covered with soldermask)
167
- if (!isCoveredWithSoldermask) {
168
- drawOval({
169
- ctx,
170
- center: { x: hole.x, y: hole.y },
171
- radius_x: hole.hole_width / 2,
172
- radius_y: hole.hole_height / 2,
173
- fill: colorMap.drill,
174
- realToCanvasMat,
175
- rotation,
176
- })
177
-
178
- // For negative margins, draw soldermask ring on top of the hole
179
- if (hasSoldermask && margin < 0) {
180
- drawSoldermaskRingForOval(
181
- ctx,
182
- { x: hole.x, y: hole.y },
183
- hole.hole_width / 2,
184
- hole.hole_height / 2,
185
- margin,
186
- rotation,
187
- realToCanvasMat,
188
- soldermaskRingColor,
189
- colorMap.drill,
190
- )
191
- }
192
- }
193
-
194
- // If fully covered, draw soldermask overlay
195
- if (isCoveredWithSoldermask) {
196
- drawOval({
197
- ctx,
198
- center: { x: hole.x, y: hole.y },
199
- radius_x: hole.hole_width / 2,
200
- radius_y: hole.hole_height / 2,
201
- fill: soldermaskOverlayColor,
202
- realToCanvasMat,
203
- rotation,
204
- })
205
- }
63
+ drawOval({
64
+ ctx,
65
+ center: { x: hole.x, y: hole.y },
66
+ radius_x: hole.hole_width / 2 - holeInset,
67
+ radius_y: hole.hole_height / 2 - holeInset,
68
+ fill: colorMap.drill,
69
+ realToCanvasMat,
70
+ rotation,
71
+ })
206
72
  return
207
73
  }
208
74
 
209
75
  if (hole.hole_shape === "rect") {
210
76
  const rotation = getRotation(hole)
211
- // For positive margins, draw extended mask area first
212
- if (hasSoldermask && margin > 0) {
213
- drawRect({
214
- ctx,
215
- center: { x: hole.x, y: hole.y },
216
- width: hole.hole_width + margin * 2,
217
- height: hole.hole_height + margin * 2,
218
- fill: positiveMarginColor,
219
- realToCanvasMat,
220
- rotation,
221
- })
222
- }
223
-
224
- // Draw the hole (only if not fully covered with soldermask)
225
- if (!isCoveredWithSoldermask) {
226
- drawRect({
227
- ctx,
228
- center: { x: hole.x, y: hole.y },
229
- width: hole.hole_width,
230
- height: hole.hole_height,
231
- fill: colorMap.drill,
232
- realToCanvasMat,
233
- rotation,
234
- })
235
-
236
- // For negative margins, draw soldermask ring on top of the hole
237
- if (hasSoldermask && margin < 0) {
238
- drawSoldermaskRingForRect(
239
- ctx,
240
- { x: hole.x, y: hole.y },
241
- hole.hole_width,
242
- hole.hole_height,
243
- margin,
244
- 0,
245
- rotation,
246
- realToCanvasMat,
247
- soldermaskRingColor,
248
- colorMap.drill,
249
- )
250
- }
251
- }
252
-
253
- // If fully covered, draw soldermask overlay
254
- if (isCoveredWithSoldermask) {
255
- drawRect({
256
- ctx,
257
- center: { x: hole.x, y: hole.y },
258
- width: hole.hole_width,
259
- height: hole.hole_height,
260
- fill: soldermaskOverlayColor,
261
- realToCanvasMat,
262
- rotation,
263
- })
264
- }
265
- return
266
- }
267
-
268
- if (hole.hole_shape === "pill") {
269
- const rotation = getRotation(hole)
270
- // For positive margins, draw extended mask area first
271
- if (hasSoldermask && margin > 0) {
272
- drawPill({
273
- ctx,
274
- center: { x: hole.x, y: hole.y },
275
- width: hole.hole_width + margin * 2,
276
- height: hole.hole_height + margin * 2,
277
- fill: positiveMarginColor,
278
- realToCanvasMat,
279
- rotation,
280
- })
281
- }
282
-
283
- // Draw the hole (only if not fully covered with soldermask)
284
- if (!isCoveredWithSoldermask) {
285
- drawPill({
286
- ctx,
287
- center: { x: hole.x, y: hole.y },
288
- width: hole.hole_width,
289
- height: hole.hole_height,
290
- fill: colorMap.drill,
291
- realToCanvasMat,
292
- rotation,
293
- })
294
-
295
- // For negative margins, draw soldermask ring on top of the hole
296
- if (hasSoldermask && margin < 0) {
297
- drawSoldermaskRingForPill(
298
- ctx,
299
- { x: hole.x, y: hole.y },
300
- hole.hole_width,
301
- hole.hole_height,
302
- margin,
303
- rotation,
304
- realToCanvasMat,
305
- soldermaskRingColor,
306
- colorMap.drill,
307
- )
308
- }
309
- }
310
-
311
- // If fully covered, draw soldermask overlay
312
- if (isCoveredWithSoldermask) {
313
- drawPill({
314
- ctx,
315
- center: { x: hole.x, y: hole.y },
316
- width: hole.hole_width,
317
- height: hole.hole_height,
318
- fill: soldermaskOverlayColor,
319
- realToCanvasMat,
320
- rotation,
321
- })
322
- }
77
+ drawRect({
78
+ ctx,
79
+ center: { x: hole.x, y: hole.y },
80
+ width: hole.hole_width - holeInset * 2,
81
+ height: hole.hole_height - holeInset * 2,
82
+ fill: colorMap.drill,
83
+ realToCanvasMat,
84
+ rotation,
85
+ })
323
86
  return
324
87
  }
325
88
 
326
- if (hole.hole_shape === "rotated_pill") {
89
+ if (hole.hole_shape === "pill" || hole.hole_shape === "rotated_pill") {
327
90
  const rotation = getRotation(hole)
328
-
329
- // For positive margins, draw extended mask area first
330
- if (hasSoldermask && margin > 0) {
331
- drawPill({
332
- ctx,
333
- center: { x: hole.x, y: hole.y },
334
- width: hole.hole_width + margin * 2,
335
- height: hole.hole_height + margin * 2,
336
- fill: positiveMarginColor,
337
- realToCanvasMat,
338
- rotation,
339
- })
340
- }
341
-
342
- // Draw the hole (only if not fully covered with soldermask)
343
- if (!isCoveredWithSoldermask) {
344
- drawPill({
345
- ctx,
346
- center: { x: hole.x, y: hole.y },
347
- width: hole.hole_width,
348
- height: hole.hole_height,
349
- fill: colorMap.drill,
350
- realToCanvasMat,
351
- rotation,
352
- })
353
-
354
- // For negative margins, draw soldermask ring on top of the hole
355
- if (hasSoldermask && margin < 0) {
356
- drawSoldermaskRingForPill(
357
- ctx,
358
- { x: hole.x, y: hole.y },
359
- hole.hole_width,
360
- hole.hole_height,
361
- margin,
362
- rotation,
363
- realToCanvasMat,
364
- soldermaskRingColor,
365
- colorMap.drill,
366
- )
367
- }
368
- }
369
-
370
- // If fully covered, draw soldermask overlay
371
- if (isCoveredWithSoldermask) {
372
- drawPill({
373
- ctx,
374
- center: { x: hole.x, y: hole.y },
375
- width: hole.hole_width,
376
- height: hole.hole_height,
377
- fill: soldermaskOverlayColor,
378
- realToCanvasMat,
379
- rotation,
380
- })
381
- }
91
+ drawPill({
92
+ ctx,
93
+ center: { x: hole.x, y: hole.y },
94
+ width: hole.hole_width - holeInset * 2,
95
+ height: hole.hole_height - holeInset * 2,
96
+ fill: colorMap.drill,
97
+ realToCanvasMat,
98
+ rotation,
99
+ })
382
100
  return
383
101
  }
384
102
  }