circuit-to-canvas 0.0.44 → 0.0.46

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 (28) hide show
  1. package/dist/index.js +407 -182
  2. package/lib/drawer/CircuitToCanvasDrawer.ts +1 -5
  3. package/lib/drawer/elements/pcb-copper-text.ts +23 -37
  4. package/lib/drawer/elements/pcb-hole.ts +248 -61
  5. package/lib/drawer/elements/pcb-plated-hole.ts +194 -102
  6. package/lib/drawer/elements/pcb-smtpad.ts +14 -17
  7. package/package.json +1 -1
  8. package/tests/board-snapshot/__snapshots__/usb-c-flashlight-board.snap.png +0 -0
  9. package/tests/elements/__snapshots__/board-with-elements.snap.png +0 -0
  10. package/tests/elements/__snapshots__/custom-outline-board.snap.png +0 -0
  11. package/tests/elements/__snapshots__/pcb-board.snap.png +0 -0
  12. package/tests/elements/__snapshots__/pcb-comprehensive-soldermask-margin.snap.png +0 -0
  13. package/tests/elements/__snapshots__/pcb-copper-text-knockout.snap.png +0 -0
  14. package/tests/elements/__snapshots__/pcb-fabrication-note-dimension.snap.png +0 -0
  15. package/tests/elements/__snapshots__/pcb-hole-soldermask-margin.snap.png +0 -0
  16. package/tests/elements/__snapshots__/pcb-keepout-layer-filter.snap.png +0 -0
  17. package/tests/elements/__snapshots__/pcb-keepout-multiple-layers.snap.png +0 -0
  18. package/tests/elements/__snapshots__/pcb-keepout-rect-and-circle.snap.png +0 -0
  19. package/tests/elements/__snapshots__/pcb-keepout-with-group-id.snap.png +0 -0
  20. package/tests/elements/__snapshots__/pcb-note-dimension-with-offset.snap.png +0 -0
  21. package/tests/elements/__snapshots__/pcb-plated-hole-soldermask-margin.snap.png +0 -0
  22. package/tests/elements/__snapshots__/pcb-silkscreen-oval.snap.png +0 -0
  23. package/tests/elements/__snapshots__/pcb-smtpad-soldermask-margin.snap.png +0 -0
  24. package/tests/elements/pcb-comprehensive-soldermask-margin.test.ts +330 -0
  25. package/tests/elements/pcb-hole-soldermask-margin.test.ts +5 -5
  26. package/tests/elements/pcb-plated-hole-soldermask-margin.test.ts +8 -8
  27. package/tests/elements/pcb-smtpad-soldermask-margin.test.ts +0 -6
  28. package/tests/shapes/__snapshots__/dimension-line.snap.png +0 -0
@@ -182,11 +182,7 @@ export class CircuitToCanvasDrawer {
182
182
  )
183
183
 
