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,