circuit-to-canvas 0.0.37 → 0.0.39

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.js CHANGED
@@ -249,17 +249,252 @@ function drawPolygon(params) {
249
249
  ctx.fill();
250
250
  }
251
251
 
252
+ // lib/drawer/elements/soldermask-margin.ts
253
+ import { applyToPoint as applyToPoint6 } from "transformation-matrix";
254
+ function drawSoldermaskRingForRect(ctx, center, width, height, margin, borderRadius, rotation, realToCanvasMat, soldermaskColor, padColor) {
255
+ const [cx, cy] = applyToPoint6(realToCanvasMat, [center.x, center.y]);
256
+ const scaledWidth = width * Math.abs(realToCanvasMat.a);
257
+ const scaledHeight = height * Math.abs(realToCanvasMat.a);
258
+ const scaledMargin = Math.abs(margin) * Math.abs(realToCanvasMat.a);
259
+ const scaledRadius = borderRadius * Math.abs(realToCanvasMat.a);
260
+ ctx.save();
261
+ ctx.translate(cx, cy);
262
+ if (rotation !== 0) {
263
+ ctx.rotate(-rotation * (Math.PI / 180));
264
+ }
265
+ const prevCompositeOp = ctx.globalCompositeOperation;
266
+ if (ctx.globalCompositeOperation !== void 0) {
267
+ ctx.globalCompositeOperation = "source-atop";
268
+ }
269
+ const outerWidth = scaledWidth;
270
+ const outerHeight = scaledHeight;
271
+ const outerRadius = scaledRadius;
272
+ ctx.beginPath();
273
+ if (outerRadius > 0) {
274
+ const x = -outerWidth / 2;
275
+ const y = -outerHeight / 2;
276
+ const r = Math.min(outerRadius, outerWidth / 2, outerHeight / 2);
277
+ ctx.moveTo(x + r, y);
278
+ ctx.lineTo(x + outerWidth - r, y);
279
+ ctx.arcTo(x + outerWidth, y, x + outerWidth, y + r, r);
280
+ ctx.lineTo(x + outerWidth, y + outerHeight - r);
281
+ ctx.arcTo(
282
+ x + outerWidth,
283
+ y + outerHeight,
284
+ x + outerWidth - r,
285
+ y + outerHeight,
286
+ r
287
+ );
288
+ ctx.lineTo(x + r, y + outerHeight);
289
+ ctx.arcTo(x, y + outerHeight, x, y + outerHeight - r, r);
290
+ ctx.lineTo(x, y + r);
291
+ ctx.arcTo(x, y, x + r, y, r);
292
+ } else {
293
+ ctx.rect(-outerWidth / 2, -outerHeight / 2, outerWidth, outerHeight);
294
+ }
295
+ ctx.fillStyle = soldermaskColor;
296
+ ctx.fill();
297
+ if (ctx.globalCompositeOperation !== void 0) {
298
+ ctx.globalCompositeOperation = prevCompositeOp || "source-over";
299
+ }
300
+ const innerWidth = scaledWidth - scaledMargin * 2;
301
+ const innerHeight = scaledHeight - scaledMargin * 2;
302
+ const innerRadius = Math.max(0, scaledRadius - scaledMargin);
303
+ if (innerWidth > 0 && innerHeight > 0) {
304
+ ctx.beginPath();
305
+ if (innerRadius > 0) {
306
+ const x = -innerWidth / 2;
307
+ const y = -innerHeight / 2;
308
+ const r = Math.min(innerRadius, innerWidth / 2, innerHeight / 2);
309
+ ctx.moveTo(x + r, y);
310
+ ctx.lineTo(x + innerWidth - r, y);
311
+ ctx.arcTo(x + innerWidth, y, x + innerWidth, y + r, r);
312
+ ctx.lineTo(x + innerWidth, y + innerHeight - r);
313
+ ctx.arcTo(
314
+ x + innerWidth,
315
+ y + innerHeight,
316
+ x + innerWidth - r,
317
+ y + innerHeight,
318
+ r
319
+ );
320
+ ctx.lineTo(x + r, y + innerHeight);
321
+ ctx.arcTo(x, y + innerHeight, x, y + innerHeight - r, r);
322
+ ctx.lineTo(x, y + r);
323
+ ctx.arcTo(x, y, x + r, y, r);
324
+ } else {
325
+ ctx.rect(-innerWidth / 2, -innerHeight / 2, innerWidth, innerHeight);
326
+ }
327
+ ctx.fillStyle = padColor;
328
+ ctx.fill();
329
+ }
330
+ ctx.restore();
331
+ }
332
+ function drawSoldermaskRingForCircle(ctx, center, radius, margin, realToCanvasMat, soldermaskColor, padColor) {
333
+ const [cx, cy] = applyToPoint6(realToCanvasMat, [center.x, center.y]);
334
+ const scaledRadius = radius * Math.abs(realToCanvasMat.a);
335
+ const scaledMargin = Math.abs(margin) * Math.abs(realToCanvasMat.a);
336
+ ctx.save();
337
+ const prevCompositeOp = ctx.globalCompositeOperation;
338
+ if (ctx.globalCompositeOperation !== void 0) {
339
+ ctx.globalCompositeOperation = "source-atop";
340
+ }
341
+ ctx.beginPath();
342
+ ctx.arc(cx, cy, scaledRadius, 0, Math.PI * 2);
343
+ ctx.fillStyle = soldermaskColor;
344
+ ctx.fill();
345
+ if (ctx.globalCompositeOperation !== void 0) {
346
+ ctx.globalCompositeOperation = prevCompositeOp || "source-over";
347
+ }
348
+ const innerRadius = Math.max(0, scaledRadius - scaledMargin);
349
+ if (innerRadius > 0) {
350
+ ctx.beginPath();
351
+ ctx.arc(cx, cy, innerRadius, 0, Math.PI * 2);
352
+ ctx.fillStyle = padColor;
353
+ ctx.fill();
354
+ }
355
+ ctx.restore();
356
+ }
357
+ function drawSoldermaskRingForPill(ctx, center, width, height, margin, rotation, realToCanvasMat, soldermaskColor, padColor) {
358
+ const [cx, cy] = applyToPoint6(realToCanvasMat, [center.x, center.y]);
359
+ const scaledWidth = width * Math.abs(realToCanvasMat.a);
360
+ const scaledHeight = height * Math.abs(realToCanvasMat.a);
361
+ const scaledMargin = Math.abs(margin) * Math.abs(realToCanvasMat.a);
362
+ ctx.save();
363
+ ctx.translate(cx, cy);
364
+ if (rotation !== 0) {
365
+ ctx.rotate(-rotation * (Math.PI / 180));
366
+ }
367
+ const prevCompositeOp = ctx.globalCompositeOperation;
368
+ if (ctx.globalCompositeOperation !== void 0) {
369
+ ctx.globalCompositeOperation = "source-atop";
370
+ }
371
+ const outerWidth = scaledWidth;
372
+ const outerHeight = scaledHeight;
373
+ ctx.beginPath();
374
+ if (outerWidth > outerHeight) {
375
+ const radius = outerHeight / 2;
376
+ const straightLength = outerWidth - outerHeight;
377
+ ctx.moveTo(-straightLength / 2, -radius);
378
+ ctx.lineTo(straightLength / 2, -radius);
379
+ ctx.arc(straightLength / 2, 0, radius, -Math.PI / 2, Math.PI / 2);
380
+ ctx.lineTo(-straightLength / 2, radius);
381
+ ctx.arc(-straightLength / 2, 0, radius, Math.PI / 2, -Math.PI / 2);
382
+ } else if (outerHeight > outerWidth) {
383
+ const radius = outerWidth / 2;
384
+ const straightLength = outerHeight - outerWidth;
385
+ ctx.moveTo(radius, -straightLength / 2);
386
+ ctx.lineTo(radius, straightLength / 2);
387
+ ctx.arc(0, straightLength / 2, radius, 0, Math.PI);
388
+ ctx.lineTo(-radius, -straightLength / 2);
389
+ ctx.arc(0, -straightLength / 2, radius, Math.PI, 0);
390
+ } else {
391
+ ctx.arc(0, 0, outerWidth / 2, 0, Math.PI * 2);
392
+ }
393
+ ctx.fillStyle = soldermaskColor;
394
+ ctx.fill();
395
+ if (ctx.globalCompositeOperation !== void 0) {
396
+ ctx.globalCompositeOperation = prevCompositeOp || "source-over";
397
+ }
398
+ const innerWidth = scaledWidth - scaledMargin * 2;
399
+ const innerHeight = scaledHeight - scaledMargin * 2;
400
+ if (innerWidth > 0 && innerHeight > 0) {
401
+ ctx.beginPath();
402
+ if (innerWidth > innerHeight) {
403
+ const radius = innerHeight / 2;
404
+ const straightLength = innerWidth - innerHeight;
405
+ ctx.moveTo(-straightLength / 2, -radius);
406
+ ctx.lineTo(straightLength / 2, -radius);
407
+ ctx.arc(straightLength / 2, 0, radius, -Math.PI / 2, Math.PI / 2);
408
+ ctx.lineTo(-straightLength / 2, radius);
409
+ ctx.arc(-straightLength / 2, 0, radius, Math.PI / 2, -Math.PI / 2);
410
+ } else if (innerHeight > innerWidth) {
411
+ const radius = innerWidth / 2;
412
+ const straightLength = innerHeight - innerWidth;
413
+ ctx.moveTo(radius, -straightLength / 2);
414
+ ctx.lineTo(radius, straightLength / 2);
415
+ ctx.arc(0, straightLength / 2, radius, 0, Math.PI);
416
+ ctx.lineTo(-radius, -straightLength / 2);
417
+ ctx.arc(0, -straightLength / 2, radius, Math.PI, 0);
418
+ } else {
419
+ ctx.arc(0, 0, innerWidth / 2, 0, Math.PI * 2);
420
+ }
421
+ ctx.fillStyle = padColor;
422
+ ctx.fill();
423
+ }
424
+ ctx.restore();
425
+ }
426
+ function drawSoldermaskRingForOval(ctx, center, radius_x, radius_y, margin, rotation, realToCanvasMat, soldermaskColor, holeColor) {
427
+ const [cx, cy] = applyToPoint6(realToCanvasMat, [center.x, center.y]);
428
+ const scaledRadiusX = radius_x * Math.abs(realToCanvasMat.a);
429
+ const scaledRadiusY = radius_y * Math.abs(realToCanvasMat.a);
430
+ const scaledMargin = Math.abs(margin) * Math.abs(realToCanvasMat.a);
431
+ ctx.save();
432
+ ctx.translate(cx, cy);
433
+ if (rotation !== 0) {
434
+ ctx.rotate(-rotation * (Math.PI / 180));
435
+ }
436
+ const prevCompositeOp = ctx.globalCompositeOperation;
437
+ if (ctx.globalCompositeOperation !== void 0) {
438
+ ctx.globalCompositeOperation = "source-atop";
439
+ }
440
+ ctx.beginPath();
441
+ ctx.ellipse(0, 0, scaledRadiusX, scaledRadiusY, 0, 0, Math.PI * 2);
442
+ ctx.fillStyle = soldermaskColor;
443
+ ctx.fill();
444
+ if (ctx.globalCompositeOperation !== void 0) {
445
+ ctx.globalCompositeOperation = prevCompositeOp || "source-over";
446
+ }
447
+ const innerRadiusX = Math.max(0, scaledRadiusX - scaledMargin);
448
+ const innerRadiusY = Math.max(0, scaledRadiusY - scaledMargin);
449
+ if (innerRadiusX > 0 && innerRadiusY > 0) {
450
+ ctx.beginPath();
451
+ ctx.ellipse(0, 0, innerRadiusX, innerRadiusY, 0, 0, Math.PI * 2);
452
+ ctx.fillStyle = holeColor;
453
+ ctx.fill();
454
+ }
455
+ ctx.restore();
456
+ }
457
+
252
458
  // lib/drawer/elements/pcb-plated-hole.ts
