imrishabh18/pcbgolf

A complete 4-layer PCBGolf controller PCB with STM32H7 MCU, USB/SD and CAN interfaces, power regulation, protection, connectors, switches, LEDs, clocking, and supporting passives.

Version
1.6.1
License
unset
Stars
0

patches/@tscircuit%2Fchecks@0.0.189.patch

diff --git a/dist/index.js b/dist/index.js
--- a/dist/index.js
+++ b/dist/index.js
@@ -221,6 +221,13 @@
   return inside;
 }
 function isPointInPad(point, pad) {
+  const cornerRadius = roundedPadRadius(pad);
+  if (cornerRadius) {
+    const local = rotatePoint({x: point.x-pad.x, y: point.y-pad.y}, -(pad.ccw_rotation ?? 0));
+    const dx = Math.max(Math.abs(local.x) - (pad.width / 2 - cornerRadius), 0);
+    const dy = Math.max(Math.abs(local.y) - (pad.height / 2 - cornerRadius), 0);
+    return Math.hypot(dx, dy) <= cornerRadius + POINT_IN_PAD_TOLERANCE_MM;
+  }
   if (pad.type === "pcb_smtpad") {
     if (pad.shape === "circle") {
       return getDistanceBetweenPoints(point, pad) <= pad.radius + POINT_IN_PAD_TOLERANCE_MM;
@@ -712,7 +719,19 @@
     ]
   };
 };
+// Rounded rectangles are the Minkowski sum of an inset rectangle and a disk.
+// Use exact offset distances rather than the sharp-corner bounding rectangle.
+var roundedPadRadius = (pad) => pad.type === "pcb_smtpad" &&
+  (pad.shape === "rect" || pad.shape === "rotated_rect") ?
+  Math.max(0, Math.min(pad.corner_radius ?? 0, pad.width / 2, pad.height / 2)) : 0;
+var insetRoundedPad = (pad, radius) => ({ ...pad, corner_radius: 0,
+  width: pad.width - 2 * radius, height: pad.height - 2 * radius });
 var getPadToPadGap = (padA, padB) => {
+  const radiusA = roundedPadRadius(padA);
+  const radiusB = roundedPadRadius(padB);
+  if (radiusA || radiusB) return getPadToPadGap(
+    radiusA ? insetRoundedPad(padA, radiusA) : padA,
+    radiusB ? insetRoundedPad(padB, radiusB) : padB) - radiusA - radiusB;
   if (isPillPad(padA) && isPillPad(padB)) {
     const pillA = getPillCenterLineForPad(padA);
     const pillB = getPillCenterLineForPad(padB);
@@ -831,6 +850,11 @@
   return midpoint(traceEdge, obstacleEdge);
 };
 var getTraceObstacleClearance = (segment, obstacle) => {
+  const cornerRadius = roundedPadRadius(obstacle);
+  if (cornerRadius) {
+    const result = getTraceObstacleClearance(segment, insetRoundedPad(obstacle, cornerRadius));
+    return { ...result, gap: result.gap - cornerRadius };
+  }
   const start = { x: segment.x1, y: segment.y1 };
   const end = { x: segment.x2, y: segment.y2 };
   const traceRadius = segment.thickness / 2;
@@ -2978,7 +3002,99 @@
     y: (firstPointCenter.y + lastPointCenter.y) / 2
   };
 }
