circuit-to-canvas 0.0.45 → 0.0.47

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 (27) hide show
  1. package/dist/index.js +530 -163
  2. package/lib/drawer/CircuitToCanvasDrawer.ts +1 -5
  3. package/lib/drawer/elements/pcb-hole.ts +248 -61
  4. package/lib/drawer/elements/pcb-plated-hole.ts +234 -107
  5. package/lib/drawer/elements/pcb-smtpad.ts +43 -21
  6. package/lib/drawer/elements/soldermask-margin.ts +130 -0
  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-fabrication-note-dimension.snap.png +0 -0
  14. package/tests/elements/__snapshots__/pcb-hole-soldermask-margin.snap.png +0 -0
  15. package/tests/elements/__snapshots__/pcb-keepout-layer-filter.snap.png +0 -0
  16. package/tests/elements/__snapshots__/pcb-keepout-multiple-layers.snap.png +0 -0
  17. package/tests/elements/__snapshots__/pcb-keepout-rect-and-circle.snap.png +0 -0
  18. package/tests/elements/__snapshots__/pcb-keepout-with-group-id.snap.png +0 -0
  19. package/tests/elements/__snapshots__/pcb-note-dimension-with-offset.snap.png +0 -0
  20. package/tests/elements/__snapshots__/pcb-plated-hole-soldermask-margin.snap.png +0 -0
  21. package/tests/elements/__snapshots__/pcb-silkscreen-oval.snap.png +0 -0
  22. package/tests/elements/__snapshots__/pcb-smtpad-soldermask-margin.snap.png +0 -0
  23. package/tests/elements/pcb-comprehensive-soldermask-margin.test.ts +1281 -0
  24. package/tests/elements/pcb-hole-soldermask-margin.test.ts +5 -5
  25. package/tests/elements/pcb-plated-hole-soldermask-margin.test.ts +8 -8
  26. package/tests/elements/pcb-smtpad-soldermask-margin.test.ts +0 -6
  27. package/tests/shapes/__snapshots__/dimension-line.snap.png +0 -0
@@ -11,6 +11,8 @@ import {
11
11
  drawSoldermaskRingForOval,
12
12
  drawSoldermaskRingForPill,
13
13
  drawSoldermaskRingForRect,
14
+ drawSoldermaskRingForPolygon,
15
+ offsetPolygonPoints,
14
16
  } from "./soldermask-margin"
15
17
 
16
18
  export interface DrawPcbPlatedHoleParams {
@@ -35,11 +37,12 @@ function getSoldermaskColor(
35
37
  export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
36
38
  const { ctx, hole, realToCanvasMat, colorMap } = params
37
39
 
40
+ const isCoveredWithSoldermask = hole.is_covered_with_solder_mask === true
41
+ const margin = isCoveredWithSoldermask ? 0 : (hole.soldermask_margin ?? 0)
38
42
  const hasSoldermask =
39
- hole.is_covered_with_solder_mask === true &&
43
+ !isCoveredWithSoldermask &&
40
44
  hole.soldermask_margin !== undefined &&
41
45
  hole.soldermask_margin !== 0
42
- const margin = hasSoldermask ? hole.soldermask_margin! : 0
43
46
  const soldermaskRingColor = getSoldermaskColor(hole.layers, colorMap)
44
47
  const positiveMarginColor = colorMap.substrate
45
48
  const copperColor = colorMap.copper.top
@@ -78,14 +81,27 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
78
81
  )
79
82
  }
80
83
 
81
- // Draw inner drill hole
82
- drawCircle({
83
- ctx,
84
- center: { x: hole.x, y: hole.y },
85
- radius: hole.hole_diameter / 2,
86
- fill: colorMap.drill,
87
- realToCanvasMat,
88
- })
84
+ // If fully covered, draw soldermask overlay
85
+ if (isCoveredWithSoldermask) {
86
+ drawCircle({
87
+ ctx,
88
+ center: { x: hole.x, y: hole.y },
89
+ radius: hole.outer_diameter / 2,
90
+ fill: soldermaskRingColor,
91
+ realToCanvasMat,
92
+ })
93
+ }
94
+
95
+ // Draw inner drill hole (only if not fully covered with soldermask)
96
+ if (!isCoveredWithSoldermask) {
97
+ drawCircle({
98
+ ctx,
99
+ center: { x: hole.x, y: hole.y },
100
+ radius: hole.hole_diameter / 2,
101
+ fill: colorMap.drill,
102
+ realToCanvasMat,
103
+ })
104
+ }
89
105
  return
