@tscircuit/checks 0.0.159 → 0.0.161
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/README.md +5 -2
- package/dist/index.d.ts +22 -3
- package/dist/index.js +476 -79
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -179,17 +179,17 @@ var getSegmentToPolygonClearanceFromPoints = (start, end, polygon) => {
|
|
|
179
179
|
};
|
|
180
180
|
};
|
|
181
181
|
var getSegmentToPillClearance = (segment, pad) => {
|
|
182
|
-
const
|
|
182
|
+
const pill2 = getPillCenterLineForPad(pad);
|
|
183
183
|
const closest = getClosestPointsBetweenSegments(
|
|
184
184
|
{ x: segment.x1, y: segment.y1 },
|
|
185
185
|
{ x: segment.x2, y: segment.y2 },
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
pill2.start,
|
|
187
|
+
pill2.end
|
|
188
188
|
);
|
|
189
189
|
return {
|
|
190
190
|
distance: closest.distance,
|
|
191
191
|
center: closest.center,
|
|
192
|
-
radius:
|
|
192
|
+
radius: pill2.radius,
|
|
193
193
|
tracePoint: closest.pointOnA,
|
|
194
194
|
obstaclePoint: closest.pointOnB
|
|
195
195
|
};
|
|
@@ -234,8 +234,8 @@ function isPointInPad(point, pad) {
|
|
|
234
234
|
}
|
|
235
235
|
if (pad.shape === "pill" || pad.shape === "rotated_pill") {
|
|
236
236
|
if (pad.shape === "rotated_pill") {
|
|
237
|
-
const
|
|
238
|
-
return pointToSegmentDistance(point,
|
|
237
|
+
const pill2 = getPillCenterLineForPad(pad);
|
|
238
|
+
return pointToSegmentDistance(point, pill2.start, pill2.end) <= pill2.radius;
|
|
239
239
|
}
|
|
240
240
|
const halfWidth = pad.width / 2;
|
|
241
241
|
const halfHeight = pad.height / 2;
|
|
@@ -632,28 +632,28 @@ var getPadToPadGap = (padA, padB) => {
|
|
|
632
632
|
) - pillA.radius - pillB.radius;
|
|
633
633
|
}
|
|
634
634
|
if (isPillPad(padA) && isCircularPad(padB)) {
|
|
635
|
-
const
|
|
636
|
-
return segmentToCircleMinDistance(
|
|
635
|
+
const pill2 = getPillCenterLineForPad(padA);
|
|
636
|
+
return segmentToCircleMinDistance(pill2.start, pill2.end, getCircleShape(padB)) - pill2.radius;
|
|
637
637
|
}
|
|
638
638
|
if (isCircularPad(padA) && isPillPad(padB)) {
|
|
639
|
-
const
|
|
640
|
-
return segmentToCircleMinDistance(
|
|
639
|
+
const pill2 = getPillCenterLineForPad(padB);
|
|
640
|
+
return segmentToCircleMinDistance(pill2.start, pill2.end, getCircleShape(padA)) - pill2.radius;
|
|
641
641
|
}
|
|
642
642
|
if (isPillPad(padA)) {
|
|
643
|
-
const
|
|
643
|
+
const pill2 = getPillCenterLineForPad(padA);
|
|
644
644
|
return getSegmentToPolygonClearanceFromPoints(
|
|
645
|
-
|
|
646
|
-
|
|
645
|
+
pill2.start,
|
|
646
|
+
pill2.end,
|
|
647
647
|
getPolygonShape(padB).points
|
|
648
|
-
).distance -
|
|
648
|
+
).distance - pill2.radius;
|
|
649
649
|
}
|
|
650
650
|
if (isPillPad(padB)) {
|
|
651
|
-
const
|
|
651
|
+
const pill2 = getPillCenterLineForPad(padB);
|
|
652
652
|
return getSegmentToPolygonClearanceFromPoints(
|
|
653
|
-
|
|
654
|
-
|
|
653
|
+
pill2.start,
|
|
654
|
+
pill2.end,
|
|
655
655
|
getPolygonShape(padA).points
|
|
656
|
-
).distance -
|
|
656
|
+
).distance - pill2.radius;
|
|
657
657
|
}
|
|
658
658
|
if (isCircularPad(padA) && isCircularPad(padB)) {
|
|
659
659
|
return distanceBetweenCircleAndCircle(
|
|
@@ -1180,12 +1180,12 @@ var getCollidableBounds = (collidable) => {
|
|
|
1180
1180
|
};
|
|
1181
1181
|
}
|
|
1182
1182
|
if (collidable.type === "pcb_smtpad" && collidable.shape === "rotated_pill") {
|
|
1183
|
-
const
|
|
1183
|
+
const pill2 = getPillCenterLineForPad(collidable);
|
|
1184
1184
|
return {
|
|
1185
|
-
minX: Math.min(
|
|
1186
|
-
minY: Math.min(
|
|
1187
|
-
maxX: Math.max(
|
|
1188
|
-
maxY: Math.max(
|
|
1185
|
+
minX: Math.min(pill2.start.x, pill2.end.x) - pill2.radius,
|
|
1186
|
+
minY: Math.min(pill2.start.y, pill2.end.y) - pill2.radius,
|
|
1187
|
+
maxX: Math.max(pill2.start.x, pill2.end.x) + pill2.radius,
|
|
1188
|
+
maxY: Math.max(pill2.start.y, pill2.end.y) + pill2.radius
|
|
1189
1189
|
};
|
|
1190
1190
|
}
|
|
1191
1191
|
}
|
|
@@ -1526,40 +1526,359 @@ var NetManager = class {
|
|
|
1526
1526
|
|
|
1527
1527
|
// lib/check-pcb-components-out-of-board/checkViasOffBoard.ts
|
|
1528
1528
|
import { getReadableNameForElement as getReadableNameForElement3 } from "@tscircuit/circuit-json-util";
|
|
1529
|
-
|
|
1529
|
+
|
|
1530
|
+
// lib/check-copper-to-board-edge-clearance.ts
|
|
1531
|
+
import * as Flatten from "@flatten-js/core";
|
|
1532
|
+
import { applyToPoint, rotateDEG } from "transformation-matrix";
|
|
1533
|
+
var toPcbComponentId = (id) => id;
|
|
1534
|
+
var GEOMETRY_EPSILON = 1e-9;
|
|
1535
|
+
var pointsToPolygon = (points) => {
|
|
1536
|
+
if (points.length < 3) return null;
|
|
1537
|
+
return new Flatten.Polygon(points.map(({ x, y }) => new Flatten.Point(x, y)));
|
|
1538
|
+
};
|
|
1539
|
+
var boardToPolygon = (board) => {
|
|
1540
|
+
if (board.outline && board.outline.length >= 3) {
|
|
1541
|
+
return pointsToPolygon(board.outline);
|
|
1542
|
+
}
|
|
1543
|
+
if (!board.center || typeof board.width !== "number" || typeof board.height !== "number") {
|
|
1544
|
+
return null;
|
|
1545
|
+
}
|
|
1546
|
+
const halfWidth = board.width / 2;
|
|
1547
|
+
const halfHeight = board.height / 2;
|
|
1548
|
+
return pointsToPolygon([
|
|
1549
|
+
{ x: board.center.x - halfWidth, y: board.center.y - halfHeight },
|
|
1550
|
+
{ x: board.center.x + halfWidth, y: board.center.y - halfHeight },
|
|
1551
|
+
{ x: board.center.x + halfWidth, y: board.center.y + halfHeight },
|
|
1552
|
+
{ x: board.center.x - halfWidth, y: board.center.y + halfHeight }
|
|
1553
|
+
]);
|
|
1554
|
+
};
|
|
1555
|
+
var getRectanglePolygon = ({
|
|
1556
|
+
x,
|
|
1557
|
+
y,
|
|
1558
|
+
width,
|
|
1559
|
+
height,
|
|
1560
|
+
ccwRotationDegrees = 0
|
|
1561
|
+
}) => {
|
|
1562
|
+
const halfWidth = width / 2;
|
|
1563
|
+
const halfHeight = height / 2;
|
|
1564
|
+
const rotationMatrix = rotateDEG(ccwRotationDegrees, x, y);
|
|
1565
|
+
return pointsToPolygon(
|
|
1566
|
+
[
|
|
1567
|
+
{ x: x - halfWidth, y: y - halfHeight },
|
|
1568
|
+
{ x: x + halfWidth, y: y - halfHeight },
|
|
1569
|
+
{ x: x + halfWidth, y: y + halfHeight },
|
|
1570
|
+
{ x: x - halfWidth, y: y + halfHeight }
|
|
1571
|
+
].map((point) => applyToPoint(rotationMatrix, point))
|
|
1572
|
+
);
|
|
1573
|
+
};
|
|
1574
|
+
var roundedRect = ({
|
|
1575
|
+
x,
|
|
1576
|
+
y,
|
|
1577
|
+
width,
|
|
1578
|
+
height,
|
|
1579
|
+
cornerRadius,
|
|
1580
|
+
ccwRotationDegrees = 0
|
|
1581
|
+
}) => {
|
|
1582
|
+
const radius = Math.max(0, Math.min(cornerRadius, width / 2, height / 2));
|
|
1583
|
+
if (radius <= GEOMETRY_EPSILON) {
|
|
1584
|
+
const polygon = getRectanglePolygon({
|
|
1585
|
+
x,
|
|
1586
|
+
y,
|
|
1587
|
+
width,
|
|
1588
|
+
height,
|
|
1589
|
+
ccwRotationDegrees
|
|
1590
|
+
});
|
|
1591
|
+
return polygon ? { kind: "shapes", shapes: [polygon] } : null;
|
|
1592
|
+
}
|
|
1593
|
+
const shapes = [];
|
|
1594
|
+
const innerWidth = width - 2 * radius;
|
|
1595
|
+
const innerHeight = height - 2 * radius;
|
|
1596
|
+
if (innerWidth > GEOMETRY_EPSILON) {
|
|
1597
|
+
const verticalBand = getRectanglePolygon({
|
|
1598
|
+
x,
|
|
1599
|
+
y,
|
|
1600
|
+
width: innerWidth,
|
|
1601
|
+
height,
|
|
1602
|
+
ccwRotationDegrees
|
|
1603
|
+
});
|
|
1604
|
+
if (verticalBand) shapes.push(verticalBand);
|
|
1605
|
+
}
|
|
1606
|
+
if (innerHeight > GEOMETRY_EPSILON) {
|
|
1607
|
+
const horizontalBand = getRectanglePolygon({
|
|
1608
|
+
x,
|
|
1609
|
+
y,
|
|
1610
|
+
width,
|
|
1611
|
+
height: innerHeight,
|
|
1612
|
+
ccwRotationDegrees
|
|
1613
|
+
});
|
|
1614
|
+
if (horizontalBand) shapes.push(horizontalBand);
|
|
1615
|
+
}
|
|
1616
|
+
const halfInnerWidth = innerWidth / 2;
|
|
1617
|
+
const halfInnerHeight = innerHeight / 2;
|
|
1618
|
+
const rotationMatrix = rotateDEG(ccwRotationDegrees, x, y);
|
|
1619
|
+
const cornerCenters = [
|
|
1620
|
+
{ x: x - halfInnerWidth, y: y - halfInnerHeight },
|
|
1621
|
+
{ x: x + halfInnerWidth, y: y - halfInnerHeight },
|
|
1622
|
+
{ x: x + halfInnerWidth, y: y + halfInnerHeight },
|
|
1623
|
+
{ x: x - halfInnerWidth, y: y + halfInnerHeight }
|
|
1624
|
+
].map((point) => applyToPoint(rotationMatrix, point)).filter(
|
|
1625
|
+
(point, index, points) => points.findIndex(
|
|
1626
|
+
(candidate) => Math.abs(candidate.x - point.x) <= GEOMETRY_EPSILON && Math.abs(candidate.y - point.y) <= GEOMETRY_EPSILON
|
|
1627
|
+
) === index
|
|
1628
|
+
);
|
|
1629
|
+
shapes.push(
|
|
1630
|
+
...cornerCenters.map(
|
|
1631
|
+
(center) => new Flatten.Circle(new Flatten.Point(center.x, center.y), radius)
|
|
1632
|
+
)
|
|
1633
|
+
);
|
|
1634
|
+
return shapes.length > 0 ? { kind: "shapes", shapes } : null;
|
|
1635
|
+
};
|
|
1636
|
+
var pill = ({
|
|
1637
|
+
x,
|
|
1638
|
+
y,
|
|
1639
|
+
width,
|
|
1640
|
+
height,
|
|
1641
|
+
radius,
|
|
1642
|
+
ccwRotationDegrees = 0
|
|
1643
|
+
}) => {
|
|
1644
|
+
const halfLineLength = Math.max(Math.max(width, height) / 2 - radius, 0);
|
|
1645
|
+
const localAxis = width >= height ? { x: halfLineLength, y: 0 } : { x: 0, y: halfLineLength };
|
|
1646
|
+
const axis = applyToPoint(rotateDEG(ccwRotationDegrees), localAxis);
|
|
1647
|
+
if (halfLineLength <= GEOMETRY_EPSILON) {
|
|
1648
|
+
return {
|
|
1649
|
+
kind: "shapes",
|
|
1650
|
+
shapes: [new Flatten.Circle(new Flatten.Point(x, y), radius)]
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
return {
|
|
1654
|
+
kind: "pill",
|
|
1655
|
+
centerLine: new Flatten.Segment(
|
|
1656
|
+
new Flatten.Point(x - axis.x, y - axis.y),
|
|
1657
|
+
new Flatten.Point(x + axis.x, y + axis.y)
|
|
1658
|
+
),
|
|
1659
|
+
radius
|
|
1660
|
+
};
|
|
1661
|
+
};
|
|
1662
|
+
var getSmtPadGeometry = (pad) => {
|
|
1663
|
+
switch (pad.shape) {
|
|
1664
|
+
case "circle":
|
|
1665
|
+
return {
|
|
1666
|
+
kind: "shapes",
|
|
1667
|
+
shapes: [
|
|
1668
|
+
new Flatten.Circle(new Flatten.Point(pad.x, pad.y), pad.radius)
|
|
1669
|
+
]
|
|
1670
|
+
};
|
|
1671
|
+
case "rect":
|
|
1672
|
+
return roundedRect({
|
|
1673
|
+
x: pad.x,
|
|
1674
|
+
y: pad.y,
|
|
1675
|
+
width: pad.width,
|
|
1676
|
+
height: pad.height,
|
|
1677
|
+
cornerRadius: pad.rect_border_radius ?? pad.corner_radius ?? 0
|
|
1678
|
+
});
|
|
1679
|
+
case "rotated_rect":
|
|
1680
|
+
return roundedRect({
|
|
1681
|
+
x: pad.x,
|
|
1682
|
+
y: pad.y,
|
|
1683
|
+
width: pad.width,
|
|
1684
|
+
height: pad.height,
|
|
1685
|
+
cornerRadius: pad.rect_border_radius ?? pad.corner_radius ?? 0,
|
|
1686
|
+
ccwRotationDegrees: pad.ccw_rotation
|
|
1687
|
+
});
|
|
1688
|
+
case "pill":
|
|
1689
|
+
return pill({
|
|
1690
|
+
x: pad.x,
|
|
1691
|
+
y: pad.y,
|
|
1692
|
+
width: pad.width,
|
|
1693
|
+
height: pad.height,
|
|
1694
|
+
radius: pad.radius
|
|
1695
|
+
});
|
|
1696
|
+
case "rotated_pill":
|
|
1697
|
+
return pill({
|
|
1698
|
+
x: pad.x,
|
|
1699
|
+
y: pad.y,
|
|
1700
|
+
width: pad.width,
|
|
1701
|
+
height: pad.height,
|
|
1702
|
+
radius: pad.radius,
|
|
1703
|
+
ccwRotationDegrees: pad.ccw_rotation
|
|
1704
|
+
});
|
|
1705
|
+
case "polygon": {
|
|
1706
|
+
const polygon = pointsToPolygon(pad.points);
|
|
1707
|
+
return polygon ? { kind: "shapes", shapes: [polygon] } : null;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
};
|
|
1711
|
+
var getPlatedHoleGeometry = (platedHole, componentCcwRotationDegrees) => {
|
|
1712
|
+
switch (platedHole.shape) {
|
|
1713
|
+
case "circle":
|
|
1714
|
+
return {
|
|
1715
|
+
kind: "shapes",
|
|
1716
|
+
shapes: [
|
|
1717
|
+
new Flatten.Circle(
|
|
1718
|
+
new Flatten.Point(platedHole.x, platedHole.y),
|
|
1719
|
+
platedHole.outer_diameter / 2
|
|
1720
|
+
)
|
|
1721
|
+
]
|
|
1722
|
+
};
|
|
1723
|
+
case "oval":
|
|
1724
|
+
case "pill":
|
|
1725
|
+
return pill({
|
|
1726
|
+
x: platedHole.x,
|
|
1727
|
+
y: platedHole.y,
|
|
1728
|
+
width: platedHole.outer_width,
|
|
1729
|
+
height: platedHole.outer_height,
|
|
1730
|
+
radius: Math.min(
|
|
1731
|
+
platedHole.outer_width / 2,
|
|
1732
|
+
platedHole.outer_height / 2
|
|
1733
|
+
),
|
|
1734
|
+
ccwRotationDegrees: platedHole.ccw_rotation
|
|
1735
|
+
});
|
|
1736
|
+
case "circular_hole_with_rect_pad":
|
|
1737
|
+
case "pill_hole_with_rect_pad":
|
|
1738
|
+
case "rotated_pill_hole_with_rect_pad":
|
|
1739
|
+
return roundedRect({
|
|
1740
|
+
x: platedHole.x,
|
|
1741
|
+
y: platedHole.y,
|
|
1742
|
+
width: platedHole.rect_pad_width,
|
|
1743
|
+
height: platedHole.rect_pad_height,
|
|
1744
|
+
cornerRadius: platedHole.rect_border_radius ?? 0,
|
|
1745
|
+
ccwRotationDegrees: "rect_ccw_rotation" in platedHole ? platedHole.rect_ccw_rotation ?? 0 : 0
|
|
1746
|
+
});
|
|
1747
|
+
case "hole_with_polygon_pad": {
|
|
1748
|
+
const ccwRotationDegrees = platedHole.ccw_rotation ?? componentCcwRotationDegrees;
|
|
1749
|
+
const rotationMatrix = rotateDEG(ccwRotationDegrees);
|
|
1750
|
+
const polygon = pointsToPolygon(
|
|
1751
|
+
platedHole.pad_outline.map((point) => {
|
|
1752
|
+
const rotatedPoint = applyToPoint(rotationMatrix, point);
|
|
1753
|
+
return {
|
|
1754
|
+
x: platedHole.x + rotatedPoint.x,
|
|
1755
|
+
y: platedHole.y + rotatedPoint.y
|
|
1756
|
+
};
|
|
1757
|
+
})
|
|
1758
|
+
);
|
|
1759
|
+
return polygon ? { kind: "shapes", shapes: [polygon] } : null;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
};
|
|
1763
|
+
var getCopperGeometry = (element, componentCcwRotationsById) => {
|
|
1764
|
+
if (element.type === "pcb_via") {
|
|
1765
|
+
return {
|
|
1766
|
+
kind: "shapes",
|
|
1767
|
+
shapes: [
|
|
1768
|
+
new Flatten.Circle(
|
|
1769
|
+
new Flatten.Point(element.x, element.y),
|
|
1770
|
+
element.outer_diameter / 2
|
|
1771
|
+
)
|
|
1772
|
+
]
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
if (element.type === "pcb_smtpad") return getSmtPadGeometry(element);
|
|
1776
|
+
return getPlatedHoleGeometry(
|
|
1777
|
+
element,
|
|
1778
|
+
element.pcb_component_id ? componentCcwRotationsById.get(
|
|
1779
|
+
toPcbComponentId(element.pcb_component_id)
|
|
1780
|
+
) ?? 0 : 0
|
|
1781
|
+
);
|
|
1782
|
+
};
|
|
1783
|
+
var getCopperElementId = (element) => {
|
|
1784
|
+
if (element.type === "pcb_via") return element.pcb_via_id;
|
|
1785
|
+
if (element.type === "pcb_smtpad") return element.pcb_smtpad_id;
|
|
1786
|
+
return element.pcb_plated_hole_id;
|
|
1787
|
+
};
|
|
1788
|
+
var getCopperElementLabel = (element) => {
|
|
1789
|
+
if (element.type === "pcb_via") return "Via";
|
|
1790
|
+
if (element.type === "pcb_smtpad") return "SMT pad";
|
|
1791
|
+
return "Plated hole";
|
|
1792
|
+
};
|
|
1793
|
+
var measureClearance = (board, geometry) => {
|
|
1794
|
+
if (geometry.kind === "shapes") {
|
|
1795
|
+
const isInside2 = geometry.shapes.every((shape) => board.contains(shape));
|
|
1796
|
+
return {
|
|
1797
|
+
isInside: isInside2,
|
|
1798
|
+
clearance: isInside2 ? Math.min(
|
|
1799
|
+
...geometry.shapes.map((shape) => board.distanceTo(shape)[0])
|
|
1800
|
+
) : 0
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
const centerLineClearance = board.distanceTo(geometry.centerLine)[0];
|
|
1804
|
+
const clearance = centerLineClearance - geometry.radius;
|
|
1805
|
+
const isInside = board.contains(geometry.centerLine) && clearance >= -GEOMETRY_EPSILON;
|
|
1806
|
+
return {
|
|
1807
|
+
isInside,
|
|
1808
|
+
clearance: isInside ? Math.max(0, clearance) : 0
|
|
1809
|
+
};
|
|
1810
|
+
};
|
|
1811
|
+
function checkCopperToBoardEdgeClearance(circuitJson) {
|
|
1530
1812
|
const board = getPcbBoard(circuitJson);
|
|
1531
1813
|
if (!board) return [];
|
|
1532
|
-
const
|
|
1533
|
-
if (
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
const
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
const
|
|
1814
|
+
const boardPolygon = boardToPolygon(board);
|
|
1815
|
+
if (!boardPolygon) return [];
|
|
1816
|
+
const requiredClearance = getBoardDrcValue(board, "min_board_edge_clearance") ?? jlcMinTolerances.min_board_edge_clearance;
|
|
1817
|
+
if (requiredClearance === void 0) return [];
|
|
1818
|
+
const copperElements = circuitJson.filter(
|
|
1819
|
+
(element) => element.type === "pcb_via" || element.type === "pcb_smtpad" || element.type === "pcb_plated_hole"
|
|
1820
|
+
);
|
|
1821
|
+
const componentCcwRotationsById = new Map(
|
|
1822
|
+
circuitJson.filter(
|
|
1823
|
+
(element) => element.type === "pcb_component"
|
|
1824
|
+
).map((component) => [
|
|
1825
|
+
toPcbComponentId(component.pcb_component_id),
|
|
1826
|
+
component.rotation
|
|
1827
|
+
])
|
|
1828
|
+
);
|
|
1540
1829
|
const errors = [];
|
|
1541
|
-
for (const
|
|
1542
|
-
const
|
|
1543
|
-
|
|
1544
|
-
const
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
if (viaMinX < boardMinX + boardEdgeClearance || viaMaxX > boardMaxX - boardEdgeClearance || viaMinY < boardMinY + boardEdgeClearance || viaMaxY > boardMaxY - boardEdgeClearance) {
|
|
1548
|
-
const viaName = getReadableNameForElement3(circuitJson, via.pcb_via_id);
|
|
1549
|
-
errors.push({
|
|
1550
|
-
type: "pcb_placement_error",
|
|
1551
|
-
pcb_placement_error_id: `out_of_board_${via.pcb_via_id}`,
|
|
1552
|
-
message: `Via ${viaName} is outside or crossing the board boundary`,
|
|
1553
|
-
error_type: "pcb_placement_error"
|
|
1554
|
-
});
|
|
1830
|
+
for (const element of copperElements) {
|
|
1831
|
+
const geometry = getCopperGeometry(element, componentCcwRotationsById);
|
|
1832
|
+
if (!geometry) continue;
|
|
1833
|
+
const { isInside, clearance } = measureClearance(boardPolygon, geometry);
|
|
1834
|
+
if (isInside && clearance + GEOMETRY_EPSILON >= requiredClearance) {
|
|
1835
|
+
continue;
|
|
1555
1836
|
}
|
|
1837
|
+
const id = getCopperElementId(element);
|
|
1838
|
+
const label = getCopperElementLabel(element);
|
|
1839
|
+
errors.push({
|
|
1840
|
+
type: "pcb_placement_error",
|
|
1841
|
+
pcb_placement_error_id: `copper_too_close_to_board_edge_${id}`,
|
|
1842
|
+
error_type: "pcb_placement_error",
|
|
1843
|
+
message: `${label} ${id} violates copper-to-board-edge clearance (measured ${clearance.toFixed(3)}mm, required ${requiredClearance.toFixed(3)}mm)`
|
|
1844
|
+
});
|
|
1556
1845
|
}
|
|
1557
1846
|
return errors;
|
|
1558
1847
|
}
|
|
1559
1848
|
|
|
1849
|
+
// lib/check-pcb-components-out-of-board/checkViasOffBoard.ts
|
|
1850
|
+
function checkViasOffBoard(circuitJson) {
|
|
1851
|
+
const vias = circuitJson.filter((element) => element.type === "pcb_via");
|
|
1852
|
+
const violationsById = new Map(
|
|
1853
|
+
checkCopperToBoardEdgeClearance(
|
|
1854
|
+
circuitJson.filter(
|
|
1855
|
+
(element) => element.type === "pcb_board" || element.type === "pcb_via"
|
|
1856
|
+
)
|
|
1857
|
+
).map((error) => [
|
|
1858
|
+
error.pcb_placement_error_id.replace(
|
|
1859
|
+
"copper_too_close_to_board_edge_",
|
|
1860
|
+
""
|
|
1861
|
+
),
|
|
1862
|
+
error
|
|
1863
|
+
])
|
|
1864
|
+
);
|
|
1865
|
+
return vias.flatMap((via) => {
|
|
1866
|
+
const violation = violationsById.get(via.pcb_via_id);
|
|
1867
|
+
if (!violation) return [];
|
|
1868
|
+
const viaName = getReadableNameForElement3(circuitJson, via.pcb_via_id);
|
|
1869
|
+
return [
|
|
1870
|
+
{
|
|
1871
|
+
...violation,
|
|
1872
|
+
pcb_placement_error_id: `out_of_board_${via.pcb_via_id}`,
|
|
1873
|
+
message: `Via ${viaName} is outside or crossing the board boundary`
|
|
1874
|
+
}
|
|
1875
|
+
];
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1560
1879
|
// lib/check-pcb-components-out-of-board/checkPcbComponentsOutOfBoard.ts
|
|
1561
|
-
import * as
|
|
1562
|
-
import { rotateDEG, applyToPoint } from "transformation-matrix";
|
|
1880
|
+
import * as Flatten2 from "@flatten-js/core";
|
|
1881
|
+
import { rotateDEG as rotateDEG2, applyToPoint as applyToPoint2 } from "transformation-matrix";
|
|
1563
1882
|
function isPolygonCCW(poly) {
|
|
1564
1883
|
return poly.area() >= 0;
|
|
1565
1884
|
}
|
|
@@ -1573,29 +1892,29 @@ function rectanglePolygon({
|
|
|
1573
1892
|
const hw = size.width / 2;
|
|
1574
1893
|
const hh = size.height / 2;
|
|
1575
1894
|
const corners = [
|
|
1576
|
-
new
|
|
1577
|
-
new
|
|
1578
|
-
new
|
|
1579
|
-
new
|
|
1895
|
+
new Flatten2.Point(cx - hw, cy - hh),
|
|
1896
|
+
new Flatten2.Point(cx + hw, cy - hh),
|
|
1897
|
+
new Flatten2.Point(cx + hw, cy + hh),
|
|
1898
|
+
new Flatten2.Point(cx - hw, cy + hh)
|
|
1580
1899
|
];
|
|
1581
|
-
let poly = new
|
|
1900
|
+
let poly = new Flatten2.Polygon(corners);
|
|
1582
1901
|
if (rotationDeg) {
|
|
1583
|
-
const matrix =
|
|
1902
|
+
const matrix = rotateDEG2(rotationDeg, cx, cy);
|
|
1584
1903
|
const rotatedCorners = corners.map((pt) => {
|
|
1585
|
-
const p =
|
|
1586
|
-
return new
|
|
1904
|
+
const p = applyToPoint2(matrix, { x: pt.x, y: pt.y });
|
|
1905
|
+
return new Flatten2.Point(p.x, p.y);
|
|
1587
1906
|
});
|
|
1588
|
-
poly = new
|
|
1907
|
+
poly = new Flatten2.Polygon(rotatedCorners);
|
|
1589
1908
|
}
|
|
1590
1909
|
if (!isPolygonCCW(poly)) poly.reverse();
|
|
1591
1910
|
return poly;
|
|
1592
1911
|
}
|
|
1593
|
-
function
|
|
1912
|
+
function boardToPolygon2({
|
|
1594
1913
|
board
|
|
1595
1914
|
}) {
|
|
1596
1915
|
if (board.outline && board.outline.length > 0) {
|
|
1597
|
-
const points = board.outline.map((p) => new
|
|
1598
|
-
const poly = new
|
|
1916
|
+
const points = board.outline.map((p) => new Flatten2.Point(p.x, p.y));
|
|
1917
|
+
const poly = new Flatten2.Polygon(points);
|
|
1599
1918
|
if (!isPolygonCCW(poly)) {
|
|
1600
1919
|
poly.reverse();
|
|
1601
1920
|
}
|
|
@@ -1625,7 +1944,7 @@ function getComponentName({
|
|
|
1625
1944
|
return getReadableNameForComponent(circuitJson, component.pcb_component_id);
|
|
1626
1945
|
}
|
|
1627
1946
|
function computeOverlapDistance(compPoly, boardPoly, componentCenter, componentWidth, componentHeight, rotationDeg) {
|
|
1628
|
-
const centerPoint = new
|
|
1947
|
+
const centerPoint = new Flatten2.Point(componentCenter.x, componentCenter.y);
|
|
1629
1948
|
if (!boardPoly.contains(centerPoint)) {
|
|
1630
1949
|
const dist = boardPoly.distanceTo(centerPoint);
|
|
1631
1950
|
return Array.isArray(dist) ? dist[0] : Number(dist) || 0;
|
|
@@ -1646,10 +1965,10 @@ function computeOverlapDistance(compPoly, boardPoly, componentCenter, componentW
|
|
|
1646
1965
|
y: (corners[i].y + corners[next].y) / 2
|
|
1647
1966
|
});
|
|
1648
1967
|
}
|
|
1649
|
-
const matrix =
|
|
1968
|
+
const matrix = rotateDEG2(rotationDeg, componentCenter.x, componentCenter.y);
|
|
1650
1969
|
const rotatePoint2 = (pt) => {
|
|
1651
|
-
const p =
|
|
1652
|
-
return new
|
|
1970
|
+
const p = applyToPoint2(matrix, pt);
|
|
1971
|
+
return new Flatten2.Point(p.x, p.y);
|
|
1653
1972
|
};
|
|
1654
1973
|
const rotatedPoints = corners.concat(midpoints).map(rotatePoint2);
|
|
1655
1974
|
let maxDistance = 0;
|
|
@@ -1664,7 +1983,7 @@ function computeOverlapDistance(compPoly, boardPoly, componentCenter, componentW
|
|
|
1664
1983
|
return maxDistance;
|
|
1665
1984
|
}
|
|
1666
1985
|
try {
|
|
1667
|
-
const intersection =
|
|
1986
|
+
const intersection = Flatten2.BooleanOperations.intersect(
|
|
1668
1987
|
compPoly,
|
|
1669
1988
|
boardPoly
|
|
1670
1989
|
);
|
|
@@ -1734,7 +2053,7 @@ function checkPcbComponentsOutOfBoard(circuitJson) {
|
|
|
1734
2053
|
(el) => el.type === "pcb_board"
|
|
1735
2054
|
);
|
|
1736
2055
|
if (!board) return [];
|
|
1737
|
-
const boardPoly =
|
|
2056
|
+
const boardPoly = boardToPolygon2({ board });
|
|
1738
2057
|
if (!boardPoly) return [];
|
|
1739
2058
|
const components = circuitJson.filter(
|
|
1740
2059
|
(el) => el.type === "pcb_component"
|
|
@@ -1791,8 +2110,8 @@ function checkPcbComponentsOutOfBoard(circuitJson) {
|
|
|
1791
2110
|
|
|
1792
2111
|
// lib/check-pcb-component-over-cutout.ts
|
|
1793
2112
|
import { doBoundsOverlap } from "@tscircuit/math-utils";
|
|
1794
|
-
import * as
|
|
1795
|
-
import { applyToPoint as
|
|
2113
|
+
import * as Flatten3 from "@flatten-js/core";
|
|
2114
|
+
import { applyToPoint as applyToPoint3, rotateDEG as rotateDEG3 } from "transformation-matrix";
|
|
1796
2115
|
var CUTOUT_CIRCLE_SEGMENTS = 32;
|
|
1797
2116
|
function rectanglePolygon2({
|
|
1798
2117
|
center,
|
|
@@ -1808,11 +2127,11 @@ function rectanglePolygon2({
|
|
|
1808
2127
|
{ x: center.x + halfWidth, y: center.y + halfHeight },
|
|
1809
2128
|
{ x: center.x - halfWidth, y: center.y + halfHeight }
|
|
1810
2129
|
];
|
|
1811
|
-
const matrix =
|
|
1812
|
-
return new
|
|
2130
|
+
const matrix = rotateDEG3(rotation, center.x, center.y);
|
|
2131
|
+
return new Flatten3.Polygon(
|
|
1813
2132
|
corners.map((corner) => {
|
|
1814
|
-
const rotated = rotation ?
|
|
1815
|
-
return new
|
|
2133
|
+
const rotated = rotation ? applyToPoint3(matrix, corner) : corner;
|
|
2134
|
+
return new Flatten3.Point(rotated.x, rotated.y);
|
|
1816
2135
|
})
|
|
1817
2136
|
);
|
|
1818
2137
|
}
|
|
@@ -1820,10 +2139,10 @@ function circlePolygon({
|
|
|
1820
2139
|
center,
|
|
1821
2140
|
radius
|
|
1822
2141
|
}) {
|
|
1823
|
-
return new
|
|
2142
|
+
return new Flatten3.Polygon(
|
|
1824
2143
|
Array.from({ length: CUTOUT_CIRCLE_SEGMENTS }, (_, index) => {
|
|
1825
2144
|
const angle = 2 * Math.PI * index / CUTOUT_CIRCLE_SEGMENTS;
|
|
1826
|
-
return new
|
|
2145
|
+
return new Flatten3.Point(
|
|
1827
2146
|
center.x + Math.cos(angle) * radius,
|
|
1828
2147
|
center.y + Math.sin(angle) * radius
|
|
1829
2148
|
);
|
|
@@ -1843,8 +2162,8 @@ function cutoutToPolygon(cutout) {
|
|
|
1843
2162
|
return circlePolygon({ center: cutout.center, radius: cutout.radius });
|
|
1844
2163
|
}
|
|
1845
2164
|
if (cutout.shape === "polygon") {
|
|
1846
|
-
return new
|
|
1847
|
-
cutout.points.map((point) => new
|
|
2165
|
+
return new Flatten3.Polygon(
|
|
2166
|
+
cutout.points.map((point) => new Flatten3.Point(point.x, point.y))
|
|
1848
2167
|
);
|
|
1849
2168
|
}
|
|
1850
2169
|
return null;
|
|
@@ -1863,7 +2182,7 @@ function doPolygonsOverlap(polygonA, polygonB) {
|
|
|
1863
2182
|
}
|
|
1864
2183
|
if (polygonA.contains(polygonB) || polygonB.contains(polygonA)) return true;
|
|
1865
2184
|
try {
|
|
1866
|
-
const intersections =
|
|
2185
|
+
const intersections = Flatten3.BooleanOperations.intersect(
|
|
1867
2186
|
polygonA,
|
|
1868
2187
|
polygonB
|
|
1869
2188
|
);
|
|
@@ -3238,6 +3557,77 @@ function checkNoGroundPinDefined(circuitJson) {
|
|
|
3238
3557
|
return warnings;
|
|
3239
3558
|
}
|
|
3240
3559
|
|
|
3560
|
+
// lib/check-schematic-component-excessive-vertical-padding.ts
|
|
3561
|
+
var DEFAULT_PIN_SPACING = 0.2;
|
|
3562
|
+
var MAX_VERTICAL_PADDING_IN_PIN_SPACINGS = 3;
|
|
3563
|
+
var FLOATING_POINT_TOLERANCE = 1e-9;
|
|
3564
|
+
function checkSchematicComponentExcessiveVerticalPadding(circuitJson) {
|
|
3565
|
+
const schematicComponents = circuitJson.filter(
|
|
3566
|
+
(element) => element.type === "schematic_component"
|
|
3567
|
+
);
|
|
3568
|
+
const schematicPorts = circuitJson.filter(
|
|
3569
|
+
(element) => element.type === "schematic_port"
|
|
3570
|
+
);
|
|
3571
|
+
const sourceComponents = circuitJson.filter(
|
|
3572
|
+
(element) => element.type === "source_component"
|
|
3573
|
+
);
|
|
3574
|
+
const sourceComponentById = new Map(
|
|
3575
|
+
sourceComponents.map((component) => [
|
|
3576
|
+
component.source_component_id,
|
|
3577
|
+
component
|
|
3578
|
+
])
|
|
3579
|
+
);
|
|
3580
|
+
const portsByComponentId = /* @__PURE__ */ new Map();
|
|
3581
|
+
for (const port of schematicPorts) {
|
|
3582
|
+
if (!port.schematic_component_id) continue;
|
|
3583
|
+
const componentPorts = portsByComponentId.get(port.schematic_component_id) ?? [];
|
|
3584
|
+
componentPorts.push(port);
|
|
3585
|
+
portsByComponentId.set(port.schematic_component_id, componentPorts);
|
|
3586
|
+
}
|
|
3587
|
+
const warnings = [];
|
|
3588
|
+
for (const component of schematicComponents) {
|
|
3589
|
+
if (!component.is_box_with_pins || component.size.height <= 0) continue;
|
|
3590
|
+
const sidePorts = (portsByComponentId.get(component.schematic_component_id) ?? []).filter(
|
|
3591
|
+
(port) => port.side_of_component === "left" || port.side_of_component === "right"
|
|
3592
|
+
);
|
|
3593
|
+
if (sidePorts.length < 2) continue;
|
|
3594
|
+
const pinYs = sidePorts.map((port) => port.center.y);
|
|
3595
|
+
const highestPinY = Math.max(...pinYs);
|
|
3596
|
+
const lowestPinY = Math.min(...pinYs);
|
|
3597
|
+
const componentTopY = component.center.y + component.size.height / 2;
|
|
3598
|
+
const componentBottomY = component.center.y - component.size.height / 2;
|
|
3599
|
+
const pinSpacing = component.pin_spacing ?? DEFAULT_PIN_SPACING;
|
|
3600
|
+
const maximumPadding = pinSpacing * MAX_VERTICAL_PADDING_IN_PIN_SPACINGS;
|
|
3601
|
+
const paddingBySide = {
|
|
3602
|
+
top: componentTopY - highestPinY,
|
|
3603
|
+
bottom: lowestPinY - componentBottomY
|
|
3604
|
+
};
|
|
3605
|
+
const sourceComponent = component.source_component_id ? sourceComponentById.get(component.source_component_id) : void 0;
|
|
3606
|
+
const componentName = sourceComponent?.name ?? component.schematic_component_id;
|
|
3607
|
+
for (const side of ["top", "bottom"]) {
|
|
3608
|
+
const padding = paddingBySide[side];
|
|
3609
|
+
if (padding <= maximumPadding + FLOATING_POINT_TOLERANCE || padding <= 0) {
|
|
3610
|
+
continue;
|
|
3611
|
+
}
|
|
3612
|
+
const relativePosition = side === "top" ? "above" : "below";
|
|
3613
|
+
const stylingIssueType = `excessive_${side}_padding`;
|
|
3614
|
+
warnings.push({
|
|
3615
|
+
type: "schematic_component_styling_warning",
|
|
3616
|
+
schematic_component_styling_warning_id: `schematic_component_styling_warning_${component.schematic_component_id}_${stylingIssueType}`,
|
|
3617
|
+
warning_type: "schematic_component_styling_warning",
|
|
3618
|
+
message: `${componentName} has excessive empty space ${relativePosition} its pins (${padding.toFixed(2)}mm, more than ${MAX_VERTICAL_PADDING_IN_PIN_SPACINGS} pin spacings)`,
|
|
3619
|
+
schematic_component_id: component.schematic_component_id,
|
|
3620
|
+
styling_issue_type: stylingIssueType,
|
|
3621
|
+
schematic_port_ids: sidePorts.map((port) => port.schematic_port_id),
|
|
3622
|
+
source_component_id: component.source_component_id,
|
|
3623
|
+
schematic_sheet_id: component.schematic_sheet_id,
|
|
3624
|
+
subcircuit_id: component.subcircuit_id
|
|
3625
|
+
});
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
return warnings;
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3241
3631
|
// lib/check-connector-accessible-orientation.ts
|
|
3242
3632
|
import { getBoardBounds } from "@tscircuit/circuit-json-util";
|
|
3243
3633
|
function getFacingDirectionFromInsertionDirection(component) {
|
|
@@ -3508,7 +3898,7 @@ function checkTestPointAccessibility(circuitJson) {
|
|
|
3508
3898
|
// lib/run-all-checks.ts
|
|
3509
3899
|
async function runAllPlacementChecks(circuitJson) {
|
|
3510
3900
|
return [
|
|
3511
|
-
...
|
|
3901
|
+
...checkCopperToBoardEdgeClearance(circuitJson),
|
|
3512
3902
|
...checkViasInPads(circuitJson),
|
|
3513
3903
|
...checkPcbComponentsOutOfBoard(circuitJson),
|
|
3514
3904
|
...checkPcbComponentOverCutout(circuitJson),
|
|
@@ -3522,6 +3912,9 @@ async function runAllPlacementChecks(circuitJson) {
|
|
|
3522
3912
|
async function runAllNetlistChecks(circuitJson) {
|
|
3523
3913
|
return [...checkPinMustBeConnected(circuitJson)];
|
|
3524
3914
|
}
|
|
3915
|
+
async function runAllSchematicChecks(circuitJson) {
|
|
3916
|
+
return checkSchematicComponentExcessiveVerticalPadding(circuitJson);
|
|
3917
|
+
}
|
|
3525
3918
|
async function runAllPinSpecificationChecks(circuitJson) {
|
|
3526
3919
|
return [
|
|
3527
3920
|
...checkAllPinsInComponentAreUnderspecified(circuitJson),
|
|
@@ -3547,6 +3940,7 @@ async function runAllRoutingChecks(circuitJson) {
|
|
|
3547
3940
|
async function runAllChecks(circuitJson) {
|
|
3548
3941
|
return [
|
|
3549
3942
|
...await runAllPlacementChecks(circuitJson),
|
|
3943
|
+
...await runAllSchematicChecks(circuitJson),
|
|
3550
3944
|
...await runAllNetlistChecks(circuitJson),
|
|
3551
3945
|
...await runAllPinSpecificationChecks(circuitJson),
|
|
3552
3946
|
...await runAllRoutingChecks(circuitJson)
|
|
@@ -3556,6 +3950,7 @@ export {
|
|
|
3556
3950
|
NetManager,
|
|
3557
3951
|
checkAllPinsInComponentAreUnderspecified,
|
|
3558
3952
|
checkConnectorAccessibleOrientation,
|
|
3953
|
+
checkCopperToBoardEdgeClearance,
|
|
3559
3954
|
checkDifferentNetViaSpacing,
|
|
3560
3955
|
checkEachPcbPortConnectedToPcbTraces,
|
|
3561
3956
|
checkEachPcbTraceNonOverlapping,
|
|
@@ -3570,6 +3965,7 @@ export {
|
|
|
3570
3965
|
checkPcbTracesOutOfBoard,
|
|
3571
3966
|
checkPinMustBeConnected,
|
|
3572
3967
|
checkSameNetViaSpacing,
|
|
3968
|
+
checkSchematicComponentExcessiveVerticalPadding,
|
|
3573
3969
|
checkSourceTracesHavePcbTraces,
|
|
3574
3970
|
checkSourceTracesMatchPcbTraceThickness,
|
|
3575
3971
|
checkTestPointAccessibility,
|
|
@@ -3583,6 +3979,7 @@ export {
|
|
|
3583
3979
|
runAllNetlistChecks,
|
|
3584
3980
|
runAllPinSpecificationChecks,
|
|
3585
3981
|
runAllPlacementChecks,
|
|
3586
|
-
runAllRoutingChecks
|
|
3982
|
+
runAllRoutingChecks,
|
|
3983
|
+
runAllSchematicChecks
|
|
3587
3984
|
};
|
|
3588
3985
|
//# sourceMappingURL=index.js.map
|