@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/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"
@@ -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 doBoundsOverlap2 } from "@tscircuit/math-utils";
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 doBoundsOverlap(bounds1, bounds2);
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 (!doBoundsOverlap2(comp1.bounds, comp2.bounds)) {
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
- ...checkViasOffBoard(circuitJson),
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,