+// Physical copper graph: logical source IDs never establish a physical contact.
+// Include trace branches, intermediate pad contacts, barrels, and filled BREP
+// regions (including their voids). The original checker only followed traces
+// and endpoint pads, so branches ending at separate vias appeared disconnected.
+function getPhysicalCopperConnectivity(circuitJson) {
+  const parent = new Map();
+  const idOf = (e) => e[e.type + "_id"];
+  const find = (id) => {
+    if (!parent.has(id)) parent.set(id, id);
+    let root = id;
+    while (parent.get(root) !== root) root = parent.get(root);
+    while (parent.get(id) !== id) { const next = parent.get(id); parent.set(id, root); id = next; }
+    return root;
+  };
+  const join = (a, b) => { if (a && b) parent.set(find(a), find(b)); };
+  const contactEpsilon = 1e-9;
+  const pads = [...getPads(circuitJson), ...circuitJson.filter(e => e.type === "pcb_via")];
+  const segments = getTraceSegments(circuitJson);
+  const copperBounds = (segment) => ({minX: Math.min(segment.x1, segment.x2)-segment.thickness/2, minY:Math.min(segment.y1,segment.y2)-segment.thickness/2,maxX:Math.max(segment.x1,segment.x2)+segment.thickness/2,maxY:Math.max(segment.y1,segment.y2)+segment.thickness/2});
+  const padIndex = new SpatialObjectIndex({ objects: pads, getBounds: getPadBounds, getId: idOf, CELL_SIZE: 2 });
+  const segmentIndex = new SpatialObjectIndex({ objects: segments, getBounds: copperBounds, CELL_SIZE: 2 });
+  // A via-only route has no wire-wire segment. Associate it with its actual
+  // barrel by geometry, never solely by a pcb_trace_id annotation.
+  for (const trace of circuitJson.filter(e => e.type === "pcb_trace")) {
+    for (const point of trace.route.filter(p => p.route_type === "via")) {
+      for (const via of pads.filter(p => p.type === "pcb_via")) {
+        if (Math.hypot(point.x-via.x, point.y-via.y) <= contactEpsilon &&
+            getLayersOfPcbElement(via).includes(point.from_layer) &&
+            getLayersOfPcbElement(via).includes(point.to_layer)) join(trace.pcb_trace_id, via.pcb_via_id);
+      }
+    }
+  }
+  for (const segment of segments) {
+    for (const other of segmentIndex.getObjectsInBounds(copperBounds(segment), contactEpsilon)) {
+      if (other.layer !== segment.layer || other.pcb_trace_id === segment.pcb_trace_id) continue;
+      const gap = segmentToSegmentMinDistance(
+        { x: segment.x1, y: segment.y1 }, { x: segment.x2, y: segment.y2 },
+        { x: other.x1, y: other.y1 }, { x: other.x2, y: other.y2 }
+      ) - (segment.thickness + other.thickness) / 2;
+      if (gap <= contactEpsilon) join(segment.pcb_trace_id, other.pcb_trace_id);
+    }
+    for (const pad of padIndex.getObjectsInBounds(copperBounds(segment), contactEpsilon)) {
+      if (!getLayersOfPcbElement(pad).includes(segment.layer)) continue;
+      if (getTraceObstacleClearance(segment, pad).gap <= contactEpsilon) join(segment.pcb_trace_id, idOf(pad));
+    }
+  }
+  for (const pad of pads) {
+    for (const other of padIndex.getObjectsInBounds(getPadBounds(pad), contactEpsilon)) {
+      if (idOf(pad) === idOf(other)) continue;
+      if (!getLayersOfPcbElement(pad).some(layer => getLayersOfPcbElement(other).includes(layer))) continue;
+      if (getPadToPadGap(pad, other) <= contactEpsilon) join(idOf(pad), idOf(other));
+    }
+  }
+  for (const pour of circuitJson.filter(e => e.type === "pcb_copper_pour" && e.shape === "brep")) {
+    const outer = pour.brep_shape.outer_ring.vertices;
+    const holes = pour.brep_shape.inner_rings ?? [];
+    const inCopper = (point) => isPointInPolygon(point, outer) &&
+      !holes.some(ring => isPointInPolygon(point, ring.vertices));
+    // A witness strictly within copper proves contact. Sampling is conservative:
+    // it can miss a tiny contact, but cannot bridge an absent plane or a void.
+    for (const pad of pads) {
+      if (!getLayersOfPcbElement(pad).includes(pour.layer)) continue;
+      const center = getPadCenter(pad);
+      const samples = [];
+      if (isCircularPad(pad)) {
+        const outerRadius = getPadRadius(pad);
+        const innerRadius = (pad.hole_diameter ?? 0) / 2;
+        const radius = (outerRadius + innerRadius) / 2;
+        for (let i = 0; i < 32; i++) samples.push({
+          x: center.x + radius * Math.cos(i * Math.PI / 16),
+          y: center.y + radius * Math.sin(i * Math.PI / 16)
+        });
+        if (!innerRadius) samples.push(center);
+      } else {
+        const bounds = getPadBounds(pad);
+        for (const fx of [-0.45, 0, 0.45]) for (const fy of [-0.45, 0, 0.45]) {
+          const point = { x: center.x + fx * (bounds.maxX-bounds.minX), y: center.y + fy * (bounds.maxY-bounds.minY) };
+          if (isPointInPad(point, pad) &&
+            (!(pad.hole_diameter > 0) || Math.hypot(point.x-pad.x, point.y-pad.y) > pad.hole_diameter/2)) samples.push(point);
+        }
+      }
+      if (samples.some(inCopper)) join(idOf(pad), idOf(pour));
+    }
+    for (const segment of segments) {
+      if (segment.layer !== pour.layer) continue;
+      if (inCopper({x: segment.x1, y: segment.y1}) || inCopper({x: segment.x2, y: segment.y2})) join(segment.pcb_trace_id, idOf(pour));
+    }
+  }
+  return { areConnected: (a, b) => Boolean(a && b && find(a) === find(b)) };
+}
+
 function checkTracesAreContiguous(circuitJson) {
+  const physicalCopper = getPhysicalCopperConnectivity(circuitJson);
   const errors = [];
   const pcbPorts = circuitJson.filter(
     (el) => el.type === "pcb_port"
@@ -3142,11 +3258,8 @@
       if (!port.pcb_port_id) continue;
       const pads = padMap.get(port.pcb_port_id);
       if (!pads?.length) continue;
-      const isConnectedByRoutedSourceTrace = getPhysicallyConnectedTraces(
-        trace
-      ).some(
-        (candidateTrace) => touchedPortIdsByTraceId.get(candidateTrace.pcb_trace_id)?.has(port.pcb_port_id)
-      );
+      const isConnectedByRoutedSourceTrace = pads.some(pad =>
+        physicalCopper.areConnected(trace.pcb_trace_id, pad[pad.type + "_id"]));
       if (isConnectedByRoutedSourceTrace) continue;
       const isFirstPointConnected = pads.some(
         (pad) => routePointTouchesPad(firstPoint, pad)
@@ -3945,7 +4058,7 @@
   const pads = getPads(circuitJson);
   if (vias.length === 0 || pads.length === 0) return [];
   const board = getPcbBoard(circuitJson);
-  const requiredClearance = minClearance ?? getBoardDrcValue(board, "min_pad_edge_to_pad_edge_clearance") ?? jlcMinTolerances.min_pad_edge_to_pad_edge_clearance;
+  const requiredClearance = minClearance ?? getBoardDrcValue(board, "min_trace_to_pad_edge_clearance") ?? jlcMinTolerances.min_trace_to_pad_edge_clearance;
   connMap ??= getFullConnectivityMapFromCircuitJson12(circuitJson);
   const padIndex = new SpatialObjectIndex({
     objects: pads,