184
184
  for (const element of elements) {
185
- if (
186
- element.type === "pcb_board" &&
187
- (hasSoldermaskPads || hasSoldermaskHoles || hasSoldermaskPlatedHoles)
188
- ) {
189
- // Draw board with soldermask fill when pads or holes have soldermask
185
+ if (element.type === "pcb_board") {
190
186
  this.drawBoardWithSoldermask(element as PcbBoard)
191
187
  } else {
192
188
  this.drawElement(element, options)
@@ -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
  }
@@ -5,6 +5,12 @@ 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"
8
14
 
9
15
  export interface DrawPcbHoleParams {
10
16
  ctx: CanvasContext
@@ -24,12 +30,15 @@ function getRotation(hole: PCBHole): number {
24
30
  export function drawPcbHole(params: DrawPcbHoleParams): void {
25
31
  const { ctx, hole, realToCanvasMat, colorMap } = params
26
32
 
33
+ const isCoveredWithSoldermask = hole.is_covered_with_solder_mask === true
34
+ const margin = isCoveredWithSoldermask ? 0 : (hole.soldermask_margin ?? 0)
27
35
  const hasSoldermask =
28
- hole.is_covered_with_solder_mask === true &&
36
+ !isCoveredWithSoldermask &&
29
37
  hole.soldermask_margin !== undefined &&
30
- hole.soldermask_margin > 0
31
- const margin = hasSoldermask ? hole.soldermask_margin! : 0
38
+ hole.soldermask_margin !== 0
32
39
  const positiveMarginColor = colorMap.substrate
40
+ const soldermaskOverlayColor = colorMap.soldermask.top
41
+ const soldermaskRingColor = colorMap.soldermask.top
33
42
 
34
43
  if (hole.hole_shape === "circle") {
35
44
  // For positive margins, draw extended mask area first
@@ -43,14 +52,40 @@ export function drawPcbHole(params: DrawPcbHoleParams): void {
43
52
  })
44
53
  }
45
54
 
46
- // Draw the hole
47
- drawCircle({
48
- ctx,
49
- center: { x: hole.x, y: hole.y },
50
- radius: hole.hole_diameter / 2,
51
- fill: colorMap.drill,
52
- realToCanvasMat,
53
- })
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
+ })
64
+
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
+ }
78
+
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
+ }
54
89
  return
55
90
  }
56
91
 
@@ -69,16 +104,47 @@ export function drawPcbHole(params: DrawPcbHoleParams): void {
69
104
  })
70
105
  }
71
106
 
72
- // Draw the hole
73
- drawRect({
74
- ctx,
75
- center: { x: hole.x, y: hole.y },
76
- width: hole.hole_diameter,
77
- height: hole.hole_diameter,
78
- fill: colorMap.drill,
79
- realToCanvasMat,
80
- rotation,
81
- })
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
+ }
82
148
  return
83
149
  }
84
150
 
@@ -97,16 +163,46 @@ export function drawPcbHole(params: DrawPcbHoleParams): void {
97
163
  })
98
164
  }
99
165
 
100
- // Draw the hole
101
- drawOval({
102
- ctx,
103
- center: { x: hole.x, y: hole.y },
104
- radius_x: hole.hole_width / 2,
105
- radius_y: hole.hole_height / 2,
106
- fill: colorMap.drill,
107
- realToCanvasMat,
108
- rotation,
109
- })
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
+ }
110
206
  return
111
207
  }
112
208
 
@@ -125,16 +221,47 @@ export function drawPcbHole(params: DrawPcbHoleParams): void {
125
221
  })
126
222
  }
127
223
 
128
- // Draw the hole
129
- drawRect({
130
- ctx,
131
- center: { x: hole.x, y: hole.y },
132
- width: hole.hole_width,
133
- height: hole.hole_height,
134
- fill: colorMap.drill,
135
- realToCanvasMat,
136
- rotation,
137
- })
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
+ }
138
265
  return
139
266
  }
140
267
 
@@ -153,16 +280,46 @@ export function drawPcbHole(params: DrawPcbHoleParams): void {
153
280
  })
154
281
  }
155
282
 
156
- // Draw the hole
157
- drawPill({
158
- ctx,
159
- center: { x: hole.x, y: hole.y },
160
- width: hole.hole_width,
161
- height: hole.hole_height,
162
- fill: colorMap.drill,
163
- realToCanvasMat,
164
- rotation,
165
- })
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
+ }
166
323
  return
167
324
  }
168
325
 
@@ -182,16 +339,46 @@ export function drawPcbHole(params: DrawPcbHoleParams): void {
182
339
  })
183
340
  }
184
341
 
185
- // Draw the hole
186
- drawPill({
187
- ctx,
188
- center: { x: hole.x, y: hole.y },
189
- width: hole.hole_width,
190
- height: hole.hole_height,
191
- fill: colorMap.drill,
192
- realToCanvasMat,
193
- rotation,
194
- })
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
+ }
195
382
  return
196
383
  }
197
384
  }