circuit-to-canvas 0.0.35 → 0.0.36

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
@@ -341,6 +341,14 @@ declare function drawSoldermaskRingForPill(ctx: CanvasContext, center: {
341
341
  x: number;
342
342
  y: number;
343
343
  }, width: number, height: number, margin: number, rotation: number, realToCanvasMat: Matrix, soldermaskColor: string, padColor: string): void;
344
+ /**
345
+ * Draws a soldermask ring for oval shapes with negative margin
346
+ * (soldermask appears inside the hole boundary)
347
+ */
348
+ declare function drawSoldermaskRingForOval(ctx: CanvasContext, center: {
349
+ x: number;
350
+ y: number;
351
+ }, radius_x: number, radius_y: number, margin: number, rotation: number, realToCanvasMat: Matrix, soldermaskColor: string, holeColor: string): void;
344
352
 
345
353
  interface DrawPcbTraceParams {
346
354
  ctx: CanvasContext;
@@ -502,4 +510,4 @@ interface DrawPcbFabricationNoteDimensionParams {
502
510
  }
503
511
  declare function drawPcbFabricationNoteDimension(params: DrawPcbFabricationNoteDimensionParams): void;
504
512
 
505
- export { type AlphabetLayout, type AnchorAlignment, type CameraBounds, type CanvasContext, CircuitToCanvasDrawer, type CopperColorMap, type CopperLayerName, DEFAULT_PCB_COLOR_MAP, type DrawArrowParams, type DrawCircleParams, type DrawContext, type DrawDimensionLineParams, type DrawElementsOptions, type DrawLineParams, type DrawOvalParams, type DrawPathParams, type DrawPcbBoardParams, type DrawPcbCopperPourParams, type DrawPcbCopperTextParams, type DrawPcbCutoutParams, type DrawPcbFabricationNoteDimensionParams, type DrawPcbFabricationNotePathParams, type DrawPcbFabricationNoteRectParams, type DrawPcbFabricationNoteTextParams, type DrawPcbHoleParams, type DrawPcbNoteDimensionParams, type DrawPcbNotePathParams, type DrawPcbNoteRectParams, type DrawPcbNoteTextParams, type DrawPcbPlatedHoleParams, type DrawPcbSilkscreenCircleParams, type DrawPcbSilkscreenLineParams, type DrawPcbSilkscreenOvalParams, type DrawPcbSilkscreenPathParams, type DrawPcbSilkscreenPillParams, type DrawPcbSilkscreenRectParams, type DrawPcbSilkscreenTextParams, type DrawPcbSmtPadParams, type DrawPcbTraceParams, type DrawPcbViaParams, type DrawPillParams, type DrawPolygonParams, type DrawRectParams, type DrawTextParams, type DrawerConfig, type PcbColorMap, drawArrow, drawCircle, drawDimensionLine, drawLine, drawOval, drawPath, drawPcbBoard, drawPcbCopperPour, drawPcbCopperText, drawPcbCutout, drawPcbFabricationNoteDimension, drawPcbFabricationNotePath, drawPcbFabricationNoteRect, drawPcbFabricationNoteText, drawPcbHole, drawPcbNoteDimension, drawPcbNotePath, drawPcbNoteRect, drawPcbNoteText, drawPcbPlatedHole, drawPcbSilkscreenCircle, drawPcbSilkscreenLine, drawPcbSilkscreenOval, drawPcbSilkscreenPath, drawPcbSilkscreenPill, drawPcbSilkscreenRect, drawPcbSilkscreenText, drawPcbSmtPad, drawPcbTrace, drawPcbVia, drawPill, drawPolygon, drawRect, drawSoldermaskRingForCircle, drawSoldermaskRingForPill, drawSoldermaskRingForRect, drawText, getAlphabetLayout, getTextStartPosition, strokeAlphabetText };
513
+ export { type AlphabetLayout, type AnchorAlignment, type CameraBounds, type CanvasContext, CircuitToCanvasDrawer, type CopperColorMap, type CopperLayerName, DEFAULT_PCB_COLOR_MAP, type DrawArrowParams, type DrawCircleParams, type DrawContext, type DrawDimensionLineParams, type DrawElementsOptions, type DrawLineParams, type DrawOvalParams, type DrawPathParams, type DrawPcbBoardParams, type DrawPcbCopperPourParams, type DrawPcbCopperTextParams, type DrawPcbCutoutParams, type DrawPcbFabricationNoteDimensionParams, type DrawPcbFabricationNotePathParams, type DrawPcbFabricationNoteRectParams, type DrawPcbFabricationNoteTextParams, type DrawPcbHoleParams, type DrawPcbNoteDimensionParams, type DrawPcbNotePathParams, type DrawPcbNoteRectParams, type DrawPcbNoteTextParams, type DrawPcbPlatedHoleParams, type DrawPcbSilkscreenCircleParams, type DrawPcbSilkscreenLineParams, type DrawPcbSilkscreenOvalParams, type DrawPcbSilkscreenPathParams, type DrawPcbSilkscreenPillParams, type DrawPcbSilkscreenRectParams, type DrawPcbSilkscreenTextParams, type DrawPcbSmtPadParams, type DrawPcbTraceParams, type DrawPcbViaParams, type DrawPillParams, type DrawPolygonParams, type DrawRectParams, type DrawTextParams, type DrawerConfig, type PcbColorMap, drawArrow, drawCircle, drawDimensionLine, drawLine, drawOval, drawPath, drawPcbBoard, drawPcbCopperPour, drawPcbCopperText, drawPcbCutout, drawPcbFabricationNoteDimension, drawPcbFabricationNotePath, drawPcbFabricationNoteRect, drawPcbFabricationNoteText, drawPcbHole, drawPcbNoteDimension, drawPcbNotePath, drawPcbNoteRect, drawPcbNoteText, drawPcbPlatedHole, drawPcbSilkscreenCircle, drawPcbSilkscreenLine, drawPcbSilkscreenOval, drawPcbSilkscreenPath, drawPcbSilkscreenPill, drawPcbSilkscreenRect, drawPcbSilkscreenText, drawPcbSmtPad, drawPcbTrace, drawPcbVia, drawPill, drawPolygon, drawRect, drawSoldermaskRingForCircle, drawSoldermaskRingForOval, drawSoldermaskRingForPill, drawSoldermaskRingForRect, drawText, getAlphabetLayout, getTextStartPosition, strokeAlphabetText };
package/dist/index.js CHANGED
@@ -457,7 +457,19 @@ function drawPcbVia(params) {
457
457
  // lib/drawer/elements/pcb-hole.ts
458
458
  function drawPcbHole(params) {
459
459
  const { ctx, hole, realToCanvasMat, colorMap } = params;
460
+ const hasSoldermask = hole.is_covered_with_solder_mask === true && hole.soldermask_margin !== void 0 && hole.soldermask_margin > 0;
461
+ const margin = hasSoldermask ? hole.soldermask_margin : 0;
462
+ const positiveMarginColor = colorMap.substrate;
460
463
  if (hole.hole_shape === "circle") {
464
+ if (hasSoldermask && margin > 0) {
465
+ drawCircle({
466
+ ctx,
467
+ center: { x: hole.x, y: hole.y },
468
+ radius: hole.hole_diameter / 2 + margin,
469
+ fill: positiveMarginColor,
470
+ realToCanvasMat
471
+ });
472
+ }
461
473
  drawCircle({
462
474
  ctx,
463
475
  center: { x: hole.x, y: hole.y },
@@ -468,6 +480,16 @@ function drawPcbHole(params) {
468
480
  return;
469
481
  }
470
482
  if (hole.hole_shape === "square") {
483
+ if (hasSoldermask && margin > 0) {
484
+ drawRect({
485
+ ctx,
486
+ center: { x: hole.x, y: hole.y },
487
+ width: hole.hole_diameter + margin * 2,
488
+ height: hole.hole_diameter + margin * 2,
489
+ fill: positiveMarginColor,
490
+ realToCanvasMat
491
+ });
492
+ }
471
493
  drawRect({
472
494
  ctx,
473
495
  center: { x: hole.x, y: hole.y },
@@ -479,6 +501,16 @@ function drawPcbHole(params) {
479
501
  return;
480
502
  }
481
503
  if (hole.hole_shape === "oval") {
504
+ if (hasSoldermask && margin > 0) {
505
+ drawOval({
506
+ ctx,
507
+ center: { x: hole.x, y: hole.y },
508
+ radius_x: hole.hole_width / 2 + margin,
509
+ radius_y: hole.hole_height / 2 + margin,
510
+ fill: positiveMarginColor,
511
+ realToCanvasMat
512
+ });
513
+ }
482
514
  drawOval({
483
515
  ctx,
484
516
  center: { x: hole.x, y: hole.y },
@@ -490,6 +522,16 @@ function drawPcbHole(params) {
490
522
  return;
491
523
  }
492
524
  if (hole.hole_shape === "rect") {
525
+ if (hasSoldermask && margin > 0) {
526
+ drawRect({
527
+ ctx,
528
+ center: { x: hole.x, y: hole.y },
529
+ width: hole.hole_width + margin * 2,
530
+ height: hole.hole_height + margin * 2,
531
+ fill: positiveMarginColor,
532
+ realToCanvasMat
533
+ });
534
+ }
493
535
  drawRect({
494
536
  ctx,
495
537
  center: { x: hole.x, y: hole.y },
@@ -501,6 +543,16 @@ function drawPcbHole(params) {
501
543
  return;
502
544
  }
503
545
  if (hole.hole_shape === "pill") {
546
+ if (hasSoldermask && margin > 0) {
547
+ drawPill({
548
+ ctx,
549
+ center: { x: hole.x, y: hole.y },
550
+ width: hole.hole_width + margin * 2,
551
+ height: hole.hole_height + margin * 2,
552
+ fill: positiveMarginColor,
553
+ realToCanvasMat
554
+ });
555
+ }
504
556
  drawPill({
505
557
  ctx,
506
558
  center: { x: hole.x, y: hole.y },
@@ -512,6 +564,18 @@ function drawPcbHole(params) {
512
564
  return;
513
565
  }
514
566
  if (hole.hole_shape === "rotated_pill") {
567
+ const rotation = hole.ccw_rotation ?? 0;
568
+ if (hasSoldermask && margin > 0) {
569
+ drawPill({
570
+ ctx,
571
+ center: { x: hole.x, y: hole.y },
572
+ width: hole.hole_width + margin * 2,
573
+ height: hole.hole_height + margin * 2,
574
+ fill: positiveMarginColor,
575
+ realToCanvasMat,
576
+ rotation
577
+ });
578
+ }
515
579
  drawPill({
516
580
  ctx,
517
581
  center: { x: hole.x, y: hole.y },
@@ -519,7 +583,7 @@ function drawPcbHole(params) {
519
583
  height: hole.hole_height,
520
584
  fill: colorMap.drill,
521
585
  realToCanvasMat,
522
- rotation: hole.ccw_rotation ?? 0
586
+ rotation
523
587
  });
524
588
  return;
525
589
  }
@@ -699,6 +763,37 @@ function drawSoldermaskRingForPill(ctx, center, width, height, margin, rotation,
699
763
  }
700
764
  ctx.restore();
701
765
  }
766
+ function drawSoldermaskRingForOval(ctx, center, radius_x, radius_y, margin, rotation, realToCanvasMat, soldermaskColor, holeColor) {
767
+ const [cx, cy] = applyToPoint6(realToCanvasMat, [center.x, center.y]);
768
+ const scaledRadiusX = radius_x * Math.abs(realToCanvasMat.a);
769
+ const scaledRadiusY = radius_y * Math.abs(realToCanvasMat.a);
770
+ const scaledMargin = Math.abs(margin) * Math.abs(realToCanvasMat.a);
771
+ ctx.save();
772
+ ctx.translate(cx, cy);
773
+ if (rotation !== 0) {
774
+ ctx.rotate(-rotation * (Math.PI / 180));
775
+ }
776
+ const prevCompositeOp = ctx.globalCompositeOperation;
777
+ if (ctx.globalCompositeOperation !== void 0) {
778
+ ctx.globalCompositeOperation = "source-atop";
779
+ }
780
+ ctx.beginPath();
781
+ ctx.ellipse(0, 0, scaledRadiusX, scaledRadiusY, 0, 0, Math.PI * 2);
782
+ ctx.fillStyle = soldermaskColor;
783
+ ctx.fill();
784
+ if (ctx.globalCompositeOperation !== void 0) {
785
+ ctx.globalCompositeOperation = prevCompositeOp || "source-over";
786
+ }
787
+ const innerRadiusX = Math.max(0, scaledRadiusX - scaledMargin);
788
+ const innerRadiusY = Math.max(0, scaledRadiusY - scaledMargin);
789
+ if (innerRadiusX > 0 && innerRadiusY > 0) {
790
+ ctx.beginPath();
791
+ ctx.ellipse(0, 0, innerRadiusX, innerRadiusY, 0, 0, Math.PI * 2);
792
+ ctx.fillStyle = holeColor;
793
+ ctx.fill();
794
+ }
795
+ ctx.restore();
796
+ }
702
797
 
703
798
  // lib/drawer/elements/pcb-smtpad.ts
704
799
  function layerToColor(layer, colorMap) {
@@ -1941,8 +2036,11 @@ var CircuitToCanvasDrawer = class {
1941
2036
  const hasSoldermaskPads = elements.some(
1942
2037
  (el) => el.type === "pcb_smtpad" && el.is_covered_with_solder_mask === true
1943
2038
  );
2039
+ const hasSoldermaskHoles = elements.some(
2040
+ (el) => el.type === "pcb_hole" && el.is_covered_with_solder_mask === true
2041
+ );
1944
2042
  for (const element of elements) {
1945
- if (element.type === "pcb_board" && hasSoldermaskPads) {
2043
+ if (element.type === "pcb_board" && (hasSoldermaskPads || hasSoldermaskHoles)) {
1946
2044
  this.drawBoardWithSoldermask(element);
1947
2045
  } else {
1948
2046
  this.drawElement(element, options);
@@ -2269,6 +2367,7 @@ export {
2269
2367
  drawPolygon,
2270
2368
  drawRect,
2271
2369
  drawSoldermaskRingForCircle,
2370
+ drawSoldermaskRingForOval,
2272
2371
  drawSoldermaskRingForPill,
2273
2372
  drawSoldermaskRingForRect,
2274
2373
  drawText,
@@ -160,16 +160,25 @@ export class CircuitToCanvasDrawer {
160
160
  elements: AnyCircuitElement[],
161
161
  options: DrawElementsOptions = {},
162
162
  ): void {
163
- // Check if any pad has is_covered_with_solder_mask: true
163
+ // Check if any pad or hole has is_covered_with_solder_mask: true
164
164
  const hasSoldermaskPads = elements.some(
165
165
  (el) =>
166
166
  el.type === "pcb_smtpad" &&
167
167
  (el as PcbSmtPad).is_covered_with_solder_mask === true,
168
168
  )
169
+ const hasSoldermaskHoles = elements.some(
170
+ (el) =>
171
+ el.type === "pcb_hole" &&
172
+ (el as PcbHole & { is_covered_with_solder_mask?: boolean })
173
+ .is_covered_with_solder_mask === true,
174
+ )
169
175
 
170
176
  for (const element of elements) {
171
- if (element.type === "pcb_board" && hasSoldermaskPads) {
172
- // Draw board with soldermask fill when pads have soldermask
177
+ if (
178
+ element.type === "pcb_board" &&
179
+ (hasSoldermaskPads || hasSoldermaskHoles)
180
+ ) {
181
+ // Draw board with soldermask fill when pads or holes have soldermask
173
182
  this.drawBoardWithSoldermask(element as PcbBoard)
174
183
  } else {
175
184
  this.drawElement(element, options)
@@ -12,6 +12,7 @@ export {
12
12
  drawSoldermaskRingForRect,
13
13
  drawSoldermaskRingForCircle,
14
14
  drawSoldermaskRingForPill,
15
+ drawSoldermaskRingForOval,
15
16
  } from "./soldermask-margin"
16
17
 
17
18
  export { drawPcbTrace, type DrawPcbTraceParams } from "./pcb-trace"
@@ -16,7 +16,26 @@ export interface DrawPcbHoleParams {
16
16
  export function drawPcbHole(params: DrawPcbHoleParams): void {
17
17
  const { ctx, hole, realToCanvasMat, colorMap } = params
18
18
 
19
+ const hasSoldermask =
20
+ hole.is_covered_with_solder_mask === true &&
21
+ hole.soldermask_margin !== undefined &&
22
+ hole.soldermask_margin > 0
23
+ const margin = hasSoldermask ? hole.soldermask_margin! : 0
24
+ const positiveMarginColor = colorMap.substrate
25
+
19
26
  if (hole.hole_shape === "circle") {
27
+ // For positive margins, draw extended mask area first
28
+ if (hasSoldermask && margin > 0) {
29
+ drawCircle({
30
+ ctx,
31
+ center: { x: hole.x, y: hole.y },
32
+ radius: hole.hole_diameter / 2 + margin,
33
+ fill: positiveMarginColor,
34
+ realToCanvasMat,
35
+ })
36
+ }
37
+
38
+ // Draw the hole
20
39
  drawCircle({
21
40
  ctx,
22
41
  center: { x: hole.x, y: hole.y },
@@ -28,6 +47,19 @@ export function drawPcbHole(params: DrawPcbHoleParams): void {
28
47
  }
29
48
 
30
49
  if (hole.hole_shape === "square") {
50
+ // For positive margins, draw extended mask area first
51
+ if (hasSoldermask && margin > 0) {
52
+ drawRect({
53
+ ctx,
54
+ center: { x: hole.x, y: hole.y },
55
+ width: hole.hole_diameter + margin * 2,
56
+ height: hole.hole_diameter + margin * 2,
57
+ fill: positiveMarginColor,
58
+ realToCanvasMat,
59
+ })
60
+ }
61
+
62
+ // Draw the hole
31
63
  drawRect({
32
64
  ctx,
33
65
  center: { x: hole.x, y: hole.y },
@@ -40,6 +72,19 @@ export function drawPcbHole(params: DrawPcbHoleParams): void {
40
72
  }
41
73
 
42
74
  if (hole.hole_shape === "oval") {
75
+ // For positive margins, draw extended mask area first
76
+ if (hasSoldermask && margin > 0) {
77
+ drawOval({
78
+ ctx,
79
+ center: { x: hole.x, y: hole.y },
80
+ radius_x: hole.hole_width / 2 + margin,
81
+ radius_y: hole.hole_height / 2 + margin,
82
+ fill: positiveMarginColor,
83
+ realToCanvasMat,
84
+ })
85
+ }
86
+
87
+ // Draw the hole
43
88
  drawOval({
44
89
  ctx,
45
90
  center: { x: hole.x, y: hole.y },
@@ -52,6 +97,19 @@ export function drawPcbHole(params: DrawPcbHoleParams): void {
52
97
  }
53
98
 
54
99
  if (hole.hole_shape === "rect") {
100
+ // For positive margins, draw extended mask area first
101
+ if (hasSoldermask && margin > 0) {
102
+ drawRect({
103
+ ctx,
104
+ center: { x: hole.x, y: hole.y },
105
+ width: hole.hole_width + margin * 2,
106
+ height: hole.hole_height + margin * 2,
107
+ fill: positiveMarginColor,
108
+ realToCanvasMat,
109
+ })
110
+ }
111
+
112
+ // Draw the hole
55
113
  drawRect({
56
114
  ctx,
57
115
  center: { x: hole.x, y: hole.y },
@@ -64,6 +122,19 @@ export function drawPcbHole(params: DrawPcbHoleParams): void {
64
122
  }
65
123
 
66
124
  if (hole.hole_shape === "pill") {
125
+ // For positive margins, draw extended mask area first
126
+ if (hasSoldermask && margin > 0) {
127
+ drawPill({
128
+ ctx,
129
+ center: { x: hole.x, y: hole.y },
130
+ width: hole.hole_width + margin * 2,
131
+ height: hole.hole_height + margin * 2,
132
+ fill: positiveMarginColor,
133
+ realToCanvasMat,
134
+ })
135
+ }
136
+
137
+ // Draw the hole
67
138
  drawPill({
68
139
  ctx,
69
140
  center: { x: hole.x, y: hole.y },
@@ -76,6 +147,22 @@ export function drawPcbHole(params: DrawPcbHoleParams): void {
76
147
  }
77
148
 
78
149
  if (hole.hole_shape === "rotated_pill") {
150
+ const rotation = (hole as any).ccw_rotation ?? 0
151
+
152
+ // For positive margins, draw extended mask area first
153
+ if (hasSoldermask && margin > 0) {
154
+ drawPill({
155
+ ctx,
156
+ center: { x: hole.x, y: hole.y },
157
+ width: hole.hole_width + margin * 2,
158
+ height: hole.hole_height + margin * 2,
159
+ fill: positiveMarginColor,
160
+ realToCanvasMat,
161
+ rotation,
162
+ })
163
+ }
164
+
165
+ // Draw the hole
79
166
  drawPill({
80
167
  ctx,
81
168
  center: { x: hole.x, y: hole.y },
@@ -83,7 +170,7 @@ export function drawPcbHole(params: DrawPcbHoleParams): void {
83
170
  height: hole.hole_height,
84
171
  fill: colorMap.drill,
85
172
  realToCanvasMat,
86
- rotation: (hole as any).ccw_rotation ?? 0,
173
+ rotation,
87
174
  })
88
175
  return
89
176
  }
@@ -264,3 +264,63 @@ export function drawSoldermaskRingForPill(
264
264
 
265
265
  ctx.restore()
266
266
  }
267
+
268
+ /**
269
+ * Draws a soldermask ring for oval shapes with negative margin
270
+ * (soldermask appears inside the hole boundary)
271
+ */
272
+ export function drawSoldermaskRingForOval(
273
+ ctx: CanvasContext,
274
+ center: { x: number; y: number },
275
+ radius_x: number,
276
+ radius_y: number,
277
+ margin: number,
278
+ rotation: number,
279
+ realToCanvasMat: Matrix,
280
+ soldermaskColor: string,
281
+ holeColor: string,
282
+ ): void {
283
+ const [cx, cy] = applyToPoint(realToCanvasMat, [center.x, center.y])
284
+ const scaledRadiusX = radius_x * Math.abs(realToCanvasMat.a)
285
+ const scaledRadiusY = radius_y * Math.abs(realToCanvasMat.a)
286
+ const scaledMargin = Math.abs(margin) * Math.abs(realToCanvasMat.a)
287
+
288
+ ctx.save()
289
+ ctx.translate(cx, cy)
290
+
291
+ if (rotation !== 0) {
292
+ ctx.rotate(-rotation * (Math.PI / 180))
293
+ }
294
+
295
+ // For negative margins, outer is hole boundary, inner is reduced by margin
296
+ // Use source-atop so the ring only appears on the hole
297
+ const prevCompositeOp = ctx.globalCompositeOperation
298
+ if (ctx.globalCompositeOperation !== undefined) {
299
+ ctx.globalCompositeOperation = "source-atop"
300
+ }
301
+
302
+ // Draw outer oval filled (at hole boundary)
303
+ ctx.beginPath()
304
+ ctx.ellipse(0, 0, scaledRadiusX, scaledRadiusY, 0, 0, Math.PI * 2)
305
+ ctx.fillStyle = soldermaskColor
306
+ ctx.fill()
307
+
308
+ // Reset composite operation and restore hole color in inner area
309
+ if (ctx.globalCompositeOperation !== undefined) {
310
+ ctx.globalCompositeOperation = prevCompositeOp || "source-over"
311
+ }
312
+
313
+ // Restore hole color in inner oval (reduced by margin)
314
+ // For ovals, we reduce both radii by the margin
315
+ const innerRadiusX = Math.max(0, scaledRadiusX - scaledMargin)
316
+ const innerRadiusY = Math.max(0, scaledRadiusY - scaledMargin)
317
+
318
+ if (innerRadiusX > 0 && innerRadiusY > 0) {
319
+ ctx.beginPath()
320
+ ctx.ellipse(0, 0, innerRadiusX, innerRadiusY, 0, 0, Math.PI * 2)
321
+ ctx.fillStyle = holeColor
322
+ ctx.fill()
323
+ }
324
+
325
+ ctx.restore()
326
+ }
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.35",
4
+ "version": "0.0.36",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "build": "tsup-node ./lib/index.ts --format esm --dts",
@@ -0,0 +1,133 @@
1
+ import { expect, test } from "bun:test"
2
+ import { createCanvas } from "@napi-rs/canvas"
3
+ import { CircuitToCanvasDrawer } from "../../lib/drawer"
4
+
5
+ test("draw holes with positive soldermask margins", async () => {
6
+ const canvas = createCanvas(800, 600)
7
+ const ctx = canvas.getContext("2d")
8
+ const drawer = new CircuitToCanvasDrawer(ctx)
9
+
10
+ ctx.fillStyle = "#1a1a1a"
11
+ ctx.fillRect(0, 0, 800, 600)
12
+
13
+ const circuit: any = [
14
+ {
15
+ type: "pcb_board",
16
+ pcb_board_id: "board0",
17
+ center: { x: 0, y: 0 },
18
+ width: 14,
19
+ height: 10,
20
+ },
21
+ // Circle with positive margin (mask extends beyond hole)
22
+ {
23
+ type: "pcb_hole",
24
+ pcb_hole_id: "hole_circle_positive",
25
+ hole_shape: "circle",
26
+ x: -4,
27
+ y: 2,
28
+ hole_diameter: 1.0,
29
+ is_covered_with_solder_mask: true,
30
+ soldermask_margin: 0.2,
31
+ },
32
+ // Square with positive margin
33
+ {
34
+ type: "pcb_hole",
35
+ pcb_hole_id: "hole_square_positive",
36
+ hole_shape: "square",
37
+ x: -1,
38
+ y: 2,
39
+ hole_diameter: 1.0,
40
+ is_covered_with_solder_mask: true,
41
+ soldermask_margin: 0.15,
42
+ },
43
+ // Oval with positive margin
44
+ {
45
+ type: "pcb_hole",
46
+ pcb_hole_id: "hole_oval_positive",
47
+ hole_shape: "oval",
48
+ x: 2,
49
+ y: 2,
50
+ hole_width: 1.5,
51
+ hole_height: 0.8,
52
+ is_covered_with_solder_mask: true,
53
+ soldermask_margin: 0.1,
54
+ },
55
+ // Rect with positive margin
56
+ {
57
+ type: "pcb_hole",
58
+ pcb_hole_id: "hole_rect_positive",
59
+ hole_shape: "rect",
60
+ x: 5,
61
+ y: 2,
62
+ hole_width: 1.6,
63
+ hole_height: 1.1,
64
+ is_covered_with_solder_mask: true,
65
+ soldermask_margin: 0.15,
66
+ },
67
+ // Pill with positive margin
68
+ {
69
+ type: "pcb_hole",
70
+ pcb_hole_id: "hole_pill_positive",
71
+ hole_shape: "pill",
72
+ x: -2.5,
73
+ y: 0,
74
+ hole_width: 2.0,
75
+ hole_height: 0.8,
76
+ is_covered_with_solder_mask: true,
77
+ soldermask_margin: 0.1,
78
+ },
79
+ // Silkscreen labels for positive margin holes
80
+ {
81
+ type: "pcb_silkscreen_text",
82
+ pcb_silkscreen_text_id: "text_circle_pos",
83
+ layer: "top",
84
+ anchor_position: { x: -4, y: 3.2 },
85
+ anchor_alignment: "center",
86
+ text: "+0.2mm",
87
+ font_size: 0.4,
88
+ },
89
+ {
90
+ type: "pcb_silkscreen_text",
91
+ pcb_silkscreen_text_id: "text_square_pos",
92
+ layer: "top",
93
+ anchor_position: { x: -1, y: 3.2 },
94
+ anchor_alignment: "center",
95
+ text: "+0.15mm",
96
+ font_size: 0.4,
97
+ },
98
+ {
99
+ type: "pcb_silkscreen_text",
100
+ pcb_silkscreen_text_id: "text_oval_pos",
101
+ layer: "top",
102
+ anchor_position: { x: 2, y: 3.2 },
103
+ anchor_alignment: "center",
104
+ text: "+0.1mm",
105
+ font_size: 0.4,
106
+ },
107
+ {
108
+ type: "pcb_silkscreen_text",
109
+ pcb_silkscreen_text_id: "text_rect_pos",
110
+ layer: "top",
111
+ anchor_position: { x: 5, y: 3.2 },
112
+ anchor_alignment: "center",
113
+ text: "+0.15mm",
114
+ font_size: 0.4,
115
+ },
116
+ {
117
+ type: "pcb_silkscreen_text",
118
+ pcb_silkscreen_text_id: "text_pill_pos",
119
+ layer: "top",
120
+ anchor_position: { x: -2.5, y: 1 },
121
+ anchor_alignment: "center",
122
+ text: "+0.1mm",
123
+ font_size: 0.4,
124
+ },
125
+ ]
126
+
127
+ drawer.setCameraBounds({ minX: -7, maxX: 7, minY: -5, maxY: 5 })
128
+ drawer.drawElements(circuit)
129
+
130
+ await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
131
+ import.meta.path,
132
+ )
133
+ })