@tscircuit/checks 0.0.124 → 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";
@@ -572,7 +572,7 @@ var getClosestPointBetweenSegments = (segmentA, segmentB) => {
572
572
  };
573
573
  const dx = closestOnA.x - closestOnB.x;
574
574
  const dy = closestOnA.y - closestOnB.y;
575
- const distance3 = Math.sqrt(dx * dx + dy * dy);
575
+ const distance4 = Math.sqrt(dx * dx + dy * dy);
576
576
  const averagePoint = {
577
577
  x: (closestOnA.x + closestOnB.x) / 2,
578
578
  y: (closestOnA.y + closestOnB.y) / 2
@@ -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 },
@@ -804,12 +966,12 @@ function checkEachPcbTraceNonOverlapping(circuitJson, {
804
966
  const isCircular = obj.type === "pcb_via" || obj.type === "pcb_plated_hole" && obj.shape === "circle" || obj.type === "pcb_hole" || obj.type === "pcb_smtpad" && obj.shape === "circle";
805
967
  if (isCircular) {
806
968
  const radius = getRadiusOfCircuitJsonElement(obj);
807
- const distance3 = segmentToCircleMinDistance(
969
+ const distance4 = segmentToCircleMinDistance(
808
970
  { x: segmentA.x1, y: segmentA.y1 },
809
971
  { x: segmentA.x2, y: segmentA.y2 },
810
972
  { x: obj.x, y: obj.y, radius }
811
973
  );
812
- const gap2 = distance3 - segmentA.thickness / 2;
974
+ const gap2 = distance4 - segmentA.thickness / 2;
813
975
  if (gap2 > minClearance - EPSILON) continue;
814
976
  const pcb_trace_error_id = `overlap_${segmentA.pcb_trace_id}_${primaryObjId}`;
815
977
  if (errorIds.has(pcb_trace_error_id)) continue;
@@ -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,14 +1660,14 @@ 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 distance3 = segmentToSegmentMinDistance2(
1663
+ const distance4 = segmentToSegmentMinDistance3(
1468
1664
  segmentStart,
1469
1665
  segmentEnd,
1470
1666
  edgeStart,
1471
1667
  edgeEnd
1472
1668
  );
1473
- if (distance3 < minDistance) {
1474
- minDistance = distance3;
1669
+ if (distance4 < minDistance) {
1670
+ minDistance = distance4;
1475
1671
  }
1476
1672
  }
1477
1673
  const minimumDistance = traceWidth / 2 + margin;
@@ -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];
@@ -2348,8 +2544,210 @@ function checkCourtyardOverlap(circuitJson) {
2348
2544
  return errors;
2349
2545
  }
2350
2546
 
2547
+ // lib/check-traces-are-contiguous/is-point-in-pad.ts
2548
+ function distance3(x1, y1, x2, y2) {
2549
+ return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
2550
+ }
2551
+ function isPointInPad(point, pad) {
2552
+ if (pad.type === "pcb_smtpad") {
2553
+ if (pad.shape === "circle") {
2554
+ return distance3(point.x, point.y, pad.x, pad.y) <= pad.radius;
2555
+ }
2556
+ if (pad.shape === "rect") {
2557
+ const halfWidth = pad.width / 2;
2558
+ const halfHeight = pad.height / 2;
2559
+ return Math.abs(point.x - pad.x) <= halfWidth && Math.abs(point.y - pad.y) <= halfHeight;
2560
+ }
2561
+ if (pad.shape === "rotated_rect") {
2562
+ const dx = point.x - pad.x;
2563
+ const dy = point.y - pad.y;
2564
+ const angle = -pad.ccw_rotation;
2565
+ const rotatedX = dx * Math.cos(angle) - dy * Math.sin(angle);
2566
+ const rotatedY = dx * Math.sin(angle) + dy * Math.cos(angle);
2567
+ return Math.abs(rotatedX) <= pad.width / 2 && Math.abs(rotatedY) <= pad.height / 2;
2568
+ }
2569
+ if (pad.shape === "pill") {
2570
+ const halfWidth = pad.width / 2;
2571
+ const halfHeight = pad.height / 2;
2572
+ const radius = pad.radius;
2573
+ if (Math.abs(point.x - pad.x) <= halfWidth - radius && Math.abs(point.y - pad.y) <= halfHeight) {
2574
+ return true;
2575
+ }
2576
+ const cornerX = Math.max(
2577
+ Math.abs(point.x - pad.x) - (halfWidth - radius),
2578
+ 0
2579
+ );
2580
+ const cornerY = Math.max(
2581
+ Math.abs(point.y - pad.y) - (halfHeight - radius),
2582
+ 0
2583
+ );
2584
+ return cornerX * cornerX + cornerY * cornerY <= radius * radius;
2585
+ }
2586
+ }
2587
+ if (pad.type === "pcb_plated_hole") {
2588
+ if (pad.shape === "circle") {
2589
+ return distance3(point.x, point.y, pad.x, pad.y) <= pad.outer_diameter / 2;
2590
+ }
2591
+ if (pad.shape === "oval" || pad.shape === "pill") {
2592
+ return Math.abs(point.x - pad.x) <= pad.outer_width / 2 && Math.abs(point.y - pad.y) <= pad.outer_height / 2;
2593
+ }
2594
+ if (pad.shape === "circular_hole_with_rect_pad") {
2595
+ return Math.abs(point.x - pad.x) <= pad.rect_pad_width / 2 && Math.abs(point.y - pad.y) <= pad.rect_pad_height / 2;
2596
+ }
2597
+ if (pad.shape === "pill_hole_with_rect_pad") {
2598
+ return Math.abs(point.x - pad.x) <= pad.rect_pad_width / 2 && Math.abs(point.y - pad.y) <= pad.rect_pad_height / 2;
2599
+ }
2600
+ }
2601
+ return false;
2602
+ }
2603
+
2351
2604
  // lib/check-traces-are-contiguous/check-traces-are-contiguous.ts
2352
- import { getReadableNameForPcbPort as getReadableNameForPcbPort2 } from "@tscircuit/circuit-json-util";
2605
+ import {
2606
+ getReadableNameForPcbPort as getReadableNameForPcbPort2,
2607
+ getReadableNameForPcbTrace
2608
+ } from "@tscircuit/circuit-json-util";
2609
+ function checkTracesAreContiguous(circuitJson) {
2610
+ const errors = [];
2611
+ const pcbPorts = circuitJson.filter(
2612
+ (el) => el.type === "pcb_port"
2613
+ );
2614
+ const pcbTraces = circuitJson.filter(
2615
+ (el) => el.type === "pcb_trace"
2616
+ );
2617
+ const sourceTraces = circuitJson.filter(
2618
+ (el) => el.type === "source_trace"
2619
+ );
2620
+ const pcbSmtPads = circuitJson.filter(
2621
+ (el) => el.type === "pcb_smtpad"
2622
+ );
2623
+ const pcbPlatedHoles = circuitJson.filter(
2624
+ (el) => el.type === "pcb_plated_hole"
2625
+ );
2626
+ const padMap = /* @__PURE__ */ new Map();
2627
+ for (const pad of pcbSmtPads) {
2628
+ if (pad.pcb_port_id) {
2629
+ padMap.set(pad.pcb_port_id, pad);
2630
+ }
2631
+ }
2632
+ for (const hole of pcbPlatedHoles) {
2633
+ if (hole.pcb_port_id) {
2634
+ padMap.set(hole.pcb_port_id, hole);
2635
+ }
2636
+ }
2637
+ for (const trace of pcbTraces) {
2638
+ if (trace.route.length === 0) continue;
2639
+ const firstPoint = trace.route[0];
2640
+ const lastPoint = trace.route[trace.route.length - 1];
2641
+ const sourceTrace = sourceTraces.find(
2642
+ (st) => st.source_trace_id === trace.source_trace_id
2643
+ );
2644
+ const expectedPorts = sourceTrace ? pcbPorts.filter(
2645
+ (port) => sourceTrace.connected_source_port_ids?.includes(port.source_port_id)
2646
+ ) : [];
2647
+ for (let i = 1; i < trace.route.length - 1; i++) {
2648
+ const prevPoint = trace.route[i - 1];
2649
+ const currentPoint = trace.route[i];
2650
+ const nextPoint = trace.route[i + 1];
2651
+ if (currentPoint.route_type === "via") {
2652
+ const prevIsWire = prevPoint.route_type === "wire";
2653
+ const nextIsWire = nextPoint.route_type === "wire";
2654
+ if (prevIsWire && nextIsWire) {
2655
+ const prevAligned = Math.abs(prevPoint.x - currentPoint.x) < 0.01 && Math.abs(prevPoint.y - currentPoint.y) < 0.01;
2656
+ const nextAligned = Math.abs(nextPoint.x - currentPoint.x) < 0.01 && Math.abs(nextPoint.y - currentPoint.y) < 0.01;
2657
+ if (!prevAligned || !nextAligned) {
2658
+ const traceName2 = getReadableNameForPcbTrace(
2659
+ circuitJson,
2660
+ trace.pcb_trace_id
2661
+ );
2662
+ errors.push({
2663
+ type: "pcb_trace_error",
2664
+ message: `Via in trace [${traceName2}] is misaligned at position {x: ${currentPoint.x}, y: ${currentPoint.y}}.`,
2665
+ source_trace_id: sourceTrace?.source_trace_id || trace.source_trace_id || `!${trace.pcb_trace_id}`,
2666
+ error_type: "pcb_trace_error",
2667
+ pcb_trace_id: trace.pcb_trace_id,
2668
+ pcb_trace_error_id: `misaligned_via_${trace.pcb_trace_id}_${i}`,
2669
+ pcb_component_ids: [],
2670
+ pcb_port_ids: []
2671
+ });
2672
+ }
2673
+ }
2674
+ }
2675
+ }
2676
+ const traceName = getReadableNameForPcbTrace(
2677
+ circuitJson,
2678
+ trace.pcb_trace_id
2679
+ );
2680
+ for (const port of expectedPorts) {
2681
+ if (!port.pcb_port_id) continue;
2682
+ const pad = padMap.get(port.pcb_port_id);
2683
+ if (!pad) continue;
2684
+ const isFirstPointConnected = firstPoint.route_type === "wire" && isPointInPad({ x: firstPoint.x, y: firstPoint.y }, pad);
2685
+ const isLastPointConnected = lastPoint.route_type === "wire" && isPointInPad({ x: lastPoint.x, y: lastPoint.y }, pad);
2686
+ if (!isFirstPointConnected && !isLastPointConnected) {
2687
+ const portName = getReadableNameForPcbPort2(
2688
+ circuitJson,
2689
+ port.pcb_port_id
2690
+ ).replace("pcb_port", "");
2691
+ const padType = pad.type.replace(/pcb_/, "");
2692
+ const errorCenter = {
2693
+ x: (firstPoint.x + lastPoint.x) / 2,
2694
+ y: (firstPoint.y + lastPoint.y) / 2
2695
+ };
2696
+ errors.push({
2697
+ type: "pcb_trace_error",
2698
+ message: `Trace [${traceName}] is missing a connection to ${padType}${portName}`,
2699
+ source_trace_id: sourceTrace?.source_trace_id || trace.source_trace_id || `!${trace.pcb_trace_id}`,
2700
+ error_type: "pcb_trace_error",
2701
+ pcb_trace_id: trace.pcb_trace_id,
2702
+ pcb_trace_error_id: `missing_connection_${trace.pcb_trace_id}_${port.pcb_port_id}`,
2703
+ center: errorCenter,
2704
+ pcb_component_ids: [],
2705
+ pcb_port_ids: [port.pcb_port_id]
2706
+ });
2707
+ }
2708
+ }
2709
+ if (expectedPorts.length === 0) {
2710
+ let firstConnectsToAnyPad = false;
2711
+ let lastConnectsToAnyPad = false;
2712
+ const endpointsAreSame = firstPoint.route_type === "wire" && lastPoint.route_type === "wire" && Math.abs(firstPoint.x - lastPoint.x) < 0.01 && Math.abs(firstPoint.y - lastPoint.y) < 0.01;
2713
+ for (const [portId, pad] of padMap) {
2714
+ if (firstPoint.route_type === "wire" && isPointInPad({ x: firstPoint.x, y: firstPoint.y }, pad)) {
2715
+ firstConnectsToAnyPad = true;
2716
+ }
2717
+ if (lastPoint.route_type === "wire" && isPointInPad({ x: lastPoint.x, y: lastPoint.y }, pad)) {
2718
+ lastConnectsToAnyPad = true;
2719
+ }
2720
+ }
2721
+ if (!firstConnectsToAnyPad && firstPoint.route_type === "wire") {
2722
+ errors.push({
2723
+ type: "pcb_trace_error",
2724
+ message: `Trace [${traceName}] has disconnected endpoint at (${firstPoint.x.toFixed(2)}, ${firstPoint.y.toFixed(2)})`,
2725
+ source_trace_id: sourceTrace?.source_trace_id || trace.source_trace_id || `!${trace.pcb_trace_id}`,
2726
+ error_type: "pcb_trace_error",
2727
+ pcb_trace_id: trace.pcb_trace_id,
2728
+ pcb_trace_error_id: `disconnected_endpoint_${trace.pcb_trace_id}_start`,
2729
+ center: { x: firstPoint.x, y: firstPoint.y },
2730
+ pcb_component_ids: [],
2731
+ pcb_port_ids: []
2732
+ });
2733
+ }
2734
+ if (!lastConnectsToAnyPad && lastPoint.route_type === "wire" && !(endpointsAreSame && !firstConnectsToAnyPad)) {
2735
+ errors.push({
2736
+ type: "pcb_trace_error",
2737
+ message: `Trace [${traceName}] has disconnected endpoint at (${lastPoint.x.toFixed(2)}, ${lastPoint.y.toFixed(2)})`,
2738
+ source_trace_id: sourceTrace?.source_trace_id || trace.source_trace_id || `!${trace.pcb_trace_id}`,
2739
+ error_type: "pcb_trace_error",
2740
+ pcb_trace_id: trace.pcb_trace_id,
2741
+ pcb_trace_error_id: `disconnected_endpoint_${trace.pcb_trace_id}_end`,
2742
+ center: { x: lastPoint.x, y: lastPoint.y },
2743
+ pcb_component_ids: [],
2744
+ pcb_port_ids: []
2745
+ });
2746
+ }
2747
+ }
2748
+ }
2749
+ return errors;
2750
+ }
2353
2751
 
2354
2752
  // lib/run-all-checks.ts
2355
2753
  async function runAllPlacementChecks(circuitJson) {
@@ -2381,7 +2779,7 @@ async function runAllRoutingChecks(circuitJson) {
2381
2779
  ...checkViaTraceClearance(circuitJson),
2382
2780
  ...checkSameNetViaSpacing(circuitJson),
2383
2781
  ...checkDifferentNetViaSpacing(circuitJson),
2384
- // ...checkTracesAreContiguous(circuitJson),
2782
+ ...checkTracesAreContiguous(circuitJson),
2385
2783
  ...checkPcbTracesOutOfBoard(circuitJson)
2386
2784
  ];
2387
2785
  }