90
106
  }
91
107
 
@@ -129,16 +145,31 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
129
145
  )
130
146
  }
131
147
 
132
- // Draw inner drill hole
133
- drawOval({
134
- ctx,
135
- center: { x: hole.x, y: hole.y },
136
- radius_x: hole.hole_width / 2,
137
- radius_y: hole.hole_height / 2,
138
- fill: colorMap.drill,
139
- realToCanvasMat,
140
- rotation: hole.ccw_rotation,
141
- })
148
+ // If fully covered, draw soldermask overlay
149
+ if (isCoveredWithSoldermask) {
150
+ drawOval({
151
+ ctx,
152
+ center: { x: hole.x, y: hole.y },
153
+ radius_x: hole.outer_width / 2,
154
+ radius_y: hole.outer_height / 2,
155
+ fill: soldermaskRingColor,
156
+ realToCanvasMat,
157
+ rotation: hole.ccw_rotation,
158
+ })
159
+ }
160
+
161
+ // Draw inner drill hole (only if not fully covered with soldermask)
162
+ if (!isCoveredWithSoldermask) {
163
+ drawOval({
164
+ ctx,
165
+ center: { x: hole.x, y: hole.y },
166
+ radius_x: hole.hole_width / 2,
167
+ radius_y: hole.hole_height / 2,
168
+ fill: colorMap.drill,
169
+ realToCanvasMat,
170
+ rotation: hole.ccw_rotation,
171
+ })
172
+ }
142
173
  return
143
174
  }
144
175
 
@@ -182,16 +213,31 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
182
213
  )
183
214
  }
184
215
 
185
- // Draw inner drill 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: hole.ccw_rotation,
194
- })
216
+ // If fully covered, draw soldermask overlay
217
+ if (isCoveredWithSoldermask) {
218
+ drawPill({
219
+ ctx,
220
+ center: { x: hole.x, y: hole.y },
221
+ width: hole.outer_width,
222
+ height: hole.outer_height,
223
+ fill: soldermaskRingColor,
224
+ realToCanvasMat,
225
+ rotation: hole.ccw_rotation,
226
+ })
227
+ }
228
+
229
+ // Draw inner drill hole (only if not fully covered with soldermask)
230
+ if (!isCoveredWithSoldermask) {
231
+ drawPill({
232
+ ctx,
233
+ center: { x: hole.x, y: hole.y },
234
+ width: hole.hole_width,
235
+ height: hole.hole_height,
236
+ fill: colorMap.drill,
237
+ realToCanvasMat,
238
+ rotation: hole.ccw_rotation,
239
+ })
240
+ }
195
241
  return
196
242
  }
197
243
 
@@ -205,7 +251,7 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
205
251
  height: hole.rect_pad_height + margin * 2,
206
252
  fill: positiveMarginColor,
207
253
  realToCanvasMat,
208
- borderRadius: (hole.rect_border_radius ?? 0) + margin,
254
+ borderRadius: hole.rect_border_radius ?? 0,
209
255
  })
210
256
  }
211
257
 
@@ -236,16 +282,31 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
236
282
  )
237
283
  }
238
284
 
