circuit-to-canvas 0.0.49 → 0.0.50

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.
package/dist/index.d.ts CHANGED
@@ -327,7 +327,12 @@ declare function drawPcbSmtPad(params: DrawPcbSmtPadParams): void;
327
327
  declare function drawSoldermaskRingForRect(ctx: CanvasContext, center: {
328
328
  x: number;
329
329
  y: number;
330
- }, width: number, height: number, margin: number, borderRadius: number, rotation: number, realToCanvasMat: Matrix, soldermaskColor: string, padColor: string): void;
330
+ }, width: number, height: number, margin: number, borderRadius: number, rotation: number, realToCanvasMat: Matrix, soldermaskColor: string, padColor: string, asymmetricMargins?: {
331
+ left: number;
332
+ right: number;
333
+ top: number;
334
+ bottom: number;
335
+ }): void;
331
336
  /**
332
337
  * Draws a soldermask ring for circular shapes with negative margin
333
338
  * (soldermask appears inside the pad boundary)
package/dist/index.js CHANGED
@@ -253,12 +253,19 @@ function drawPolygon(params) {
253
253
 
254
254
  // lib/drawer/elements/soldermask-margin.ts
255
255
  import { applyToPoint as applyToPoint6 } from "transformation-matrix";
256
- function drawSoldermaskRingForRect(ctx, center, width, height, margin, borderRadius, rotation, realToCanvasMat, soldermaskColor, padColor) {
256
+ function drawSoldermaskRingForRect(ctx, center, width, height, margin, borderRadius, rotation, realToCanvasMat, soldermaskColor, padColor, asymmetricMargins) {
257
257
  const [cx, cy] = applyToPoint6(realToCanvasMat, [center.x, center.y]);
258
258
  const scaledWidth = width * Math.abs(realToCanvasMat.a);
259
259
  const scaledHeight = height * Math.abs(realToCanvasMat.a);
260
- const scaledMargin = Math.abs(margin) * Math.abs(realToCanvasMat.a);
261
260
  const scaledRadius = borderRadius * Math.abs(realToCanvasMat.a);
261
+ const ml = asymmetricMargins?.left ?? margin;
262
+ const mr = asymmetricMargins?.right ?? margin;
263
+ const mt = asymmetricMargins?.top ?? margin;
264
+ const mb = asymmetricMargins?.bottom ?? margin;
265
+ const scaledThicknessL = Math.max(0, -ml) * Math.abs(realToCanvasMat.a);
266
+ const scaledThicknessR = Math.max(0, -mr) * Math.abs(realToCanvasMat.a);
267
+ const scaledThicknessT = Math.max(0, -mt) * Math.abs(realToCanvasMat.a);
268
+ const scaledThicknessB = Math.max(0, -mb) * Math.abs(realToCanvasMat.a);
262
269
  ctx.save();
263
270
  ctx.translate(cx, cy);
264
271
  if (rotation !== 0) {
@@ -299,14 +306,23 @@ function drawSoldermaskRingForRect(ctx, center, width, height, margin, borderRad
299
306
  if (ctx.globalCompositeOperation !== void 0) {
300
307
  ctx.globalCompositeOperation = prevCompositeOp || "source-over";
301
308
  }
302
- const innerWidth = scaledWidth - scaledMargin * 2;
303
- const innerHeight = scaledHeight - scaledMargin * 2;
304
- const innerRadius = Math.max(0, scaledRadius - scaledMargin);
309
+ const innerWidth = Math.max(
310
+ 0,
311
+ scaledWidth - (scaledThicknessL + scaledThicknessR)
312
+ );
313
+ const innerHeight = Math.max(
314
+ 0,
315
+ scaledHeight - (scaledThicknessT + scaledThicknessB)
316
+ );
317
+ const innerRadius = Math.max(
318
+ 0,
319
+ scaledRadius - (scaledThicknessL + scaledThicknessR + scaledThicknessT + scaledThicknessB) / 4
320
+ );
305
321
  if (innerWidth > 0 && innerHeight > 0) {
306
322
  ctx.beginPath();
323
+ const x = -scaledWidth / 2 + scaledThicknessL;
324
+ const y = -scaledHeight / 2 + scaledThicknessT;
307
325
  if (innerRadius > 0) {
308
- const x = -innerWidth / 2;
309
- const y = -innerHeight / 2;
310
326
  const r = Math.min(innerRadius, innerWidth / 2, innerHeight / 2);
311
327
  ctx.moveTo(x + r, y);
312
328
  ctx.lineTo(x + innerWidth - r, y);
@@ -324,7 +340,7 @@ function drawSoldermaskRingForRect(ctx, center, width, height, margin, borderRad
324
340
  ctx.lineTo(x, y + r);
325
341
  ctx.arcTo(x, y, x + r, y, r);
326
342
  } else {
327
- ctx.rect(-innerWidth / 2, -innerHeight / 2, innerWidth, innerHeight);
343
+ ctx.rect(x, y, innerWidth, innerHeight);
328
344
  }
329
345
  ctx.fillStyle = padColor;
330
346
  ctx.fill();
@@ -1324,24 +1340,40 @@ function getSoldermaskColor2(layer, colorMap) {
1324
1340
  return colorMap.soldermaskOverCopper[layer] ?? colorMap.soldermaskOverCopper.top;
1325
1341
  }
1326
1342
  function getBorderRadius(pad, margin = 0) {
1327
- return (pad.corner_radius ?? pad.rect_border_radius ?? 0) + margin;
1343
+ let r = 0;
1344
+ if (pad.shape === "rect" || pad.shape === "rotated_rect") {
1345
+ r = pad.corner_radius ?? pad.rect_border_radius ?? 0;
1346
+ }
1347
+ return r + margin;
1328
1348
  }
1329
1349
  function drawPcbSmtPad(params) {
1330
1350
  const { ctx, pad, realToCanvasMat, colorMap } = params;
1331
1351
  const color = layerToColor(pad.layer, colorMap);
1332
1352
  const isCoveredWithSoldermask = pad.is_covered_with_solder_mask === true;
1333
1353
  const margin = isCoveredWithSoldermask ? 0 : pad.soldermask_margin ?? 0;
1334
- const hasSoldermask = !isCoveredWithSoldermask && pad.soldermask_margin !== void 0 && pad.soldermask_margin !== 0;
1335
1354
  const soldermaskRingColor = getSoldermaskColor2(pad.layer, colorMap);
1336
1355
  const positiveMarginColor = colorMap.substrate;
1337
1356
  const soldermaskOverlayColor = getSoldermaskColor2(pad.layer, colorMap);
1357
+ const hasSoldermask = !isCoveredWithSoldermask && margin !== 0;
1358
+ let ml = margin;
1359
+ let mr = margin;
1360
+ let mt = margin;
1361
+ let mb = margin;
1362
+ let hasAnySoldermask = hasSoldermask;
1363
+ if (!isCoveredWithSoldermask && (pad.shape === "rect" || pad.shape === "rotated_rect")) {
1364
+ ml = pad.soldermask_margin_left ?? pad.soldermask_margin ?? 0;
1365
+ mr = pad.soldermask_margin_right ?? pad.soldermask_margin ?? 0;
1366
+ mt = pad.soldermask_margin_top ?? pad.soldermask_margin ?? 0;
1367
+ mb = pad.soldermask_margin_bottom ?? pad.soldermask_margin ?? 0;
1368
+ hasAnySoldermask = ml !== 0 || mr !== 0 || mt !== 0 || mb !== 0;
1369
+ }
1338
1370
  if (pad.shape === "rect") {
1339
- if (hasSoldermask && margin > 0) {
1371
+ if (hasAnySoldermask && (ml > 0 || mr > 0 || mt > 0 || mb > 0)) {
1340
1372
  drawRect({
1341
1373
  ctx,
1342
- center: { x: pad.x, y: pad.y },
1343
- width: pad.width + margin * 2,
1344
- height: pad.height + margin * 2,
1374
+ center: { x: pad.x + (mr - ml) / 2, y: pad.y + (mt - mb) / 2 },
1375
+ width: pad.width + ml + mr,
1376
+ height: pad.height + mt + mb,
1345
1377
  fill: positiveMarginColor,
1346
1378
  realToCanvasMat,
1347
1379
  borderRadius: getBorderRadius(pad)
@@ -1356,21 +1388,22 @@ function drawPcbSmtPad(params) {
1356
1388
  realToCanvasMat,
1357
1389
  borderRadius: getBorderRadius(pad)
1358
1390
  });
1359
- if (hasSoldermask && margin < 0) {
1391
+ if (hasAnySoldermask && (ml < 0 || mr < 0 || mt < 0 || mb < 0)) {
1360
1392
  drawSoldermaskRingForRect(
1361
1393
  ctx,
1362
1394
  { x: pad.x, y: pad.y },
1363
1395
  pad.width,
1364
1396
  pad.height,
1365
- margin,
1397
+ pad.soldermask_margin ?? 0,
1366
1398
  getBorderRadius(pad),
1367
1399
  0,
1368
1400
  realToCanvasMat,
1369
1401
  soldermaskRingColor,
1370
- color
1402
+ color,
1403
+ { left: ml, right: mr, top: mt, bottom: mb }
1371
1404
  );
1372
1405
  }
1373
- if (isCoveredWithSoldermask && margin === 0) {
1406
+ if (isCoveredWithSoldermask) {
1374
1407
  drawRect({
1375
1408
  ctx,
1376
1409
  center: { x: pad.x, y: pad.y },
@@ -1384,12 +1417,17 @@ function drawPcbSmtPad(params) {
1384
1417
  return;
1385
1418
  }
1386
1419
  if (pad.shape === "rotated_rect") {
1387
- if (hasSoldermask && margin > 0) {
1420
+ const radians = (pad.ccw_rotation ?? 0) * Math.PI / 180;
1421
+ const dxLocal = (mr - ml) / 2;
1422
+ const dyLocal = (mt - mb) / 2;
1423
+ const dxGlobal = dxLocal * Math.cos(radians) - dyLocal * Math.sin(radians);
1424
+ const dyGlobal = dxLocal * Math.sin(radians) + dyLocal * Math.cos(radians);
1425
+ if (hasAnySoldermask && (ml > 0 || mr > 0 || mt > 0 || mb > 0)) {
1388
1426
  drawRect({
1389
1427
  ctx,
1390
- center: { x: pad.x, y: pad.y },
1391
- width: pad.width + margin * 2,
1392
- height: pad.height + margin * 2,
1428
+ center: { x: pad.x + dxGlobal, y: pad.y + dyGlobal },
1429
+ width: pad.width + ml + mr,
1430
+ height: pad.height + mt + mb,
1393
1431
  fill: positiveMarginColor,
1394
1432
  realToCanvasMat,
1395
1433
  borderRadius: getBorderRadius(pad),
@@ -1406,21 +1444,22 @@ function drawPcbSmtPad(params) {
1406
1444
  borderRadius: getBorderRadius(pad),
1407
1445
  rotation: pad.ccw_rotation ?? 0
1408
1446
  });
1409
- if (hasSoldermask && margin < 0) {
1447
+ if (hasAnySoldermask && (ml < 0 || mr < 0 || mt < 0 || mb < 0)) {
1410
1448
  drawSoldermaskRingForRect(
1411
1449
  ctx,
1412
1450
  { x: pad.x, y: pad.y },
1413
1451
  pad.width,
1414
1452
  pad.height,
1415
- margin,
1453
+ pad.soldermask_margin ?? 0,
1416
1454
  getBorderRadius(pad),
1417
1455
  pad.ccw_rotation ?? 0,
1418
1456
  realToCanvasMat,
1419
1457
  soldermaskRingColor,
1420
- color
1458
+ color,
1459
+ { left: ml, right: mr, top: mt, bottom: mb }
1421
1460
  );
1422
1461
  }
1423
- if (isCoveredWithSoldermask && margin === 0) {
1462
+ if (isCoveredWithSoldermask) {
1424
1463
  drawRect({
1425
1464
  ctx,
1426
1465
  center: { x: pad.x, y: pad.y },
@@ -36,11 +36,11 @@ function getSoldermaskColor(layer: string, colorMap: PcbColorMap): string {
36
36
  }
37
37
 
38
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
- )
39
+ let r = 0
40
+ if (pad.shape === "rect" || pad.shape === "rotated_rect") {
41
+ r = pad.corner_radius ?? pad.rect_border_radius ?? 0
42
+ }
43
+ return r + margin
44
44
  }
45
45
 
46
46
  export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
@@ -48,25 +48,40 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
48
48
 
49
49
  const color = layerToColor(pad.layer, colorMap)
50
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
51
  const margin = isCoveredWithSoldermask ? 0 : (pad.soldermask_margin ?? 0)
53
- const hasSoldermask =
54
- !isCoveredWithSoldermask &&
55
- pad.soldermask_margin !== undefined &&
56
- pad.soldermask_margin !== 0
52
+
57
53
  const soldermaskRingColor = getSoldermaskColor(pad.layer, colorMap)
58
54
  const positiveMarginColor = colorMap.substrate
59
55
  const soldermaskOverlayColor = getSoldermaskColor(pad.layer, colorMap)
60
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
+
61
76
  // Draw the copper pad
62
77
  if (pad.shape === "rect") {
63
78
  // For positive margins, draw extended mask area first
64
- if (hasSoldermask && margin > 0) {
79
+ if (hasAnySoldermask && (ml > 0 || mr > 0 || mt > 0 || mb > 0)) {
65
80
  drawRect({
66
81
  ctx,
67
- center: { x: pad.x, y: pad.y },
68
- width: pad.width + margin * 2,
69
- height: pad.height + margin * 2,
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,
70
85
  fill: positiveMarginColor,
71
86
  realToCanvasMat,
72
87
  borderRadius: getBorderRadius(pad),
@@ -85,23 +100,24 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
85
100
  })
86
101
 
87
102
  // For negative margins, draw soldermask ring on top of the pad
88
- if (hasSoldermask && margin < 0) {
103
+ if (hasAnySoldermask && (ml < 0 || mr < 0 || mt < 0 || mb < 0)) {
89
104
  drawSoldermaskRingForRect(
90
105
  ctx,
91
106
  { x: pad.x, y: pad.y },
92
107
  pad.width,
93
108
  pad.height,
94
- margin,
109
+ pad.soldermask_margin ?? 0,
95
110
  getBorderRadius(pad),
96
111
  0,
97
112
  realToCanvasMat,
98
113
  soldermaskRingColor,
99
114
  color,
115
+ { left: ml, right: mr, top: mt, bottom: mb },
100
116
  )
101
117
  }
102
118
 
103
- // If covered with soldermask and margin == 0 (treat as 0 positive margin), draw soldermaskOverCopper overlay
104
- if (isCoveredWithSoldermask && margin === 0) {
119
+ // If covered with soldermask, draw soldermaskOverCopper overlay
120
+ if (isCoveredWithSoldermask) {
105
121
  drawRect({
106
122
  ctx,
107
123
  center: { x: pad.x, y: pad.y },
@@ -116,13 +132,19 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
116
132
  }
117
133
 
118
134
  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
+
119
141
  // For positive margins, draw extended mask area first
120
- if (hasSoldermask && margin > 0) {
142
+ if (hasAnySoldermask && (ml > 0 || mr > 0 || mt > 0 || mb > 0)) {
121
143
  drawRect({
122
144
  ctx,
123
- center: { x: pad.x, y: pad.y },
124
- width: pad.width + margin * 2,
125
- height: pad.height + margin * 2,
145
+ center: { x: pad.x + dxGlobal, y: pad.y + dyGlobal },
146
+ width: pad.width + ml + mr,
147
+ height: pad.height + mt + mb,
126
148
  fill: positiveMarginColor,
127
149
  realToCanvasMat,
128
150
  borderRadius: getBorderRadius(pad),
@@ -143,23 +165,24 @@ export function drawPcbSmtPad(params: DrawPcbSmtPadParams): void {
143
165
  })
144
166
 
145
167
  // For negative margins, draw soldermask ring on top of the pad
146
- if (hasSoldermask && margin < 0) {
168
+ if (hasAnySoldermask && (ml < 0 || mr < 0 || mt < 0 || mb < 0)) {
147
169
  drawSoldermaskRingForRect(
148
170
  ctx,
149
171
  { x: pad.x, y: pad.y },
150
172
  pad.width,
151
173
  pad.height,
152
- margin,
174
+ pad.soldermask_margin ?? 0,
153
175
  getBorderRadius(pad),
154
176
  pad.ccw_rotation ?? 0,
155
177
  realToCanvasMat,
156
178
  soldermaskRingColor,
157
179
  color,
180
+ { left: ml, right: mr, top: mt, bottom: mb },
158
181
  )
159
182
  }
160
183
 
161
- // If covered with soldermask and margin == 0 (treat as 0 positive margin), draw soldermaskOverCopper overlay
162
- if (isCoveredWithSoldermask && margin === 0) {
184
+ // If covered with soldermask, draw soldermaskOverCopper overlay
185
+ if (isCoveredWithSoldermask) {
163
186
  drawRect({
164
187
  ctx,
165
188
  center: { x: pad.x, y: pad.y },
@@ -17,13 +17,29 @@ export function drawSoldermaskRingForRect(
17
17
  realToCanvasMat: Matrix,
18
18
  soldermaskColor: string,
19
19
  padColor: string,
20
+ asymmetricMargins?: {
21
+ left: number
22
+ right: number
23
+ top: number
24
+ bottom: number
25
+ },
20
26
  ): void {
21
27
  const [cx, cy] = applyToPoint(realToCanvasMat, [center.x, center.y])
22
28
  const scaledWidth = width * Math.abs(realToCanvasMat.a)
23
29
  const scaledHeight = height * Math.abs(realToCanvasMat.a)
24
- const scaledMargin = Math.abs(margin) * Math.abs(realToCanvasMat.a)
25
30
  const scaledRadius = borderRadius * Math.abs(realToCanvasMat.a)
26
31
 
32
+ const ml = asymmetricMargins?.left ?? margin
33
+ const mr = asymmetricMargins?.right ?? margin
34
+ const mt = asymmetricMargins?.top ?? margin
35
+ const mb = asymmetricMargins?.bottom ?? margin
36
+
37
+ // Thickness of the soldermask ring (only if negative margin)
38
+ const scaledThicknessL = Math.max(0, -ml) * Math.abs(realToCanvasMat.a)
39
+ const scaledThicknessR = Math.max(0, -mr) * Math.abs(realToCanvasMat.a)
40
+ const scaledThicknessT = Math.max(0, -mt) * Math.abs(realToCanvasMat.a)
41
+ const scaledThicknessB = Math.max(0, -mb) * Math.abs(realToCanvasMat.a)
42
+
27
43
  ctx.save()
28
44
  ctx.translate(cx, cy)
29
45
 
@@ -76,16 +92,31 @@ export function drawSoldermaskRingForRect(
76
92
  ctx.globalCompositeOperation = prevCompositeOp || "source-over"
77
93
  }
78
94
 
79
- // Restore pad color in inner rectangle (reduced by margin)
80
- const innerWidth = scaledWidth - scaledMargin * 2
81
- const innerHeight = scaledHeight - scaledMargin * 2
82
- const innerRadius = Math.max(0, scaledRadius - scaledMargin)
95
+ // Restore pad color in inner rectangle (reduced by margins)
96
+ const innerWidth = Math.max(
97
+ 0,
98
+ scaledWidth - (scaledThicknessL + scaledThicknessR),
99
+ )
100
+ const innerHeight = Math.max(
101
+ 0,
102
+ scaledHeight - (scaledThicknessT + scaledThicknessB),
103
+ )
104
+ const innerRadius = Math.max(
105
+ 0,
106
+ scaledRadius -
107
+ (scaledThicknessL +
108
+ scaledThicknessR +
109
+ scaledThicknessT +
110
+ scaledThicknessB) /
111
+ 4,
112
+ )
83
113
 
84
114
  if (innerWidth > 0 && innerHeight > 0) {
85
115
  ctx.beginPath()
116
+ const x = -scaledWidth / 2 + scaledThicknessL
117
+ const y = -scaledHeight / 2 + scaledThicknessT
118
+
86
119
  if (innerRadius > 0) {
87
- const x = -innerWidth / 2
88
- const y = -innerHeight / 2
89
120
  const r = Math.min(innerRadius, innerWidth / 2, innerHeight / 2)
90
121
 
91
122
  ctx.moveTo(x + r, y)
@@ -104,7 +135,7 @@ export function drawSoldermaskRingForRect(
104
135
  ctx.lineTo(x, y + r)
105
136
  ctx.arcTo(x, y, x + r, y, r)
106
137
  } else {
107
- ctx.rect(-innerWidth / 2, -innerHeight / 2, innerWidth, innerHeight)
138
+ ctx.rect(x, y, innerWidth, innerHeight)
108
139
  }
109
140
 
110
141
  ctx.fillStyle = padColor
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "circuit-to-canvas",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.49",
4
+ "version": "0.0.50",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "build": "tsup-node ./lib/index.ts --format esm --dts",
@@ -17,7 +17,7 @@
17
17
  "@tscircuit/math-utils": "^0.0.29",
18
18
  "@types/bun": "latest",
19
19
  "bun-match-svg": "^0.0.14",
20
- "circuit-json": "^0.0.348",
20
+ "circuit-json": "^0.0.356",
21
21
  "circuit-json-to-connectivity-map": "^0.0.23",
22
22
  "circuit-to-svg": "^0.0.303",
23
23
  "looks-same": "^10.0.1",
@@ -0,0 +1,140 @@
1
+ import { expect, test } from "bun:test"
2
+ import { createCanvas } from "@napi-rs/canvas"
3
+ import type { AnyCircuitElement } from "circuit-json"
4
+ import { CircuitToCanvasDrawer } from "../../lib/drawer"
5
+
6
+ test("draw smt pads with asymmetric soldermask margins", async () => {
7
+ const canvas = createCanvas(800, 600)
8
+ const ctx = canvas.getContext("2d")
9
+ const drawer = new CircuitToCanvasDrawer(ctx)
10
+
11
+ ctx.fillStyle = "#1a1a1a"
12
+ ctx.fillRect(0, 0, 800, 600)
13
+
14
+ const circuit: AnyCircuitElement[] = [
15
+ {
16
+ type: "pcb_board",
17
+ pcb_board_id: "board0",
18
+ center: { x: 0, y: 0 },
19
+ width: 14,
20
+ height: 10,
21
+ thickness: 1.6,
22
+ num_layers: 2,
23
+ material: "fr4",
24
+ },
25
+ // Rectangle with asymmetric positive margins
26
+ {
27
+ type: "pcb_smtpad",
28
+ pcb_smtpad_id: "pad_rect_pos_asym",
29
+ shape: "rect",
30
+ layer: "top",
31
+ x: -4,
32
+ y: 2,
33
+ width: 2,
34
+ height: 1,
35
+ soldermask_margin_left: 0.5,
36
+ soldermask_margin_right: 0.1,
37
+ soldermask_margin_top: 0.2,
38
+ soldermask_margin_bottom: 0.8,
39
+ },
40
+ // Rectangle with asymmetric negative margins
41
+ {
42
+ type: "pcb_smtpad",
43
+ pcb_smtpad_id: "pad_rect_neg_asym",
44
+ shape: "rect",
45
+ layer: "top",
46
+ x: -4,
47
+ y: -2,
48
+ width: 2,
49
+ height: 1.5,
50
+ soldermask_margin_left: -0.2,
51
+ soldermask_margin_right: -0.6,
52
+ soldermask_margin_top: -0.1,
53
+ soldermask_margin_bottom: -0.4,
54
+ },
55
+ // Rotated rectangle with asymmetric positive margins
56
+ {
57
+ type: "pcb_smtpad",
58
+ pcb_smtpad_id: "pad_rot_rect_pos_asym",
59
+ shape: "rotated_rect",
60
+ layer: "top",
61
+ x: 4,
62
+ y: 2,
63
+ width: 2,
64
+ height: 1,
65
+ ccw_rotation: 45,
66
+ soldermask_margin_left: 0.8,
67
+ soldermask_margin_right: 0.2,
68
+ soldermask_margin_top: 0.1,
69
+ soldermask_margin_bottom: 0.5,
70
+ },
71
+ // Rotated rectangle with asymmetric negative margins
72
+ {
73
+ type: "pcb_smtpad",
74
+ pcb_smtpad_id: "pad_rot_rect_neg_asym",
75
+ shape: "rotated_rect",
76
+ layer: "top",
77
+ x: 4,
78
+ y: -2,
79
+ width: 2,
80
+ height: 1.5,
81
+ ccw_rotation: 30,
82
+ soldermask_margin_left: -0.5,
83
+ soldermask_margin_right: -0.1,
84
+ soldermask_margin_top: -0.4,
85
+ soldermask_margin_bottom: -0.2,
86
+ },
87
+ // Labels
88
+ {
89
+ type: "pcb_silkscreen_text",
90
+ pcb_silkscreen_text_id: "text1",
91
+ pcb_component_id: "comp1",
92
+ layer: "top",
93
+ anchor_position: { x: -4, y: 3.5 },
94
+ anchor_alignment: "center",
95
+ text: "Rect Pos Asym",
96
+ font_size: 0.4,
97
+ font: "tscircuit2024",
98
+ },
99
+ {
100
+ type: "pcb_silkscreen_text",
101
+ pcb_silkscreen_text_id: "text2",
102
+ pcb_component_id: "comp1",
103
+ layer: "top",
104
+ anchor_position: { x: -4, y: -3.8 },
105
+ anchor_alignment: "center",
106
+ text: "Rect Neg Asym",
107
+ font_size: 0.4,
108
+ font: "tscircuit2024",
109
+ },
110
+ {
111
+ type: "pcb_silkscreen_text",
112
+ pcb_silkscreen_text_id: "text3",
113
+ pcb_component_id: "comp1",
114
+ layer: "top",
115
+ anchor_position: { x: 4, y: 3.8 },
116
+ anchor_alignment: "center",
117
+ text: "Rot Rect Pos Asym",
118
+ font_size: 0.4,
119
+ font: "tscircuit2024",
120
+ },
121
+ {
122
+ type: "pcb_silkscreen_text",
123
+ pcb_silkscreen_text_id: "text4",
124
+ pcb_component_id: "comp1",
125
+ layer: "top",
126
+ anchor_position: { x: 4, y: -3.8 },
127
+ anchor_alignment: "center",
128
+ text: "Rot Rect Neg Asym",
129
+ font_size: 0.4,
130
+ font: "tscircuit2024",
131
+ },
132
+ ]
133
+
134
+ drawer.setCameraBounds({ minX: -7, maxX: 7, minY: -5, maxY: 5 })
135
+ drawer.drawElements(circuit)
136
+
137
+ await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
138
+ import.meta.path,
139
+ )
140
+ })
@@ -1,5 +1,6 @@
1
1
  import { expect, test } from "bun:test"
2
2
  import { createCanvas } from "@napi-rs/canvas"
3
+ import type { AnyCircuitElement } from "circuit-json"
3
4
  import { CircuitToCanvasDrawer } from "../../lib/drawer"
4
5
 
5
6
  test("draw smt pads with positive and negative soldermask margins", async () => {
@@ -10,13 +11,16 @@ test("draw smt pads with positive and negative soldermask margins", async () =>
10
11
  ctx.fillStyle = "#1a1a1a"
11
12
  ctx.fillRect(0, 0, 800, 600)
12
13
 
13
- const circuit: any = [
14
+ const circuit: AnyCircuitElement[] = [
14
15
  {
15
16
  type: "pcb_board",
16
17
  pcb_board_id: "board0",
17
18
  center: { x: 0, y: 0 },
18
19
  width: 14,
19
20
  height: 10,
21
+ thickness: 1.6,
22
+ num_layers: 2,
23
+ material: "fr4",
20
24
  },
21
25
  // Rectangle with positive margin (mask extends beyond pad)
22
26
  {
@@ -94,57 +98,69 @@ test("draw smt pads with positive and negative soldermask margins", async () =>
94
98
  {
95
99
  type: "pcb_silkscreen_text",
96
100
  pcb_silkscreen_text_id: "text_rect_pos",
101
+ pcb_component_id: "comp1",
97
102
  layer: "top",
98
103
  anchor_position: { x: -4, y: 3.2 },
99
104
  anchor_alignment: "center",
100
105
  text: "+0.2mm",
101
106
  font_size: 0.4,
107
+ font: "tscircuit2024",
102
108
  },
103
109
  {
104
110
  type: "pcb_silkscreen_text",
105
111
  pcb_silkscreen_text_id: "text_circle_pos",
112
+ pcb_component_id: "comp1",
106
113
  layer: "top",
107
114
  anchor_position: { x: 0, y: 3.2 },
108
115
  anchor_alignment: "center",
109
116
  text: "+0.15mm",
110
117
  font_size: 0.4,
118
+ font: "tscircuit2024",
111
119
  },
112
120
  {
113
121
  type: "pcb_silkscreen_text",
114
122
  pcb_silkscreen_text_id: "text_pill_pos",
123
+ pcb_component_id: "comp1",
115
124
  layer: "top",
116
125
  anchor_position: { x: 4, y: 3.2 },
117
126
  anchor_alignment: "center",
118
127
  text: "+0.1mm",
119
128
  font_size: 0.4,
129
+ font: "tscircuit2024",
120
130
  },
121
131
  // Silkscreen labels for negative margin pads (bottom row)
122
132
  {
123
133
  type: "pcb_silkscreen_text",
124
134
  pcb_silkscreen_text_id: "text_rect_neg",
135
+ pcb_component_id: "comp1",
125
136
  layer: "top",
126
137
  anchor_position: { x: -4, y: -3.2 },
127
138
  anchor_alignment: "center",
128
139
  text: "-0.15mm",
129
140
  font_size: 0.4,
141
+ font: "tscircuit2024",
130
142
  },
131
143
  {
132
144
  type: "pcb_silkscreen_text",
133
145
  pcb_silkscreen_text_id: "text_circle_neg",
146
+ pcb_component_id: "comp1",
134
147
  layer: "top",
135
148
  anchor_position: { x: 0, y: -3.2 },
136
149
  anchor_alignment: "center",
137
150
  text: "-0.2mm",
138
151
  font_size: 0.4,
152
+ font: "tscircuit2024",
139
153
  },
140
154
  {
141
155
  type: "pcb_silkscreen_text",
142
156
  pcb_silkscreen_text_id: "text_pill_neg",
157
+ pcb_component_id: "comp1",
143
158
  layer: "top",
144
159
  anchor_position: { x: 4, y: -3.2 },
145
160
  anchor_alignment: "center",
146
161
  text: "-0.12mm",
147
162
  font_size: 0.4,
163
+ font: "tscircuit2024",
148
164
  },
149
165
  ]
150
166