@tscircuit/checks 0.0.158 → 0.0.160
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 +2 -1
- package/dist/index.d.ts +18 -1
- package/dist/index.js +519 -71
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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"
|
|
@@ -1789,6 +2108,132 @@ function checkPcbComponentsOutOfBoard(circuitJson) {
|
|
|
1789
2108
|
return errors;
|
|
1790
2109
|
}
|
|
1791
2110
|
|
|
2111
|
+
// lib/check-pcb-component-over-cutout.ts
|
|
2112
|
+
import { doBoundsOverlap } from "@tscircuit/math-utils";
|
|
2113
|
+
import * as Flatten3 from "@flatten-js/core";
|
|
2114
|
+
import { applyToPoint as applyToPoint3, rotateDEG as rotateDEG3 } from "transformation-matrix";
|
|
2115
|
+
var CUTOUT_CIRCLE_SEGMENTS = 32;
|
|
2116
|
+
function rectanglePolygon2({
|
|
2117
|
+
center,
|
|
2118
|
+
width,
|
|
2119
|
+
height,
|
|
2120
|
+
rotation = 0
|
|
2121
|
+
}) {
|
|
2122
|
+
const halfWidth = width / 2;
|
|
2123
|
+
const halfHeight = height / 2;
|
|
2124
|
+
const corners = [
|
|
2125
|
+
{ x: center.x - halfWidth, y: center.y - halfHeight },
|
|
2126
|
+
{ x: center.x + halfWidth, y: center.y - halfHeight },
|
|
2127
|
+
{ x: center.x + halfWidth, y: center.y + halfHeight },
|
|
2128
|
+
{ x: center.x - halfWidth, y: center.y + halfHeight }
|
|
2129
|
+
];
|
|
2130
|
+
const matrix = rotateDEG3(rotation, center.x, center.y);
|
|
2131
|
+
return new Flatten3.Polygon(
|
|
2132
|
+
corners.map((corner) => {
|
|
2133
|
+
const rotated = rotation ? applyToPoint3(matrix, corner) : corner;
|
|
2134
|
+
return new Flatten3.Point(rotated.x, rotated.y);
|
|
2135
|
+
})
|
|
2136
|
+
);
|
|
2137
|
+
}
|
|
2138
|
+
function circlePolygon({
|
|
2139
|
+
center,
|
|
2140
|
+
radius
|
|
2141
|
+
}) {
|
|
2142
|
+
return new Flatten3.Polygon(
|
|
2143
|
+
Array.from({ length: CUTOUT_CIRCLE_SEGMENTS }, (_, index) => {
|
|
2144
|
+
const angle = 2 * Math.PI * index / CUTOUT_CIRCLE_SEGMENTS;
|
|
2145
|
+
return new Flatten3.Point(
|
|
2146
|
+
center.x + Math.cos(angle) * radius,
|
|
2147
|
+
center.y + Math.sin(angle) * radius
|
|
2148
|
+
);
|
|
2149
|
+
})
|
|
2150
|
+
);
|
|
2151
|
+
}
|
|
2152
|
+
function cutoutToPolygon(cutout) {
|
|
2153
|
+
if (cutout.shape === "rect") {
|
|
2154
|
+
return rectanglePolygon2({
|
|
2155
|
+
center: cutout.center,
|
|
2156
|
+
width: cutout.width,
|
|
2157
|
+
height: cutout.height,
|
|
2158
|
+
rotation: cutout.rotation ?? 0
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
if (cutout.shape === "circle") {
|
|
2162
|
+
return circlePolygon({ center: cutout.center, radius: cutout.radius });
|
|
2163
|
+
}
|
|
2164
|
+
if (cutout.shape === "polygon") {
|
|
2165
|
+
return new Flatten3.Polygon(
|
|
2166
|
+
cutout.points.map((point) => new Flatten3.Point(point.x, point.y))
|
|
2167
|
+
);
|
|
2168
|
+
}
|
|
2169
|
+
return null;
|
|
2170
|
+
}
|
|
2171
|
+
function polygonBoxToBounds(polygon) {
|
|
2172
|
+
return {
|
|
2173
|
+
minX: polygon.box.xmin,
|
|
2174
|
+
minY: polygon.box.ymin,
|
|
2175
|
+
maxX: polygon.box.xmax,
|
|
2176
|
+
maxY: polygon.box.ymax
|
|
2177
|
+
};
|
|
2178
|
+
}
|
|
2179
|
+
function doPolygonsOverlap(polygonA, polygonB) {
|
|
2180
|
+
if (!doBoundsOverlap(polygonBoxToBounds(polygonA), polygonBoxToBounds(polygonB))) {
|
|
2181
|
+
return false;
|
|
2182
|
+
}
|
|
2183
|
+
if (polygonA.contains(polygonB) || polygonB.contains(polygonA)) return true;
|
|
2184
|
+
try {
|
|
2185
|
+
const intersections = Flatten3.BooleanOperations.intersect(
|
|
2186
|
+
polygonA,
|
|
2187
|
+
polygonB
|
|
2188
|
+
);
|
|
2189
|
+
if (Array.isArray(intersections)) {
|
|
2190
|
+
return intersections.some((polygon) => polygon.area() > 0);
|
|
2191
|
+
}
|
|
2192
|
+
return intersections.area() > 0;
|
|
2193
|
+
} catch {
|
|
2194
|
+
return false;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
function checkPcbComponentOverCutout(circuitJson) {
|
|
2198
|
+
const cutouts = circuitJson.filter(
|
|
2199
|
+
(element) => element.type === "pcb_cutout"
|
|
2200
|
+
);
|
|
2201
|
+
const components = circuitJson.filter(
|
|
2202
|
+
(element) => element.type === "pcb_component"
|
|
2203
|
+
);
|
|
2204
|
+
if (cutouts.length === 0 || components.length === 0) return [];
|
|
2205
|
+
const cutoutPolygons = cutouts.map((cutout) => ({ cutout, polygon: cutoutToPolygon(cutout) })).filter(
|
|
2206
|
+
(entry) => entry.polygon !== null && entry.polygon.area() > 0
|
|
2207
|
+
);
|
|
2208
|
+
const errors = [];
|
|
2209
|
+
for (const component of components) {
|
|
2210
|
+
if (!component.center || component.width <= 0 || component.height <= 0) {
|
|
2211
|
+
continue;
|
|
2212
|
+
}
|
|
2213
|
+
const componentPolygon = rectanglePolygon2({
|
|
2214
|
+
center: component.center,
|
|
2215
|
+
width: component.width,
|
|
2216
|
+
height: component.height
|
|
2217
|
+
});
|
|
2218
|
+
for (const { cutout, polygon: cutoutPolygon } of cutoutPolygons) {
|
|
2219
|
+
if (!doPolygonsOverlap(componentPolygon, cutoutPolygon)) continue;
|
|
2220
|
+
const componentName = getReadableNameForComponent(
|
|
2221
|
+
circuitJson,
|
|
2222
|
+
component.pcb_component_id
|
|
2223
|
+
);
|
|
2224
|
+
const cutoutId = cutout.pcb_cutout_id;
|
|
2225
|
+
errors.push({
|
|
2226
|
+
type: "pcb_placement_error",
|
|
2227
|
+
pcb_placement_error_id: `component_over_cutout_${component.pcb_component_id}_${cutoutId}`,
|
|
2228
|
+
error_type: "pcb_placement_error",
|
|
2229
|
+
message: `Component ${componentName} overlaps with pcb_cutout [${cutoutId}]`,
|
|
2230
|
+
subcircuit_id: component.subcircuit_id
|
|
2231
|
+
});
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
return errors;
|
|
2235
|
+
}
|
|
2236
|
+
|
|
1792
2237
|
// lib/check-same-net-via-spacing.ts
|
|
1793
2238
|
import { getReadableNameForElement as getReadableNameForElement4 } from "@tscircuit/circuit-json-util";
|
|
1794
2239
|
import {
|
|
@@ -2377,12 +2822,12 @@ import {
|
|
|
2377
2822
|
getBoundsOfPcbElements as getBoundsOfPcbElements5,
|
|
2378
2823
|
getPrimaryId as getPrimaryId2
|
|
2379
2824
|
} from "@tscircuit/circuit-json-util";
|
|
2380
|
-
import { doBoundsOverlap as
|
|
2825
|
+
import { doBoundsOverlap as doBoundsOverlap3 } from "@tscircuit/math-utils";
|
|
2381
2826
|
import { getFullConnectivityMapFromCircuitJson as getFullConnectivityMapFromCircuitJson7 } from "circuit-json-to-connectivity-map";
|
|
2382
2827
|
|
|
2383
2828
|
// lib/check-pcb-components-overlap/doPcbElementsOverlap.ts
|
|
2384
2829
|
import { getBoundsOfPcbElements as getBoundsOfPcbElements4 } from "@tscircuit/circuit-json-util";
|
|
2385
|
-
import { doBoundsOverlap } from "@tscircuit/math-utils";
|
|
2830
|
+
import { doBoundsOverlap as doBoundsOverlap2 } from "@tscircuit/math-utils";
|
|
2386
2831
|
function getElementLayers(elem) {
|
|
2387
2832
|
if (elem.type === "pcb_courtyard_circle" || elem.type === "pcb_courtyard_outline" || elem.type === "pcb_courtyard_polygon" || elem.type === "pcb_courtyard_rect") {
|
|
2388
2833
|
return [elem.layer];
|
|
@@ -2402,7 +2847,7 @@ function doPcbElementsOverlap(elem1, elem2) {
|
|
|
2402
2847
|
}
|
|
2403
2848
|
const bounds1 = getBoundsOfPcbElements4([elem1]);
|
|
2404
2849
|
const bounds2 = getBoundsOfPcbElements4([elem2]);
|
|
2405
|
-
return
|
|
2850
|
+
return doBoundsOverlap2(bounds1, bounds2);
|
|
2406
2851
|
}
|
|
2407
2852
|
|
|
2408
2853
|
// lib/check-pcb-components-overlap/checkPcbComponentOverlap.ts
|
|
@@ -2475,7 +2920,7 @@ function checkPcbComponentOverlap(circuitJson) {
|
|
|
2475
2920
|
if (!comp1.bounds || !comp2.bounds) {
|
|
2476
2921
|
continue;
|
|
2477
2922
|
}
|
|
2478
|
-
if (!
|
|
2923
|
+
if (!doBoundsOverlap3(comp1.bounds, comp2.bounds)) {
|
|
2479
2924
|
continue;
|
|
2480
2925
|
}
|
|
2481
2926
|
for (const elem1 of comp1.elements) {
|
|
@@ -3382,9 +3827,10 @@ function checkTestPointAccessibility(circuitJson) {
|
|
|
3382
3827
|
// lib/run-all-checks.ts
|
|
3383
3828
|
async function runAllPlacementChecks(circuitJson) {
|
|
3384
3829
|
return [
|
|
3385
|
-
...
|
|
3830
|
+
...checkCopperToBoardEdgeClearance(circuitJson),
|
|
3386
3831
|
...checkViasInPads(circuitJson),
|
|
3387
3832
|
...checkPcbComponentsOutOfBoard(circuitJson),
|
|
3833
|
+
...checkPcbComponentOverCutout(circuitJson),
|
|
3388
3834
|
...checkPcbComponentOverlap(circuitJson),
|
|
3389
3835
|
...checkPadPadClearance(circuitJson),
|
|
3390
3836
|
...checkCourtyardOverlap(circuitJson),
|
|
@@ -3429,6 +3875,7 @@ export {
|
|
|
3429
3875
|
NetManager,
|
|
3430
3876
|
checkAllPinsInComponentAreUnderspecified,
|
|
3431
3877
|
checkConnectorAccessibleOrientation,
|
|
3878
|
+
checkCopperToBoardEdgeClearance,
|
|
3432
3879
|
checkDifferentNetViaSpacing,
|
|
3433
3880
|
checkEachPcbPortConnectedToPcbTraces,
|
|
3434
3881
|
checkEachPcbTraceNonOverlapping,
|
|
@@ -3436,6 +3883,7 @@ export {
|
|
|
3436
3883
|
checkNoPowerPinDefined,
|
|
3437
3884
|
checkPadPadClearance,
|
|
3438
3885
|
checkPadTraceClearance,
|
|
3886
|
+
checkPcbComponentOverCutout,
|
|
3439
3887
|
checkPcbComponentOverlap,
|
|
3440
3888
|
checkPcbComponentsOutOfBoard,
|
|
3441
3889
|
checkPcbTraceLengths,
|