239
- // Draw circular drill hole (with offset)
240
- const holeX = hole.x + (hole.hole_offset_x ?? 0)
241
- const holeY = hole.y + (hole.hole_offset_y ?? 0)
242
- drawCircle({
243
- ctx,
244
- center: { x: holeX, y: holeY },
245
- radius: hole.hole_diameter / 2,
246
- fill: colorMap.drill,
247
- realToCanvasMat,
248
- })
285
+ // If fully covered, draw soldermask overlay
286
+ if (isCoveredWithSoldermask) {
287
+ drawRect({
288
+ ctx,
289
+ center: { x: hole.x, y: hole.y },
290
+ width: hole.rect_pad_width,
291
+ height: hole.rect_pad_height,
292
+ fill: soldermaskRingColor,
293
+ realToCanvasMat,
294
+ borderRadius: hole.rect_border_radius ?? 0,
295
+ })
296
+ }
297
+
298
+ // Draw circular drill hole (with offset, only if not fully covered with soldermask)
299
+ if (!isCoveredWithSoldermask) {
300
+ const holeX = hole.x + (hole.hole_offset_x ?? 0)
301
+ const holeY = hole.y + (hole.hole_offset_y ?? 0)
302
+ drawCircle({
303
+ ctx,
304
+ center: { x: holeX, y: holeY },
305
+ radius: hole.hole_diameter / 2,
306
+ fill: colorMap.drill,
307
+ realToCanvasMat,
308
+ })
309
+ }
249
310
  return
250
311
  }
251
312
 
@@ -259,7 +320,7 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
259
320
  height: hole.rect_pad_height + margin * 2,
260
321
  fill: positiveMarginColor,
261
322
  realToCanvasMat,
262
- borderRadius: (hole.rect_border_radius ?? 0) + margin,
323
+ borderRadius: hole.rect_border_radius ?? 0,
263
324
  })
264
325
  }
265
326
 
@@ -290,17 +351,32 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
290
351
  )
291
352
  }
292
353
 
293
- // Draw pill drill hole (with offset)
294
- const holeX = hole.x + (hole.hole_offset_x ?? 0)
295
- const holeY = hole.y + (hole.hole_offset_y ?? 0)
296
- drawPill({
297
- ctx,
298
- center: { x: holeX, y: holeY },
299
- width: hole.hole_width,
300
- height: hole.hole_height,
301
- fill: colorMap.drill,
302
- realToCanvasMat,
303
- })
354
+ // If fully covered, draw soldermask overlay
355
+ if (isCoveredWithSoldermask) {
356
+ drawRect({
357
+ ctx,
358
+ center: { x: hole.x, y: hole.y },
359
+ width: hole.rect_pad_width,
360
+ height: hole.rect_pad_height,
361
+ fill: soldermaskRingColor,
362
+ realToCanvasMat,
363
+ borderRadius: hole.rect_border_radius ?? 0,
364
+ })
365
+ }
366
+
367
+ // Draw pill drill hole (with offset, only if not fully covered with soldermask)
368
+ if (!isCoveredWithSoldermask) {
369
+ const holeX = hole.x + (hole.hole_offset_x ?? 0)
370
+ const holeY = hole.y + (hole.hole_offset_y ?? 0)
371
+ drawPill({
372
+ ctx,
373
+ center: { x: holeX, y: holeY },
374
+ width: hole.hole_width,
375
+ height: hole.hole_height,
376
+ fill: colorMap.drill,
377
+ realToCanvasMat,
378
+ })
379
+ }
304
380
  return
305
381
  }
306
382
 
@@ -314,7 +390,7 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
314
390
  height: hole.rect_pad_height + margin * 2,
315
391
  fill: positiveMarginColor,
316
392
  realToCanvasMat,
317
- borderRadius: (hole.rect_border_radius ?? 0) + margin,
393
+ borderRadius: hole.rect_border_radius ?? 0,
318
394
  rotation: hole.rect_ccw_rotation,
319
395
  })
320
396
  }
@@ -347,24 +423,38 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
347
423
  )
348
424
  }
349
425
 
