@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 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 (`checkViasOffBoard`, `checkPcbComponentsOutOfBoard`, `checkPcbComponentOverlap`, `checkPadPadClearance`, `checkCourtyardOverlap`, `checkConnectorAccessibleOrientation`, and `checkTestPointAccessibility`). |
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 pill = getPillCenterLineForPad(pad);
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
- pill.start,
187
- pill.end
186
+ pill2.start,
187
+ pill2.end
188
188
  );
189
189
  return {
190
190
  distance: closest.distance,
191
191
  center: closest.center,
192
- radius: pill.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 pill = getPillCenterLineForPad(pad);
238
- return pointToSegmentDistance(point, pill.start, pill.end) <= pill.radius;
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 pill = getPillCenterLineForPad(padA);
636
- return segmentToCircleMinDistance(pill.start, pill.end, getCircleShape(padB)) - pill.radius;
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 pill = getPillCenterLineForPad(padB);
640
- return segmentToCircleMinDistance(pill.start, pill.end, getCircleShape(padA)) - pill.radius;
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 pill = getPillCenterLineForPad(padA);
643
+ const pill2 = getPillCenterLineForPad(padA);
644
644
  return getSegmentToPolygonClearanceFromPoints(
645
- pill.start,
646
- pill.end,
645
+ pill2.start,
646
+ pill2.end,
647
647
  getPolygonShape(padB).points
648
- ).distance - pill.radius;
648
+ ).distance - pill2.radius;
649
649
  }
650
650
  if (isPillPad(padB)) {
651
- const pill = getPillCenterLineForPad(padB);
651
+ const pill2 = getPillCenterLineForPad(padB);
652
652
  return getSegmentToPolygonClearanceFromPoints(
653
- pill.start,
654
- pill.end,
653
+ pill2.start,
654
+ pill2.end,
655
655
  getPolygonShape(padA).points
656
- ).distance - pill.radius;
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 pill = getPillCenterLineForPad(collidable);
1183
+ const pill2 = getPillCenterLineForPad(collidable);
1184
1184
  return {
1185
- minX: Math.min(pill.start.x, pill.end.x) - pill.radius,
1186
- minY: Math.min(pill.start.y, pill.end.y) - pill.radius,
1187
- maxX: Math.max(pill.start.x, pill.end.x) + pill.radius,
1188
- maxY: Math.max(pill.start.y, pill.end.y) + pill.radius
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
- function checkViasOffBoard(circuitJson) {
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 vias = circuitJson.filter((el) => el.type === "pcb_via");
1533
- if (vias.length === 0) return [];
1534
- if (board.width === void 0 || board.height === void 0) return [];
1535
- const boardEdgeClearance = getBoardDrcValue(board, "min_board_edge_clearance") ?? jlcMinTolerances.min_board_edge_clearance;
1536
- const boardMinX = board.center.x - board.width / 2;
1537
- const boardMaxX = board.center.x + board.width / 2;
1538
- const boardMinY = board.center.y - board.height / 2;
1539
- const boardMaxY = board.center.y + board.height / 2;
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 via of vias) {
1542
- const viaRadius = via.outer_diameter / 2;
1543
- const viaMinX = via.x - viaRadius;
1544
- const viaMaxX = via.x + viaRadius;
1545
- const viaMinY = via.y - viaRadius;
1546
- const viaMaxY = via.y + viaRadius;
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 Flatten from "@flatten-js/core";
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 Flatten.Point(cx - hw, cy - hh),
1577
- new Flatten.Point(cx + hw, cy - hh),
1578
- new Flatten.Point(cx + hw, cy + hh),
1579
- new Flatten.Point(cx - hw, cy + hh)
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 Flatten.Polygon(corners);
1900
+ let poly = new Flatten2.Polygon(corners);
1582
1901
  if (rotationDeg) {
1583
- const matrix = rotateDEG(rotationDeg, cx, cy);
1902
+ const matrix = rotateDEG2(rotationDeg, cx, cy);
1584
1903
  const rotatedCorners = corners.map((pt) => {
1585
- const p = applyToPoint(matrix, { x: pt.x, y: pt.y });
1586
- return new Flatten.Point(p.x, p.y);
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 Flatten.Polygon(rotatedCorners);
1907
+ poly = new Flatten2.Polygon(rotatedCorners);
1589
1908
  }
1590
1909
  if (!isPolygonCCW(poly)) poly.reverse();
1591
1910
  return poly;
1592
1911
  }
1593
- function boardToPolygon({
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 Flatten.Point(p.x, p.y));
1598
- const poly = new Flatten.Polygon(points);
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 Flatten.Point(componentCenter.x, componentCenter.y);
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 = rotateDEG(rotationDeg, componentCenter.x, componentCenter.y);
1968
+ const matrix = rotateDEG2(rotationDeg, componentCenter.x, componentCenter.y);
1650
1969
  const rotatePoint2 = (pt) => {
1651
- const p = applyToPoint(matrix, pt);
1652
- return new Flatten.Point(p.x, p.y);
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 = Flatten.BooleanOperations.intersect(
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 = boardToPolygon({ board });
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 Flatten2 from "@flatten-js/core";
1795
- import { applyToPoint as applyToPoint2, rotateDEG as rotateDEG2 } from "transformation-matrix";
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 = rotateDEG2(rotation, center.x, center.y);
1812
- return new Flatten2.Polygon(
2130
+ const matrix = rotateDEG3(rotation, center.x, center.y);
2131
+ return new Flatten3.Polygon(
1813
2132
  corners.map((corner) => {
1814
- const rotated = rotation ? applyToPoint2(matrix, corner) : corner;
1815
- return new Flatten2.Point(rotated.x, rotated.y);
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 Flatten2.Polygon(
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 Flatten2.Point(
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 Flatten2.Polygon(
1847
- cutout.points.map((point) => new Flatten2.Point(point.x, point.y))
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 = Flatten2.BooleanOperations.intersect(
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
- ...checkViasOffBoard(circuitJson),
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,