@tscircuit/checks 0.0.159 → 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 +12 -1
- package/dist/index.js +398 -78
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,12 +27,13 @@ and output an array of arrays for any issues found.
|
|
|
27
27
|
| [`checkSourceTracesHavePcbTraces`](./lib/check-source-traces-have-pcb-traces.ts) | Returns `pcb_trace_error` when source traces are missing corresponding `pcb_trace` routes. |
|
|
28
28
|
| [`checkTracesAreContiguous`](./lib/check-traces-are-contiguous/check-traces-are-contiguous.ts) | Returns `pcb_trace_error` when trace endpoints are floating or do not connect as expected. |
|
|
29
29
|
| [`checkViasOffBoard`](./lib/check-pcb-components-out-of-board/checkViasOffBoard.ts) | Returns `pcb_placement_error` if any PCB via lies outside or crosses the board boundary. |
|
|
30
|
+
| [`checkCopperToBoardEdgeClearance`](./lib/check-copper-to-board-edge-clearance.ts) | Checks via, SMT-pad, and plated-hole copper against the polygon board outline and required edge clearance. |
|
|
30
31
|
|
|
31
32
|
## Aggregate check runner functions
|
|
32
33
|
|
|
33
34
|
| Function | Description |
|
|
34
35
|
| --- | --- |
|
|
35
|
-
| [`runAllPlacementChecks`](./lib/run-all-checks.ts) | Runs placement checks (`
|
|
36
|
+
| [`runAllPlacementChecks`](./lib/run-all-checks.ts) | Runs placement checks (`checkCopperToBoardEdgeClearance`, `checkPcbComponentsOutOfBoard`, `checkPcbComponentOverlap`, `checkPadPadClearance`, `checkCourtyardOverlap`, `checkConnectorAccessibleOrientation`, and `checkTestPointAccessibility`). |
|
|
36
37
|
| [`runAllNetlistChecks`](./lib/run-all-checks.ts) | Runs netlist connectivity checks (currently `checkPinMustBeConnected`). |
|
|
37
38
|
| [`runAllPinSpecificationChecks`](./lib/run-all-checks.ts) | Runs pin specification checks (e.g. `checkAllPinsInComponentAreUnderspecified`, `checkNoPowerPinDefined`, and `checkNoGroundPinDefined`). |
|
|
38
39
|
| [`runAllRoutingChecks`](./lib/run-all-checks.ts) | Runs all routing checks currently enabled (`checkEachPcbPortConnectedToPcbTraces`, `checkSourceTracesHavePcbTraces`, `checkEachPcbTraceNonOverlapping`, `checkPadTraceClearance`, `checkViaTraceClearance`, same/different net via spacing, and `checkPcbTracesOutOfBoard`). Trace-obstacle pairs are classified before aggregation, so each pair produces one overlap or clearance diagnostic, never both. |
|
package/dist/index.d.ts
CHANGED
|
@@ -17,6 +17,17 @@ declare class NetManager {
|
|
|
17
17
|
|
|
18
18
|
declare function checkViasOffBoard(circuitJson: AnyCircuitElement[]): PcbPlacementError[];
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Checks the finished copper geometry of every via, SMT pad, and plated hole
|
|
22
|
+
* against the real board outline and its configured edge-clearance rule.
|
|
23
|
+
*
|
|
24
|
+
* Component and footprint rotations are normally composed into Circuit JSON's
|
|
25
|
+
* absolute coordinates and `ccw_rotation` fields. Polygon-pad plated holes are
|
|
26
|
+
* the exception: their outline stays local, so it is translated and rotated by
|
|
27
|
+
* the owning component here.
|
|
28
|
+
*/
|
|
29
|
+
declare function checkCopperToBoardEdgeClearance(circuitJson: AnyCircuitElement[]): PcbPlacementError[];
|
|
30
|
+
|
|
20
31
|
/**
|
|
21
32
|
* Main function — polygon-first: construct polygons, test containment / intersection,
|
|
22
33
|
* compute overlap distance using boolean intersection area or geometric distance.
|
|
@@ -172,4 +183,4 @@ declare function checkConnectorAccessibleOrientation(circuitJson: AnyCircuitElem
|
|
|
172
183
|
*/
|
|
173
184
|
declare function checkTestPointAccessibility(circuitJson: AnyCircuitElement[]): PcbPlacementError[];
|
|
174
185
|
|
|
175
|
-
export { NetManager, checkAllPinsInComponentAreUnderspecified, checkConnectorAccessibleOrientation, checkDifferentNetViaSpacing, checkEachPcbPortConnectedToPcbTraces, checkEachPcbTraceNonOverlapping, checkNoGroundPinDefined, checkNoPowerPinDefined, checkPadPadClearance, checkPadTraceClearance, checkPcbComponentOverCutout, checkPcbComponentOverlap, checkPcbComponentsOutOfBoard, checkPcbTraceLengths, checkPcbTracesOutOfBoard, checkPinMustBeConnected, checkSameNetViaSpacing, checkSourceTracesHavePcbTraces, checkSourceTracesMatchPcbTraceThickness, checkTestPointAccessibility, checkTracesAreContiguous, checkViaPadClearance, checkViaTraceClearance, checkViasInPads, checkViasOffBoard, dedupePcbDrcErrors, runAllChecks, runAllNetlistChecks, runAllPinSpecificationChecks, runAllPlacementChecks, runAllRoutingChecks };
|
|
186
|
+
export { NetManager, checkAllPinsInComponentAreUnderspecified, checkConnectorAccessibleOrientation, checkCopperToBoardEdgeClearance, checkDifferentNetViaSpacing, checkEachPcbPortConnectedToPcbTraces, checkEachPcbTraceNonOverlapping, checkNoGroundPinDefined, checkNoPowerPinDefined, checkPadPadClearance, checkPadTraceClearance, checkPcbComponentOverCutout, checkPcbComponentOverlap, checkPcbComponentsOutOfBoard, checkPcbTraceLengths, checkPcbTracesOutOfBoard, checkPinMustBeConnected, checkSameNetViaSpacing, checkSourceTracesHavePcbTraces, checkSourceTracesMatchPcbTraceThickness, checkTestPointAccessibility, checkTracesAreContiguous, checkViaPadClearance, checkViaTraceClearance, checkViasInPads, checkViasOffBoard, dedupePcbDrcErrors, runAllChecks, runAllNetlistChecks, runAllPinSpecificationChecks, runAllPlacementChecks, runAllRoutingChecks };
|
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
|
);
|
|
@@ -3508,7 +3827,7 @@ function checkTestPointAccessibility(circuitJson) {
|
|
|
3508
3827
|
// lib/run-all-checks.ts
|
|
3509
3828
|
async function runAllPlacementChecks(circuitJson) {
|
|
3510
3829
|
return [
|
|
3511
|
-
...
|
|
3830
|
+
...checkCopperToBoardEdgeClearance(circuitJson),
|
|
3512
3831
|
...checkViasInPads(circuitJson),
|
|
3513
3832
|
...checkPcbComponentsOutOfBoard(circuitJson),
|
|
3514
3833
|
...checkPcbComponentOverCutout(circuitJson),
|
|
@@ -3556,6 +3875,7 @@ export {
|
|
|
3556
3875
|
NetManager,
|
|
3557
3876
|
checkAllPinsInComponentAreUnderspecified,
|
|
3558
3877
|
checkConnectorAccessibleOrientation,
|
|
3878
|
+
checkCopperToBoardEdgeClearance,
|
|
3559
3879
|
checkDifferentNetViaSpacing,
|
|
3560
3880
|
checkEachPcbPortConnectedToPcbTraces,
|
|
3561
3881
|
checkEachPcbTraceNonOverlapping,
|