350
- // Draw rotated pill drill hole (with offset)
351
- const holeX = hole.x + (hole.hole_offset_x ?? 0)
352
- const holeY = hole.y + (hole.hole_offset_y ?? 0)
353
- drawPill({
354
- ctx,
355
- center: { x: holeX, y: holeY },
356
- width: hole.hole_width,
357
- height: hole.hole_height,
358
- fill: colorMap.drill,
359
- realToCanvasMat,
360
- rotation: hole.hole_ccw_rotation,
361
- })
426
+ // If fully covered, draw soldermask overlay
427
+ if (isCoveredWithSoldermask) {
428
+ drawRect({
429
+ ctx,
430
+ center: { x: hole.x, y: hole.y },
431
+ width: hole.rect_pad_width,
432
+ height: hole.rect_pad_height,
433
+ fill: soldermaskRingColor,
434
+ realToCanvasMat,
435
+ borderRadius: hole.rect_border_radius ?? 0,
436
+ rotation: hole.rect_ccw_rotation,
437
+ })
438
+ }
439
+
440
+ // Draw rotated pill drill hole (with offset, only if not fully covered with soldermask)
441
+ if (!isCoveredWithSoldermask) {
442
+ const holeX = hole.x + (hole.hole_offset_x ?? 0)
443
+ const holeY = hole.y + (hole.hole_offset_y ?? 0)
444
+ drawPill({
445
+ ctx,
446
+ center: { x: holeX, y: holeY },
447
+ width: hole.hole_width,
448
+ height: hole.hole_height,
449
+ fill: colorMap.drill,
450
+ realToCanvasMat,
451
+ rotation: hole.hole_ccw_rotation,
452
+ })
453
+ }
362
454
  return
363
455
  }
364
456
 
365
457
  if (hole.shape === "hole_with_polygon_pad") {
366
- // Note: Polygon pads don't support soldermask margins (similar to SMT polygon pads)
367
- // Draw polygon pad
368
458
  const padOutline = hole.pad_outline
369
459
  if (padOutline && padOutline.length >= 3) {
370
460
  // Transform pad_outline points to be relative to hole.x, hole.y
@@ -372,54 +462,91 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
372
462
  x: hole.x + point.x,
373
463
  y: hole.y + point.y,
374
464
  }))
465
+
466
+ // For positive margins, draw extended mask area first
467
+ if (hasSoldermask && margin > 0) {
468
+ const expandedPoints = offsetPolygonPoints(padPoints, margin)
469
+ drawPolygon({
470
+ ctx,
471
+ points: expandedPoints,
472
+ fill: positiveMarginColor,
473
+ realToCanvasMat,
474
+ })
475
+ }
476
+
477
+ // Draw polygon pad
375
478
  drawPolygon({
376
479
  ctx,
377
480
  points: padPoints,
378
481
  fill: copperColor,
379
482
  realToCanvasMat,
380
483
  })
484
+
485
+ // For negative margins, draw soldermask ring on top of the pad
486
+ if (hasSoldermask && margin < 0) {
487
+ drawSoldermaskRingForPolygon(
488
+ ctx,
489
+ padPoints,
490
+ margin,
491
+ realToCanvasMat,
492
+ soldermaskRingColor,
493
+ copperColor,
494
+ )
495
+ }
496
+
497
+ // If fully covered, draw soldermask overlay
498
+ if (isCoveredWithSoldermask) {
499
+ drawPolygon({
500
+ ctx,
501
+ points: padPoints,
502
+ fill: soldermaskRingColor,
503
+ realToCanvasMat,
504
+ })
505
+ }
381
506
  }
382
507
 
