@tscircuit/checks 0.0.125 → 0.0.126

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
@@ -243,7 +243,7 @@ import {
243
243
  segmentToBoundsMinDistance,
244
244
  segmentToCircleMinDistance
245
245
  } from "@tscircuit/math-utils";
246
- import { segmentToSegmentMinDistance } from "@tscircuit/math-utils";
246
+ import { segmentToSegmentMinDistance as segmentToSegmentMinDistance2 } from "@tscircuit/math-utils";
247
247
  import {
248
248
  getFullConnectivityMapFromCircuitJson as getFullConnectivityMapFromCircuitJson2
249
249
  } from "circuit-json-to-connectivity-map";
@@ -636,6 +636,156 @@ var clamp = (value, min, max) => {
636
636
 
637
637
  // lib/check-each-pcb-trace-non-overlapping/getCollidableBounds.ts
638
638
  import { getBoundsOfPcbElements as getBoundsOfPcbElements2 } from "@tscircuit/circuit-json-util";
639
+
640
+ // lib/check-each-pcb-trace-non-overlapping/segment-to-polygon-clearance.ts
641
+ import {
642
+ distSq,
643
+ getSegmentIntersection,
644
+ isPointInsidePolygon,
645
+ pointToSegmentClosestPoint,
646
+ segmentToSegmentMinDistance
647
+ } from "@tscircuit/math-utils";
648
+ var rotatePoint = (point, angleDegrees) => {
649
+ const angle = angleDegrees * Math.PI / 180;
650
+ return {
651
+ x: point.x * Math.cos(angle) - point.y * Math.sin(angle),
652
+ y: point.x * Math.sin(angle) + point.y * Math.cos(angle)
653
+ };
654
+ };
655
+ var getRotatedRectPoints = ({
656
+ x,
657
+ y,
658
+ width,
659
+ height,
660
+ ccwRotation
661
+ }) => {
662
+ const halfWidth = width / 2;
663
+ const halfHeight = height / 2;
664
+ return [
665
+ { x: -halfWidth, y: -halfHeight },
666
+ { x: halfWidth, y: -halfHeight },
667
+ { x: halfWidth, y: halfHeight },
668
+ { x: -halfWidth, y: halfHeight }
669
+ ].map((point) => {
670
+ const rotated = rotatePoint(point, ccwRotation);
671
+ return { x: x + rotated.x, y: y + rotated.y };
672
+ });
673
+ };
674
+ var getPolygonPointsForPad = (pad) => {
675
+ if (pad.type === "pcb_smtpad") {
676
+ if (pad.shape === "polygon") return pad.points;
677
+ if (pad.shape === "rotated_rect") {
678
+ return getRotatedRectPoints({
679
+ x: pad.x,
680
+ y: pad.y,
681
+ width: pad.width,
682
+ height: pad.height,
683
+ ccwRotation: pad.ccw_rotation
684
+ });
685
+ }
686
+ }
687
+ if (pad.type === "pcb_plated_hole" && "rect_pad_width" in pad && "rect_pad_height" in pad) {
688
+ return getRotatedRectPoints({
689
+ x: pad.x,
690
+ y: pad.y,
691
+ width: pad.rect_pad_width,
692
+ height: pad.rect_pad_height,
693
+ ccwRotation: "rect_ccw_rotation" in pad && typeof pad.rect_ccw_rotation === "number" ? pad.rect_ccw_rotation : 0
694
+ });
695
+ }
696
+ throw new Error(
697
+ `Expected polygonal pad geometry, got ${pad.type} with shape "${pad.shape}"`
698
+ );
699
+ };
700
+ var getPolygonEdges = (points) => points.map(
701
+ (point, index) => [point, points[(index + 1) % points.length]]
702
+ );
703
+ var getClosestPointsBetweenSegments = (a1, a2, b1, b2) => {
704
+ const intersection = getSegmentIntersection(a1, a2, b1, b2);
705
+ if (intersection) {
706
+ return {
707
+ distance: 0,
708
+ pointOnA: intersection,
709
+ pointOnB: intersection,
710
+ center: intersection
711
+ };
712
+ }
713
+ const candidates = [
714
+ { pointOnA: a1, pointOnB: pointToSegmentClosestPoint(a1, b1, b2) },
715
+ { pointOnA: a2, pointOnB: pointToSegmentClosestPoint(a2, b1, b2) },
716
+ { pointOnA: pointToSegmentClosestPoint(b1, a1, a2), pointOnB: b1 },
717
+ { pointOnA: pointToSegmentClosestPoint(b2, a1, a2), pointOnB: b2 }
718
+ ];
719
+ let best = candidates[0];
720
+ let bestDistanceSquared = distSq(best.pointOnA, best.pointOnB);
721
+ for (const candidate of candidates.slice(1)) {
722
+ const candidateDistanceSquared = distSq(
723
+ candidate.pointOnA,
724
+ candidate.pointOnB
725
+ );
726
+ if (candidateDistanceSquared < bestDistanceSquared) {
727
+ best = candidate;
728
+ bestDistanceSquared = candidateDistanceSquared;
729
+ }
730
+ }
731
+ return {
732
+ distance: segmentToSegmentMinDistance(a1, a2, b1, b2),
733
+ pointOnA: best.pointOnA,
734
+ pointOnB: best.pointOnB,
735
+ center: {
736
+ x: (best.pointOnA.x + best.pointOnB.x) / 2,
737
+ y: (best.pointOnA.y + best.pointOnB.y) / 2
738
+ }
739
+ };
740
+ };
741
+ var getSegmentToPolygonClearance = (segment, polygon) => {
742
+ const start = { x: segment.x1, y: segment.y1 };
743
+ const end = { x: segment.x2, y: segment.y2 };
744
+ if (polygon.length < 3) {
745
+ return { distance: Number.POSITIVE_INFINITY, center: start };
746
+ }
747
+ const intersections = getPolygonEdges(polygon).map(
748
+ ([edgeStart, edgeEnd]) => getSegmentIntersection(start, end, edgeStart, edgeEnd)
749
+ ).filter((point) => point !== null);
750
+ if (intersections.length > 0) {
751
+ const dx = end.x - start.x;
752
+ const dy = end.y - start.y;
753
+ const lengthSquared = dx * dx + dy * dy;
754
+ intersections.sort((a, b) => {
755
+ const ta = ((a.x - start.x) * dx + (a.y - start.y) * dy) / lengthSquared;
756
+ const tb = ((b.x - start.x) * dx + (b.y - start.y) * dy) / lengthSquared;
757
+ return ta - tb;
758
+ });
759
+ return { distance: 0, center: intersections[0] };
760
+ }
761
+ if (isPointInsidePolygon(start, polygon) || isPointInsidePolygon(end, polygon)) {
762
+ return {
763
+ distance: 0,
764
+ center: {
765
+ x: (start.x + end.x) / 2,
766
+ y: (start.y + end.y) / 2
767
+ }
768
+ };
769
+ }
770
+ let best = getClosestPointsBetweenSegments(
771
+ start,
772
+ end,
773
+ polygon[0],
774
+ polygon[1]
775
+ );
776
+ for (const [edgeStart, edgeEnd] of getPolygonEdges(polygon).slice(1)) {
777
+ const candidate = getClosestPointsBetweenSegments(
778
+ start,
779
+ end,
780
+ edgeStart,
781
+ edgeEnd
782
+ );
783
+ if (candidate.distance < best.distance) best = candidate;
784
+ }
785
+ return { distance: best.distance, center: best.center };
786
+ };
787
+
788
+ // lib/check-each-pcb-trace-non-overlapping/getCollidableBounds.ts
639
789
  var getCollidableBounds = (collidable) => {
640
790
  if (collidable.type === "pcb_trace_segment") {
641
791
  return {
@@ -645,6 +795,18 @@ var getCollidableBounds = (collidable) => {
645
795
  maxY: Math.max(collidable.y1, collidable.y2)
646
796
  };
647
797
  }
798
+ if (collidable.type === "pcb_smtpad" || collidable.type === "pcb_plated_hole") {
799
+ const isPolygon = collidable.type === "pcb_smtpad" && (collidable.shape === "rotated_rect" || collidable.shape === "polygon") || collidable.type === "pcb_plated_hole" && "rect_pad_width" in collidable && "rect_pad_height" in collidable;
800
+ if (isPolygon) {
801
+ const polygonPoints = getPolygonPointsForPad(collidable);
802
+ return {
803
+ minX: Math.min(...polygonPoints.map((point) => point.x)),
804
+ minY: Math.min(...polygonPoints.map((point) => point.y)),
805
+ maxX: Math.max(...polygonPoints.map((point) => point.x)),
806
+ maxY: Math.max(...polygonPoints.map((point) => point.y))
807
+ };
808
+ }
809
+ }
648
810
  return getBoundsOfPcbElements2([collidable]);
649
811
  };
650
812
 
@@ -763,7 +925,7 @@ function checkEachPcbTraceNonOverlapping(circuitJson, {
763
925
  if (segmentA.layer !== segmentB.layer) continue;
764
926
  if (connMap.areIdsConnected(segmentA.pcb_trace_id, segmentB.pcb_trace_id))
765
927
  continue;
766
- const gap2 = segmentToSegmentMinDistance(
928
+ const gap2 = segmentToSegmentMinDistance2(
767
929
  { x: segmentA.x1, y: segmentA.y1 },
768
930
  { x: segmentA.x2, y: segmentA.y2 },
769
931
  { x: segmentB.x1, y: segmentB.y1 },
@@ -838,6 +1000,40 @@ function checkEachPcbTraceNonOverlapping(circuitJson, {
838
1000
  ].filter(Boolean)
839
1001
  });
840
1002
  }
1003
+ const isPolygon = obj.type === "pcb_smtpad" && (obj.shape === "rotated_rect" || obj.shape === "polygon") || obj.type === "pcb_plated_hole" && "rect_pad_width" in obj && "rect_pad_height" in obj;
1004
+ if (isPolygon) {
1005
+ const padOutline = getPolygonPointsForPad(obj);
1006
+ const { distance: distance4, center } = getSegmentToPolygonClearance(
1007
+ segmentA,
1008
+ padOutline
1009
+ );
1010
+ const gap2 = distance4 - segmentA.thickness / 2;
1011
+ if (gap2 > minClearance - EPSILON) continue;
1012
+ const pcb_trace_error_id = `overlap_${segmentA.pcb_trace_id}_${primaryObjId}`;
1013
+ if (errorIds.has(pcb_trace_error_id)) continue;
1014
+ errorIds.add(pcb_trace_error_id);
1015
+ errors.push({
1016
+ type: "pcb_trace_error",
1017
+ error_type: "pcb_trace_error",
1018
+ message: constructErrorMessage(
1019
+ getReadableName(segmentA.pcb_trace_id),
1020
+ `${obj.type} "${getReadableName(getPrimaryId(obj))}"`,
1021
+ gap2
1022
+ ),
1023
+ pcb_trace_id: segmentA.pcb_trace_id,
1024
+ center,
1025
+ source_trace_id: "",
1026
+ pcb_trace_error_id,
1027
+ pcb_component_ids: [
1028
+ "pcb_component_id" in obj ? obj.pcb_component_id : void 0
1029
+ ].filter(Boolean),
1030
+ pcb_port_ids: [
1031
+ ...getPcbPortIdsConnectedToTraces([segmentA._pcbTrace]),
1032
+ "pcb_port_id" in obj ? obj.pcb_port_id : void 0
1033
+ ].filter(Boolean)
1034
+ });
1035
+ continue;
1036
+ }
841
1037
  const gap = segmentToBoundsMinDistance(
842
1038
  { x: segmentA.x1, y: segmentA.y1 },
843
1039
  { x: segmentA.x2, y: segmentA.y2 },
@@ -1041,11 +1237,11 @@ function computeOverlapDistance(compPoly, boardPoly, componentCenter, componentW
1041
1237
  });
1042
1238
  }
1043
1239
  const matrix = rotateDEG(rotationDeg, componentCenter.x, componentCenter.y);
1044
- const rotatePoint = (pt) => {
1240
+ const rotatePoint2 = (pt) => {
1045
1241
  const p = applyToPoint(matrix, pt);
1046
1242
  return new Flatten.Point(p.x, p.y);
1047
1243
  };
1048
- const rotatedPoints = corners.concat(midpoints).map(rotatePoint);
1244
+ const rotatedPoints = corners.concat(midpoints).map(rotatePoint2);
1049
1245
  let maxDistance = 0;
1050
1246
  for (const pt of rotatedPoints) {
1051
1247
  if (!boardPoly.contains(pt)) {
@@ -1420,7 +1616,7 @@ function checkSourceTracesHavePcbTraces(circuitJson) {
1420
1616
 
1421
1617
  // lib/check-trace-out-of-board/checkTraceOutOfBoard.ts
1422
1618
  import { cju as cju3 } from "@tscircuit/circuit-json-util";
1423
- import { segmentToSegmentMinDistance as segmentToSegmentMinDistance2 } from "@tscircuit/math-utils";
1619
+ import { segmentToSegmentMinDistance as segmentToSegmentMinDistance3 } from "@tscircuit/math-utils";
1424
1620
  function getBoardPolygonPoints(board) {
1425
1621
  if (board.outline && board.outline.length > 0) {
1426
1622
  return board.outline.map((p) => ({ x: p.x, y: p.y }));
@@ -1464,7 +1660,7 @@ function checkPcbTracesOutOfBoard(circuitJson, config = {}) {
1464
1660
  for (let j = 0; j < boardPoints.length; j++) {
1465
1661
  const edgeStart = boardPoints[j];
1466
1662
  const edgeEnd = boardPoints[(j + 1) % boardPoints.length];
1467
- const distance4 = segmentToSegmentMinDistance2(
1663
+ const distance4 = segmentToSegmentMinDistance3(
1468
1664
  segmentStart,
1469
1665
  segmentEnd,
1470
1666
  edgeStart,
@@ -2245,7 +2441,7 @@ function checkConnectorAccessibleOrientation(circuitJson) {
2245
2441
  // lib/check-courtyard-overlap/checkCourtyardOverlap.ts
2246
2442
  import {
2247
2443
  doSegmentsIntersect,
2248
- isPointInsidePolygon
2444
+ isPointInsidePolygon as isPointInsidePolygon2
2249
2445
  } from "@tscircuit/math-utils";
2250
2446
  function getCourtyardPolygon(el) {
2251
2447
  if (el.type === "pcb_courtyard_rect") {
@@ -2291,8 +2487,8 @@ function getComponentName2(circuitJson, pcbComponentId) {
2291
2487
  return pcbComponentId;
2292
2488
  }
2293
2489
  function polygonsOverlap(polyA, polyB) {
2294
- if (polyA.some((p) => isPointInsidePolygon(p, polyB))) return true;
2295
- if (polyB.some((p) => isPointInsidePolygon(p, polyA))) return true;
2490
+ if (polyA.some((p) => isPointInsidePolygon2(p, polyB))) return true;
2491
+ if (polyB.some((p) => isPointInsidePolygon2(p, polyA))) return true;
2296
2492
  for (let i = 0; i < polyA.length; i++) {
2297
2493
  const a1 = polyA[i];
2298
2494
  const a2 = polyA[(i + 1) % polyA.length];