459
+ function getSoldermaskColor(layers, colorMap) {
460
+ const layer = layers?.includes("top") ? "top" : "bottom";
461
+ return colorMap.soldermaskOverCopper[layer] ?? colorMap.soldermaskOverCopper.top;
462
+ }
253
463
  function drawPcbPlatedHole(params) {
254
464
  const { ctx, hole, realToCanvasMat, colorMap } = params;
465
+ const hasSoldermask = hole.is_covered_with_solder_mask === true && hole.soldermask_margin !== void 0 && hole.soldermask_margin !== 0;
466
+ const margin = hasSoldermask ? hole.soldermask_margin : 0;
467
+ const soldermaskRingColor = getSoldermaskColor(hole.layers, colorMap);
468
+ const positiveMarginColor = colorMap.substrate;
469
+ const copperColor = colorMap.copper.top;
255
470
  if (hole.shape === "circle") {
471
+ if (hasSoldermask && margin > 0) {
472
+ drawCircle({
473
+ ctx,
474
+ center: { x: hole.x, y: hole.y },
475
+ radius: hole.outer_diameter / 2 + margin,
476
+ fill: positiveMarginColor,
477
+ realToCanvasMat
478
+ });
479
+ }
256
480
  drawCircle({
257
481
  ctx,
258
482
  center: { x: hole.x, y: hole.y },
259
483
  radius: hole.outer_diameter / 2,
260
- fill: colorMap.copper.top,
484
+ fill: copperColor,
261
485
  realToCanvasMat
262
486
  });
487
+ if (hasSoldermask && margin < 0) {
488
+ drawSoldermaskRingForCircle(
489
+ ctx,
490
+ { x: hole.x, y: hole.y },
491
+ hole.outer_diameter / 2,
492
+ margin,
493
+ realToCanvasMat,
494
+ soldermaskRingColor,
495
+ copperColor
496
+ );
497
+ }
263
498
  drawCircle({
264
499
  ctx,
265
500
  center: { x: hole.x, y: hole.y },
@@ -270,15 +505,39 @@ function drawPcbPlatedHole(params) {
270
505
  return;
271
506
  }
272
507
  if (hole.shape === "oval") {
508
+ if (hasSoldermask && margin > 0) {
509
+ drawOval({
510
+ ctx,
511
+ center: { x: hole.x, y: hole.y },
512
+ radius_x: hole.outer_width / 2 + margin,
513
+ radius_y: hole.outer_height / 2 + margin,
514
+ fill: positiveMarginColor,
515
+ realToCanvasMat,
516
+ rotation: hole.ccw_rotation
517
+ });
518
+ }
273
519
  drawOval({
274
520
  ctx,
275
521
  center: { x: hole.x, y: hole.y },
276
522
  radius_x: hole.outer_width / 2,
277
523
  radius_y: hole.outer_height / 2,
278
- fill: colorMap.copper.top,
524
+ fill: copperColor,
279
525
  realToCanvasMat,
280
526
  rotation: hole.ccw_rotation
281
527
  });
528
+ if (hasSoldermask && margin < 0) {
529
+ drawSoldermaskRingForOval(
530
+ ctx,
531
+ { x: hole.x, y: hole.y },
532
+ hole.outer_width / 2,
533
+ hole.outer_height / 2,
534
+ margin,
535
+ hole.ccw_rotation ?? 0,
536
+ realToCanvasMat,
537
+ soldermaskRingColor,
538
+ copperColor
539
+ );
540
+ }
282
541
  drawOval({
283
542
  ctx,
284
543
  center: { x: hole.x, y: hole.y },
@@ -291,15 +550,39 @@ function drawPcbPlatedHole(params) {
291
550
  return;
292
551
  }
293
552
  if (hole.shape === "pill") {
553
+ if (hasSoldermask && margin > 0) {
554
+ drawPill({
555
+ ctx,
556
+ center: { x: hole.x, y: hole.y },
557
+ width: hole.outer_width + margin * 2,
558
+ height: hole.outer_height + margin * 2,
559
+ fill: positiveMarginColor,
560
+ realToCanvasMat,
561
+ rotation: hole.ccw_rotation
562
+ });
563
+ }
294
564
  drawPill({
295
565
  ctx,
296
566
  center: { x: hole.x, y: hole.y },
297
567
  width: hole.outer_width,
298
568
  height: hole.outer_height,
299
- fill: colorMap.copper.top,
569
+ fill: copperColor,
300
570
  realToCanvasMat,
301
571
  rotation: hole.ccw_rotation
302
572
  });
573
+ if (hasSoldermask && margin < 0) {
574
+ drawSoldermaskRingForPill(
575
+ ctx,
576
+ { x: hole.x, y: hole.y },
577
+ hole.outer_width,
578
+ hole.outer_height,
579
+ margin,
580
+ hole.ccw_rotation ?? 0,
581
+ realToCanvasMat,
582
+ soldermaskRingColor,
583
+ copperColor
584
+ );
585
+ }
303
586
  drawPill({
304
587
  ctx,
305
588
  center: { x: hole.x, y: hole.y },
@@ -312,15 +595,40 @@ function drawPcbPlatedHole(params) {
312
595
  return;
313
596
  }
314
597
  if (hole.shape === "circular_hole_with_rect_pad") {
598
+ if (hasSoldermask && margin > 0) {
599
+ drawRect({
600
+ ctx,
601
+ center: { x: hole.x, y: hole.y },
602
+ width: hole.rect_pad_width + margin * 2,
603
+ height: hole.rect_pad_height + margin * 2,
604
+ fill: positiveMarginColor,
605
+ realToCanvasMat,
606
+ borderRadius: (hole.rect_border_radius ?? 0) + margin
607
+ });
608
+ }
315
609
  drawRect({
316
610
  ctx,
317
611
  center: { x: hole.x, y: hole.y },
318
612
  width: hole.rect_pad_width,
319
613
  height: hole.rect_pad_height,
320
- fill: colorMap.copper.top,
614
+ fill: copperColor,
321
615
  realToCanvasMat,
322
616
  borderRadius: hole.rect_border_radius ?? 0
323
617
  });
618
+ if (hasSoldermask && margin < 0) {
619
+ drawSoldermaskRingForRect(
620
+ ctx,
621
+ { x: hole.x, y: hole.y },
622
+ hole.rect_pad_width,
623
+ hole.rect_pad_height,
624
+ margin,
625
+ hole.rect_border_radius ?? 0,
626
+ 0,
627
+ realToCanvasMat,
628
+ soldermaskRingColor,
629
+ copperColor
630
+ );
631
+ }
324
632
  const holeX = hole.x + (hole.hole_offset_x ?? 0);
325
633
  const holeY = hole.y + (hole.hole_offset_y ?? 0);
326
634
  drawCircle({
@@ -333,15 +641,40 @@ function drawPcbPlatedHole(params) {
333
641
  return;
334
642
  }
335
643
  if (hole.shape === "pill_hole_with_rect_pad") {
644
+ if (hasSoldermask && margin > 0) {
645
+ drawRect({
646
+ ctx,
647
+ center: { x: hole.x, y: hole.y },
648
+ width: hole.rect_pad_width + margin * 2,
649
+ height: hole.rect_pad_height + margin * 2,
650
+ fill: positiveMarginColor,
651
+ realToCanvasMat,
652
+ borderRadius: (hole.rect_border_radius ?? 0) + margin
653
+ });
654
+ }
336
655
  drawRect({
337
656
  ctx,
338
657
  center: { x: hole.x, y: hole.y },
339
658
  width: hole.rect_pad_width,
340
659
  height: hole.rect_pad_height,
341
- fill: colorMap.copper.top,
660
+ fill: copperColor,
342
661
  realToCanvasMat,
343
662
  borderRadius: hole.rect_border_radius ?? 0
344
663
  });
664
+ if (hasSoldermask && margin < 0) {
665
+ drawSoldermaskRingForRect(
666
+ ctx,
667
+ { x: hole.x, y: hole.y },
668
+ hole.rect_pad_width,
669
+ hole.rect_pad_height,
670
+ margin,
671
+ hole.rect_border_radius ?? 0,
672
+ 0,
673
+ realToCanvasMat,
674
+ soldermaskRingColor,
675
+ copperColor
676
+ );
677
+ }
345
678
  const holeX = hole.x + (hole.hole_offset_x ?? 0);
346
679
  const holeY = hole.y + (hole.hole_offset_y ?? 0);
347
680
  drawPill({
@@ -355,16 +688,42 @@ function drawPcbPlatedHole(params) {
355
688
  return;
356
689
  }
357
690
  if (hole.shape === "rotated_pill_hole_with_rect_pad") {
691
+ if (hasSoldermask && margin > 0) {
692
+ drawRect({
693
+ ctx,
694
+ center: { x: hole.x, y: hole.y },
695
+ width: hole.rect_pad_width + margin * 2,
696
+ height: hole.rect_pad_height + margin * 2,
697
+ fill: positiveMarginColor,
698
+ realToCanvasMat,
699
+ borderRadius: (hole.rect_border_radius ?? 0) + margin,
700
+ rotation: hole.rect_ccw_rotation
701
+ });
702
+ }
358
703
  drawRect({
359
704
  ctx,
360
705
  center: { x: hole.x, y: hole.y },
361
706
  width: hole.rect_pad_width,
362
707
  height: hole.rect_pad_height,
363
- fill: colorMap.copper.top,
708
+ fill: copperColor,
364
709
  realToCanvasMat,
365
710
  borderRadius: hole.rect_border_radius ?? 0,
366
711
  rotation: hole.rect_ccw_rotation
367
712
  });
713
+ if (hasSoldermask && margin < 0) {
714
+ drawSoldermaskRingForRect(
715
+ ctx,
716
+ { x: hole.x, y: hole.y },
717
+ hole.rect_pad_width,
718
+ hole.rect_pad_height,
719
+ margin,
720
+ hole.rect_border_radius ?? 0,
721
+ hole.rect_ccw_rotation ?? 0,
722
+ realToCanvasMat,
723
+ soldermaskRingColor,
724
+ copperColor
725
+ );
726
+ }
368
727
  const holeX = hole.x + (hole.hole_offset_x ?? 0);
369
728
  const holeY = hole.y + (hole.hole_offset_y ?? 0);
370
729
  drawPill({
@@ -388,7 +747,7 @@ function drawPcbPlatedHole(params) {
388
747
  drawPolygon({
389
748
  ctx,
390
749
  points: padPoints,
391
- fill: colorMap.copper.top,
750
+ fill: copperColor,
392
751
  realToCanvasMat
393
752
  });
394
753
  }
@@ -589,217 +948,11 @@ function drawPcbHole(params) {
589
948
  }
590
949
  }
591
950
 
592
- // lib/drawer/elements/soldermask-margin.ts
593
- import { applyToPoint as applyToPoint6 } from "transformation-matrix";
594
- function drawSoldermaskRingForRect(ctx, center, width, height, margin, borderRadius, rotation, realToCanvasMat, soldermaskColor, padColor) {
595
- const [cx, cy] = applyToPoint6(realToCanvasMat, [center.x, center.y]);
596
- const scaledWidth = width * Math.abs(realToCanvasMat.a);
597
- const scaledHeight = height * Math.abs(realToCanvasMat.a);
598
- const scaledMargin = Math.abs(margin) * Math.abs(realToCanvasMat.a);
599
- const scaledRadius = borderRadius * Math.abs(realToCanvasMat.a);
600
- ctx.save();
601
- ctx.translate(cx, cy);
602
- if (rotation !== 0) {
603
- ctx.rotate(-rotation * (Math.PI / 180));
604
- }
605
- const prevCompositeOp = ctx.globalCompositeOperation;
606
- if (ctx.globalCompositeOperation !== void 0) {
607
- ctx.globalCompositeOperation = "source-atop";
608
- }
609
- const outerWidth = scaledWidth;
610
- const outerHeight = scaledHeight;
611
- const outerRadius = scaledRadius;
612
- ctx.beginPath();
613
- if (outerRadius > 0) {
614
- const x = -outerWidth / 2;
615
- const y = -outerHeight / 2;
616
- const r = Math.min(outerRadius, outerWidth / 2, outerHeight / 2);
617
- ctx.moveTo(x + r, y);
618
- ctx.lineTo(x + outerWidth - r, y);
619
- ctx.arcTo(x + outerWidth, y, x + outerWidth, y + r, r);
620
- ctx.lineTo(x + outerWidth, y + outerHeight - r);
621
- ctx.arcTo(
622
- x + outerWidth,
623
- y + outerHeight,
624
- x + outerWidth - r,
625
- y + outerHeight,
626
- r
627
- );
628
- ctx.lineTo(x + r, y + outerHeight);
629
- ctx.arcTo(x, y + outerHeight, x, y + outerHeight - r, r);
630
- ctx.lineTo(x, y + r);
631
- ctx.arcTo(x, y, x + r, y, r);
632
- } else {
633
- ctx.rect(-outerWidth / 2, -outerHeight / 2, outerWidth, outerHeight);
634
- }
635
- ctx.fillStyle = soldermaskColor;
636
- ctx.fill();
637
- if (ctx.globalCompositeOperation !== void 0) {
638
- ctx.globalCompositeOperation = prevCompositeOp || "source-over";
639
- }
640
- const innerWidth = scaledWidth - scaledMargin * 2;
641
- const innerHeight = scaledHeight - scaledMargin * 2;
642
- const innerRadius = Math.max(0, scaledRadius - scaledMargin);
643
- if (innerWidth > 0 && innerHeight > 0) {
644
- ctx.beginPath();
645
- if (innerRadius > 0) {
646
- const x = -innerWidth / 2;
647
- const y = -innerHeight / 2;
648
- const r = Math.min(innerRadius, innerWidth / 2, innerHeight / 2);
649
- ctx.moveTo(x + r, y);
650
- ctx.lineTo(x + innerWidth - r, y);
651
- ctx.arcTo(x + innerWidth, y, x + innerWidth, y + r, r);
652
- ctx.lineTo(x + innerWidth, y + innerHeight - r);
653
- ctx.arcTo(
654
- x + innerWidth,
655
- y + innerHeight,
656
- x + innerWidth - r,
657
- y + innerHeight,
658
- r
659
- );
660
- ctx.lineTo(x + r, y + innerHeight);
661
- ctx.arcTo(x, y + innerHeight, x, y + innerHeight - r, r);
662
- ctx.lineTo(x, y + r);
663
- ctx.arcTo(x, y, x + r, y, r);
664
- } else {
665
- ctx.rect(-innerWidth / 2, -innerHeight / 2, innerWidth, innerHeight);
666
- }
667
- ctx.fillStyle = padColor;
668
- ctx.fill();
669
- }
670
- ctx.restore();
671
- }
672
- function drawSoldermaskRingForCircle(ctx, center, radius, margin, realToCanvasMat, soldermaskColor, padColor) {
673
- const [cx, cy] = applyToPoint6(realToCanvasMat, [center.x, center.y]);
674
- const scaledRadius = radius * Math.abs(realToCanvasMat.a);
675
- const scaledMargin = Math.abs(margin) * Math.abs(realToCanvasMat.a);
676
- ctx.save();
677
- const prevCompositeOp = ctx.globalCompositeOperation;
678
- if (ctx.globalCompositeOperation !== void 0) {
679
- ctx.globalCompositeOperation = "source-atop";
680
- }
681
- ctx.beginPath();
682
- ctx.arc(cx, cy, scaledRadius, 0, Math.PI * 2);
683
- ctx.fillStyle = soldermaskColor;
684
- ctx.fill();
685
- if (ctx.globalCompositeOperation !== void 0) {
686
- ctx.globalCompositeOperation = prevCompositeOp || "source-over";
687
- }
688
- const innerRadius = Math.max(0, scaledRadius - scaledMargin);
689
- if (innerRadius > 0) {
690
- ctx.beginPath();
691
- ctx.arc(cx, cy, innerRadius, 0, Math.PI * 2);
692
- ctx.fillStyle = padColor;
693
- ctx.fill();
694
- }
695
- ctx.restore();
696
- }
697
- function drawSoldermaskRingForPill(ctx, center, width, height, margin, rotation, realToCanvasMat, soldermaskColor, padColor) {
698
- const [cx, cy] = applyToPoint6(realToCanvasMat, [center.x, center.y]);
699
- const scaledWidth = width * Math.abs(realToCanvasMat.a);
700
- const scaledHeight = height * Math.abs(realToCanvasMat.a);
701
- const scaledMargin = Math.abs(margin) * Math.abs(realToCanvasMat.a);
702
- ctx.save();
703
- ctx.translate(cx, cy);
704
- if (rotation !== 0) {
705
- ctx.rotate(-rotation * (Math.PI / 180));
706
- }
707
- const prevCompositeOp = ctx.globalCompositeOperation;
708
- if (ctx.globalCompositeOperation !== void 0) {
709
- ctx.globalCompositeOperation = "source-atop";
710
- }
711
- const outerWidth = scaledWidth;
712
- const outerHeight = scaledHeight;
713
- ctx.beginPath();
714
- if (outerWidth > outerHeight) {
715
- const radius = outerHeight / 2;
716
- const straightLength = outerWidth - outerHeight;
717
- ctx.moveTo(-straightLength / 2, -radius);
718
- ctx.lineTo(straightLength / 2, -radius);
719
- ctx.arc(straightLength / 2, 0, radius, -Math.PI / 2, Math.PI / 2);
720
- ctx.lineTo(-straightLength / 2, radius);
721
- ctx.arc(-straightLength / 2, 0, radius, Math.PI / 2, -Math.PI / 2);
722
- } else if (outerHeight > outerWidth) {
723
- const radius = outerWidth / 2;
724
- const straightLength = outerHeight - outerWidth;
725
- ctx.moveTo(radius, -straightLength / 2);
726
- ctx.lineTo(radius, straightLength / 2);
727
- ctx.arc(0, straightLength / 2, radius, 0, Math.PI);
728
- ctx.lineTo(-radius, -straightLength / 2);
729
- ctx.arc(0, -straightLength / 2, radius, Math.PI, 0);
730
- } else {
731
- ctx.arc(0, 0, outerWidth / 2, 0, Math.PI * 2);
732
- }
733
- ctx.fillStyle = soldermaskColor;
734
- ctx.fill();
735
- if (ctx.globalCompositeOperation !== void 0) {
736
- ctx.globalCompositeOperation = prevCompositeOp || "source-over";
737
- }
738
- const innerWidth = scaledWidth - scaledMargin * 2;
739
- const innerHeight = scaledHeight - scaledMargin * 2;
740
- if (innerWidth > 0 && innerHeight > 0) {
741
- ctx.beginPath();
742
- if (innerWidth > innerHeight) {
743
- const radius = innerHeight / 2;
744
- const straightLength = innerWidth - innerHeight;
745
- ctx.moveTo(-straightLength / 2, -radius);
746
- ctx.lineTo(straightLength / 2, -radius);
747
- ctx.arc(straightLength / 2, 0, radius, -Math.PI / 2, Math.PI / 2);
748
- ctx.lineTo(-straightLength / 2, radius);
749
- ctx.arc(-straightLength / 2, 0, radius, Math.PI / 2, -Math.PI / 2);
750
- } else if (innerHeight > innerWidth) {
751
- const radius = innerWidth / 2;
752
- const straightLength = innerHeight - innerWidth;
753
- ctx.moveTo(radius, -straightLength / 2);
754
- ctx.lineTo(radius, straightLength / 2);
755
- ctx.arc(0, straightLength / 2, radius, 0, Math.PI);
756
- ctx.lineTo(-radius, -straightLength / 2);
757
- ctx.arc(0, -straightLength / 2, radius, Math.PI, 0);
758
- } else {
759
- ctx.arc(0, 0, innerWidth / 2, 0, Math.PI * 2);
760
- }
761
- ctx.fillStyle = padColor;
762
- ctx.fill();
763
- }
764
- ctx.restore();
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
- }
797
-
798
951
  // lib/drawer/elements/pcb-smtpad.ts
799
952
  function layerToColor(layer, colorMap) {
800
953
  return colorMap.copper[layer] ?? colorMap.copper.top;
801
954
  }
802
- function getSoldermaskColor(layer, colorMap) {
955
+ function getSoldermaskColor2(layer, colorMap) {
803
956
  return colorMap.soldermaskOverCopper[layer] ?? colorMap.soldermaskOverCopper.top;
804
957
  }
805
958
  function drawPcbSmtPad(params) {
@@ -807,7 +960,7 @@ function drawPcbSmtPad(params) {
807
960
  const color = layerToColor(pad.layer, colorMap);
808
961
  const hasSoldermask = pad.is_covered_with_solder_mask === true && pad.soldermask_margin !== void 0 && pad.soldermask_margin !== 0;
809
962
  const margin = hasSoldermask ? pad.soldermask_margin : 0;
810
- const soldermaskRingColor = getSoldermaskColor(pad.layer, colorMap);
963
+ const soldermaskRingColor = getSoldermaskColor2(pad.layer, colorMap);
811
964
  const positiveMarginColor = colorMap.substrate;
812
965
  if (pad.shape === "rect") {
813
966
  if (hasSoldermask && margin > 0) {
@@ -2037,8 +2190,11 @@ var CircuitToCanvasDrawer = class {
2037
2190
  const hasSoldermaskHoles = elements.some(
2038
2191
  (el) => el.type === "pcb_hole" && el.is_covered_with_solder_mask === true
2039
2192
  );
2193
+ const hasSoldermaskPlatedHoles = elements.some(
2194
+ (el) => el.type === "pcb_plated_hole" && el.is_covered_with_solder_mask === true
2195
+ );
2040
2196
  for (const element of elements) {
2041
- if (element.type === "pcb_board" && (hasSoldermaskPads || hasSoldermaskHoles)) {
2197
+ if (element.type === "pcb_board" && (hasSoldermaskPads || hasSoldermaskHoles || hasSoldermaskPlatedHoles)) {
2042
2198
  this.drawBoardWithSoldermask(element);
2043
2199
  } else {
2044
2200
  this.drawElement(element, options);
@@ -172,11 +172,17 @@ export class CircuitToCanvasDrawer {
172
172
  (el as PcbHole & { is_covered_with_solder_mask?: boolean })
173
173
  .is_covered_with_solder_mask === true,
174
174
  )
175
+ const hasSoldermaskPlatedHoles = elements.some(
176
+ (el) =>
177
+ el.type === "pcb_plated_hole" &&
178
+ (el as PcbPlatedHole & { is_covered_with_solder_mask?: boolean })
179
+ .is_covered_with_solder_mask === true,
180
+ )
175
181
 
176
182
  for (const element of elements) {
177
183
  if (
178
184
  element.type === "pcb_board" &&
179
- (hasSoldermaskPads || hasSoldermaskHoles)
185
+ (hasSoldermaskPads || hasSoldermaskHoles || hasSoldermaskPlatedHoles)
180
186
  ) {
181
187
  // Draw board with soldermask fill when pads or holes have soldermask
182
188
  this.drawBoardWithSoldermask(element as PcbBoard)
@@ -6,6 +6,12 @@ import { drawRect } from "../shapes/rect"
6
6
  import { drawOval } from "../shapes/oval"
7
7
  import { drawPill } from "../shapes/pill"
8
8
  import { drawPolygon } from "../shapes/polygon"
9
+ import {
10
+ drawSoldermaskRingForCircle,
11
+ drawSoldermaskRingForOval,
12
+ drawSoldermaskRingForPill,
13
+ drawSoldermaskRingForRect,
14
+ } from "./soldermask-margin"
9
15
 
10
16
  export interface DrawPcbPlatedHoleParams {
11
17
  ctx: CanvasContext
@@ -14,19 +20,64 @@ export interface DrawPcbPlatedHoleParams {
14
20
  colorMap: PcbColorMap
15
21
  }
16
22
 
23
+ function getSoldermaskColor(
24
+ layers: string[] | undefined,
25
+ colorMap: PcbColorMap,
26
+ ): string {
27
+ const layer = layers?.includes("top") ? "top" : "bottom"
28
+ return (
29
+ colorMap.soldermaskOverCopper[
30
+ layer as keyof typeof colorMap.soldermaskOverCopper
31
+ ] ?? colorMap.soldermaskOverCopper.top
32
+ )
33
+ }
34
+
17
35
  export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
18
36
  const { ctx, hole, realToCanvasMat, colorMap } = params
19
37
 
38
+ const hasSoldermask =
39
+ hole.is_covered_with_solder_mask === true &&
40
+ hole.soldermask_margin !== undefined &&
41
+ hole.soldermask_margin !== 0
42
+ const margin = hasSoldermask ? hole.soldermask_margin! : 0
43
+ const soldermaskRingColor = getSoldermaskColor(hole.layers, colorMap)
44
+ const positiveMarginColor = colorMap.substrate
45
+ const copperColor = colorMap.copper.top
46
+
20
47
  if (hole.shape === "circle") {
48
+ // For positive margins, draw extended mask area first
49
+ if (hasSoldermask && margin > 0) {
50
+ drawCircle({
51
+ ctx,
52
+ center: { x: hole.x, y: hole.y },
53
+ radius: hole.outer_diameter / 2 + margin,
54
+ fill: positiveMarginColor,
55
+ realToCanvasMat,
56
+ })
57
+ }
58
+
21
59
  // Draw outer copper ring
22
60
  drawCircle({
23
61
  ctx,
24
62
  center: { x: hole.x, y: hole.y },
25
63
  radius: hole.outer_diameter / 2,
26
- fill: colorMap.copper.top,
64
+ fill: copperColor,
27
65
  realToCanvasMat,
28
66
  })
29
67
 
68
+ // For negative margins, draw soldermask ring on top of the copper ring
69
+ if (hasSoldermask && margin < 0) {
70
+ drawSoldermaskRingForCircle(
71
+ ctx,
72
+ { x: hole.x, y: hole.y },
73
+ hole.outer_diameter / 2,
74
+ margin,
75
+ realToCanvasMat,
76
+ soldermaskRingColor,
77
+ copperColor,
78
+ )
79
+ }
80
+
30
81
  // Draw inner drill hole
31
82
  drawCircle({
32
83
  ctx,
@@ -39,17 +90,45 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
39
90
  }
40
91
 
41
92
  if (hole.shape === "oval") {
93
+ // For positive margins, draw extended mask area first
94
+ if (hasSoldermask && margin > 0) {
95
+ drawOval({
96
+ ctx,
97
+ center: { x: hole.x, y: hole.y },
98
+ radius_x: hole.outer_width / 2 + margin,
99
+ radius_y: hole.outer_height / 2 + margin,
100
+ fill: positiveMarginColor,
101
+ realToCanvasMat,
102
+ rotation: hole.ccw_rotation,
103
+ })
104
+ }
105
+
42
106
  // Draw outer copper oval
43
107
  drawOval({
44
108
  ctx,
45
109
  center: { x: hole.x, y: hole.y },
46
110
  radius_x: hole.outer_width / 2,
47
111
  radius_y: hole.outer_height / 2,
48
- fill: colorMap.copper.top,
112
+ fill: copperColor,
49
113
  realToCanvasMat,
50
114
  rotation: hole.ccw_rotation,
51
115
  })
52
116
 
117
+ // For negative margins, draw soldermask ring on top of the copper oval
118
+ if (hasSoldermask && margin < 0) {
119
+ drawSoldermaskRingForOval(
120
+ ctx,
121
+ { x: hole.x, y: hole.y },
122
+ hole.outer_width / 2,
123
+ hole.outer_height / 2,
124
+ margin,
125
+ hole.ccw_rotation ?? 0,
126
+ realToCanvasMat,
127
+ soldermaskRingColor,
128
+ copperColor,
129
+ )
130
+ }
131
+
53
132
  // Draw inner drill hole
54
133
  drawOval({
55
134
  ctx,
@@ -64,17 +143,45 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
64
143
  }
65
144
 
66
145
  if (hole.shape === "pill") {
146
+ // For positive margins, draw extended mask area first
147
+ if (hasSoldermask && margin > 0) {
148
+ drawPill({
149
+ ctx,
150
+ center: { x: hole.x, y: hole.y },
151
+ width: hole.outer_width + margin * 2,
152
+ height: hole.outer_height + margin * 2,
153
+ fill: positiveMarginColor,
154
+ realToCanvasMat,
155
+ rotation: hole.ccw_rotation,
156
+ })
157
+ }
158
+
67
159
  // Draw outer copper pill
68
160
  drawPill({
69
161
  ctx,
70
162
  center: { x: hole.x, y: hole.y },
71
163
  width: hole.outer_width,
72
164
  height: hole.outer_height,
73
- fill: colorMap.copper.top,
165
+ fill: copperColor,
74
166
  realToCanvasMat,
75
167
  rotation: hole.ccw_rotation,
76
168
  })
77
169
 
170
+ // For negative margins, draw soldermask ring on top of the copper pill
171
+ if (hasSoldermask && margin < 0) {
172
+ drawSoldermaskRingForPill(
173
+ ctx,
174
+ { x: hole.x, y: hole.y },
175
+ hole.outer_width,
176
+ hole.outer_height,
177
+ margin,
178
+ hole.ccw_rotation ?? 0,
179
+ realToCanvasMat,
180
+ soldermaskRingColor,
181
+ copperColor,
182
+ )
183
+ }
184
+
78
185
  // Draw inner drill hole
79
186
  drawPill({
80
187
  ctx,
@@ -89,17 +196,46 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
89
196
  }
90
197
 
91
198
  if (hole.shape === "circular_hole_with_rect_pad") {
199
+ // For positive margins, draw extended mask area first
200
+ if (hasSoldermask && margin > 0) {
201
+ drawRect({
202
+ ctx,
203
+ center: { x: hole.x, y: hole.y },
204
+ width: hole.rect_pad_width + margin * 2,
205
+ height: hole.rect_pad_height + margin * 2,
206
+ fill: positiveMarginColor,
207
+ realToCanvasMat,
208
+ borderRadius: (hole.rect_border_radius ?? 0) + margin,
209
+ })
210
+ }
211
+
92
212
  // Draw rectangular pad
93
213
  drawRect({
94
214
  ctx,
95
215
  center: { x: hole.x, y: hole.y },
96
216
  width: hole.rect_pad_width,
97
217
  height: hole.rect_pad_height,
98
- fill: colorMap.copper.top,
218
+ fill: copperColor,
99
219
  realToCanvasMat,
100
220
  borderRadius: hole.rect_border_radius ?? 0,
101
221
  })
102
222
 
223
+ // For negative margins, draw soldermask ring on top of the pad
224
+ if (hasSoldermask && margin < 0) {
225
+ drawSoldermaskRingForRect(
226
+ ctx,
227
+ { x: hole.x, y: hole.y },
228
+ hole.rect_pad_width,
229
+ hole.rect_pad_height,
230
+ margin,
231
+ hole.rect_border_radius ?? 0,
232
+ 0,
233
+ realToCanvasMat,
234
+ soldermaskRingColor,
235
+ copperColor,
236
+ )
237
+ }
238
+
103
239
  // Draw circular drill hole (with offset)
104
240
  const holeX = hole.x + (hole.hole_offset_x ?? 0)
105
241
  const holeY = hole.y + (hole.hole_offset_y ?? 0)
@@ -114,17 +250,46 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
114
250
  }
115
251
 
116
252
  if (hole.shape === "pill_hole_with_rect_pad") {
253
+ // For positive margins, draw extended mask area first
254
+ if (hasSoldermask && margin > 0) {
255
+ drawRect({
256
+ ctx,
257
+ center: { x: hole.x, y: hole.y },
258
+ width: hole.rect_pad_width + margin * 2,
259
+ height: hole.rect_pad_height + margin * 2,
260
+ fill: positiveMarginColor,
261
+ realToCanvasMat,
262
+ borderRadius: (hole.rect_border_radius ?? 0) + margin,
263
+ })
264
+ }
265
+
117
266
  // Draw rectangular pad
118
267
  drawRect({
119
268
  ctx,
120
269
  center: { x: hole.x, y: hole.y },
121
270
  width: hole.rect_pad_width,
122
271
  height: hole.rect_pad_height,
123
- fill: colorMap.copper.top,
272
+ fill: copperColor,
124
273
  realToCanvasMat,
125
274
  borderRadius: hole.rect_border_radius ?? 0,
126
275
  })
127
276
 
277
+ // For negative margins, draw soldermask ring on top of the pad
278
+ if (hasSoldermask && margin < 0) {
279
+ drawSoldermaskRingForRect(
280
+ ctx,
281
+ { x: hole.x, y: hole.y },
282
+ hole.rect_pad_width,
283
+ hole.rect_pad_height,
284
+ margin,
285
+ hole.rect_border_radius ?? 0,
286
+ 0,
287
+ realToCanvasMat,
288
+ soldermaskRingColor,
289
+ copperColor,
290
+ )
291
+ }
292
+
128
293
  // Draw pill drill hole (with offset)
129
294
  const holeX = hole.x + (hole.hole_offset_x ?? 0)
130
295
  const holeY = hole.y + (hole.hole_offset_y ?? 0)
@@ -140,18 +305,48 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
140
305
  }
141
306
 
142
307
  if (hole.shape === "rotated_pill_hole_with_rect_pad") {
308
+ // For positive margins, draw extended mask area first
309
+ if (hasSoldermask && margin > 0) {
310
+ drawRect({
311
+ ctx,
312
+ center: { x: hole.x, y: hole.y },
313
+ width: hole.rect_pad_width + margin * 2,
314
+ height: hole.rect_pad_height + margin * 2,
315
+ fill: positiveMarginColor,
316
+ realToCanvasMat,
317
+ borderRadius: (hole.rect_border_radius ?? 0) + margin,
318
+ rotation: hole.rect_ccw_rotation,
319
+ })
320
+ }
321
+
143
322
  // Draw rotated rectangular pad
144
323
  drawRect({
145
324
  ctx,
146
325
  center: { x: hole.x, y: hole.y },
147
326
  width: hole.rect_pad_width,
148
327
  height: hole.rect_pad_height,
149
- fill: colorMap.copper.top,
328
+ fill: copperColor,
150
329
  realToCanvasMat,
151
330
  borderRadius: hole.rect_border_radius ?? 0,
152
331
  rotation: hole.rect_ccw_rotation,
153
332
  })
154
333
 
334
+ // For negative margins, draw soldermask ring on top of the pad
335
+ if (hasSoldermask && margin < 0) {
336
+ drawSoldermaskRingForRect(
337
+ ctx,
338
+ { x: hole.x, y: hole.y },
339
+ hole.rect_pad_width,
340
+ hole.rect_pad_height,
341
+ margin,
342
+ hole.rect_border_radius ?? 0,
343
+ hole.rect_ccw_rotation ?? 0,
344
+ realToCanvasMat,
345
+ soldermaskRingColor,
346
+ copperColor,
347
+ )
348
+ }
349
+
155
350
  // Draw rotated pill drill hole (with offset)
156
351
  const holeX = hole.x + (hole.hole_offset_x ?? 0)
157
352
  const holeY = hole.y + (hole.hole_offset_y ?? 0)
@@ -168,6 +363,7 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
168
363
  }
169
364
 
170
365
  if (hole.shape === "hole_with_polygon_pad") {
366
+ // Note: Polygon pads don't support soldermask margins (similar to SMT polygon pads)
171
367
  // Draw polygon pad
172
368
  const padOutline = hole.pad_outline
173
369
  if (padOutline && padOutline.length >= 3) {
@@ -179,7 +375,7 @@ export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
179
375
  drawPolygon({
180
376
  ctx,
181
377
  points: padPoints,
182
- fill: colorMap.copper.top,
378
+ fill: copperColor,
183
379
  realToCanvasMat,
184
380
  })
185
381
  }
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.37",
4
+ "version": "0.0.39",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "build": "tsup-node ./lib/index.ts --format esm --dts",
@@ -0,0 +1,225 @@
1
+ import { expect, test } from "bun:test"
2
+ import { createCanvas } from "@napi-rs/canvas"
3
+ import type { PcbPlatedHole } from "circuit-json"
4
+ import { CircuitToCanvasDrawer } from "../../lib/drawer"
5
+
6
+ test("draw plated holes with positive and negative 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: any = [
15
+ {
16
+ type: "pcb_board",
17
+ pcb_board_id: "board0",
18
+ center: { x: 0, y: 0 },
19
+ width: 16,
20
+ height: 14,
21
+ },
22
+ // Circle with positive margin (mask extends beyond pad)
23
+ {
24
+ type: "pcb_plated_hole",
25
+ pcb_plated_hole_id: "hole_circle_positive",
26
+ shape: "circle",
27
+ x: -5,
28
+ y: 3.5,
29
+ outer_diameter: 2,
30
+ hole_diameter: 1,
31
+ layers: ["top", "bottom"],
32
+ is_covered_with_solder_mask: true,
33
+ soldermask_margin: 0.2,
34
+ },
35
+ // Circle with negative margin (spacing around copper, copper visible)
36
+ {
37
+ type: "pcb_plated_hole",
38
+ pcb_plated_hole_id: "hole_circle_negative",
39
+ shape: "circle",
40
+ x: -5,
41
+ y: -3.5,
42
+ outer_diameter: 2,
43
+ hole_diameter: 1,
44
+ layers: ["top", "bottom"],
45
+ is_covered_with_solder_mask: true,
46
+ soldermask_margin: -0.15,
47
+ },
48
+ // Oval with positive margin
49
+ {
50
+ type: "pcb_plated_hole",
51
+ pcb_plated_hole_id: "hole_oval_positive",
52
+ shape: "oval",
53
+ x: 0,
54
+ y: 3.5,
55
+ outer_width: 2.4,
56
+ outer_height: 1.6,
57
+ hole_width: 1.6,
58
+ hole_height: 0.8,
59
+ layers: ["top", "bottom"],
60
+ ccw_rotation: 0,
61
+ is_covered_with_solder_mask: true,
62
+ soldermask_margin: 0.15,
63
+ },
64
+ // Oval with negative margin
65
+ {
66
+ type: "pcb_plated_hole",
67
+ pcb_plated_hole_id: "hole_oval_negative",
68
+ shape: "oval",
69
+ x: 0,
70
+ y: -3.5,
71
+ outer_width: 2.4,
72
+ outer_height: 1.6,
73
+ hole_width: 1.6,
74
+ hole_height: 0.8,
75
+ layers: ["top", "bottom"],
76
+ ccw_rotation: 0,
77
+ is_covered_with_solder_mask: true,
78
+ soldermask_margin: -0.2,
79
+ },
80
+ // Pill with positive margin
81
+ {
82
+ type: "pcb_plated_hole",
83
+ pcb_plated_hole_id: "hole_pill_positive",
84
+ shape: "pill",
85
+ x: 5,
86
+ y: 3.5,
87
+ outer_width: 3,
88
+ outer_height: 1.5,
89
+ hole_width: 2,
90
+ hole_height: 1,
91
+ layers: ["top", "bottom"],
92
+ ccw_rotation: 0,
93
+ is_covered_with_solder_mask: true,
94
+ soldermask_margin: 0.1,
95
+ },
96
+ // Pill with negative margin
97
+ {
98
+ type: "pcb_plated_hole",
99
+ pcb_plated_hole_id: "hole_pill_negative",
100
+ shape: "pill",
101
+ x: 5,
102
+ y: -3.5,
103
+ outer_width: 3,
104
+ outer_height: 1.5,
105
+ hole_width: 2,
106
+ hole_height: 1,
107
+ layers: ["top", "bottom"],
108
+ ccw_rotation: 0,
109
+ is_covered_with_solder_mask: true,
110
+ soldermask_margin: -0.12,
111
+ },
112
+ // Rectangular pad with circular hole - positive margin
113
+ {
114
+ type: "pcb_plated_hole",
115
+ pcb_plated_hole_id: "hole_rect_circle_positive",
116
+ shape: "circular_hole_with_rect_pad",
117
+ x: -2.5,
118
+ y: 0,
119
+ rect_pad_width: 2.4,
120
+ rect_pad_height: 1.6,
121
+ rect_border_radius: 0.2,
122
+ hole_diameter: 1,
123
+ layers: ["top", "bottom"],
124
+ is_covered_with_solder_mask: true,
125
+ soldermask_margin: 0.15,
126
+ },
127
+ // Rectangular pad with circular hole - negative margin
128
+ {
129
+ type: "pcb_plated_hole",
130
+ pcb_plated_hole_id: "hole_rect_circle_negative",
131
+ shape: "circular_hole_with_rect_pad",
132
+ x: 2.5,
133
+ y: 0,
134
+ rect_pad_width: 2.4,
135
+ rect_pad_height: 1.6,
136
+ rect_border_radius: 0.2,
137
+ hole_diameter: 1,
138
+ layers: ["top", "bottom"],
139
+ is_covered_with_solder_mask: true,
140
+ soldermask_margin: -0.1,
141
+ },
142
+ // Silkscreen labels for positive margin holes (top row)
143
+ {
144
+ type: "pcb_silkscreen_text",
145
+ pcb_silkscreen_text_id: "text_circle_pos",
146
+ layer: "top",
147
+ anchor_position: { x: -5, y: 5.2 },
148
+ anchor_alignment: "center",
149
+ text: "+0.2mm",
150
+ font_size: 0.4,
151
+ },
152
+ {
153
+ type: "pcb_silkscreen_text",
154
+ pcb_silkscreen_text_id: "text_oval_pos",
155
+ layer: "top",
156
+ anchor_position: { x: 0, y: 5.2 },
157
+ anchor_alignment: "center",
158
+ text: "+0.15mm",
159
+ font_size: 0.4,
160
+ },
161
+ {
162
+ type: "pcb_silkscreen_text",
163
+ pcb_silkscreen_text_id: "text_pill_pos",
164
+ layer: "top",
165
+ anchor_position: { x: 5, y: 5.2 },
166
+ anchor_alignment: "center",
167
+ text: "+0.1mm",
168
+ font_size: 0.4,
169
+ },
170
+ // Silkscreen labels for middle row (rectangular pads)
171
+ {
172
+ type: "pcb_silkscreen_text",
173
+ pcb_silkscreen_text_id: "text_rect_pos",
174
+ layer: "top",
175
+ anchor_position: { x: -2.5, y: 1.8 },
176
+ anchor_alignment: "center",
177
+ text: "+0.15mm",
178
+ font_size: 0.4,
179
+ },
180
+ {
181
+ type: "pcb_silkscreen_text",
182
+ pcb_silkscreen_text_id: "text_rect_neg",
183
+ layer: "top",
184
+ anchor_position: { x: 2.5, y: -1.8 },
185
+ anchor_alignment: "center",
186
+ text: "-0.1mm",
187
+ font_size: 0.4,
188
+ },
189
+ // Silkscreen labels for negative margin holes (bottom row)
190
+ {
191
+ type: "pcb_silkscreen_text",
192
+ pcb_silkscreen_text_id: "text_circle_neg",
193
+ layer: "top",
194
+ anchor_position: { x: -5, y: -5.2 },
195
+ anchor_alignment: "center",
196
+ text: "-0.15mm",
197
+ font_size: 0.4,
198
+ },
199
+ {
200
+ type: "pcb_silkscreen_text",
201
+ pcb_silkscreen_text_id: "text_oval_neg",
202
+ layer: "top",
203
+ anchor_position: { x: 0, y: -5.2 },
204
+ anchor_alignment: "center",
205
+ text: "-0.2mm",
206
+ font_size: 0.4,
207
+ },
208
+ {
209
+ type: "pcb_silkscreen_text",
210
+ pcb_silkscreen_text_id: "text_pill_neg",
211
+ layer: "top",
212
+ anchor_position: { x: 5, y: -5.2 },
213
+ anchor_alignment: "center",
214
+ text: "-0.12mm",
215
+ font_size: 0.4,
216
+ },
217
+ ]
218
+
219
+ drawer.setCameraBounds({ minX: -8, maxX: 8, minY: -6.5, maxY: 6.5 })
220
+ drawer.drawElements(circuit)
221
+
222
+ await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
223
+ import.meta.path,
224
+ )
225
+ })