383
- // Draw drill hole (with offset)
384
- const holeX = hole.x + (hole.hole_offset_x ?? 0)
385
- const holeY = hole.y + (hole.hole_offset_y ?? 0)
386
- const holeShape = hole.hole_shape
508
+ // Draw drill hole (with offset, only if not fully covered with soldermask)
509
+ if (!isCoveredWithSoldermask) {
510
+ const holeX = hole.x + (hole.hole_offset_x ?? 0)
511
+ const holeY = hole.y + (hole.hole_offset_y ?? 0)
512
+ const holeShape = hole.hole_shape
387
513
 
388
- if (holeShape === "circle") {
389
- drawCircle({
390
- ctx,
391
- center: { x: holeX, y: holeY },
392
- radius: (hole.hole_diameter ?? 0) / 2,
393
- fill: colorMap.drill,
394
- realToCanvasMat,
395
- })
396
- } else if (holeShape === "oval") {
397
- drawOval({
398
- ctx,
399
- center: { x: holeX, y: holeY },
400
- radius_x: (hole.hole_width ?? 0) / 2,
401
- radius_y: (hole.hole_height ?? 0) / 2,
402
- fill: colorMap.drill,
403
- realToCanvasMat,
404
- })
405
- } else if (holeShape === "pill") {
406
- drawPill({
407
- ctx,
408
- center: { x: holeX, y: holeY },
409
- width: hole.hole_width ?? 0,
410
- height: hole.hole_height ?? 0,
411
- fill: colorMap.drill,
412
- realToCanvasMat,
413
- })
414
- } else if (holeShape === "rotated_pill") {
415
- drawPill({
416
- ctx,
417
- center: { x: holeX, y: holeY },
418
- width: hole.hole_width ?? 0,
419
- height: hole.hole_height ?? 0,
420
- fill: colorMap.drill,
421
- realToCanvasMat,
422
- })
514
+ if (holeShape === "circle") {
515
+ drawCircle({
516
+ ctx,
517
+ center: { x: holeX, y: holeY },
518
+ radius: (hole.hole_diameter ?? 0) / 2,
519
+ fill: colorMap.drill,
520
+ realToCanvasMat,
521
+ })
522
+ } else if (holeShape === "oval") {
523
+ drawOval({
524
+ ctx,
525
+ center: { x: holeX, y: holeY },
526
+ radius_x: (hole.hole_width ?? 0) / 2,
527
+ radius_y: (hole.hole_height ?? 0) / 2,
528
+ fill: colorMap.drill,
529
+ realToCanvasMat,
530
+ })
531
+ } else if (holeShape === "pill") {
532
+ drawPill({
533
+ ctx,
534
+ center: { x: holeX, y: holeY },
535
+ width: hole.hole_width ?? 0,
536
+ height: hole.hole_height ?? 0,
537
+ fill: colorMap.drill,
538
+ realToCanvasMat,
539
+ })
540
+ } else if (holeShape === "rotated_pill") {
541
+ drawPill({
542
+ ctx,
543
+ center: { x: holeX, y: holeY },
544
+ width: hole.hole_width ?? 0,
545
+ height: hole.hole_height ?? 0,
546
+ fill: colorMap.drill,
547
+ realToCanvasMat,
548
+ })
549
+ }
423
550
  }
424
551
  return
425
552
  }
@@ -9,6 +9,8 @@ import {
9
9
  drawSoldermaskRingForRect,
10
10
  drawSoldermaskRingForCircle,
11
11
  drawSoldermaskRingForPill,
12
+ drawSoldermaskRingForPolygon,
13
+ offsetPolygonPoints,
12
14
  } from "./soldermask-margin"
13
15
 
14
16
  export interface DrawPcbSmtPadParams {
@@ -33,7 +35,7 @@ function getSoldermaskColor(layer: string, colorMap: PcbColorMap): string {
33
35
  )
34
36
  }
35
37
 
