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 +9 -1
- package/dist/index.js +203 -77
- package/lib/drawer/CircuitToCanvasDrawer.ts +12 -3
- package/lib/drawer/elements/index.ts +1 -0
- package/lib/drawer/elements/pcb-hole.ts +88 -1
- package/lib/drawer/elements/soldermask-margin.ts +60 -0
- package/lib/drawer/shapes/arrow.ts +3 -6
- package/lib/drawer/shapes/dimension-line.ts +115 -58
- package/package.json +1 -1
- package/tests/elements/__snapshots__/pcb-fabrication-note-dimension.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-hole-soldermask-margin.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-note-dimension-angled-and-vertical.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-note-dimension-basic.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-note-dimension-vertical.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-note-dimension-with-offset.snap.png +0 -0
- package/tests/elements/pcb-hole-soldermask-margin.test.ts +133 -0
- package/tests/shapes/__snapshots__/dimension-line.snap.png +0 -0
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
|
|
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 =
|
|
1677
|
-
|
|
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
|
|
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
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
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
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
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
|
-
|
|
1702
|
-
|
|
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
|
|
1876
|
+
const halfWidth2 = textWidth / 2;
|
|
1771
1877
|
const halfHeight = textHeight / 2;
|
|
1772
|
-
const maxExtension =
|
|
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 (
|
|
172
|
-
|
|
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)
|
|
@@ -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
|
|
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.
|
|
30
|
-
ctx.
|
|
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 {
|
|
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 =
|
|
71
|
-
|
|
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
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
116
|
-
const
|
|
117
|
-
fromOffset
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
+
})
|
|
Binary file
|