circuit-to-canvas 0.0.34 → 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) {
@@ -1620,27 +1715,6 @@ function drawPcbNoteText(params) {
1620
1715
 
1621
1716
  // lib/drawer/shapes/dimension-line.ts
1622
1717
  import { applyToPoint as applyToPoint13 } from "transformation-matrix";
1623
-
1624
- // lib/drawer/shapes/arrow.ts
1625
- function drawArrow(params) {
1626
- const { ctx, x, y, angle, arrowSize, color, strokeWidth } = params;
1627
- ctx.save();
1628
- ctx.translate(x, y);
1629
- ctx.rotate(angle);
1630
- ctx.beginPath();
1631
- ctx.moveTo(0, 0);
1632
- ctx.lineTo(-arrowSize, -arrowSize / 2);
1633
- ctx.moveTo(0, 0);
1634
- ctx.lineTo(-arrowSize, arrowSize / 2);
1635
- ctx.lineWidth = strokeWidth;
1636
- ctx.strokeStyle = color;
1637
- ctx.lineCap = "round";
1638
- ctx.lineJoin = "round";
1639
- ctx.stroke();
1640
- ctx.restore();
1641
- }
1642
-
1643
- // lib/drawer/shapes/dimension-line.ts
1644
1718
  var TEXT_OFFSET_MULTIPLIER = 1.5;
1645
1719
  var CHARACTER_WIDTH_MULTIPLIER = 0.6;
1646
1720
  var TEXT_INTERSECTION_PADDING_MULTIPLIER = 0.3;
@@ -1673,71 +1747,103 @@ function drawDimensionLine(params) {
1673
1747
  };
1674
1748
  const fromOffset = { x: from.x + offsetVector.x, y: from.y + offsetVector.y };
1675
1749
  const toOffset = { x: to.x + offsetVector.x, y: to.y + offsetVector.y };
1676
- const fromBase = fromOffset;
1677
- const toBase = toOffset;
1750
+ const fromBase = {
1751
+ x: fromOffset.x + direction.x * arrowSize,
1752
+ y: fromOffset.y + direction.y * arrowSize
1753
+ };
1754
+ const toBase = {
1755
+ x: toOffset.x - direction.x * arrowSize,
1756
+ y: toOffset.y - direction.y * arrowSize
1757
+ };
1678
1758
  const scaleValue = Math.abs(realToCanvasMat.a);
1679
1759
  const strokeWidth = manualStrokeWidth ?? arrowSize / 5;
1680
1760
  const lineColor = color || "rgba(255,255,255,0.5)";
1681
1761
  const extensionDirection = hasOffsetDirection && (Math.abs(normalizedOffsetDirection.x) > Number.EPSILON || Math.abs(normalizedOffsetDirection.y) > Number.EPSILON) ? normalizedOffsetDirection : perpendicular;
1682
1762
  const extensionLength = offsetMagnitude + 0.5;
1683
- const drawExtension = (anchor) => {
1763
+ const allPoints = [];
1764
+ const getExtensionPoints = (anchor) => {
1684
1765
  const endPoint = {
1685
1766
  x: anchor.x + extensionDirection.x * extensionLength,
1686
1767
  y: anchor.y + extensionDirection.y * extensionLength
1687
1768
  };
1688
- drawLine({
1689
- ctx,
1690
- start: anchor,
1691
- end: endPoint,
1692
- strokeWidth,
1693
- stroke: lineColor,
1694
- realToCanvasMat
1695
- });
1769
+ const halfWidth2 = strokeWidth / 2;
1770
+ const extPerpendicular = {
1771
+ x: -extensionDirection.y,
1772
+ y: extensionDirection.x
1773
+ };
1774
+ return [
1775
+ {
1776
+ x: anchor.x + extPerpendicular.x * halfWidth2,
1777
+ y: anchor.y + extPerpendicular.y * halfWidth2
1778
+ },
1779
+ {
1780
+ x: anchor.x - extPerpendicular.x * halfWidth2,
1781
+ y: anchor.y - extPerpendicular.y * halfWidth2
1782
+ },
1783
+ {
1784
+ x: endPoint.x - extPerpendicular.x * halfWidth2,
1785
+ y: endPoint.y - extPerpendicular.y * halfWidth2
1786
+ },
1787
+ {
1788
+ x: endPoint.x + extPerpendicular.x * halfWidth2,
1789
+ y: endPoint.y + extPerpendicular.y * halfWidth2
1790
+ }
1791
+ ];
1696
1792
  };
1697
- drawExtension(from);
1698
- drawExtension(to);
1699
- drawLine({
1793
+ const ext1 = getExtensionPoints(from);
1794
+ allPoints.push(...ext1, ext1[0]);
1795
+ const ext2 = getExtensionPoints(to);
1796
+ allPoints.push(...ext2, ext2[0]);
1797
+ const halfWidth = strokeWidth / 2;
1798
+ const mainLine = [
1799
+ {
1800
+ x: fromBase.x + perpendicular.x * halfWidth,
1801
+ y: fromBase.y + perpendicular.y * halfWidth
1802
+ },
1803
+ {
1804
+ x: fromBase.x - perpendicular.x * halfWidth,
1805
+ y: fromBase.y - perpendicular.y * halfWidth
1806
+ },
1807
+ {
1808
+ x: toBase.x - perpendicular.x * halfWidth,
1809
+ y: toBase.y - perpendicular.y * halfWidth
1810
+ },
1811
+ {
1812
+ x: toBase.x + perpendicular.x * halfWidth,
1813
+ y: toBase.y + perpendicular.y * halfWidth
1814
+ }
1815
+ ];
1816
+ allPoints.push(...mainLine, mainLine[0]);
1817
+ const arrow1 = [
1818
+ fromOffset,
1819
+ {
1820
+ x: fromOffset.x + direction.x * arrowSize + perpendicular.x * (arrowSize / 2),
1821
+ y: fromOffset.y + direction.y * arrowSize + perpendicular.y * (arrowSize / 2)
1822
+ },
1823
+ {
1824
+ x: fromOffset.x + direction.x * arrowSize - perpendicular.x * (arrowSize / 2),
1825
+ y: fromOffset.y + direction.y * arrowSize - perpendicular.y * (arrowSize / 2)
1826
+ }
1827
+ ];
1828
+ allPoints.push(...arrow1, arrow1[0]);
1829
+ const arrow2 = [
1830
+ toOffset,
1831
+ {
1832
+ x: toOffset.x - direction.x * arrowSize + perpendicular.x * (arrowSize / 2),
1833
+ y: toOffset.y - direction.y * arrowSize + perpendicular.y * (arrowSize / 2)
1834
+ },
1835
+ {
1836
+ x: toOffset.x - direction.x * arrowSize - perpendicular.x * (arrowSize / 2),
1837
+ y: toOffset.y - direction.y * arrowSize - perpendicular.y * (arrowSize / 2)
1838
+ }
1839
+ ];
1840
+ allPoints.push(...arrow2, arrow2[0]);
1841
+ drawPolygon({
1700
1842
  ctx,
1701
- start: fromBase,
1702
- end: toBase,
1703
- strokeWidth,
1704
- stroke: lineColor,
1843
+ points: allPoints,
1844
+ fill: lineColor,
1705
1845
  realToCanvasMat
1706
1846
  });
1707
- const [canvasFromX, canvasFromY] = applyToPoint13(realToCanvasMat, [
1708
- fromOffset.x,
1709
- fromOffset.y
1710
- ]);
1711
- const [canvasToX, canvasToY] = applyToPoint13(realToCanvasMat, [
1712
- toOffset.x,
1713
- toOffset.y
1714
- ]);
1715
- const [canvasToDirX, canvasToDirY] = applyToPoint13(realToCanvasMat, [
1716
- toOffset.x + direction.x,
1717
- toOffset.y + direction.y
1718
- ]);
1719
- const canvasLineAngle = Math.atan2(
1720
- canvasToDirY - canvasToY,
1721
- canvasToDirX - canvasToX
1722
- );
1723
- drawArrow({
1724
- ctx,
1725
- x: canvasFromX,
1726
- y: canvasFromY,
1727
- angle: canvasLineAngle + Math.PI,
1728
- arrowSize: arrowSize * scaleValue,
1729
- color: lineColor,
1730
- strokeWidth: strokeWidth * scaleValue
1731
- });
1732
- drawArrow({
1733
- ctx,
1734
- x: canvasToX,
1735
- y: canvasToY,
1736
- angle: canvasLineAngle,
1737
- arrowSize: arrowSize * scaleValue,
1738
- color: lineColor,
1739
- strokeWidth: strokeWidth * scaleValue
1740
- });
1741
1847
  if (text) {
1742
1848
  const midPoint = {
1743
1849
  x: (from.x + to.x) / 2 + offsetVector.x,
@@ -1767,9 +1873,9 @@ function drawDimensionLine(params) {
1767
1873
  const rotationRad = textRotation * Math.PI / 180;
1768
1874
  const sinRot = Math.abs(Math.sin(rotationRad));
1769
1875
  const cosRot = Math.abs(Math.cos(rotationRad));
1770
- const halfWidth = textWidth / 2;
1876
+ const halfWidth2 = textWidth / 2;
1771
1877
  const halfHeight = textHeight / 2;
1772
- const maxExtension = halfWidth * sinRot + halfHeight * cosRot;
1878
+ const maxExtension = halfWidth2 * sinRot + halfHeight * cosRot;
1773
1879
  additionalOffset = maxExtension + fontSize * TEXT_INTERSECTION_PADDING_MULTIPLIER;
1774
1880
  }
1775
1881
  const textOffset = arrowSize * TEXT_OFFSET_MULTIPLIER + additionalOffset;
@@ -1930,8 +2036,11 @@ var CircuitToCanvasDrawer = class {
1930
2036
  const hasSoldermaskPads = elements.some(
1931
2037
  (el) => el.type === "pcb_smtpad" && el.is_covered_with_solder_mask === true
1932
2038
  );
2039
+ const hasSoldermaskHoles = elements.some(
2040
+ (el) => el.type === "pcb_hole" && el.is_covered_with_solder_mask === true
2041
+ );
1933
2042
  for (const element of elements) {
1934
- if (element.type === "pcb_board" && hasSoldermaskPads) {
2043
+ if (element.type === "pcb_board" && (hasSoldermaskPads || hasSoldermaskHoles)) {
1935
2044
  this.drawBoardWithSoldermask(element);
1936
2045
  } else {
1937
2046
  this.drawElement(element, options);
@@ -2205,6 +2314,22 @@ var CircuitToCanvasDrawer = class {
2205
2314
  }
2206
2315
  }
2207
2316
  };
2317
+
2318
+ // lib/drawer/shapes/arrow.ts
2319
+ function drawArrow(params) {
2320
+ const { ctx, x, y, angle, arrowSize, color, strokeWidth } = params;
2321
+ ctx.save();
2322
+ ctx.translate(x, y);
2323
+ ctx.rotate(angle);
2324
+ ctx.beginPath();
2325
+ ctx.moveTo(0, 0);
2326
+ ctx.lineTo(-arrowSize, -arrowSize / 2);
2327
+ ctx.lineTo(-arrowSize, arrowSize / 2);
2328
+ ctx.closePath();
2329
+ ctx.fillStyle = color;
2330
+ ctx.fill();
2331
+ ctx.restore();
2332
+ }
2208
2333
  export {
2209
2334
  CircuitToCanvasDrawer,
2210
2335
  DEFAULT_PCB_COLOR_MAP,
@@ -2242,6 +2367,7 @@ export {
2242
2367
  drawPolygon,
2243
2368
  drawRect,
2244
2369
  drawSoldermaskRingForCircle,
2370
+ drawSoldermaskRingForOval,
2245
2371
  drawSoldermaskRingForPill,
2246
2372
  drawSoldermaskRingForRect,
2247
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
+ }
@@ -23,14 +23,11 @@ export function drawArrow(params: DrawArrowParams): void {
23
23
  ctx.beginPath()
24
24
  ctx.moveTo(0, 0)
25
25
  ctx.lineTo(-arrowSize, -arrowSize / 2)
26
- ctx.moveTo(0, 0)
27
26
  ctx.lineTo(-arrowSize, arrowSize / 2)
27
+ ctx.closePath()
28
28
 
29
- ctx.lineWidth = strokeWidth
30
- ctx.strokeStyle = color
31
- ctx.lineCap = "round"
32
- ctx.lineJoin = "round"
33
- ctx.stroke()
29
+ ctx.fillStyle = color
30
+ ctx.fill()
34
31
 
35
32
  ctx.restore()
36
33
  }
@@ -1,9 +1,8 @@
1
1
  import type { Matrix } from "transformation-matrix"
2
2
  import { applyToPoint } from "transformation-matrix"
3
3
  import type { CanvasContext } from "../types"
4
- import { drawLine } from "./line"
4
+ import { drawPolygon } from "./polygon"
5
5
  import { drawText } from "./text"
6
- import { drawArrow } from "./arrow"
7
6
 
8
7
  export interface DrawDimensionLineParams {
9
8
  ctx: CanvasContext
@@ -67,8 +66,14 @@ export function drawDimensionLine(params: DrawDimensionLineParams): void {
67
66
  const fromOffset = { x: from.x + offsetVector.x, y: from.y + offsetVector.y }
68
67
  const toOffset = { x: to.x + offsetVector.x, y: to.y + offsetVector.y }
69
68
 
70
- const fromBase = fromOffset
71
- const toBase = toOffset
69
+ const fromBase = {
70
+ x: fromOffset.x + direction.x * arrowSize,
71
+ y: fromOffset.y + direction.y * arrowSize,
72
+ }
73
+ const toBase = {
74
+ x: toOffset.x - direction.x * arrowSize,
75
+ y: toOffset.y - direction.y * arrowSize,
76
+ }
72
77
 
73
78
  const scaleValue = Math.abs(realToCanvasMat.a)
74
79
  const strokeWidth = manualStrokeWidth ?? arrowSize / 5
@@ -84,71 +89,123 @@ export function drawDimensionLine(params: DrawDimensionLineParams): void {
84
89
 
85
90
  const extensionLength = offsetMagnitude + 0.5
86
91
 
87
- const drawExtension = (anchor: { x: number; y: number }) => {
92
+ const allPoints: Array<{ x: number; y: number }> = []
93
+
94
+ const getExtensionPoints = (anchor: { x: number; y: number }) => {
88
95
  const endPoint = {
89
96
  x: anchor.x + extensionDirection.x * extensionLength,
90
97
  y: anchor.y + extensionDirection.y * extensionLength,
91
98
  }
92
- drawLine({
93
- ctx,
94
- start: anchor,
95
- end: endPoint,
96
- strokeWidth,
97
- stroke: lineColor,
98
- realToCanvasMat,
99
- })
99
+ const halfWidth = strokeWidth / 2
100
+ const extPerpendicular = {
101
+ x: -extensionDirection.y,
102
+ y: extensionDirection.x,
103
+ }
104
+ return [
105
+ {
106
+ x: anchor.x + extPerpendicular.x * halfWidth,
107
+ y: anchor.y + extPerpendicular.y * halfWidth,
108
+ },
109
+ {
110
+ x: anchor.x - extPerpendicular.x * halfWidth,
111
+ y: anchor.y - extPerpendicular.y * halfWidth,
112
+ },
113
+ {
114
+ x: endPoint.x - extPerpendicular.x * halfWidth,
115
+ y: endPoint.y - extPerpendicular.y * halfWidth,
116
+ },
117
+ {
118
+ x: endPoint.x + extPerpendicular.x * halfWidth,
119
+ y: endPoint.y + extPerpendicular.y * halfWidth,
120
+ },
121
+ ]
100
122
  }
101
123
 
102
- drawExtension(from)
103
- drawExtension(to)
124
+ // Extension lines (ticks)
125
+ const ext1 = getExtensionPoints(from)
126
+ allPoints.push(...ext1, ext1[0]!)
127
+
128
+ const ext2 = getExtensionPoints(to)
129
+ allPoints.push(...ext2, ext2[0]!)
104
130
 
105
131
  // Main dimension line
106
- drawLine({
107
- ctx,
108
- start: fromBase,
109
- end: toBase,
110
- strokeWidth,
111
- stroke: lineColor,
112
- realToCanvasMat,
113
- })
132
+ const halfWidth = strokeWidth / 2
133
+ const mainLine = [
134
+ {
135
+ x: fromBase.x + perpendicular.x * halfWidth,
136
+ y: fromBase.y + perpendicular.y * halfWidth,
137
+ },
138
+ {
139
+ x: fromBase.x - perpendicular.x * halfWidth,
140
+ y: fromBase.y - perpendicular.y * halfWidth,
141
+ },
142
+ {
143
+ x: toBase.x - perpendicular.x * halfWidth,
144
+ y: toBase.y - perpendicular.y * halfWidth,
145
+ },
146
+ {
147
+ x: toBase.x + perpendicular.x * halfWidth,
148
+ y: toBase.y + perpendicular.y * halfWidth,
149
+ },
150
+ ]
151
+ allPoints.push(...mainLine, mainLine[0]!)
114
152
 
115
- // Arrows (Keep V-shaped but matching size)
116
- const [canvasFromX, canvasFromY] = applyToPoint(realToCanvasMat, [
117
- fromOffset.x,
118
- fromOffset.y,
119
- ])
120
- const [canvasToX, canvasToY] = applyToPoint(realToCanvasMat, [
121
- toOffset.x,
122
- toOffset.y,
123
- ])
124
- const [canvasToDirX, canvasToDirY] = applyToPoint(realToCanvasMat, [
125
- toOffset.x + direction.x,
126
- toOffset.y + direction.y,
127
- ])
128
-
129
- const canvasLineAngle = Math.atan2(
130
- canvasToDirY - canvasToY,
131
- canvasToDirX - canvasToX,
132
- )
133
-
134
- drawArrow({
135
- ctx,
136
- x: canvasFromX,
137
- y: canvasFromY,
138
- angle: canvasLineAngle + Math.PI,
139
- arrowSize: arrowSize * scaleValue,
140
- color: lineColor,
141
- strokeWidth: strokeWidth * scaleValue,
142
- })
153
+ // Arrows
154
+ const arrow1 = [
155
+ fromOffset,
156
+ {
157
+ x:
158
+ fromOffset.x +
159
+ direction.x * arrowSize +
160
+ perpendicular.x * (arrowSize / 2),
161
+ y:
162
+ fromOffset.y +
163
+ direction.y * arrowSize +
164
+ perpendicular.y * (arrowSize / 2),
165
+ },
166
+ {
167
+ x:
168
+ fromOffset.x +
169
+ direction.x * arrowSize -
170
+ perpendicular.x * (arrowSize / 2),
171
+ y:
172
+ fromOffset.y +
173
+ direction.y * arrowSize -
174
+ perpendicular.y * (arrowSize / 2),
175
+ },
176
+ ]
177
+ allPoints.push(...arrow1, arrow1[0]!)
143
178
 
144
- drawArrow({
179
+ const arrow2 = [
180
+ toOffset,
181
+ {
182
+ x:
183
+ toOffset.x -
184
+ direction.x * arrowSize +
185
+ perpendicular.x * (arrowSize / 2),
186
+ y:
187
+ toOffset.y -
188
+ direction.y * arrowSize +
189
+ perpendicular.y * (arrowSize / 2),
190
+ },
191
+ {
192
+ x:
193
+ toOffset.x -
194
+ direction.x * arrowSize -
195
+ perpendicular.x * (arrowSize / 2),
196
+ y:
197
+ toOffset.y -
198
+ direction.y * arrowSize -
199
+ perpendicular.y * (arrowSize / 2),
200
+ },
201
+ ]
202
+ allPoints.push(...arrow2, arrow2[0]!)
203
+
204
+ drawPolygon({
145
205
  ctx,
146
- x: canvasToX,
147
- y: canvasToY,
148
- angle: canvasLineAngle,
149
- arrowSize: arrowSize * scaleValue,
150
- color: lineColor,
151
- strokeWidth: strokeWidth * scaleValue,
206
+ points: allPoints,
207
+ fill: lineColor,
208
+ realToCanvasMat,
152
209
  })
153
210
 
154
211
  // Text
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.34",
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
+ })