36
- function getBorderRadius(pad: PcbSmtPad, margin: number = 0): number {
38
+ function getBorderRadius(pad: PcbSmtPad, margin = 0): number {
37
39
  return (
38
40
  ((pad as { corner_radius?: number }).corner_radius ??
39
41
  (pad as { rect_border_radius?: number }).rect_border_radius ??
@@ -46,13 +48,10 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
46
48
 
47
49
  const color = layerToColor(pad.layer, colorMap)
48
50
  const isCoveredWithSoldermask = pad.is_covered_with_solder_mask === true
49
- // When covered with soldermask, treat margin 0 or undefined as 0 positive margin
50
- const margin =
51
- isCoveredWithSoldermask && pad.soldermask_margin !== undefined
52
- ? pad.soldermask_margin
53
- : 0
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)
54
53
  const hasSoldermask =
55
- isCoveredWithSoldermask &&
54
+ !isCoveredWithSoldermask &&
56
55
  pad.soldermask_margin !== undefined &&
57
56
  pad.soldermask_margin !== 0
58
57
  const soldermaskRingColor = getSoldermaskColor(pad.layer, colorMap)
@@ -61,8 +60,8 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
61
60
 
62
61
  // Draw the copper pad
63
62
  if (pad.shape === "rect") {
64
- // For positive margins (including 0), draw extended mask area first
65
- if (isCoveredWithSoldermask && margin >= 0) {
63
+ // For positive margins, draw extended mask area first
64
+ if (hasSoldermask && margin > 0) {
66
65
  drawRect({
67
66
  ctx,
68
67
  center: { x: pad.x, y: pad.y },
@@ -70,7 +69,7 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
70
69
  height: pad.height + margin * 2,
71
70
  fill: positiveMarginColor,
72
71
  realToCanvasMat,
73
- borderRadius: getBorderRadius(pad, margin),
72
+ borderRadius: getBorderRadius(pad),
74
73
  })
75
74
  }
76
75
 
@@ -117,8 +116,8 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
117
116
  }
118
117
 
119
118
  if (pad.shape === "rotated_rect") {
120
- // For positive margins (including 0), draw extended mask area first
121
- if (isCoveredWithSoldermask && margin >= 0) {
119
+ // For positive margins, draw extended mask area first
120
+ if (hasSoldermask && margin > 0) {
122
121
  drawRect({
123
122
  ctx,
124
123
  center: { x: pad.x, y: pad.y },
@@ -126,7 +125,7 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
126
125
  height: pad.height + margin * 2,
127
126
  fill: positiveMarginColor,
128
127
  realToCanvasMat,
129
- borderRadius: getBorderRadius(pad, margin),
128
+ borderRadius: getBorderRadius(pad),
130
129
  rotation: pad.ccw_rotation ?? 0,
131
130
  })
132
131
  }
@@ -176,8 +175,8 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
176
175
  }
177
176
 
178
177
  if (pad.shape === "circle") {
179
- // For positive margins (including 0), draw extended mask area first
180
- if (isCoveredWithSoldermask && margin >= 0) {
178
+ // For positive margins, draw extended mask area first
179
+ if (hasSoldermask && margin > 0) {
181
180
  drawCircle({
182
181
  ctx,
183
182
  center: { x: pad.x, y: pad.y },
@@ -223,8 +222,8 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
223
222
  }
224
223
 
225
224
  if (pad.shape === "pill") {
226
- // For positive margins (including 0), draw extended mask area first
227
- if (isCoveredWithSoldermask && margin >= 0) {
225
+ // For positive margins, draw extended mask area first
226
+ if (hasSoldermask && margin > 0) {
228
227
  drawPill({
229
228
  ctx,
230
229
  center: { x: pad.x, y: pad.y },
@@ -275,8 +274,8 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
275
274
  }
276
275
 
277
276
  if (pad.shape === "rotated_pill") {
278
- // For positive margins (including 0), draw extended mask area first
279
- if (isCoveredWithSoldermask && margin >= 0) {
277
+ // For positive margins, draw extended mask area first
278
+ if (hasSoldermask && margin > 0) {
280
279
  drawPill({
281
280
  ctx,
282
281
  center: { x: pad.x, y: pad.y },
@@ -331,6 +330,17 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
331
330
 
332
331
  if (pad.shape === "polygon") {
333
332
  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
+
334
344
  // Draw the copper pad
335
345
  drawPolygon({
336
346
  ctx,
@@ -339,8 +349,20 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
339
349
  realToCanvasMat,
340
350
  })
341
351
 
342
- // If covered with soldermask and margin >= 0, draw soldermaskOverCopper overlay
343
- if (isCoveredWithSoldermask && margin >= 0) {
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) {
344
366
  drawPolygon({
345
367
  ctx,
346
368
  points: pad.points,