imrishabh18/corne-keyboard
The code defines and renders two surface-mount chip components with SMT pads, silkscreen outlines, and 3D CAD models (OBJ and STEP) for PCB assembly.
- Version
- 2.0.21
- License
- unset
- Stars
- 1
vendor/checks/index.js
// lib/check-traces-are-contiguous/is-point-in-pad.ts
import { pointToSegmentDistance } from "@tscircuit/math-utils";
// lib/check-each-pcb-trace-non-overlapping/segment-to-polygon-clearance.ts
import {
distSq,
getSegmentIntersection,
isPointInsidePolygon,
pointToSegmentClosestPoint,
segmentToSegmentMinDistance
} from "@tscircuit/math-utils";
var rotatePoint = (point, angleDegrees) => {
const angle = angleDegrees * Math.PI / 180;
return {
x: point.x * Math.cos(angle) - point.y * Math.sin(angle),
y: point.x * Math.sin(angle) + point.y * Math.cos(angle)
};
};
var getRotatedRectPoints = ({
x,
y,
width,
height,
ccwRotation
}) => {
const halfWidth = width / 2;
const halfHeight = height / 2;
return [
{ x: -halfWidth, y: -halfHeight },
{ x: halfWidth, y: -halfHeight },
{ x: halfWidth, y: halfHeight },
{ x: -halfWidth, y: halfHeight }
].map((point) => {
const rotated = rotatePoint(point, ccwRotation);
return { x: x + rotated.x, y: y + rotated.y };
});
};
var getPillCenterLineForPad = (pad) => {
const width = pad.type === "pcb_plated_hole" ? pad.outer_width : pad.width;
const height = pad.type === "pcb_plated_hole" ? pad.outer_height : pad.height;
const radius = pad.type === "pcb_plated_hole" ? Math.min(width, height) / 2 : pad.radius;
const ccwRotation = pad.type === "pcb_plated_hole" ? pad.ccw_rotation : pad.shape === "rotated_pill" ? pad.ccw_rotation : 0;
const halfLineLength = Math.max(Math.max(width, height) / 2 - radius, 0);
const axis = width >= height ? { x: halfLineLength, y: 0 } : { x: 0, y: halfLineLength };
const rotatedAxis = rotatePoint(axis, ccwRotation);
return {
start: { x: pad.x - rotatedAxis.x, y: pad.y - rotatedAxis.y },
end: { x: pad.x + rotatedAxis.x, y: pad.y + rotatedAxis.y },
radius
};
};
var getPolygonPointsForPad = (pad) => {
if (pad.type === "pcb_smtpad") {
if (pad.shape === "polygon") return pad.points;
if (pad.shape === "rotated_rect") {
return getRotatedRectPoints({
x: pad.x,
y: pad.y,
width: pad.width,
height: pad.height,
ccwRotation: pad.ccw_rotation
});
}
}
if (pad.type === "pcb_plated_hole" && "rect_pad_width" in pad && "rect_pad_height" in pad) {
return getRotatedRectPoints({
x: pad.x,
y: pad.y,
width: pad.rect_pad_width,
height: pad.rect_pad_height,
ccwRotation: "rect_ccw_rotation" in pad && typeof pad.rect_ccw_rotation === "number" ? pad.rect_ccw_rotation : 0
});
}
throw new Error(
`Expected polygonal pad geometry, got ${pad.type} with shape "${pad.shape}"`
);
};
var getPolygonEdges = (points) => points.map(
(point, index) => [point, points[(index + 1) % points.length]]
);
var getClosestPointsBetweenSegments = (a1, a2, b1, b2) => {
const intersection = getSegmentIntersection(a1, a2, b1, b2);
if (intersection) {
return {
distance: 0,
pointOnA: intersection,
pointOnB: intersection,
center: intersection
};
}
const candidates = [
{ pointOnA: a1, pointOnB: pointToSegmentClosestPoint(a1, b1, b2) },
{ pointOnA: a2, pointOnB: pointToSegmentClosestPoint(a2, b1, b2) },
{ pointOnA: pointToSegmentClosestPoint(b1, a1, a2), pointOnB: b1 },
{ pointOnA: pointToSegmentClosestPoint(b2, a1, a2), pointOnB: b2 }
];
let best = candidates[0];
let bestDistanceSquared = distSq(best.pointOnA, best.pointOnB);
for (const candidate of candidates.slice(1)) {
const candidateDistanceSquared = distSq(
candidate.pointOnA,
candidate.pointOnB
);
if (candidateDistanceSquared < bestDistanceSquared) {
best = candidate;
bestDistanceSquared = candidateDistanceSquared;
}
}
return {
distance: segmentToSegmentMinDistance(a1, a2, b1, b2),
pointOnA: best.pointOnA,
pointOnB: best.pointOnB,
center: {
x: (best.pointOnA.x + best.pointOnB.x) / 2,
y: (best.pointOnA.y + best.pointOnB.y) / 2
}
};
};
var getSegmentToPolygonClearanceFromPoints = (start, end, polygon) => {
if (polygon.length < 3) {
return {
distance: Number.POSITIVE_INFINITY,
center: start,
tracePoint: start,
obstaclePoint: start
};
}
const intersections = getPolygonEdges(polygon).map(
([edgeStart, edgeEnd]) => getSegmentIntersection(start, end, edgeStart, edgeEnd)
).filter((point) => point !== null);
if (intersections.length > 0) {
const dx = end.x - start.x;
const dy = end.y - start.y;
const lengthSquared = dx * dx + dy * dy;
intersections.sort((a, b) => {
const ta = ((a.x - start.x) * dx + (a.y - start.y) * dy) / lengthSquared;
const tb = ((b.x - start.x) * dx + (b.y - start.y) * dy) / lengthSquared;
return ta - tb;
});
return {
distance: 0,
center: intersections[0],
tracePoint: intersections[0],
obstaclePoint: intersections[0]
};
}
if (isPointInsidePolygon(start, polygon) || isPointInsidePolygon(end, polygon)) {
const center = {
x: (start.x + end.x) / 2,
y: (start.y + end.y) / 2
};
return {
distance: 0,
center,
tracePoint: center,
obstaclePoint: center
};
}
let best = getClosestPointsBetweenSegments(
start,
end,
polygon[0],
polygon[1]
);
for (const [edgeStart, edgeEnd] of getPolygonEdges(polygon).slice(1)) {
const candidate = getClosestPointsBetweenSegments(
start,
end,
edgeStart,
edgeEnd
);
if (candidate.distance < best.distance) best = candidate;
}
return {
distance: best.distance,
center: best.center,
tracePoint: best.pointOnA,
obstaclePoint: best.pointOnB
};
};
var getSegmentToPillClearance = (segment, pad) => {
const pill2 = getPillCenterLineForPad(pad);
const closest = getClosestPointsBetweenSegments(
{ x: segment.x1, y: segment.y1 },
{ x: segment.x2, y: segment.y2 },
pill2.start,
pill2.end
);
return {
distance: closest.distance,
center: closest.center,
radius: pill2.radius,
tracePoint: closest.pointOnA,
obstaclePoint: closest.pointOnB
};
};
// lib/check-traces-are-contiguous/is-point-in-pad.ts
function getDistanceBetweenPoints(pointA, pointB) {
return Math.sqrt((pointB.x - pointA.x) ** 2 + (pointB.y - pointA.y) ** 2);
}
var POINT_ON_SEGMENT_TOLERANCE_MM = 1e-9;
var POINT_IN_PAD_TOLERANCE_MM = 1e-9;
function isPointOnSegment(point, segment) {
const crossProduct = (point.y - segment.start.y) * (segment.end.x - segment.start.x) - (point.x - segment.start.x) * (segment.end.y - segment.start.y);
if (Math.abs(crossProduct) > POINT_ON_SEGMENT_TOLERANCE_MM) return false;
const dotProduct = (point.x - segment.start.x) * (segment.end.x - segment.start.x) + (point.y - segment.start.y) * (segment.end.y - segment.start.y);
if (dotProduct < -POINT_ON_SEGMENT_TOLERANCE_MM) return false;
const squaredLength = (segment.end.x - segment.start.x) ** 2 + (segment.end.y - segment.start.y) ** 2;
return dotProduct <= squaredLength + POINT_ON_SEGMENT_TOLERANCE_MM;
}
function isPointInPolygon(point, polygon) {
let inside = false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const pi = polygon[i];
const pj = polygon[j];
if (isPointOnSegment(point, { start: pi, end: pj })) return true;
const intersects = pi.y > point.y !== pj.y > point.y && point.x < (pj.x - pi.x) * (point.y - pi.y) / (pj.y - pi.y) + pi.x;
if (intersects) inside = !inside;
}
return inside;
}
function isPointInPad(point, pad) {
if (pad.type === "pcb_smtpad") {
if (pad.shape === "circle") {
return getDistanceBetweenPoints(point, pad) <= pad.radius + POINT_IN_PAD_TOLERANCE_MM;
}
if (pad.shape === "rect") {
const halfWidth = pad.width / 2;
const halfHeight = pad.height / 2;
return Math.abs(point.x - pad.x) <= halfWidth + POINT_IN_PAD_TOLERANCE_MM && Math.abs(point.y - pad.y) <= halfHeight + POINT_IN_PAD_TOLERANCE_MM;
}
if (pad.shape === "rotated_rect") {
return isPointInPolygon(point, getPolygonPointsForPad(pad));
}
if (pad.shape === "pill" || pad.shape === "rotated_pill") {
if (pad.shape === "rotated_pill") {
const pill2 = getPillCenterLineForPad(pad);
return pointToSegmentDistance(point, pill2.start, pill2.end) <= pill2.radius + POINT_IN_PAD_TOLERANCE_MM;
}
const halfWidth = pad.width / 2;
const halfHeight = pad.height / 2;
const radius = pad.radius;
if (Math.abs(point.x - pad.x) <= halfWidth - radius + POINT_IN_PAD_TOLERANCE_MM && Math.abs(point.y - pad.y) <= halfHeight + POINT_IN_PAD_TOLERANCE_MM) {
return true;
}
const cornerX = Math.max(
Math.abs(point.x - pad.x) - (halfWidth - radius),
0
);
const cornerY = Math.max(
Math.abs(point.y - pad.y) - (halfHeight - radius),
0
);
const radiusWithTolerance = radius + POINT_IN_PAD_TOLERANCE_MM;
return cornerX * cornerX + cornerY * cornerY <= radiusWithTolerance * radiusWithTolerance;
}
if (pad.shape === "polygon") {
return isPointInPolygon(point, pad.points);
}
}
if (pad.type === "pcb_plated_hole") {
if (pad.shape === "circle") {
return getDistanceBetweenPoints(point, pad) <= pad.outer_diameter / 2 + POINT_IN_PAD_TOLERANCE_MM;
}
if ("rect_pad_width" in pad && "rect_pad_height" in pad) {
return isPointInPolygon(point, getPolygonPointsForPad(pad));
}
if (pad.shape === "oval" || pad.shape === "pill") {
return Math.abs(point.x - pad.x) <= pad.outer_width / 2 + POINT_IN_PAD_TOLERANCE_MM && Math.abs(point.y - pad.y) <= pad.outer_height / 2 + POINT_IN_PAD_TOLERANCE_MM;
}
}
return false;
}
// lib/add-start-and-end-port-ids-if-missing.ts
function distance(x1, y1, x2, y2) {
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
}
var addStartAndEndPortIdsIfMissing = (soup) => {
const pcbPorts = soup.filter((item) => item.type === "pcb_port");
const pcbSmtPads = soup.filter(
(item) => item.type === "pcb_smtpad"
);
const pcbTraces = soup.filter((item) => item.type === "pcb_trace");
function findPortIdOverlappingPoint(point, options = {}) {
const traceWidth = options.traceWidth || 0;
const directPort = pcbPorts.find(
(port) => distance(port.x, port.y, point.x, point.y) < 0.01
);
if (directPort) return directPort.pcb_port_id;
if (options.isFirstOrLastPoint) {
const smtPad = pcbSmtPads.find((pad) => {
if (pad.shape === "rect") {
return Math.abs(point.x - pad.x) < pad.width / 2 + traceWidth / 2 && Math.abs(point.y - pad.y) < pad.height / 2 + traceWidth / 2;
} else if (pad.shape === "circle") {
return distance(point.x, point.y, pad.x, pad.y) < pad.radius;
} else if (pad.shape === "pill" || pad.shape === "rotated_pill") {
return isPointInPad(point, pad);
}
});
if (smtPad) return smtPad.pcb_port_id ?? null;
}
return null;
}
for (const trace of pcbTraces) {
for (let index = 0; index < trace.route.length; index++) {
const segment = trace.route[index];
const isFirstOrLastPoint = index === 0 || index === trace.route.length - 1;
if (segment.route_type === "wire") {
if (!segment.start_pcb_port_id && index === 0) {
const startPortId = findPortIdOverlappingPoint(segment, {
isFirstOrLastPoint,
traceWidth: segment.width
});
if (startPortId) {
segment.start_pcb_port_id = startPortId;
}
}
if (!segment.end_pcb_port_id && index === trace.route.length - 1) {
const endPortId = findPortIdOverlappingPoint(segment, {
isFirstOrLastPoint,
traceWidth: segment.width
});
if (endPortId) {
segment.end_pcb_port_id = endPortId;
}
}
}
}
}
};
// lib/check-each-pcb-port-connected-to-pcb-trace.ts
import {
getFullConnectivityMapFromCircuitJson,
PcbConnectivityMap
} from "circuit-json-to-connectivity-map";
// lib/util/copper-pour-connectivity.ts
import * as Flatten2 from "@flatten-js/core";
import { getPrimaryId } from "@tscircuit/circuit-json-util";
// lib/check-copper-to-board-edge-clearance.ts
import * as Flatten from "@flatten-js/core";
// node_modules/@tscircuit/jlcpcb-manufacturing-specs/lib/jlcpcb-manufacturing-specs.ts
var jlcMinTolerances = {
min_trace_width: 0.1,
min_via_hole_edge_to_via_hole_edge_clearance: 0.1,
min_plated_hole_drill_edge_to_drill_edge_clearance: 0.15,
min_trace_to_pad_edge_clearance: 0.1,
min_pad_edge_to_pad_edge_clearance: 0.1,
min_board_edge_clearance: 0.2,
min_via_hole_diameter: 0.2,
min_via_pad_diameter: 0.3
};
// lib/drc-defaults.ts
var DEFAULT_TRACE_MARGIN = 0.1;
var DEFAULT_TRACE_THICKNESS = jlcMinTolerances.min_trace_width;
var DEFAULT_VIA_DIAMETER = jlcMinTolerances.min_via_pad_diameter;
var DEFAULT_VIA_BOARD_MARGIN = jlcMinTolerances.min_board_edge_clearance;
var DEFAULT_SAME_NET_VIA_MARGIN = jlcMinTolerances.min_via_hole_edge_to_via_hole_edge_clearance;
var DEFAULT_DIFFERENT_NET_VIA_MARGIN = jlcMinTolerances.min_via_hole_edge_to_via_hole_edge_clearance;
var DEFAULT_PAD_PAD_CLEARANCE = jlcMinTolerances.min_pad_edge_to_pad_edge_clearance;
var EPSILON = 5e-3;
var getPcbBoard = (circuitJson) => circuitJson.find((el) => el.type === "pcb_board");
var getBoardDrcValue = (board, key) => board?.[key];
// lib/check-copper-to-board-edge-clearance.ts
import { applyToPoint, rotateDEG } from "transformation-matrix";
var toPcbComponentId = (id) => id;
var GEOMETRY_EPSILON = 1e-9;
var pointsToPolygon = (points) => {
if (points.length < 3) return null;
return new Flatten.Polygon(points.map(({ x, y }) => new Flatten.Point(x, y)));
};
var brepRingToPolygon = (vertices) => {
const ring = [];
for (const vertex of vertices) {
const previous = ring.at(-1);
if (!previous || !new Flatten.Point(previous.x, previous.y).equalTo(
new Flatten.Point(vertex.x, vertex.y)
)) ring.push(vertex);
}
if (ring.length > 1 && new Flatten.Point(ring[0].x, ring[0].y).equalTo(
new Flatten.Point(ring.at(-1).x, ring.at(-1).y)
)) {
ring.pop();
}
if (ring.length < 3) return null;
const edges = [];
for (let index = 0; index < ring.length; index++) {
const start = ring[index];
const end = ring[(index + 1) % ring.length];
const startPoint = new Flatten.Point(start.x, start.y);
const endPoint = new Flatten.Point(end.x, end.y);
const bulge = start.bulge ?? 0;
if (Math.abs(bulge) <= GEOMETRY_EPSILON) {
edges.push(new Flatten.Segment(startPoint, endPoint));
continue;
}
const chordLength = startPoint.distanceTo(endPoint)[0];
if (chordLength <= GEOMETRY_EPSILON) continue;
const midpoint2 = {
x: (start.x + end.x) / 2,
y: (start.y + end.y) / 2
};
const leftNormal = {
x: -(end.y - start.y) / chordLength,
y: (end.x - start.x) / chordLength
};
const centerOffset = chordLength * (1 - bulge * bulge) / (4 * bulge);
const center = new Flatten.Point(
midpoint2.x + leftNormal.x * centerOffset,
midpoint2.y + leftNormal.y * centerOffset
);
const radius = chordLength * (1 + bulge * bulge) / (4 * Math.abs(bulge));
edges.push(
new Flatten.Arc(
center,
radius,
Math.atan2(start.y - center.y, start.x - center.x),
Math.atan2(end.y - center.y, end.x - center.x),
bulge > 0
)
);
}
if (edges.length < 3) return null;
const polygon = new Flatten.Polygon();
polygon.addFace(edges);
return polygon;
};
var boardToPolygon = (board) => {
if (board.outline && board.outline.length >= 3) {
return pointsToPolygon(board.outline);
}
if (!board.center || typeof board.width !== "number" || typeof board.height !== "number") {
return null;
}
const halfWidth = board.width / 2;
const halfHeight = board.height / 2;
return pointsToPolygon([
{ x: board.center.x - halfWidth, y: board.center.y - halfHeight },
{ x: board.center.x + halfWidth, y: board.center.y - halfHeight },
{ x: board.center.x + halfWidth, y: board.center.y + halfHeight },
{ x: board.center.x - halfWidth, y: board.center.y + halfHeight }
]);
};
var getRectanglePolygon = ({
x,
y,
width,
height,
ccwRotationDegrees = 0
}) => {
const halfWidth = width / 2;
const halfHeight = height / 2;
const rotationMatrix = rotateDEG(ccwRotationDegrees, x, y);
return pointsToPolygon(
[
{ x: x - halfWidth, y: y - halfHeight },
{ x: x + halfWidth, y: y - halfHeight },
{ x: x + halfWidth, y: y + halfHeight },
{ x: x - halfWidth, y: y + halfHeight }
].map((point) => applyToPoint(rotationMatrix, point))
);
};
var roundedRect = ({
x,
y,
width,
height,
cornerRadius,
ccwRotationDegrees = 0
}) => {
const radius = Math.max(0, Math.min(cornerRadius, width / 2, height / 2));
if (radius <= GEOMETRY_EPSILON) {
const polygon = getRectanglePolygon({
x,
y,
width,
height,
ccwRotationDegrees
});
return polygon ? { kind: "shapes", shapes: [polygon] } : null;
}
const shapes = [];
const innerWidth = width - 2 * radius;
const innerHeight = height - 2 * radius;
if (innerWidth > GEOMETRY_EPSILON) {
const verticalBand = getRectanglePolygon({
x,
y,
width: innerWidth,
height,
ccwRotationDegrees
});
if (verticalBand) shapes.push(verticalBand);
}
if (innerHeight > GEOMETRY_EPSILON) {
const horizontalBand = getRectanglePolygon({
x,
y,
width,
height: innerHeight,
ccwRotationDegrees
});
if (horizontalBand) shapes.push(horizontalBand);
}
const halfInnerWidth = innerWidth / 2;
const halfInnerHeight = innerHeight / 2;
const rotationMatrix = rotateDEG(ccwRotationDegrees, x, y);
const cornerCenters = [
{ x: x - halfInnerWidth, y: y - halfInnerHeight },
{ x: x + halfInnerWidth, y: y - halfInnerHeight },
{ x: x + halfInnerWidth, y: y + halfInnerHeight },
{ x: x - halfInnerWidth, y: y + halfInnerHeight }
].map((point) => applyToPoint(rotationMatrix, point)).filter(
(point, index, points) => points.findIndex(
(candidate) => Math.abs(candidate.x - point.x) <= GEOMETRY_EPSILON && Math.abs(candidate.y - point.y) <= GEOMETRY_EPSILON
) === index
);
shapes.push(
...cornerCenters.map(
(center) => new Flatten.Circle(new Flatten.Point(center.x, center.y), radius)
)
);
return shapes.length > 0 ? { kind: "shapes", shapes } : null;
};
var pill = ({
x,
y,
width,
height,
radius,
ccwRotationDegrees = 0
}) => {
const halfLineLength = Math.max(Math.max(width, height) / 2 - radius, 0);
const localAxis = width >= height ? { x: halfLineLength, y: 0 } : { x: 0, y: halfLineLength };
const axis = applyToPoint(rotateDEG(ccwRotationDegrees), localAxis);
if (halfLineLength <= GEOMETRY_EPSILON) {
return {
kind: "shapes",
shapes: [new Flatten.Circle(new Flatten.Point(x, y), radius)]
};
}
return {
kind: "pill",
centerLine: new Flatten.Segment(
new Flatten.Point(x - axis.x, y - axis.y),
new Flatten.Point(x + axis.x, y + axis.y)
),
radius
};
};
var getSmtPadGeometry = (pad) => {
switch (pad.shape) {
case "circle":
return {
kind: "shapes",
shapes: [
new Flatten.Circle(new Flatten.Point(pad.x, pad.y), pad.radius)
]
};
case "rect":
return roundedRect({
x: pad.x,
y: pad.y,
width: pad.width,
height: pad.height,
cornerRadius: pad.rect_border_radius ?? pad.corner_radius ?? 0
});
case "rotated_rect":
return roundedRect({
x: pad.x,
y: pad.y,
width: pad.width,
height: pad.height,
cornerRadius: pad.rect_border_radius ?? pad.corner_radius ?? 0,
ccwRotationDegrees: pad.ccw_rotation
});
case "pill":
return pill({
x: pad.x,
y: pad.y,
width: pad.width,
height: pad.height,
radius: pad.radius
});
case "rotated_pill":
return pill({
x: pad.x,
y: pad.y,
width: pad.width,
height: pad.height,
radius: pad.radius,
ccwRotationDegrees: pad.ccw_rotation
});
case "polygon": {
const polygon = pointsToPolygon(pad.points);
return polygon ? { kind: "shapes", shapes: [polygon] } : null;
}
}
};
var getPlatedHoleGeometry = (platedHole, componentCcwRotationDegrees) => {
switch (platedHole.shape) {
case "circle":
return {
kind: "shapes",
shapes: [
new Flatten.Circle(
new Flatten.Point(platedHole.x, platedHole.y),
platedHole.outer_diameter / 2
)
]
};
case "oval":
case "pill":
return pill({
x: platedHole.x,
y: platedHole.y,
width: platedHole.outer_width,
height: platedHole.outer_height,
radius: Math.min(
platedHole.outer_width / 2,
platedHole.outer_height / 2
),
ccwRotationDegrees: platedHole.ccw_rotation
});
case "circular_hole_with_rect_pad":
case "pill_hole_with_rect_pad":
case "rotated_pill_hole_with_rect_pad":
return roundedRect({
x: platedHole.x,
y: platedHole.y,
width: platedHole.rect_pad_width,
height: platedHole.rect_pad_height,
cornerRadius: platedHole.rect_border_radius ?? 0,
ccwRotationDegrees: "rect_ccw_rotation" in platedHole ? platedHole.rect_ccw_rotation ?? 0 : 0
});
case "hole_with_polygon_pad": {
const ccwRotationDegrees = platedHole.ccw_rotation ?? componentCcwRotationDegrees;
const rotationMatrix = rotateDEG(ccwRotationDegrees);
const polygon = pointsToPolygon(
platedHole.pad_outline.map((point) => {
const rotatedPoint = applyToPoint(rotationMatrix, point);
return {
x: platedHole.x + rotatedPoint.x,
y: platedHole.y + rotatedPoint.y
};
})
);
return polygon ? { kind: "shapes", shapes: [polygon] } : null;
}
}
};
var getCopperGeometry = (element, componentCcwRotationsById) => {
if (element.type === "pcb_via") {
return {
kind: "shapes",
shapes: [
new Flatten.Circle(
new Flatten.Point(element.x, element.y),
element.outer_diameter / 2
)
]
};
}
if (element.type === "pcb_smtpad") return getSmtPadGeometry(element);
if (element.type === "pcb_plated_hole") {
return getPlatedHoleGeometry(
element,
element.pcb_component_id ? componentCcwRotationsById.get(
toPcbComponentId(element.pcb_component_id)
) ?? 0 : 0
);
}
let polygon;
switch (element.shape) {
case "rect":
polygon = getRectanglePolygon({
x: element.center.x,
y: element.center.y,
width: element.width,
height: element.height,
ccwRotationDegrees: element.rotation ?? 0
});
break;
case "polygon":
polygon = pointsToPolygon(element.points);
break;
case "brep":
polygon = brepRingToPolygon(element.brep_shape.outer_ring.vertices);
break;
}
return polygon ? { kind: "shapes", shapes: [polygon] } : null;
};
var getCopperElementId = (element) => {
if (element.type === "pcb_via") return element.pcb_via_id;
if (element.type === "pcb_smtpad") return element.pcb_smtpad_id;
if (element.type === "pcb_plated_hole") return element.pcb_plated_hole_id;
return element.pcb_copper_pour_id;
};
var getCopperElementLabel = (element) => {
if (element.type === "pcb_via") return "Via";
if (element.type === "pcb_smtpad") return "SMT pad";
if (element.type === "pcb_plated_hole") return "Plated hole";
return "Copper pour";
};
var measureClearance = (board, geometry) => {
if (geometry.kind === "shapes") {
const isInside2 = geometry.shapes.every((shape) => board.contains(shape));
return {
isInside: isInside2,
clearance: isInside2 ? Math.min(
...geometry.shapes.map((shape) => board.distanceTo(shape)[0])
) : 0
};
}
const centerLineClearance = board.distanceTo(geometry.centerLine)[0];
const clearance = centerLineClearance - geometry.radius;
const isInside = board.contains(geometry.centerLine) && clearance >= -GEOMETRY_EPSILON;
return {
isInside,
clearance: isInside ? Math.max(0, clearance) : 0
};
};
function checkCopperToBoardEdgeClearance(circuitJson) {
const board = getPcbBoard(circuitJson);
if (!board) return [];
const boardPolygon = boardToPolygon(board);
if (!boardPolygon) return [];
const requiredClearance = getBoardDrcValue(board, "min_board_edge_clearance") ?? jlcMinTolerances.min_board_edge_clearance;
if (requiredClearance === void 0) return [];
const allowedOffBoardComponentIds = new Set(
circuitJson.filter(
(element) => element.type === "pcb_component"
).filter((component) => component.is_allowed_to_be_off_board).map((component) => toPcbComponentId(component.pcb_component_id))
);
const copperElements = circuitJson.filter(
(element) => element.type === "pcb_via" || element.type === "pcb_smtpad" || element.type === "pcb_plated_hole" || element.type === "pcb_copper_pour"
);
const componentCcwRotationsById = new Map(
circuitJson.filter(
(element) => element.type === "pcb_component"
).map((component) => [
toPcbComponentId(component.pcb_component_id),
component.rotation
])
);
const errors = [];
for (const element of copperElements) {
if ((element.type === "pcb_smtpad" || element.type === "pcb_plated_hole") && element.pcb_component_id && allowedOffBoardComponentIds.has(
toPcbComponentId(element.pcb_component_id)
)) {
continue;
}
const geometry = getCopperGeometry(element, componentCcwRotationsById);
if (!geometry) continue;
const { isInside, clearance } = measureClearance(boardPolygon, geometry);
if (isInside && clearance + GEOMETRY_EPSILON >= requiredClearance) {
continue;
}
const id = getCopperElementId(element);
const label = getCopperElementLabel(element);
errors.push({
type: "pcb_placement_error",
pcb_placement_error_id: `copper_too_close_to_board_edge_${id}`,
error_type: "pcb_placement_error",
message: `${label} ${id} violates copper-to-board-edge clearance (measured ${clearance.toFixed(3)}mm, required ${requiredClearance.toFixed(3)}mm)`
});
}
return errors;
}
// lib/util/getLayersOfPcbElement.ts
import { all_layers } from "circuit-json";
function getLayersOfPcbElement(obj) {
if (obj.type === "pcb_trace_segment") {
return [obj.layer];
}
if (obj.type === "pcb_smtpad") {
return [obj.layer];
}
if (obj.type === "pcb_plated_hole") {
return Array.isArray(obj.layers) ? obj.layers : [...all_layers];
}
if (obj.type === "pcb_hole") {
return [...all_layers];
}
if (obj.type === "pcb_via") {
return Array.isArray(obj.layers) ? obj.layers : [...all_layers];
}
if (obj.type === "pcb_keepout") {
return Array.isArray(obj.layers) ? obj.layers : [];
}
return [];
}
// lib/data-structures/SpatialIndex.ts
var SpatialObjectIndex = class {
buckets;
objectsById;
getBounds;
getId;
CELL_SIZE = 0.4;
constructor({
objects,
getBounds,
getId,
CELL_SIZE
}) {
this.buckets = /* @__PURE__ */ new Map();
this.objectsById = /* @__PURE__ */ new Map();
this.getBounds = getBounds;
this.getId = getId ?? (() => this._getNextId());
this.CELL_SIZE = CELL_SIZE ?? this.CELL_SIZE;
for (const obj of objects) {
this.addObject(obj);
}
}
_idCounter = 0;
_getNextId() {
return `${this._idCounter++}`;
}
addObject(obj) {
const bounds2 = this.getBounds(obj);
const spatialIndexId = this.getId(obj);
const objWithId = { ...obj, spatialIndexId };
this.objectsById.set(spatialIndexId, objWithId);
const minBucketX = Math.floor(bounds2.minX / this.CELL_SIZE);
const minBucketY = Math.floor(bounds2.minY / this.CELL_SIZE);
const maxBucketX = Math.floor(bounds2.maxX / this.CELL_SIZE);
const maxBucketY = Math.floor(bounds2.maxY / this.CELL_SIZE);
for (let bx = minBucketX; bx <= maxBucketX; bx++) {
for (let by = minBucketY; by <= maxBucketY; by++) {
const bucketKey = `${bx}x${by}`;
const bucket = this.buckets.get(bucketKey);
if (!bucket) {
this.buckets.set(bucketKey, [objWithId]);
} else {
bucket.push(objWithId);
}
}
}
}
removeObject(id) {
const obj = this.objectsById.get(id);
if (!obj) return false;
this.objectsById.delete(id);
const bounds2 = this.getBounds(obj);
const minBucketX = Math.floor(bounds2.minX / this.CELL_SIZE);
const minBucketY = Math.floor(bounds2.minY / this.CELL_SIZE);
const maxBucketX = Math.floor(bounds2.maxX / this.CELL_SIZE);
const maxBucketY = Math.floor(bounds2.maxY / this.CELL_SIZE);
for (let bx = minBucketX; bx <= maxBucketX; bx++) {
for (let by = minBucketY; by <= maxBucketY; by++) {
const bucketKey = `${bx}x${by}`;
const bucket = this.buckets.get(bucketKey);
if (bucket) {
const index = bucket.findIndex((item) => item.spatialIndexId === id);
if (index !== -1) {
bucket.splice(index, 1);
if (bucket.length === 0) {
this.buckets.delete(bucketKey);
}
}
}
}
}
return true;
}
getBucketKey(x, y) {
return `${Math.floor(x / this.CELL_SIZE)}x${Math.floor(y / this.CELL_SIZE)}`;
}
getObjectsInBounds(bounds2, margin = 0) {
const objects = [];
const addedIds = /* @__PURE__ */ new Set();
const minBucketX = Math.floor((bounds2.minX - margin) / this.CELL_SIZE);
const minBucketY = Math.floor((bounds2.minY - margin) / this.CELL_SIZE);
const maxBucketX = Math.floor((bounds2.maxX + margin) / this.CELL_SIZE);
const maxBucketY = Math.floor((bounds2.maxY + margin) / this.CELL_SIZE);
for (let bx = minBucketX; bx <= maxBucketX; bx++) {
for (let by = minBucketY; by <= maxBucketY; by++) {
const bucketKey = `${bx}x${by}`;
const bucket = this.buckets.get(bucketKey) || [];
for (const obj of bucket) {
const id = obj.spatialIndexId;
if (addedIds.has(id)) continue;
addedIds.add(id);
objects.push(obj);
}
}
}
return objects;
}
};
// lib/util/copper-pour-connectivity.ts
var EPSILON2 = 1e-9;
var polygonFromPoints = (points) => new Flatten2.Polygon(points.map(({ x, y }) => new Flatten2.Point(x, y)));
function pourPolygon(pour) {
if (pour.shape === "polygon") return polygonFromPoints(pour.points);
if (pour.shape === "rect")
return polygonFromPoints(
getRotatedRectPoints({
x: pour.center.x,
y: pour.center.y,
width: pour.width,
height: pour.height,
ccwRotation: pour.rotation ?? 0
})
);
const polygon = brepRingToPolygon(pour.brep_shape.outer_ring.vertices);
if (!polygon) return;
for (const ring of pour.brep_shape.inner_rings) {
const hole = brepRingToPolygon(ring.vertices);
if (!hole) return;
for (const face of hole.faces) polygon.addFace(face.shapes);
}
return polygon;
}
function capsulePolygon(start, end, radius) {
if (start.distanceTo(end)[0] <= EPSILON2)
return new Flatten2.Polygon(new Flatten2.Circle(start, radius));
const angle = Math.atan2(end.y - start.y, end.x - start.x) + Math.PI / 2;
const dx = radius * Math.cos(angle), dy = radius * Math.sin(angle);
return new Flatten2.Polygon([
new Flatten2.Segment(
new Flatten2.Point(start.x + dx, start.y + dy),
new Flatten2.Point(end.x + dx, end.y + dy)
),
new Flatten2.Arc(end, radius, angle, angle - Math.PI, false),
new Flatten2.Segment(
new Flatten2.Point(end.x - dx, end.y - dy),
new Flatten2.Point(start.x - dx, start.y - dy)
),
new Flatten2.Arc(start, radius, angle - Math.PI, angle - 2 * Math.PI, false)
]);
}
function platedHoleDrill(pad) {
const x = pad.x + ("hole_offset_x" in pad ? pad.hole_offset_x : 0);
const y = pad.y + ("hole_offset_y" in pad ? pad.hole_offset_y : 0);
if ("hole_diameter" in pad && pad.hole_diameter !== void 0)
return pad.hole_diameter > 0 ? new Flatten2.Polygon(
new Flatten2.Circle(new Flatten2.Point(x, y), pad.hole_diameter / 2)
) : void 0;
if (!("hole_width" in pad && "hole_height" in pad) || pad.hole_width === void 0 || pad.hole_height === void 0)
return void 0;
const rotation = "hole_ccw_rotation" in pad ? pad.hole_ccw_rotation : "ccw_rotation" in pad ? pad.ccw_rotation ?? 0 : 0;
const line = getPillCenterLineForPad({
type: "pcb_smtpad",
pcb_smtpad_id: "drill",
shape: "rotated_pill",
layer: "top",
x,
y,
width: pad.hole_width,
height: pad.hole_height,
radius: Math.min(pad.hole_width, pad.hole_height) / 2,
ccw_rotation: rotation
});
return capsulePolygon(
new Flatten2.Point(line.start.x, line.start.y),
new Flatten2.Point(line.end.x, line.end.y),
line.radius
);
}
function platedCopperPolygon(geometry, drill) {
const shapes = geometry.kind === "pill" ? [
capsulePolygon(
geometry.centerLine.start,
geometry.centerLine.end,
geometry.radius
)
] : geometry.shapes.map(
(s) => s instanceof Flatten2.Circle ? new Flatten2.Polygon(s) : s
);
const outer = shapes.reduce((a, b) => Flatten2.BooleanOperations.unify(a, b));
return drill ? Flatten2.BooleanOperations.subtract(outer, drill) : outer;
}
function representativePoints(shape) {
if (shape instanceof Flatten2.Point) return [shape];
if (shape instanceof Flatten2.Segment) return [shape.start, shape.end];
return shape.vertices;
}
function touches(a, b) {
const aShape = a.shape, bShape = b.shape;
if (aShape instanceof Flatten2.Polygon && representativePoints(bShape).some((p) => aShape.contains(p)))
return true;
if (bShape instanceof Flatten2.Polygon && representativePoints(aShape).some((p) => bShape.contains(p)))
return true;
return a.shape.distanceTo(b.shape)[0] <= a.radius + b.radius + EPSILON2;
}
function bounds({ shape, radius }) {
const box = shape.box;
return {
minX: box.xmin - radius,
minY: box.ymin - radius,
maxX: box.xmax + radius,
maxY: box.ymax + radius
};
}
function overlap(a, b) {
return a.minX <= b.maxX + EPSILON2 && a.maxX + EPSILON2 >= b.minX && a.minY <= b.maxY + EPSILON2 && a.maxY + EPSILON2 >= b.minY;
}
var CopperPourConnectivity = class {
constructor(circuitJson, connectivity) {
this.connectivity = connectivity;
const pours = circuitJson.filter((e) => e.type === "pcb_copper_pour");
const netIds = new Set(
pours.map(
(p) => p.source_net_id ? connectivity.getNetConnectedToId(p.source_net_id) : void 0
)
);
netIds.delete(void 0);
const add = (id, layers, geometry, net, isPour = false, portId) => {
if (!net || !netIds.has(net)) return;
const node = {
...geometry,
index: this.nodes.length,
id,
layers,
net,
isPour,
portId,
bounds: bounds(geometry)
};
if (!Object.values(node.bounds).every(Number.isFinite)) return;
this.nodes.push(node);
this.parents.push(node.index);
this.byId.set(id, [...this.byId.get(id) ?? [], node]);
if (isPour)
this.poursByNet.set(net, [...this.poursByNet.get(net) ?? [], node]);
};
const addShapes = (id, layers, geometry, net, portId) => {
if (!geometry) return;
if (geometry.kind === "pill")
add(
id,
layers,
{ shape: geometry.centerLine, radius: geometry.radius },
net,
false,
portId
);
else
for (const shape of geometry.shapes)
add(
id,
layers,
shape instanceof Flatten2.Circle ? { shape: shape.center, radius: shape.r } : { shape, radius: 0 },
net,
false,
portId
);
};
for (const element of circuitJson) {
if (element.type === "pcb_port") {
const net = connectivity.getNetConnectedToId(element.pcb_port_id);
if (net && netIds.has(net)) {
this.sourceNetByPort.set(element.pcb_port_id, net);
this.portsByNet.set(net, [
...this.portsByNet.get(net) ?? [],
element
]);
}
}
if (element.type === "pcb_copper_pour") {
const polygon = pourPolygon(element);
if (polygon)
add(
element.pcb_copper_pour_id,
[element.layer],
{ shape: polygon, radius: 0 },
element.source_net_id ? connectivity.getNetConnectedToId(element.source_net_id) : void 0,
true
);
}
if (element.type === "pcb_smtpad" || element.type === "pcb_plated_hole") {
const id = getPrimaryId(element);
const net = connectivity.getNetConnectedToId(id) ?? (element.pcb_port_id ? connectivity.getNetConnectedToId(element.pcb_port_id) : void 0);
if (!net || !netIds.has(net)) continue;
const geometry = element.type === "pcb_smtpad" ? getSmtPadGeometry(element) : getPlatedHoleGeometry(element, 0);
if (element.type === "pcb_plated_hole" && geometry) {
const copper = platedCopperPolygon(geometry, platedHoleDrill(element));
if (!copper.isEmpty())
add(
id,
getLayersOfPcbElement(element),
{ shape: copper, radius: 0 },
net,
false,
element.pcb_port_id
);
} else
addShapes(
id,
getLayersOfPcbElement(element),
geometry,
net,
element.pcb_port_id
);
}
if (element.type === "pcb_via") {
const net = connectivity.getNetConnectedToId(element.pcb_via_id) ?? (element.pcb_trace_id ? connectivity.getNetConnectedToId(element.pcb_trace_id) : void 0);
if (!net || !netIds.has(net) || element.outer_diameter <= 0) continue;
const center = new Flatten2.Point(element.x, element.y);
const outer = new Flatten2.Polygon(
new Flatten2.Circle(center, element.outer_diameter / 2)
);
const copper = element.hole_diameter > 0 ? Flatten2.BooleanOperations.subtract(
outer,
new Flatten2.Polygon(
new Flatten2.Circle(center, element.hole_diameter / 2)
)
) : outer;
if (!copper.isEmpty())
add(
element.pcb_via_id,
getLayersOfPcbElement(element),
{ shape: copper, radius: 0 },
net
);
}
if (element.type === "pcb_trace") {
if (element.route_thickness_mode === "interpolated") continue;
const net = connectivity.getNetConnectedToId(element.pcb_trace_id);
if (!net || !netIds.has(net)) continue;
for (let i = 1; i < element.route.length; i++) {
const a = element.route[i - 1], b = element.route[i];
if (a.route_type !== "wire" || b.route_type !== "wire" || a.layer !== b.layer || a.width <= 0)
continue;
add(
element.pcb_trace_id,
[a.layer],
{
shape: new Flatten2.Segment(
new Flatten2.Point(a.x, a.y),
new Flatten2.Point(b.x, b.y)
),
radius: a.width / 2
},
net
);
}
}
}
const spatial = new SpatialObjectIndex({
objects: this.nodes,
getBounds: (n) => n.bounds,
getId: (n) => String(n.index),
CELL_SIZE: 5
});
for (const a of this.nodes) {
for (const b of spatial.getObjectsInBounds(a.bounds, EPSILON2)) {
if (b.index >= a.index || a.net !== b.net || !a.layers.some((l) => b.layers.includes(l)) || !overlap(a.bounds, b.bounds))
continue;
if (this.root(a.index) === this.root(b.index)) continue;
if (touches(a, b)) this.parents[this.root(a.index)] = this.root(b.index);
}
}
for (const node of this.nodes) {
if (node.isPour) this.groupsWithPour.add(this.root(node.index));
if (node.portId) this.groupsWithPort.add(this.root(node.index));
}
}
connectivity;
nodes = [];
parents = [];
byId = /* @__PURE__ */ new Map();
poursByNet = /* @__PURE__ */ new Map();
portsByNet = /* @__PURE__ */ new Map();
groupsWithPour = /* @__PURE__ */ new Set();
groupsWithPort = /* @__PURE__ */ new Set();
sourceNetByPort = /* @__PURE__ */ new Map();
root(index) {
if (this.parents[index] !== index)
this.parents[index] = this.root(this.parents[index]);
return this.parents[index];
}
portGroups(portId) {
return new Set(
this.nodes.filter((n) => n.portId === portId).map((n) => this.root(n.index))
);
}
/** A pour connection must physically reach the other required ports. */
portsConnectedThroughPour(portIds) {
const first = portIds[0];
if (!first) return false;
const groups = [...this.portGroups(first)].filter(
(group) => this.groupsWithPour.has(group)
);
return groups.some(
(group) => portIds.every((id) => this.portGroups(id).has(group))
);
}
portConnectedToPourNet(portId) {
const net = this.sourceNetByPort.get(portId);
if (!net) return false;
const ports = this.portsByNet.get(net) ?? [];
return ports.length > 0 && this.portsConnectedThroughPour(ports.map((p) => p.pcb_port_id));
}
traceConnectedToPortThroughPour(traceId, portId) {
const groups = this.portGroups(portId);
return (this.byId.get(traceId) ?? []).some(
(node) => this.groupsWithPour.has(this.root(node.index)) && groups.has(this.root(node.index))
);
}
endpointTouchesConnectedPour(point, traceId, width) {
if (point.route_type !== "wire" || !Number.isFinite(width) || width < 0)
return false;
const net = this.connectivity.getNetConnectedToId(traceId);
if (!net) return false;
const endpoint = {
shape: new Flatten2.Point(point.x, point.y),
radius: width / 2
};
const endpointBounds = bounds(endpoint);
return (this.poursByNet.get(net) ?? []).some(
(pour) => pour.layers.includes(point.layer) && this.groupsWithPort.has(this.root(pour.index)) && overlap(endpointBounds, pour.bounds) && touches(endpoint, pour)
);
}
};
// lib/util/get-readable-names.ts
import {
getReadableNameForElement,
getReadableNameForPcbPort,
getBoundsOfPcbElements
} from "@tscircuit/circuit-json-util";
var CIRCUIT_JSON_ID_PATTERN = /\b(?:pcb|source|schematic|subcircuit)_[a-z0-9_]+\b/i;
var sanitizeReadableName = (candidate, id, fallbackLabel) => {
if (!candidate || candidate === id || CIRCUIT_JSON_ID_PATTERN.test(candidate)) {
return fallbackLabel;
}
return candidate;
};
var firstReadableName = (candidates, id) => {
for (const candidate of candidates) {
const readableName = sanitizeReadableName(candidate, id, "");
if (readableName) return readableName;
}
return "";
};
var getReadableNameForComponent = (circuitJson, pcbComponentId) => sanitizeReadableName(
getReadableNameForElement(circuitJson, pcbComponentId),
pcbComponentId,
"component"
);
var getReadableNameForPort = (circuitJson, pcbPortId) => {
const pcbPort = circuitJson.find(
(element) => element.type === "pcb_port" && element.pcb_port_id === pcbPortId
);
if (pcbPort?.type === "pcb_port") {
const sourcePort = circuitJson.find(
(element) => element.type === "source_port" && element.source_port_id === pcbPort.source_port_id
);
const sourceComponent = sourcePort?.type === "source_port" ? circuitJson.find(
(element) => element.type === "source_component" && element.source_component_id === sourcePort.source_component_id
) : null;
const readableSourceComponentName = firstReadableName(
[
sourceComponent?.type === "source_component" ? sourceComponent.name : null
],
sourceComponent?.type === "source_component" ? sourceComponent.source_component_id : ""
);
const readableSourcePortName = firstReadableName(
[
sourcePort?.type === "source_port" ? sourcePort.name : null,
sourcePort?.type === "source_port" ? sourcePort.pin_number?.toString() : null,
sourcePort?.type === "source_port" ? sourcePort.port_hints?.[0] : null
],
sourcePort?.type === "source_port" ? sourcePort.source_port_id : ""
);
if (readableSourceComponentName && readableSourcePortName) {
return `${readableSourceComponentName}.${readableSourcePortName}`;
}
if (readableSourcePortName) {
return readableSourcePortName;
}
}
return sanitizeReadableName(
getReadableNameForPcbPort(circuitJson, pcbPortId) ?? getReadableNameForElement(circuitJson, pcbPortId),
pcbPortId,
"port"
);
};
var getReadableNameForSourceTrace = (circuitJson, sourceTrace) => {
const displayName = sanitizeReadableName(
sourceTrace.display_name,
sourceTrace.source_trace_id,
""
);
if (displayName) return displayName;
const connectedPortNames = (sourceTrace.connected_source_port_ids ?? []).map((sourcePortId) => {
const pcbPort = circuitJson.find(
(element) => element.type === "pcb_port" && element.source_port_id === sourcePortId
);
if (pcbPort?.type === "pcb_port") {
return getReadableNameForPort(circuitJson, pcbPort.pcb_port_id);
}
const sourcePort = circuitJson.find(
(element) => element.type === "source_port" && element.source_port_id === sourcePortId
);
if (sourcePort?.type !== "source_port") return null;
const sourceComponent = circuitJson.find(
(element) => element.type === "source_component" && element.source_component_id === sourcePort.source_component_id
);
const sourceComponentName = sourceComponent?.type === "source_component" ? sanitizeReadableName(
sourceComponent.name,
sourceComponent.source_component_id,
""
) : "";
const sourcePortName = firstReadableName(
[
sourcePort.name,
sourcePort.pin_number?.toString(),
sourcePort.port_hints?.[0]
],
sourcePort.source_port_id
);
if (sourceComponentName && sourcePortName) {
return `${sourceComponentName}.${sourcePortName}`;
}
return sourcePortName || null;
}).filter((name) => Boolean(name));
if (connectedPortNames.length >= 2) {
return `${connectedPortNames[0]} to ${connectedPortNames[1]}`;
}
if (connectedPortNames.length === 1) {
return `trace connected to ${connectedPortNames[0]}`;
}
return `trace ${sourceTrace.source_trace_id}`;
};
var getReadableNameForElementId = (circuitJson, elementId) => sanitizeReadableName(
getReadableNameForElement(circuitJson, elementId),
elementId,
"element"
);
var containsCircuitJsonId = (message) => CIRCUIT_JSON_ID_PATTERN.test(message);
function getReadableNameForFootprintPad(circuitJson, pad, ordinal) {
const padKind = pad.type === "pcb_smtpad" ? "SMD pad" : "through-hole pad";
const portRef = pad.pcb_port_id ? getReadableNameForPort(circuitJson, pad.pcb_port_id) : null;
const bounds2 = getBoundsOfPcbElements([pad]);
const centerX = (bounds2.minX + bounds2.maxX) / 2;
const centerY = (bounds2.minY + bounds2.maxY) / 2;
const location = `(${centerX.toFixed(2)}mm, ${centerY.toFixed(2)}mm)`;
if (portRef) return `${padKind} ${portRef} at ${location}`;
return `${padKind} #${ordinal + 1} at ${location}`;
}
// lib/check-each-pcb-port-connected-to-pcb-trace.ts
function checkEachPcbPortConnectedToPcbTraces(circuitJson) {
addStartAndEndPortIdsIfMissing(circuitJson);
const sourceTraces = circuitJson.filter(
(item) => item.type === "source_trace"
);
const pcbPorts = circuitJson.filter(
(item) => item.type === "pcb_port"
);
const sourceNets = circuitJson.filter(
(item) => item.type === "source_net"
);
const errors = [];
const connectivityMap = getFullConnectivityMapFromCircuitJson(circuitJson);
const pcbConnectivityMap = new PcbConnectivityMap(circuitJson);
let pourConnectivity;
const getPourConnectivity = () => pourConnectivity ??= new CopperPourConnectivity(
circuitJson,
connectivityMap
);
const sourcePortToPcbPort = /* @__PURE__ */ new Map();
for (const pcbPort of pcbPorts) {
sourcePortToPcbPort.set(pcbPort.source_port_id, pcbPort);
}
const sourceNetNameById = new Map(
sourceNets.map((sourceNet) => [sourceNet.source_net_id, sourceNet.name])
);
for (const sourceTrace of sourceTraces) {
const connectedSourcePortIds = sourceTrace.connected_source_port_ids;
if (connectedSourcePortIds.length === 1 && sourceTrace.connected_source_net_ids.length > 0) {
const pcbPort = sourcePortToPcbPort.get(connectedSourcePortIds[0]);
if (!pcbPort) continue;
const connectedPcbTraces = pcbConnectivityMap.getAllTracesConnectedToPort(
pcbPort.pcb_port_id
);
if (connectedPcbTraces.length === 0 && !getPourConnectivity().portConnectedToPourNet(pcbPort.pcb_port_id)) {
const connectedNetNames = sourceTrace.connected_source_net_ids.map((sourceNetId) => sourceNetNameById.get(sourceNetId)).filter((name) => Boolean(name));
const netDescription = connectedNetNames.length > 0 ? `net [${connectedNetNames.join(", ")}]` : "its connected net";
errors.push({
type: "pcb_port_not_connected_error",
message: `Port [${getReadableNameForPort(circuitJson, pcbPort.pcb_port_id)}] is not connected to ${netDescription} by a PCB trace.`,
error_type: "pcb_port_not_connected_error",
pcb_port_ids: [pcbPort.pcb_port_id],
pcb_component_ids: pcbPort.pcb_component_id ? [pcbPort.pcb_component_id] : [],
pcb_port_not_connected_error_id: `pcb_port_not_connected_error_trace_${sourceTrace.source_trace_id}`
});
}
continue;
}
if (connectedSourcePortIds.length < 2) {
continue;
}
const pcbPortsInTrace = [];
const missingPcbPorts = [];
for (const sourcePortId of connectedSourcePortIds) {
const pcbPort = sourcePortToPcbPort.get(sourcePortId);
if (pcbPort) {
pcbPortsInTrace.push(pcbPort);
} else {
missingPcbPorts.push(sourcePortId);
}
}
if (pcbPortsInTrace.length < 2) {
continue;
}
const firstPcbPort = pcbPortsInTrace[0];
const referenceNetId = connectivityMap.getNetConnectedToId(
firstPcbPort.pcb_port_id
);
const netElementIds = connectivityMap.getIdsConnectedToNet(referenceNetId);
const pcbTraceIds = netElementIds.filter(
(id) => circuitJson.some(
(element) => element.type === "pcb_trace" && ("pcb_trace_id" in element && element.pcb_trace_id === id || "route_id" in element && element.route_id === id)
)
);
if (pcbTraceIds.length === 0 && !getPourConnectivity().portsConnectedThroughPour(
pcbPortsInTrace.map((p) => p.pcb_port_id)
)) {
const uniqueComponentIds = new Set(
pcbPortsInTrace.map((p) => p.pcb_component_id)
);
if (uniqueComponentIds.size > 1) {
errors.push({
type: "pcb_port_not_connected_error",
message: `Ports [${pcbPortsInTrace.map((p) => getReadableNameForPort(circuitJson, p.pcb_port_id)).join(", ")}] are not connected together through the same net.`,
error_type: "pcb_port_not_connected_error",
pcb_port_ids: pcbPortsInTrace.map((p) => p.pcb_port_id),
pcb_component_ids: pcbPortsInTrace.map((p) => p.pcb_component_id).filter((id) => id !== void 0),
pcb_port_not_connected_error_id: `pcb_port_not_connected_error_trace_${sourceTrace.source_trace_id}`
});
}
}
}
return errors;
}
// lib/check-each-pcb-trace-non-overlapping/check-each-pcb-trace-non-overlapping.ts
import { cju as cju2, getReadableNameForElement as getReadableNameForElement2 } from "@tscircuit/circuit-json-util";
import { getPrimaryId as getPrimaryId2 } from "@tscircuit/circuit-json-util";
import {
segmentToBoundsMinDistance,
segmentToCircleMinDistance as segmentToCircleMinDistance2
} from "@tscircuit/math-utils";
import { segmentToSegmentMinDistance as segmentToSegmentMinDistance3 } from "@tscircuit/math-utils";
import {
getFullConnectivityMapFromCircuitJson as getFullConnectivityMapFromCircuitJson2
} from "circuit-json-to-connectivity-map";
// lib/check-pad-clearance/common.ts
import {
cju,
distanceBetweenCircleAndCircle,
distanceBetweenCircleAndPolygon,
distanceBetweenPolygonAndPolygon,
getBoundsOfPcbElements as getBoundsOfPcbElements2
} from "@tscircuit/circuit-json-util";
import {
midpoint,
pointToSegmentClosestPoint as pointToSegmentClosestPoint2,
segmentToCircleMinDistance,
segmentToSegmentMinDistance as segmentToSegmentMinDistance2
} from "@tscircuit/math-utils";
var getPadBounds = (pad) => {
if (pad.type === "pcb_keepout") {
if (pad.shape === "outline") {
return {
minX: Math.min(...pad.outline.map((point) => point.x)),
minY: Math.min(...pad.outline.map((point) => point.y)),
maxX: Math.max(...pad.outline.map((point) => point.x)),
maxY: Math.max(...pad.outline.map((point) => point.y))
};
}
if (pad.shape === "circle") {
return {
minX: pad.center.x - pad.radius,
minY: pad.center.y - pad.radius,
maxX: pad.center.x + pad.radius,
maxY: pad.center.y + pad.radius
};
}
return {
minX: pad.center.x - pad.width / 2,
minY: pad.center.y - pad.height / 2,
maxX: pad.center.x + pad.width / 2,
maxY: pad.center.y + pad.height / 2
};
}
return getBoundsOfPcbElements2([pad]);
};
var getPadCenter = (pad) => {
if (pad.type === "pcb_keepout" && pad.shape !== "outline") return pad.center;
const bounds2 = getPadBounds(pad);
return midpoint(
{ x: bounds2.minX, y: bounds2.minY },
{ x: bounds2.maxX, y: bounds2.maxY }
);
};
var getPadRadius = (pad) => {
if (pad.type === "pcb_keepout" && pad.shape === "circle") return pad.radius;
const bounds2 = getPadBounds(pad);
return Math.min(bounds2.maxX - bounds2.minX, bounds2.maxY - bounds2.minY) / 2;
};
var isCircularPad = (pad) => pad.type === "pcb_via" || pad.shape === "circle";
var isPillPad = (pad) => pad.type === "pcb_smtpad" && (pad.shape === "pill" || pad.shape === "rotated_pill") || pad.type === "pcb_plated_hole" && (pad.shape === "oval" || pad.shape === "pill");
var getCircleShape = (pad) => {
const center = getPadCenter(pad);
return {
kind: "circle",
x: center.x,
y: center.y,
radius: getPadRadius(pad)
};
};
var getPolygonShape = (pad) => {
if (pad.type === "pcb_keepout") {
if (pad.shape === "outline") {
return {
kind: "polygon",
points: pad.outline
};
}
if (pad.shape !== "rect") {
throw new Error(`Expected rectangular keepout, got ${pad.shape}`);
}
return {
kind: "polygon",
points: [
{
x: pad.center.x - pad.width / 2,
y: pad.center.y - pad.height / 2
},
{
x: pad.center.x + pad.width / 2,
y: pad.center.y - pad.height / 2
},
{
x: pad.center.x + pad.width / 2,
y: pad.center.y + pad.height / 2
},
{
x: pad.center.x - pad.width / 2,
y: pad.center.y + pad.height / 2
}
]
};
}
if (pad.type === "pcb_smtpad" && (pad.shape === "polygon" || pad.shape === "rotated_rect")) {
return {
kind: "polygon",
points: getPolygonPointsForPad(pad)
};
}
if (pad.type === "pcb_plated_hole" && "rect_pad_width" in pad && "rect_pad_height" in pad) {
return {
kind: "polygon",
points: getPolygonPointsForPad(pad)
};
}
const bounds2 = getPadBounds(pad);
return {
kind: "polygon",
points: [
{ x: bounds2.minX, y: bounds2.minY },
{ x: bounds2.maxX, y: bounds2.minY },
{ x: bounds2.maxX, y: bounds2.maxY },
{ x: bounds2.minX, y: bounds2.maxY }
]
};
};
var getPadToPadGap = (padA, padB) => {
if (isPillPad(padA) && isPillPad(padB)) {
const pillA = getPillCenterLineForPad(padA);
const pillB = getPillCenterLineForPad(padB);
return segmentToSegmentMinDistance2(
pillA.start,
pillA.end,
pillB.start,
pillB.end
) - pillA.radius - pillB.radius;
}
if (isPillPad(padA) && isCircularPad(padB)) {
const pill2 = getPillCenterLineForPad(padA);
return segmentToCircleMinDistance(pill2.start, pill2.end, getCircleShape(padB)) - pill2.radius;
}
if (isCircularPad(padA) && isPillPad(padB)) {
const pill2 = getPillCenterLineForPad(padB);
return segmentToCircleMinDistance(pill2.start, pill2.end, getCircleShape(padA)) - pill2.radius;
}
if (isPillPad(padA)) {
const pill2 = getPillCenterLineForPad(padA);
return getSegmentToPolygonClearanceFromPoints(
pill2.start,
pill2.end,
getPolygonShape(padB).points
).distance - pill2.radius;
}
if (isPillPad(padB)) {
const pill2 = getPillCenterLineForPad(padB);
return getSegmentToPolygonClearanceFromPoints(
pill2.start,
pill2.end,
getPolygonShape(padA).points
).distance - pill2.radius;
}
if (isCircularPad(padA) && isCircularPad(padB)) {
return distanceBetweenCircleAndCircle(
getCircleShape(padA),
getCircleShape(padB)
);
}
if (isCircularPad(padA)) {
return distanceBetweenCircleAndPolygon(
getCircleShape(padA),
getPolygonShape(padB)
);
}
if (isCircularPad(padB)) {
return distanceBetweenCircleAndPolygon(
getCircleShape(padB),
getPolygonShape(padA)
);
}
return distanceBetweenPolygonAndPolygon(
getPolygonShape(padA),
getPolygonShape(padB)
);
};
var getPads = (circuitJson) => [
...cju(circuitJson).pcb_smtpad.list(),
...cju(circuitJson).pcb_plated_hole.list()
];
var getTraceSegments = (circuitJson) => {
const pcbTraces = cju(circuitJson).pcb_trace.list();
return pcbTraces.flatMap((pcbTrace) => {
const segments = [];
for (let i = 0; i < pcbTrace.route.length - 1; i++) {
const p1 = pcbTrace.route[i];
const p2 = pcbTrace.route[i + 1];
if (p1.route_type !== "wire") continue;
if (p2.route_type !== "wire") continue;
if (p1.layer !== p2.layer) continue;
segments.push({
type: "pcb_trace_segment",
pcb_trace_id: pcbTrace.pcb_trace_id,
_pcbTrace: pcbTrace,
thickness: "width" in p1 ? p1.width : "width" in p2 ? p2.width : DEFAULT_TRACE_THICKNESS,
layer: p1.layer,
x1: p1.x,
y1: p1.y,
x2: p2.x,
y2: p2.y
});
}
return segments;
});
};
var getTraceCenter = (segment) => {
const routePoints = segment._pcbTrace.route.flatMap((routePoint) => {
if (routePoint.route_type === "through_pad") {
return [routePoint.start, routePoint.end];
}
return [{ x: routePoint.x, y: routePoint.y }];
});
const firstPoint = routePoints[0];
const lastPoint = routePoints[routePoints.length - 1];
if (!firstPoint || !lastPoint) {
return midpoint(
{ x: segment.x1, y: segment.y1 },
{ x: segment.x2, y: segment.y2 }
);
}
return midpoint(firstPoint, lastPoint);
};
var getCenterBetweenCopperEdges = ({
tracePoint,
obstaclePoint,
traceRadius,
obstacleRadius
}) => {
const dx = obstaclePoint.x - tracePoint.x;
const dy = obstaclePoint.y - tracePoint.y;
const distance3 = Math.hypot(dx, dy);
if (distance3 === 0) return midpoint(tracePoint, obstaclePoint);
const unitX = dx / distance3;
const unitY = dy / distance3;
const traceEdge = {
x: tracePoint.x + unitX * traceRadius,
y: tracePoint.y + unitY * traceRadius
};
const obstacleEdge = {
x: obstaclePoint.x - unitX * obstacleRadius,
y: obstaclePoint.y - unitY * obstacleRadius
};
return midpoint(traceEdge, obstacleEdge);
};
var getTraceObstacleClearance = (segment, obstacle) => {
const start = { x: segment.x1, y: segment.y1 };
const end = { x: segment.x2, y: segment.y2 };
const traceRadius = segment.thickness / 2;
if (obstacle.type === "pcb_via" || isCircularPad(obstacle)) {
const circle = obstacle.type === "pcb_via" ? {
x: obstacle.x,
y: obstacle.y,
radius: obstacle.outer_diameter / 2
} : getCircleShape(obstacle);
const closestPoint = pointToSegmentClosestPoint2(circle, start, end);
return {
gap: segmentToCircleMinDistance(start, end, circle) - traceRadius,
center: getCenterBetweenCopperEdges({
tracePoint: closestPoint,
obstaclePoint: circle,
traceRadius,
obstacleRadius: circle.radius
})
};
}
if (isPillPad(obstacle)) {
const clearance2 = getSegmentToPillClearance(segment, obstacle);
return {
gap: clearance2.distance - traceRadius - clearance2.radius,
center: getCenterBetweenCopperEdges({
tracePoint: clearance2.tracePoint,
obstaclePoint: clearance2.obstaclePoint,
traceRadius,
obstacleRadius: clearance2.radius
})
};
}
const clearance = getSegmentToPolygonClearanceFromPoints(
start,
end,
getPolygonShape(obstacle).points
);
return {
gap: clearance.distance - traceRadius,
center: getCenterBetweenCopperEdges({
tracePoint: clearance.tracePoint,
obstaclePoint: clearance.obstaclePoint,
traceRadius,
obstacleRadius: 0
})
};
};
var isTraceObstacleOverlap = (gap) => gap <= 0;
// lib/check-each-pcb-trace-non-overlapping/getClosestPointBetweenSegmentAndBounds.ts
var getClosestPointBetweenSegmentAndBounds = (segment, bounds2) => {
const p1 = { x: segment.x1, y: segment.y1 };
const p2 = { x: segment.x2, y: segment.y2 };
const minX = bounds2.minX;
const minY = bounds2.minY;
const maxX = bounds2.maxX;
const maxY = bounds2.maxY;
if (p1.x === p2.x && p1.y === p2.y) {
const closestX = Math.max(minX, Math.min(maxX, p1.x));
const closestY = Math.max(minY, Math.min(maxY, p1.y));
if (closestX === p1.x && closestY === p1.y) {
return { x: p1.x, y: p1.y };
}
return { x: closestX, y: closestY };
}
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const tMinX = dx !== 0 ? (minX - p1.x) / dx : Number.NEGATIVE_INFINITY;
const tMaxX = dx !== 0 ? (maxX - p1.x) / dx : Number.POSITIVE_INFINITY;
const tMinY = dy !== 0 ? (minY - p1.y) / dy : Number.NEGATIVE_INFINITY;
const tMaxY = dy !== 0 ? (maxY - p1.y) / dy : Number.POSITIVE_INFINITY;
const tEnter = Math.max(Math.min(tMinX, tMaxX), Math.min(tMinY, tMaxY));
const tExit = Math.min(Math.max(tMinX, tMaxX), Math.max(tMinY, tMaxY));
if (tEnter <= tExit && tExit >= 0 && tEnter <= 1) {
const t = Math.max(0, Math.min(1, tEnter));
return {
x: p1.x + t * dx,
y: p1.y + t * dy
};
}
const closestToP1 = {
x: Math.max(minX, Math.min(maxX, p1.x)),
y: Math.max(minY, Math.min(maxY, p1.y))
};
const closestToP2 = {
x: Math.max(minX, Math.min(maxX, p2.x)),
y: Math.max(minY, Math.min(maxY, p2.y))
};
const distToP1Squared = (closestToP1.x - p1.x) ** 2 + (closestToP1.y - p1.y) ** 2;
const distToP2Squared = (closestToP2.x - p2.x) ** 2 + (closestToP2.y - p2.y) ** 2;
const edges = [
{ start: { x: minX, y: minY }, end: { x: maxX, y: minY } },
// Bottom edge
{ start: { x: maxX, y: minY }, end: { x: maxX, y: maxY } },
// Right edge
{ start: { x: maxX, y: maxY }, end: { x: minX, y: maxY } },
// Top edge
{ start: { x: minX, y: maxY }, end: { x: minX, y: minY } }
// Left edge
];
let minDistance = Math.min(distToP1Squared, distToP2Squared);
let closestPoint = distToP1Squared <= distToP2Squared ? closestToP1 : closestToP2;
const clamp2 = (value, min, max) => {
return Math.max(min, Math.min(max, value));
};
for (const edge of edges) {
const va = { x: p2.x - p1.x, y: p2.y - p1.y };
const vb = { x: edge.end.x - edge.start.x, y: edge.end.y - edge.start.y };
const w = { x: p1.x - edge.start.x, y: p1.y - edge.start.y };
const dotAA = va.x * va.x + va.y * va.y;
const dotAB = va.x * vb.x + va.y * vb.y;
const dotAW = va.x * w.x + va.y * w.y;
const dotBB = vb.x * vb.x + vb.y * vb.y;
const dotBW = vb.x * w.x + vb.y * w.y;
const denominator = dotAA * dotBB - dotAB * dotAB;
if (Math.abs(denominator) < 1e-10) continue;
let tA = (dotAB * dotBW - dotBB * dotAW) / denominator;
let tB = (dotAA * dotBW - dotAB * dotAW) / denominator;
tA = clamp2(tA, 0, 1);
tB = clamp2(tB, 0, 1);
const closestOnSegment = {
x: p1.x + tA * va.x,
y: p1.y + tA * va.y
};
const closestOnEdge = {
x: edge.start.x + tB * vb.x,
y: edge.start.y + tB * vb.y
};
const dx2 = closestOnSegment.x - closestOnEdge.x;
const dy2 = closestOnSegment.y - closestOnEdge.y;
const distSquared = dx2 * dx2 + dy2 * dy2;
if (distSquared < minDistance) {
minDistance = distSquared;
closestPoint = {
x: (closestOnSegment.x + closestOnEdge.x) / 2,
y: (closestOnSegment.y + closestOnEdge.y) / 2
};
}
}
return closestPoint;
};
// lib/check-each-pcb-trace-non-overlapping/getClosestPointBetweenSegments.ts
var getClosestPointBetweenSegments = (segmentA, segmentB) => {
const a1 = { x: segmentA.x1, y: segmentA.y1 };
const a2 = { x: segmentA.x2, y: segmentA.y2 };
const b1 = { x: segmentB.x1, y: segmentB.y1 };
const b2 = { x: segmentB.x2, y: segmentB.y2 };
const va = { x: a2.x - a1.x, y: a2.y - a1.y };
const vb = { x: b2.x - b1.x, y: b2.y - b1.y };
const lenSqrA = va.x * va.x + va.y * va.y;
const lenSqrB = vb.x * vb.x + vb.y * vb.y;
if (lenSqrA === 0 || lenSqrB === 0) {
if (lenSqrA === 0 && lenSqrB === 0) {
return {
x: (a1.x + b1.x) / 2,
y: (a1.y + b1.y) / 2
};
}
if (lenSqrA === 0) {
const t2 = clamp(
((a1.x - b1.x) * vb.x + (a1.y - b1.y) * vb.y) / lenSqrB,
0,
1
);
const closestOnB2 = {
x: b1.x + t2 * vb.x,
y: b1.y + t2 * vb.y
};
return {
x: (a1.x + closestOnB2.x) / 2,
y: (a1.y + closestOnB2.y) / 2
};
}
const t = clamp(
((b1.x - a1.x) * va.x + (b1.y - a1.y) * va.y) / lenSqrA,
0,
1
);
const closestOnA2 = {
x: a1.x + t * va.x,
y: a1.y + t * va.y
};
return {
x: (closestOnA2.x + b1.x) / 2,
y: (closestOnA2.y + b1.y) / 2
};
}
const w = { x: a1.x - b1.x, y: a1.y - b1.y };
const dotAA = va.x * va.x + va.y * va.y;
const dotAB = va.x * vb.x + va.y * vb.y;
const dotAW = va.x * w.x + va.y * w.y;
const dotBB = vb.x * vb.x + vb.y * vb.y;
const dotBW = vb.x * w.x + vb.y * w.y;
const denominator = dotAA * dotBB - dotAB * dotAB;
if (denominator < 1e-10) {
return closestPointsParallelSegments(
a1,
a2,
b1,
b2,
va,
vb,
lenSqrA,
lenSqrB
);
}
let tA = (dotAB * dotBW - dotBB * dotAW) / denominator;
let tB = (dotAA * dotBW - dotAB * dotAW) / denominator;
tA = clamp(tA, 0, 1);
tB = clamp(tB, 0, 1);
tB = (tA * dotAB + dotBW) / dotBB;
tB = clamp(tB, 0, 1);
tA = (tB * dotAB - dotAW) / dotAA;
tA = clamp(tA, 0, 1);
const closestOnA = {
x: a1.x + tA * va.x,
y: a1.y + tA * va.y
};
const closestOnB = {
x: b1.x + tB * vb.x,
y: b1.y + tB * vb.y
};
const dx = closestOnA.x - closestOnB.x;
const dy = closestOnA.y - closestOnB.y;
const distance3 = Math.sqrt(dx * dx + dy * dy);
const averagePoint = {
x: (closestOnA.x + closestOnB.x) / 2,
y: (closestOnA.y + closestOnB.y) / 2
};
return averagePoint;
};
var closestPointsParallelSegments = (a1, a2, b1, b2, va, vb, lenSqrA, lenSqrB) => {
let tA = ((b1.x - a1.x) * va.x + (b1.y - a1.y) * va.y) / lenSqrA;
tA = clamp(tA, 0, 1);
const pointOnA1 = { x: a1.x + tA * va.x, y: a1.y + tA * va.y };
let tA2 = ((b2.x - a1.x) * va.x + (b2.y - a1.y) * va.y) / lenSqrA;
tA2 = clamp(tA2, 0, 1);
const pointOnA2 = { x: a1.x + tA2 * va.x, y: a1.y + tA2 * va.y };
let tB = ((a1.x - b1.x) * vb.x + (a1.y - b1.y) * vb.y) / lenSqrB;
tB = clamp(tB, 0, 1);
const pointOnB1 = { x: b1.x + tB * vb.x, y: b1.y + tB * vb.y };
let tB2 = ((a2.x - b1.x) * vb.x + (a2.y - b1.y) * vb.y) / lenSqrB;
tB2 = clamp(tB2, 0, 1);
const pointOnB2 = { x: b1.x + tB2 * vb.x, y: b1.y + tB2 * vb.y };
const distances = [
{
pointA: pointOnA1,
pointB: b1,
distance: Math.sqrt(
(pointOnA1.x - b1.x) ** 2 + (pointOnA1.y - b1.y) ** 2
)
},
{
pointA: pointOnA2,
pointB: b2,
distance: Math.sqrt(
(pointOnA2.x - b2.x) ** 2 + (pointOnA2.y - b2.y) ** 2
)
},
{
pointA: a1,
pointB: pointOnB1,
distance: Math.sqrt(
(a1.x - pointOnB1.x) ** 2 + (a1.y - pointOnB1.y) ** 2
)
},
{
pointA: a2,
pointB: pointOnB2,
distance: Math.sqrt(
(a2.x - pointOnB2.x) ** 2 + (a2.y - pointOnB2.y) ** 2
)
}
];
const closestPair = distances.reduce(
(closest, current) => current.distance < closest.distance ? current : closest
);
return {
x: (closestPair.pointA.x + closestPair.pointB.x) / 2,
y: (closestPair.pointA.y + closestPair.pointB.y) / 2
};
};
var clamp = (value, min, max) => {
return Math.max(min, Math.min(max, value));
};
// lib/check-each-pcb-trace-non-overlapping/getCollidableBounds.ts
import { getBoundsOfPcbElements as getBoundsOfPcbElements3 } from "@tscircuit/circuit-json-util";
var getCollidableBounds = (collidable) => {
if (collidable.type === "pcb_trace_segment") {
return {
minX: Math.min(collidable.x1, collidable.x2),
minY: Math.min(collidable.y1, collidable.y2),
maxX: Math.max(collidable.x1, collidable.x2),
maxY: Math.max(collidable.y1, collidable.y2)
};
}
if (collidable.type === "pcb_smtpad" || collidable.type === "pcb_plated_hole") {
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;
if (isPolygon) {
const polygonPoints = getPolygonPointsForPad(collidable);
return {
minX: Math.min(...polygonPoints.map((point) => point.x)),
minY: Math.min(...polygonPoints.map((point) => point.y)),
maxX: Math.max(...polygonPoints.map((point) => point.x)),
maxY: Math.max(...polygonPoints.map((point) => point.y))
};
}
if (collidable.type === "pcb_smtpad" && collidable.shape === "rotated_pill") {
const pill2 = getPillCenterLineForPad(collidable);
return {
minX: Math.min(pill2.start.x, pill2.end.x) - pill2.radius,
minY: Math.min(pill2.start.y, pill2.end.y) - pill2.radius,
maxX: Math.max(pill2.start.x, pill2.end.x) + pill2.radius,
maxY: Math.max(pill2.start.y, pill2.end.y) + pill2.radius
};
}
}
return getBoundsOfPcbElements3([collidable]);
};
// lib/check-each-pcb-trace-non-overlapping/getPcbPortIdsConnectedToTraces.ts
function getPcbPortIdsConnectedToRoutePoint(routePoint) {
if (routePoint.route_type !== "wire") return [];
return [routePoint.start_pcb_port_id, routePoint.end_pcb_port_id].filter(
(portId) => Boolean(portId)
);
}
function getPcbPortIdsConnectedToTrace(trace) {
const connectedPcbPorts = /* @__PURE__ */ new Set();
for (const segment of trace.route) {
for (const portId of getPcbPortIdsConnectedToRoutePoint(segment)) {
connectedPcbPorts.add(portId);
}
}
return Array.from(connectedPcbPorts);
}
function getPcbPortIdsConnectedToTraces(traces) {
const connectedPorts = /* @__PURE__ */ new Set();
for (const trace of traces) {
for (const portId of getPcbPortIdsConnectedToTrace(trace)) {
connectedPorts.add(portId);
}
}
return Array.from(connectedPorts);
}
// lib/check-each-pcb-trace-non-overlapping/getRadiusOfCircuitJsonElement.ts
var getRadiusOfCircuitJsonElement = (obj) => {
if (obj.type === "pcb_via") {
return obj.outer_diameter / 2;
}
if (obj.type === "pcb_plated_hole" && obj.shape === "circle") {
return obj.outer_diameter / 2;
}
if (obj.type === "pcb_hole" && obj.hole_shape === "circle") {
return obj.hole_diameter / 2;
}
if (obj.type === "pcb_smtpad" && obj.shape === "circle") {
return obj.radius;
}
throw new Error(
`Could not determine radius of element: ${JSON.stringify(obj)}`
);
};
// lib/check-each-pcb-trace-non-overlapping/check-each-pcb-trace-non-overlapping.ts
var getPcbComponentConnectionElementId = (element) => {
if (element.type === "pcb_port") return element.pcb_port_id;
if (element.type === "pcb_smtpad") return element.pcb_smtpad_id;
return element.pcb_plated_hole_id;
};
function checkEachPcbTraceNonOverlapping(circuitJson, {
connMap,
minClearance
} = {}) {
const errors = [];
addStartAndEndPortIdsIfMissing(circuitJson);
connMap ??= getFullConnectivityMapFromCircuitJson2(circuitJson);
const board = getPcbBoard(circuitJson);
minClearance ??= getBoardDrcValue(board, "min_trace_to_pad_edge_clearance") ?? DEFAULT_TRACE_MARGIN;
const pcbTraces = cju2(circuitJson).pcb_trace.list();
const pcbTraceSegments = pcbTraces.flatMap((pcbTrace) => {
const segments = [];
for (let i = 0; i < pcbTrace.route.length - 1; i++) {
const p1 = pcbTrace.route[i];
const p2 = pcbTrace.route[i + 1];
if (p1.route_type !== "wire") continue;
if (p2.route_type !== "wire") continue;
if (p1.layer !== p2.layer) continue;
segments.push({
type: "pcb_trace_segment",
pcb_trace_id: pcbTrace.pcb_trace_id,
_pcbTrace: pcbTrace,
thickness: "width" in p1 ? p1.width : "width" in p2 ? p2.width : DEFAULT_TRACE_THICKNESS,
layer: p1.layer,
x1: p1.x,
y1: p1.y,
x2: p2.x,
y2: p2.y
});
}
return segments;
});
const pcbSmtPads = cju2(circuitJson).pcb_smtpad.list();
const pcbPlatedHoles = cju2(circuitJson).pcb_plated_hole.list();
const pcbPorts = cju2(circuitJson).pcb_port.list();
const pcbHoles = cju2(circuitJson).pcb_hole.list();
const pcbVias = cju2(circuitJson).pcb_via.list();
const pcbKeepouts = cju2(circuitJson).pcb_keepout.list();
const pcbComponentConnectionElements = [
...pcbPorts,
...pcbSmtPads,
...pcbPlatedHoles
];
const excludedConnectionIdsByKeepoutId = /* @__PURE__ */ new Map();
for (const keepout of pcbKeepouts) {
const excludedPcbComponentIds = new Set(
keepout.excluded_pcb_component_ids ?? []
);
if (excludedPcbComponentIds.size === 0) continue;
excludedConnectionIdsByKeepoutId.set(
keepout.pcb_keepout_id,
pcbComponentConnectionElements.filter(
(element) => element.pcb_component_id && excludedPcbComponentIds.has(element.pcb_component_id)
).map(getPcbComponentConnectionElementId)
);
}
const allObjects = [
...pcbTraceSegments,
...pcbSmtPads,
...pcbPlatedHoles,
...pcbHoles,
...pcbVias,
...pcbKeepouts
];
const spatialIndex = new SpatialObjectIndex({
objects: allObjects,
getBounds: getCollidableBounds
});
const getReadableName = (id) => getReadableNameForElement2(circuitJson, id);
const constructErrorMessage = (traceName, otherName, gap) => {
if (isTraceObstacleOverlap(gap)) {
return `PCB trace ${traceName} overlaps with ${otherName} (accidental contact)`;
}
return `PCB trace ${traceName} is too close to ${otherName} (gap: ${gap.toFixed(3)}mm)`;
};
const errorIds = /* @__PURE__ */ new Set();
for (const segmentA of pcbTraceSegments) {
const requiredMargin = minClearance;
const bounds2 = getCollidableBounds(segmentA);
const nearbyObjects = spatialIndex.getObjectsInBounds(
bounds2,
requiredMargin + segmentA.thickness / 2
);
if (segmentA.x1 === segmentA.x2 && segmentA.y1 === segmentA.y2) continue;
for (const obj of nearbyObjects) {
if (!getLayersOfPcbElement(obj).includes(segmentA.layer)) {
continue;
}
if (obj.type === "pcb_keepout" && (excludedConnectionIdsByKeepoutId.get(obj.pcb_keepout_id) ?? []).some(
(connectionId) => connMap.areIdsConnected(segmentA.pcb_trace_id, connectionId)
)) {
continue;
}
if (obj.type === "pcb_trace_segment") {
const segmentB = obj;
if (segmentA.layer !== segmentB.layer) continue;
if (connMap.areIdsConnected(segmentA.pcb_trace_id, segmentB.pcb_trace_id))
continue;
const gap2 = segmentToSegmentMinDistance3(
{ x: segmentA.x1, y: segmentA.y1 },
{ x: segmentA.x2, y: segmentA.y2 },
{ x: segmentB.x1, y: segmentB.y1 },
{ x: segmentB.x2, y: segmentB.y2 }
) - segmentA.thickness / 2 - segmentB.thickness / 2;
if (gap2 > minClearance - EPSILON) continue;
const pcb_trace_error_id = `overlap_${segmentA.pcb_trace_id}_${segmentB.pcb_trace_id}`;
const pcb_trace_error_id_reverse = `overlap_${segmentB.pcb_trace_id}_${segmentA.pcb_trace_id}`;
if (errorIds.has(pcb_trace_error_id)) continue;
if (errorIds.has(pcb_trace_error_id_reverse)) continue;
errorIds.add(pcb_trace_error_id);
errors.push({
type: "pcb_trace_error",
error_type: "pcb_trace_error",
message: constructErrorMessage(
getReadableName(segmentA.pcb_trace_id),
getReadableName(segmentB.pcb_trace_id),
gap2
),
pcb_trace_id: segmentA.pcb_trace_id,
source_trace_id: "",
pcb_trace_error_id,
pcb_component_ids: [],
center: getClosestPointBetweenSegments(segmentA, segmentB),
pcb_port_ids: getPcbPortIdsConnectedToTraces([
segmentA._pcbTrace,
segmentB._pcbTrace
])
});
continue;
}
const primaryObjId = getPrimaryId2(obj);
if (connMap.areIdsConnected(
segmentA.pcb_trace_id,
"pcb_trace_id" in obj ? obj.pcb_trace_id : primaryObjId
))
continue;
if (obj.type === "pcb_smtpad" || obj.type === "pcb_plated_hole" || obj.type === "pcb_via") {
const { gap: gap2, center } = getTraceObstacleClearance(segmentA, obj);
if (!isTraceObstacleOverlap(gap2)) continue;
const pcb_trace_error_id = `overlap_${segmentA.pcb_trace_id}_${primaryObjId}`;
if (errorIds.has(pcb_trace_error_id)) continue;
errorIds.add(pcb_trace_error_id);
errors.push({
type: "pcb_trace_error",
error_type: "pcb_trace_error",
message: constructErrorMessage(
getReadableName(segmentA.pcb_trace_id),
`${obj.type} "${getReadableName(primaryObjId)}"`,
gap2
),
pcb_trace_id: segmentA.pcb_trace_id,
center,
source_trace_id: "",
pcb_trace_error_id,
pcb_component_ids: [
"pcb_component_id" in obj ? obj.pcb_component_id : void 0
].filter(Boolean),
pcb_port_ids: [
...getPcbPortIdsConnectedToTraces([segmentA._pcbTrace]),
"pcb_port_id" in obj ? obj.pcb_port_id : void 0
].filter(Boolean)
});
continue;
}
const isCircular = obj.type === "pcb_hole";
if (isCircular) {
const radius = getRadiusOfCircuitJsonElement(obj);
const distance3 = segmentToCircleMinDistance2(
{ x: segmentA.x1, y: segmentA.y1 },
{ x: segmentA.x2, y: segmentA.y2 },
{ x: obj.x, y: obj.y, radius }
);
const gap2 = distance3 - segmentA.thickness / 2;
if (gap2 > minClearance - EPSILON) continue;
const pcb_trace_error_id = `overlap_${segmentA.pcb_trace_id}_${primaryObjId}`;
if (errorIds.has(pcb_trace_error_id)) continue;
errorIds.add(pcb_trace_error_id);
errors.push({
type: "pcb_trace_error",
error_type: "pcb_trace_error",
message: constructErrorMessage(
getReadableName(segmentA.pcb_trace_id),
`${obj.type} "${getReadableName(getPrimaryId2(obj))}"`,
gap2
),
pcb_trace_id: segmentA.pcb_trace_id,
center: getClosestPointBetweenSegmentAndBounds(
segmentA,
getCollidableBounds(obj)
),
source_trace_id: "",
pcb_trace_error_id,
pcb_component_ids: [
"pcb_component_id" in obj ? obj.pcb_component_id : void 0
].filter(Boolean),
pcb_port_ids: [
...getPcbPortIdsConnectedToTraces([segmentA._pcbTrace]),
"pcb_port_id" in obj ? obj.pcb_port_id : void 0
].filter(Boolean)
});
}
const gap = segmentToBoundsMinDistance(
{ x: segmentA.x1, y: segmentA.y1 },
{ x: segmentA.x2, y: segmentA.y2 },
getCollidableBounds(obj)
) - segmentA.thickness / 2;
if (gap + EPSILON < requiredMargin) {
const pcb_trace_error_id = `overlap_${segmentA.pcb_trace_id}_${primaryObjId}`;
if (errorIds.has(pcb_trace_error_id)) continue;
errorIds.add(pcb_trace_error_id);
errors.push({
type: "pcb_trace_error",
error_type: "pcb_trace_error",
message: constructErrorMessage(
getReadableName(segmentA.pcb_trace_id),
`${obj.type} "${getReadableName(getPrimaryId2(obj))}"`,
gap
),
pcb_trace_id: segmentA.pcb_trace_id,
source_trace_id: "",
pcb_trace_error_id,
pcb_component_ids: [
"pcb_component_id" in obj ? obj.pcb_component_id : void 0
].filter(Boolean),
center: getClosestPointBetweenSegmentAndBounds(
segmentA,
getCollidableBounds(obj)
),
pcb_port_ids: [
...getPcbPortIdsConnectedToTraces([segmentA._pcbTrace]),
"pcb_port_id" in obj ? obj.pcb_port_id : void 0
].filter(Boolean)
});
}
}
}
return errors;
}
// lib/net-manager.ts
var NetManager = class {
networks = /* @__PURE__ */ new Set();
setConnected(nodes) {
if (nodes.length < 2) return;
let targetNetwork = null;
for (const network of this.networks) {
for (const node of nodes) {
if (network.has(node)) {
if (targetNetwork === null) {
targetNetwork = network;
} else if (targetNetwork !== network) {
for (const mergeNode of network) {
targetNetwork.add(mergeNode);
}
this.networks.delete(network);
}
break;
}
}
if (targetNetwork !== null && targetNetwork !== network) break;
}
if (targetNetwork === null) {
targetNetwork = new Set(nodes);
this.networks.add(targetNetwork);
} else {
for (const node of nodes) {
targetNetwork.add(node);
}
}
}
isConnected(nodes) {
if (nodes.length < 2) return true;
for (const network of this.networks) {
if (nodes.every((node) => network.has(node))) {
return true;
}
}
return false;
}
};
// lib/check-pcb-components-out-of-board/checkViasOffBoard.ts
import { getReadableNameForElement as getReadableNameForElement3 } from "@tscircuit/circuit-json-util";
function checkViasOffBoard(circuitJson) {
const vias = circuitJson.filter((element) => element.type === "pcb_via");
const violationsById = new Map(
checkCopperToBoardEdgeClearance(
circuitJson.filter(
(element) => element.type === "pcb_board" || element.type === "pcb_via"
)
).map((error) => [
error.pcb_placement_error_id.replace(
"copper_too_close_to_board_edge_",
""
),
error
])
);
return vias.flatMap((via) => {
const violation = violationsById.get(via.pcb_via_id);
if (!violation) return [];
const viaName = getReadableNameForElement3(circuitJson, via.pcb_via_id);
return [
{
...violation,
pcb_placement_error_id: `out_of_board_${via.pcb_via_id}`,
message: `Via ${viaName} is outside or crossing the board boundary`
}
];
});
}
// lib/check-pcb-components-out-of-board/checkPcbComponentsOutOfBoard.ts
import * as Flatten3 from "@flatten-js/core";
import { rotateDEG as rotateDEG2, applyToPoint as applyToPoint2 } from "transformation-matrix";
function isPolygonCCW(poly) {
return poly.area() >= 0;
}
function rectanglePolygon({
center,
size,
rotationDeg = 0
}) {
const cx = center.x;
const cy = center.y;
const hw = size.width / 2;
const hh = size.height / 2;
const corners = [
new Flatten3.Point(cx - hw, cy - hh),
new Flatten3.Point(cx + hw, cy - hh),
new Flatten3.Point(cx + hw, cy + hh),
new Flatten3.Point(cx - hw, cy + hh)
];
let poly = new Flatten3.Polygon(corners);
if (rotationDeg) {
const matrix = rotateDEG2(rotationDeg, cx, cy);
const rotatedCorners = corners.map((pt) => {
const p = applyToPoint2(matrix, { x: pt.x, y: pt.y });
return new Flatten3.Point(p.x, p.y);
});
poly = new Flatten3.Polygon(rotatedCorners);
}
if (!isPolygonCCW(poly)) poly.reverse();
return poly;
}
function boardToPolygon2({
board
}) {
if (board.outline && board.outline.length > 0) {
const points = board.outline.map((p) => new Flatten3.Point(p.x, p.y));
const poly = new Flatten3.Polygon(points);
if (!isPolygonCCW(poly)) {
poly.reverse();
}
return poly;
}
if (board.center && typeof board.width === "number" && typeof board.height === "number") {
return rectanglePolygon({
center: board.center,
size: { width: board.width, height: board.height },
rotationDeg: 0
});
}
return null;
}
function getComponentName({
circuitJson,
component
}) {
if (component.source_component_id) {
const sourceComponent = circuitJson.find(
(el) => el.type === "source_component" && el.source_component_id === component.source_component_id
);
if (sourceComponent && "name" in sourceComponent && sourceComponent.name) {
return sourceComponent.name;
}
}
return getReadableNameForComponent(circuitJson, component.pcb_component_id);
}
function computeOverlapDistance(compPoly, boardPoly, componentCenter, componentWidth, componentHeight, rotationDeg) {
const centerPoint = new Flatten3.Point(componentCenter.x, componentCenter.y);
if (!boardPoly.contains(centerPoint)) {
const dist = boardPoly.distanceTo(centerPoint);
return Array.isArray(dist) ? dist[0] : Number(dist) || 0;
}
const hw = componentWidth / 2;
const hh = componentHeight / 2;
const corners = [
{ x: componentCenter.x - hw, y: componentCenter.y - hh },
{ x: componentCenter.x + hw, y: componentCenter.y - hh },
{ x: componentCenter.x + hw, y: componentCenter.y + hh },
{ x: componentCenter.x - hw, y: componentCenter.y + hh }
];
const midpoints = [];
for (let i = 0; i < 4; i++) {
const next = (i + 1) % 4;
midpoints.push({
x: (corners[i].x + corners[next].x) / 2,
y: (corners[i].y + corners[next].y) / 2
});
}
const matrix = rotateDEG2(rotationDeg, componentCenter.x, componentCenter.y);
const rotatePoint2 = (pt) => {
const p = applyToPoint2(matrix, pt);
return new Flatten3.Point(p.x, p.y);
};
const rotatedPoints = corners.concat(midpoints).map(rotatePoint2);
let maxDistance = 0;
for (const pt of rotatedPoints) {
if (!boardPoly.contains(pt)) {
const dist = boardPoly.distanceTo(pt);
const d = Array.isArray(dist) ? dist[0] : Number(dist) || 0;
if (d > maxDistance) maxDistance = d;
}
}
if (maxDistance > 0) {
return maxDistance;
}
try {
const intersection = Flatten3.BooleanOperations.intersect(
compPoly,
boardPoly
);
let intersectionArea = 0;
if (!intersection) {
intersectionArea = 0;
} else if (Array.isArray(intersection)) {
intersectionArea = intersection.reduce(
(sum, p) => sum + (typeof p.area === "function" ? p.area() : 0),
0
);
} else if (typeof intersection.area === "function") {
intersectionArea = intersection.area();
} else {
intersectionArea = 0;
}
const compArea = compPoly.area();
if (intersectionArea > 0 && intersectionArea < compArea) {
const overlapRatio = 1 - intersectionArea / compArea;
const compWidth = Math.abs(componentWidth);
const compHeight = Math.abs(componentHeight);
return Math.min(compWidth, compHeight) * overlapRatio;
} else if (intersectionArea === 0) {
return 0.1;
} else {
return 0.1;
}
} catch {
return 0.1;
}
}
function getRepositionSuggestion({
componentPoly,
boardPoly
}) {
const boardBox = boardPoly.box;
const componentBox = componentPoly.box;
let deltaX = 0;
let deltaY = 0;
if (componentBox.xmin < boardBox.xmin) {
deltaX = boardBox.xmin - componentBox.xmin;
} else if (componentBox.xmax > boardBox.xmax) {
deltaX = boardBox.xmax - componentBox.xmax;
}
if (componentBox.ymin < boardBox.ymin) {
deltaY = boardBox.ymin - componentBox.ymin;
} else if (componentBox.ymax > boardBox.ymax) {
deltaY = boardBox.ymax - componentBox.ymax;
}
if (deltaX === 0 && deltaY === 0) {
return null;
}
const xDir = deltaX >= 0 ? "right" : "left";
const yDir = deltaY >= 0 ? "up" : "down";
const absDx = Math.abs(Math.round(deltaX * 100) / 100);
const absDy = Math.abs(Math.round(deltaY * 100) / 100);
if (absDx > 0 && absDy > 0) {
return `Try moving it ${absDx}mm ${xDir} and ${absDy}mm ${yDir} to fit within the board edge.`;
}
if (absDx > 0) {
return `Try moving it ${absDx}mm ${xDir} to fit within the board edge.`;
}
return `Try moving it ${absDy}mm ${yDir} to fit within the board edge.`;
}
function checkPcbComponentsOutOfBoard(circuitJson) {
const board = circuitJson.find(
(el) => el.type === "pcb_board"
);
if (!board) return [];
const boardPoly = boardToPolygon2({ board });
if (!boardPoly) return [];
const components = circuitJson.filter(
(el) => el.type === "pcb_component"
);
if (components.length === 0) return [];
const errors = [];
for (const c of components) {
if (c.is_allowed_to_be_off_board) continue;
if (!c.center || typeof c.width !== "number" || typeof c.height !== "number")
continue;
if (c.width <= 0 || c.height <= 0) continue;
const compPoly = rectanglePolygon({
center: c.center,
size: { width: c.width, height: c.height },
rotationDeg: 0
});
if (compPoly.area() === 0) continue;
const isInside = boardPoly.contains(compPoly);
if (isInside) continue;
const overlapDistance = computeOverlapDistance(
compPoly,
boardPoly,
c.center,
c.width,
c.height,
0
);
const compName = getComponentName({ circuitJson, component: c });
const overlapDistanceMm = Math.round(overlapDistance * 100) / 100;
const repositionSuggestion = getRepositionSuggestion({
componentPoly: compPoly,
boardPoly
});
errors.push({
type: "pcb_component_outside_board_error",
error_type: "pcb_component_outside_board_error",
pcb_component_outside_board_error_id: `pcb_component_outside_board_${c.pcb_component_id}`,
message: `Component ${compName} extends outside board boundaries by ${overlapDistanceMm}mm.${repositionSuggestion ? ` ${repositionSuggestion}` : ""}`,
pcb_component_id: c.pcb_component_id,
pcb_board_id: board.pcb_board_id,
component_center: c.center,
component_bounds: {
min_x: compPoly.box.xmin,
max_x: compPoly.box.xmax,
min_y: compPoly.box.ymin,
max_y: compPoly.box.ymax
},
subcircuit_id: c.subcircuit_id,
source_component_id: c.source_component_id
});
}
return errors;
}
// lib/check-pcb-component-over-cutout.ts
import { doBoundsOverlap } from "@tscircuit/math-utils";
import * as Flatten4 from "@flatten-js/core";
import { applyToPoint as applyToPoint3, rotateDEG as rotateDEG3 } from "transformation-matrix";
var CUTOUT_CIRCLE_SEGMENTS = 32;
function rectanglePolygon2({
center,
width,
height,
rotation = 0
}) {
const halfWidth = width / 2;
const halfHeight = height / 2;
const corners = [
{ x: center.x - halfWidth, y: center.y - halfHeight },
{ x: center.x + halfWidth, y: center.y - halfHeight },
{ x: center.x + halfWidth, y: center.y + halfHeight },
{ x: center.x - halfWidth, y: center.y + halfHeight }
];
const matrix = rotateDEG3(rotation, center.x, center.y);
return new Flatten4.Polygon(
corners.map((corner) => {
const rotated = rotation ? applyToPoint3(matrix, corner) : corner;
return new Flatten4.Point(rotated.x, rotated.y);
})
);
}
function circlePolygon({
center,
radius
}) {
return new Flatten4.Polygon(
Array.from({ length: CUTOUT_CIRCLE_SEGMENTS }, (_, index) => {
const angle = 2 * Math.PI * index / CUTOUT_CIRCLE_SEGMENTS;
return new Flatten4.Point(
center.x + Math.cos(angle) * radius,
center.y + Math.sin(angle) * radius
);
})
);
}
function cutoutToPolygon(cutout) {
if (cutout.shape === "rect") {
return rectanglePolygon2({
center: cutout.center,
width: cutout.width,
height: cutout.height,
rotation: cutout.rotation ?? 0
});
}
if (cutout.shape === "circle") {
return circlePolygon({ center: cutout.center, radius: cutout.radius });
}
if (cutout.shape === "polygon") {
return new Flatten4.Polygon(
cutout.points.map((point) => new Flatten4.Point(point.x, point.y))
);
}
return null;
}
function polygonBoxToBounds(polygon) {
return {
minX: polygon.box.xmin,
minY: polygon.box.ymin,
maxX: polygon.box.xmax,
maxY: polygon.box.ymax
};
}
function doPolygonsOverlap(polygonA, polygonB) {
if (!doBoundsOverlap(polygonBoxToBounds(polygonA), polygonBoxToBounds(polygonB))) {
return false;
}
if (polygonA.contains(polygonB) || polygonB.contains(polygonA)) return true;
try {
const intersections = Flatten4.BooleanOperations.intersect(
polygonA,
polygonB
);
if (Array.isArray(intersections)) {
return intersections.some((polygon) => polygon.area() > 0);
}
return intersections.area() > 0;
} catch {
return false;
}
}
function checkPcbComponentOverCutout(circuitJson) {
const cutouts = circuitJson.filter(
(element) => element.type === "pcb_cutout"
);
const components = circuitJson.filter(
(element) => element.type === "pcb_component"
);
if (cutouts.length === 0 || components.length === 0) return [];
const cutoutPolygons = cutouts.map((cutout) => ({ cutout, polygon: cutoutToPolygon(cutout) })).filter(
(entry) => entry.polygon !== null && entry.polygon.area() > 0
);
const errors = [];
for (const component of components) {
if (!component.center || component.width <= 0 || component.height <= 0) {
continue;
}
const componentPolygon = rectanglePolygon2({
center: component.center,
width: component.width,
height: component.height
});
for (const { cutout, polygon: cutoutPolygon } of cutoutPolygons) {
if (cutout.pcb_component_id === component.pcb_component_id) continue;
if (!doPolygonsOverlap(componentPolygon, cutoutPolygon)) continue;
const componentName = getReadableNameForComponent(
circuitJson,
component.pcb_component_id
);
const cutoutId = cutout.pcb_cutout_id;
errors.push({
type: "pcb_placement_error",
pcb_placement_error_id: `component_over_cutout_${component.pcb_component_id}_${cutoutId}`,
error_type: "pcb_placement_error",
message: `Component ${componentName} overlaps with pcb_cutout [${cutoutId}]`,
subcircuit_id: component.subcircuit_id
});
}
}
return errors;
}
// lib/check-pcb-copper-over-keepout.ts
import { cju as cju3, getPrimaryId as getPrimaryId3 } from "@tscircuit/circuit-json-util";
var getErrorOwnerId = (copper) => "pcb_component_id" in copper && copper.pcb_component_id ? copper.pcb_component_id : getPrimaryId3(copper);
var getReadableCopperName = (circuitJson, copper) => {
if ("pcb_component_id" in copper && copper.pcb_component_id) {
const pcbComponent = circuitJson.find(
(element) => element.type === "pcb_component" && element.pcb_component_id === copper.pcb_component_id
);
const sourceComponent = pcbComponent?.type === "pcb_component" ? circuitJson.find(
(element) => element.type === "source_component" && element.source_component_id === pcbComponent.source_component_id
) : void 0;
const componentName = sourceComponent?.type === "source_component" && sourceComponent.name ? sourceComponent.name : getReadableNameForComponent(circuitJson, copper.pcb_component_id);
return `component ${componentName}`;
}
return copper.type === "pcb_via" ? `via ${copper.pcb_via_id}` : `${copper.type} ${getPrimaryId3(copper)}`;
};
function checkPcbCopperOverKeepout(circuitJson) {
const keepouts = cju3(circuitJson).pcb_keepout.list();
if (keepouts.length === 0) return [];
const copper = [
...getPads(circuitJson),
...cju3(circuitJson).pcb_via.list()
];
const errors = /* @__PURE__ */ new Map();
for (const keepout of keepouts) {
const excludedComponentIds = new Set(
keepout.excluded_pcb_component_ids ?? []
);
for (const copperElement of copper) {
const copperComponentId = "pcb_component_id" in copperElement ? copperElement.pcb_component_id : void 0;
if (copperComponentId && excludedComponentIds.has(copperComponentId)) {
continue;
}
const copperLayers = getLayersOfPcbElement(copperElement);
if (!copperLayers.some((layer) => keepout.layers.includes(layer))) {
continue;
}
if (getPadToPadGap(copperElement, keepout) > EPSILON) continue;
const ownerId = getErrorOwnerId(copperElement);
const errorId = `copper_over_keepout_${ownerId}_${keepout.pcb_keepout_id}`;
if (errors.has(errorId)) continue;
errors.set(errorId, {
type: "pcb_placement_error",
pcb_placement_error_id: errorId,
error_type: "pcb_placement_error",
message: `Copper for ${getReadableCopperName(
circuitJson,
copperElement
)} overlaps ${keepout.description ? `PCB keepout "${keepout.description}"` : "a PCB keepout"}`,
subcircuit_id: copperElement.subcircuit_id ?? keepout.subcircuit_id
});
}
}
return [...errors.values()];
}
// lib/check-same-net-via-spacing.ts
import { getReadableNameForElement as getReadableNameForElement4 } from "@tscircuit/circuit-json-util";
import {
getFullConnectivityMapFromCircuitJson as getFullConnectivityMapFromCircuitJson3
} from "circuit-json-to-connectivity-map";
// lib/util/distance.ts
function distance2(a, b) {
return Math.hypot(a.x - b.x, a.y - b.y);
}
// lib/util/viasAreAtSameLocation.ts
function viasAreAtSameLocation(a, b) {
return distance2(a, b) <= EPSILON;
}
// lib/check-same-net-via-spacing.ts
function checkSameNetViaSpacing(circuitJson, {
connMap,
minClearance
} = {}) {
const vias = circuitJson.filter((el) => el.type === "pcb_via");
if (vias.length < 2) return [];
const board = getPcbBoard(circuitJson);
minClearance ??= getBoardDrcValue(board, "min_via_hole_edge_to_via_hole_edge_clearance") ?? jlcMinTolerances.min_via_hole_edge_to_via_hole_edge_clearance;
connMap ??= getFullConnectivityMapFromCircuitJson3(circuitJson);
const errors = [];
const reported = /* @__PURE__ */ new Set();
for (let i = 0; i < vias.length; i++) {
for (let j = i + 1; j < vias.length; j++) {
const viaA = vias[i];
const viaB = vias[j];
if (viasAreAtSameLocation(viaA, viaB)) continue;
if (!connMap.areIdsConnected(viaA.pcb_via_id, viaB.pcb_via_id)) continue;
const gap = distance2(viaA, viaB) - viaA.hole_diameter / 2 - viaB.hole_diameter / 2;
if (gap + EPSILON >= minClearance) continue;
const pairId = [viaA.pcb_via_id, viaB.pcb_via_id].sort().join("_");
if (reported.has(pairId)) continue;
reported.add(pairId);
errors.push({
type: "pcb_via_clearance_error",
pcb_error_id: `same_net_vias_close_${pairId}`,
message: `Vias ${getReadableNameForElement4(
circuitJson,
viaA.pcb_via_id
)} and ${getReadableNameForElement4(
circuitJson,
viaB.pcb_via_id
)} are too close together (gap: ${gap.toFixed(3)}mm)`,
error_type: "pcb_via_clearance_error",
pcb_via_ids: [viaA.pcb_via_id, viaB.pcb_via_id],
minimum_clearance: minClearance,
actual_clearance: gap,
pcb_center: {
x: (viaA.x + viaB.x) / 2,
y: (viaA.y + viaB.y) / 2
}
});
}
}
return errors;
}
// lib/check-different-net-via-spacing.ts
import { getReadableNameForElement as getReadableNameForElement5 } from "@tscircuit/circuit-json-util";
import {
getFullConnectivityMapFromCircuitJson as getFullConnectivityMapFromCircuitJson4
} from "circuit-json-to-connectivity-map";
function checkDifferentNetViaSpacing(circuitJson, {
connMap,
minClearance
} = {}) {
const vias = circuitJson.filter((el) => el.type === "pcb_via");
if (vias.length < 2) return [];
const board = getPcbBoard(circuitJson);
minClearance ??= getBoardDrcValue(board, "min_via_hole_edge_to_via_hole_edge_clearance") ?? jlcMinTolerances.min_via_hole_edge_to_via_hole_edge_clearance;
connMap ??= getFullConnectivityMapFromCircuitJson4(circuitJson);
const errors = [];
const reported = /* @__PURE__ */ new Set();
for (let i = 0; i < vias.length; i++) {
for (let j = i + 1; j < vias.length; j++) {
const viaA = vias[i];
const viaB = vias[j];
if (viasAreAtSameLocation(viaA, viaB)) continue;
if (connMap.areIdsConnected(viaA.pcb_via_id, viaB.pcb_via_id)) continue;
const gap = distance2(viaA, viaB) - viaA.hole_diameter / 2 - viaB.hole_diameter / 2;
if (gap + EPSILON >= minClearance) continue;
const pairId = [viaA.pcb_via_id, viaB.pcb_via_id].sort().join("_");
if (reported.has(pairId)) continue;
reported.add(pairId);
errors.push({
type: "pcb_via_clearance_error",
pcb_error_id: `different_net_vias_close_${pairId}`,
message: `Vias ${getReadableNameForElement5(
circuitJson,
viaA.pcb_via_id
)} and ${getReadableNameForElement5(
circuitJson,
viaB.pcb_via_id
)} from different nets are too close together (gap: ${gap.toFixed(
3
)}mm)`,
error_type: "pcb_via_clearance_error",
pcb_via_ids: [viaA.pcb_via_id, viaB.pcb_via_id],
minimum_clearance: minClearance,
actual_clearance: gap,
pcb_center: {
x: (viaA.x + viaB.x) / 2,
y: (viaA.y + viaB.y) / 2
}
});
}
}
return errors;
}
// lib/check-source-traces-match-pcb-trace-thickness.ts
import { cju as cju4 } from "@tscircuit/circuit-json-util";
import { getFullConnectivityMapFromCircuitJson as getFullConnectivityMapFromCircuitJson5 } from "circuit-json-to-connectivity-map";
function checkSourceTracesMatchPcbTraceThickness(circuitJson) {
const warnings = [];
const db = cju4(circuitJson);
const sourceTraces = db.source_trace.list();
const pcbTraces = db.pcb_trace.list();
const pcbPorts = db.pcb_port.list();
const connectivityMap = getFullConnectivityMapFromCircuitJson5(circuitJson);
for (const sourceTrace of sourceTraces) {
const requestedThickness = sourceTrace.min_trace_thickness;
if (requestedThickness === void 0) continue;
const connectedPcbPorts = pcbPorts.filter(
(pcbPort) => sourceTrace.connected_source_port_ids?.includes(pcbPort.source_port_id)
);
if (connectedPcbPorts.length < 2) continue;
const referenceNetId = connectivityMap.getNetConnectedToId(
connectedPcbPorts[0].pcb_port_id
);
if (!referenceNetId) continue;
const netElementIds = connectivityMap.getIdsConnectedToNet(referenceNetId);
const relatedPcbTraces = pcbTraces.filter(
(pcbTrace) => netElementIds.includes(pcbTrace.pcb_trace_id)
);
if (relatedPcbTraces.length === 0) continue;
const actualWireWidths = relatedPcbTraces.flatMap(
(pcbTrace) => pcbTrace.route.filter((point) => point.route_type === "wire").map((point) => point.width)
);
if (actualWireWidths.length === 0) continue;
const actualThickness = Math.min(...actualWireWidths);
if (actualThickness >= requestedThickness) continue;
let undersizedSegment;
for (const relatedPcbTrace of relatedPcbTraces) {
for (let i = 0; i < relatedPcbTrace.route.length - 1; i++) {
const point = relatedPcbTrace.route[i];
const nextPoint = relatedPcbTrace.route[i + 1];
if (!point || !nextPoint) continue;
if (point.route_type !== "wire" || nextPoint.route_type !== "wire") {
continue;
}
if (point.width !== actualThickness) continue;
undersizedSegment = {
pcb_trace_id: relatedPcbTrace.pcb_trace_id,
center: {
x: (point.x + nextPoint.x) / 2,
y: (point.y + nextPoint.y) / 2
}
};
break;
}
if (undersizedSegment) break;
}
if (!undersizedSegment) continue;
warnings.push({
type: "pcb_trace_warning",
pcb_trace_warning_id: `pcb_trace_warning_${sourceTrace.source_trace_id}`,
warning_type: "pcb_trace_warning",
message: `Trace [${getReadableNameForSourceTrace(circuitJson, sourceTrace)}] is routed thinner than requested (requested: ${requestedThickness}mm, actual: ${actualThickness}mm).`,
center: undersizedSegment.center,
source_trace_id: sourceTrace.source_trace_id,
pcb_trace_id: undersizedSegment.pcb_trace_id,
pcb_component_ids: Array.from(
new Set(
connectedPcbPorts.map((pcbPort) => pcbPort.pcb_component_id).filter((id) => id !== void 0)
)
),
pcb_port_ids: connectedPcbPorts.map((pcbPort) => pcbPort.pcb_port_id)
});
}
return warnings;
}
// lib/check-source-traces-have-pcb-traces.ts
import { getFullConnectivityMapFromCircuitJson as getFullConnectivityMapFromCircuitJson6 } from "circuit-json-to-connectivity-map";
function checkSourceTracesHavePcbTraces(circuitJson) {
const errors = [];
const sourceTraces = circuitJson.filter(
(el) => el.type === "source_trace"
);
const pcbTraces = circuitJson.filter(
(el) => el.type === "pcb_trace"
);
const pcbPorts = circuitJson.filter(
(el) => el.type === "pcb_port"
);
const sourcePortToPcbPort = new Map(
pcbPorts.map((pcbPort) => [pcbPort.source_port_id, pcbPort])
);
const connectivityMap = getFullConnectivityMapFromCircuitJson6(circuitJson);
for (const sourceTrace of sourceTraces) {
if (!sourceTrace.connected_source_port_ids?.length) continue;
if ((sourceTrace.connected_source_net_ids?.length ?? 0) > 0) continue;
if (sourceTrace.connected_source_port_ids.length < 2) continue;
const hasPcbTrace = pcbTraces.some(
(pcbTrace) => connectivityMap.areIdsConnected(
sourceTrace.source_trace_id,
pcbTrace.pcb_trace_id
)
);
if (!hasPcbTrace) {
const connectedPcbPorts = sourceTrace.connected_source_port_ids.map((sourcePortId) => sourcePortToPcbPort.get(sourcePortId)).filter((pcbPort) => pcbPort !== void 0);
const connectedPcbComponentIds = Array.from(
new Set(
connectedPcbPorts.map((port) => port.pcb_component_id).filter((id) => id !== void 0)
)
);
errors.push({
type: "pcb_trace_missing_error",
pcb_trace_missing_error_id: `pcb_trace_missing_${sourceTrace.source_trace_id}`,
error_type: "pcb_trace_missing_error",
message: `Trace [${sourceTrace.display_name && !containsCircuitJsonId(sourceTrace.display_name) ? sourceTrace.display_name : "trace"}] is not connected (it has no PCB trace)`,
source_trace_id: sourceTrace.source_trace_id,
pcb_component_ids: connectedPcbComponentIds,
pcb_port_ids: connectedPcbPorts.map((port) => port.pcb_port_id)
});
}
}
return errors;
}
// lib/check-traces-are-contiguous/check-traces-are-contiguous.ts
import { pointToSegmentDistance as pointToSegmentDistance3 } from "@tscircuit/math-utils";
import {
getReadableNameForPcbPort as getReadableNameForPcbPort2,
getReadableNameForPcbTrace
} from "@tscircuit/circuit-json-util";
import {
getFullConnectivityMapFromCircuitJson as getFullConnectivityMapFromCircuitJson7,
PcbConnectivityMap as PcbConnectivityMap2
} from "circuit-json-to-connectivity-map";
// lib/check-traces-are-contiguous/via-contact-index.ts
import {
all_layers as all_layers2
} from "circuit-json";
import { getPrimaryId as getPrimaryId4 } from "@tscircuit/circuit-json-util";
import { pointToSegmentDistance as pointToSegmentDistance2 } from "@tscircuit/math-utils";
var CONTACT_EPSILON = 1e-9;
function getViaContactIndex(circuitJson, connectivity) {
const index = /* @__PURE__ */ new Map();
const pads = getPads(circuitJson);
const traces = circuitJson.filter((element) => element.type === "pcb_trace");
const vias = circuitJson.filter((element) => element.type === "pcb_via");
const board = circuitJson.find((element) => element.type === "pcb_board");
const layerCount = board?.num_layers;
const innerLayers = all_layers2.filter((layer) => layer.startsWith("inner"));
const stack = [
"top",
...innerLayers.slice(
0,
layerCount === void 0 ? void 0 : Math.max(0, layerCount - 2)
),
...layerCount === 1 ? [] : ["bottom"]
];
const add = (id, layers, contact) => {
if (![contact.x, contact.y].every(Number.isFinite)) return;
const net = connectivity.getNetConnectedToId(id);
if (!net) return;
const touchesPad = pads.some((pad) => {
if (connectivity.getNetConnectedToId(getPrimaryId4(pad)) !== net || !getLayersOfPcbElement(pad).some((layer) => layers.includes(layer)))
return false;
if (contact.radius === void 0) return isPointInPad(contact, pad);
const viaGeometry = {
type: "pcb_via",
pcb_via_id: id,
x: contact.x,
y: contact.y,
outer_diameter: contact.radius * 2,
hole_diameter: 0,
layers
};
return getPadToPadGap(viaGeometry, pad) <= CONTACT_EPSILON;
});
const touchingTraceIds = /* @__PURE__ */ new Set();
for (const trace of traces) {
if (trace.route_thickness_mode === "interpolated" || connectivity.getNetConnectedToId(trace.pcb_trace_id) !== net)
continue;
for (let i = 1; i < trace.route.length; i++) {
const a = trace.route[i - 1], b = trace.route[i];
if (a.route_type !== "wire" || b.route_type !== "wire" || a.layer !== b.layer || !layers.includes(a.layer) || !Number.isFinite(a.width) || a.width <= 0 || Math.hypot(a.x - b.x, a.y - b.y) <= CONTACT_EPSILON)
continue;
const reach = contact.radius === void 0 ? 0 : contact.radius + a.width / 2;
if (pointToSegmentDistance2(contact, a, b) <= reach + CONTACT_EPSILON) {
touchingTraceIds.add(trace.pcb_trace_id);
break;
}
}
}
const copper = { ...contact, touchesPad, touchingTraceIds };
const byLayer = index.get(net) ?? /* @__PURE__ */ new Map();
for (const layer of layers) {
const contacts = byLayer.get(layer) ?? [];
contacts.push(copper);
byLayer.set(layer, contacts);
}
index.set(net, byLayer);
};
for (const via of vias) {
if (!Number.isFinite(via.outer_diameter) || via.outer_diameter <= 0)
continue;
add(via.pcb_via_id, getLayersOfPcbElement(via), {
x: via.x,
y: via.y,
ownerTraceId: via.pcb_trace_id,
radius: via.outer_diameter / 2
});
}
for (const trace of circuitJson) {
if (trace.type !== "pcb_trace") continue;
for (const point of trace.route) {
if (point.route_type !== "via") continue;
if (vias.some(
(via) => Math.hypot(via.x - point.x, via.y - point.y) <= CONTACT_EPSILON && (via.pcb_trace_id === trace.pcb_trace_id || !via.pcb_trace_id && connectivity.areIdsConnected(
via.pcb_via_id,
trace.pcb_trace_id
))
))
continue;
const from = stack.indexOf(point.from_layer);
const to = stack.indexOf(point.to_layer);
if (from < 0 || to < 0) continue;
const diameter = point.outer_diameter;
if (diameter !== void 0 && (!Number.isFinite(diameter) || diameter <= 0))
continue;
add(
trace.pcb_trace_id,
stack.slice(Math.min(from, to), Math.max(from, to) + 1),
{
x: point.x,
y: point.y,
ownerTraceId: trace.pcb_trace_id,
radius: diameter === void 0 ? void 0 : diameter / 2
}
);
}
}
return index;
}
function endpointTouchesVia({
point,
width,
ownerTrace,
index,
connectivity
}) {
if (point.route_type !== "wire" || !Number.isFinite(width) || width <= 0)
return false;
const net = connectivity.getNetConnectedToId(ownerTrace.pcb_trace_id);
if (!net) return false;
return (index.get(net)?.get(point.layer) ?? []).some((via) => {
if (via.ownerTraceId === ownerTrace.pcb_trace_id) return false;
if (!via.touchesPad && ![...via.touchingTraceIds].some((id) => id !== ownerTrace.pcb_trace_id))
return false;
const contactDistance = via.radius === void 0 ? 0 : via.radius + width / 2;
return Math.hypot(point.x - via.x, point.y - via.y) <= contactDistance + CONTACT_EPSILON;
});
}
// lib/check-traces-are-contiguous/check-traces-are-contiguous.ts
var ENDPOINT_CONTACT_EPSILON = 1e-9;
var TRACE_SEGMENT_GEOMETRY_EPSILON = 1e-9;
function routePointTouchesPad(point, pad) {
return point.route_type === "wire" && getLayersOfPcbElement(pad).includes(point.layer) && isPointInPad(point, pad);
}
function getTraceWireSegmentsByNetAndLayer(pcbTraces, fullConnectivityMap) {
const segmentsByNetAndLayer = /* @__PURE__ */ new Map();
for (const trace of pcbTraces) {
if (trace.route_thickness_mode === "interpolated") continue;
const netId = fullConnectivityMap.getNetConnectedToId(trace.pcb_trace_id);
if (!netId) continue;
for (let i = 0; i < trace.route.length - 1; i++) {
const start = trace.route[i];
const end = trace.route[i + 1];
if (start.route_type !== "wire" || end.route_type !== "wire") continue;
if (start.layer !== end.layer) continue;
if (Math.hypot(start.x - end.x, start.y - end.y) <= TRACE_SEGMENT_GEOMETRY_EPSILON) {
continue;
}
const segmentsByLayer = segmentsByNetAndLayer.get(netId) ?? /* @__PURE__ */ new Map();
const segments = segmentsByLayer.get(start.layer) ?? [];
segments.push({ trace, start, end });
segmentsByLayer.set(start.layer, segments);
segmentsByNetAndLayer.set(netId, segmentsByLayer);
}
}
return segmentsByNetAndLayer;
}
function getEndpointTraceCopperWidth(trace, endpoint) {
if (trace.route_thickness_mode === "interpolated") return void 0;
let segmentStartIndex = endpoint === "start" ? 0 : trace.route.length - 2;
const indexStep = endpoint === "start" ? 1 : -1;
while (segmentStartIndex >= 0 && segmentStartIndex < trace.route.length - 1) {
const segmentStart = trace.route[segmentStartIndex];
const segmentEnd = trace.route[segmentStartIndex + 1];
if (segmentStart?.route_type !== "wire" || segmentEnd?.route_type !== "wire" || segmentStart.layer !== segmentEnd.layer) {
return void 0;
}
if (Math.hypot(segmentStart.x - segmentEnd.x, segmentStart.y - segmentEnd.y) > TRACE_SEGMENT_GEOMETRY_EPSILON) {
return segmentStart.width;
}
segmentStartIndex += indexStep;
}
return void 0;
}
function routePointTouchesLogicallyConnectedTraceCopper({
point,
endpointTraceCopperWidth,
ownerTrace,
traceWireSegmentsByNetAndLayer,
fullConnectivityMap
}) {
if (point.route_type !== "wire") return false;
const ownerNetId = fullConnectivityMap.getNetConnectedToId(
ownerTrace.pcb_trace_id
);
if (!ownerNetId) return false;
const candidateSegments = traceWireSegmentsByNetAndLayer.get(ownerNetId)?.get(point.layer) ?? [];
for (const segment of candidateSegments) {
if (segment.trace.pcb_trace_id === ownerTrace.pcb_trace_id) continue;
const maximumContactDistance = endpointTraceCopperWidth / 2 + segment.start.width / 2 + ENDPOINT_CONTACT_EPSILON;
if (pointToSegmentDistance3(point, segment.start, segment.end) <= maximumContactDistance) {
return true;
}
}
return false;
}
function getRoutePointCenter(point) {
if (point.route_type === "through_pad") {
return {
x: (point.start.x + point.end.x) / 2,
y: (point.start.y + point.end.y) / 2
};
}
return { x: point.x, y: point.y };
}
function routePointConnectsToAnotherExpectedPort(point, expectedPorts, missingPcbPortId, padMap) {
return expectedPorts.some((expectedPort) => {
if (!expectedPort.pcb_port_id || expectedPort.pcb_port_id === missingPcbPortId) {
return false;
}
const expectedPads = padMap.get(expectedPort.pcb_port_id);
return expectedPads?.some((pad) => routePointTouchesPad(point, pad)) ?? false;
});
}
function getMissingConnectionErrorCenter({
firstPoint,
lastPoint,
port,
expectedPorts,
padMap
}) {
let errorLocation;
const firstWirePoint = firstPoint.route_type === "wire" ? firstPoint : void 0;
const lastWirePoint = lastPoint.route_type === "wire" ? lastPoint : void 0;
const firstWirePointReferencesPort = getPcbPortIdsConnectedToRoutePoint(
firstPoint
).includes(port.pcb_port_id);
const lastWirePointReferencesPort = getPcbPortIdsConnectedToRoutePoint(
lastPoint
).includes(port.pcb_port_id);
if (firstWirePointReferencesPort && firstWirePoint) {
errorLocation = firstWirePoint;
} else if (lastWirePointReferencesPort && lastWirePoint) {
errorLocation = lastWirePoint;
} else if (routePointConnectsToAnotherExpectedPort(
firstPoint,
expectedPorts,
port.pcb_port_id,
padMap
) && lastWirePoint) {
errorLocation = lastWirePoint;
} else if (routePointConnectsToAnotherExpectedPort(
lastPoint,
expectedPorts,
port.pcb_port_id,
padMap
) && firstWirePoint) {
errorLocation = firstWirePoint;
} else if (firstWirePoint && lastWirePoint) {
errorLocation = distance2(firstWirePoint, port) <= distance2(lastWirePoint, port) ? firstWirePoint : lastWirePoint;
} else if (firstWirePoint) {
errorLocation = firstWirePoint;
} else if (lastWirePoint) {
errorLocation = lastWirePoint;
}
const firstPointCenter = getRoutePointCenter(firstPoint);
const lastPointCenter = getRoutePointCenter(lastPoint);
return errorLocation ? { x: errorLocation.x, y: errorLocation.y } : {
x: (firstPointCenter.x + lastPointCenter.x) / 2,
y: (firstPointCenter.y + lastPointCenter.y) / 2
};
}
function checkTracesAreContiguous(circuitJson) {
const errors = [];
const pcbPorts = circuitJson.filter(
(el) => el.type === "pcb_port"
);
const pcbTraces = circuitJson.filter(
(el) => el.type === "pcb_trace"
);
const sourceTraces = circuitJson.filter(
(el) => el.type === "source_trace"
);
const pcbSmtPads = circuitJson.filter(
(el) => el.type === "pcb_smtpad"
);
const pcbPlatedHoles = circuitJson.filter(
(el) => el.type === "pcb_plated_hole"
);
const padMap = /* @__PURE__ */ new Map();
const pcbConnectivityMap = new PcbConnectivityMap2(circuitJson);
let fullConnectivityMap;
let traceWireSegmentsByNetAndLayer;
const getFullConnectivityMap = () => {
fullConnectivityMap ??= getFullConnectivityMapFromCircuitJson7(circuitJson);
return fullConnectivityMap;
};
const getTraceWireSegmentIndex = () => {
traceWireSegmentsByNetAndLayer ??= getTraceWireSegmentsByNetAndLayer(
pcbTraces,
getFullConnectivityMap()
);
return traceWireSegmentsByNetAndLayer;
};
let viaContactIndex;
const getViaIndex = () => {
viaContactIndex ??= getViaContactIndex(
circuitJson,
getFullConnectivityMap()
);
return viaContactIndex;
};
let pourConnectivity;
const getPourConnectivity = () => pourConnectivity ??= new CopperPourConnectivity(
circuitJson,
getFullConnectivityMap()
);
const checkedSourceTraceIds = /* @__PURE__ */ new Set();
for (const pad of pcbSmtPads) {
if (pad.pcb_port_id) {
padMap.set(pad.pcb_port_id, [...padMap.get(pad.pcb_port_id) ?? [], pad]);
}
}
for (const hole of pcbPlatedHoles) {
if (hole.pcb_port_id) {
padMap.set(hole.pcb_port_id, [
...padMap.get(hole.pcb_port_id) ?? [],
hole
]);
}
}
const touchedPortIdsByTraceId = /* @__PURE__ */ new Map();
const traceIdsByTouchedPortId = /* @__PURE__ */ new Map();
for (const trace of pcbTraces) {
const touchedPortIds = /* @__PURE__ */ new Set();
const firstPoint = trace.route[0];
const lastPoint = trace.route.at(-1);
for (const point of [firstPoint, lastPoint]) {
if (!point) continue;
for (const [pcbPortId, pads] of padMap) {
if (pads.some((pad) => routePointTouchesPad(point, pad))) {
touchedPortIds.add(pcbPortId);
}
}
}
touchedPortIdsByTraceId.set(trace.pcb_trace_id, touchedPortIds);
for (const pcbPortId of touchedPortIds) {
const traceIds = traceIdsByTouchedPortId.get(pcbPortId) ?? /* @__PURE__ */ new Set();
traceIds.add(trace.pcb_trace_id);
traceIdsByTouchedPortId.set(pcbPortId, traceIds);
}
}
const physicallyConnectedTracesByTraceId = /* @__PURE__ */ new Map();
const getPhysicallyConnectedTraces = (startTrace) => {
const cached = physicallyConnectedTracesByTraceId.get(
startTrace.pcb_trace_id
);
if (cached) return cached;
const connectedTraceIds = /* @__PURE__ */ new Set();
const pendingTraceIds = [startTrace.pcb_trace_id];
while (pendingTraceIds.length > 0) {
const traceId = pendingTraceIds.pop();
if (connectedTraceIds.has(traceId)) continue;
connectedTraceIds.add(traceId);
for (const connectedTrace of pcbConnectivityMap.getAllTracesConnectedToTrace(
traceId
)) {
if (!connectedTraceIds.has(connectedTrace.pcb_trace_id)) {
pendingTraceIds.push(connectedTrace.pcb_trace_id);
}
}
for (const pcbPortId of touchedPortIdsByTraceId.get(traceId) ?? []) {
for (const touchingTraceId of traceIdsByTouchedPortId.get(pcbPortId) ?? []) {
if (!connectedTraceIds.has(touchingTraceId)) {
pendingTraceIds.push(touchingTraceId);
}
}
}
}
const connectedTraces = pcbTraces.filter(
(trace) => connectedTraceIds.has(trace.pcb_trace_id)
);
for (const trace of connectedTraces) {
physicallyConnectedTracesByTraceId.set(
trace.pcb_trace_id,
connectedTraces
);
}
return connectedTraces;
};
for (const trace of pcbTraces) {
if (trace.route.length === 0) continue;
const firstPoint = trace.route[0];
const lastPoint = trace.route[trace.route.length - 1];
const sourceTrace = sourceTraces.find(
(st) => st.source_trace_id === trace.source_trace_id
);
const expectedPorts = sourceTrace ? pcbPorts.filter(
(port) => sourceTrace.connected_source_port_ids?.includes(port.source_port_id)
) : [];
for (let i = 1; i < trace.route.length - 1; i++) {
const prevPoint = trace.route[i - 1];
const currentPoint = trace.route[i];
const nextPoint = trace.route[i + 1];
if (currentPoint.route_type === "via") {
const prevIsWire = prevPoint.route_type === "wire";
const nextIsWire = nextPoint.route_type === "wire";
if (prevIsWire && nextIsWire) {
const prevAligned = Math.abs(prevPoint.x - currentPoint.x) < 0.01 && Math.abs(prevPoint.y - currentPoint.y) < 0.01;
const nextAligned = Math.abs(nextPoint.x - currentPoint.x) < 0.01 && Math.abs(nextPoint.y - currentPoint.y) < 0.01;
if (!prevAligned || !nextAligned) {
const traceName2 = getReadableNameForPcbTrace(
circuitJson,
trace.pcb_trace_id
);
errors.push({
type: "pcb_trace_error",
message: `Via in trace [${traceName2}] is misaligned at position {x: ${currentPoint.x}, y: ${currentPoint.y}}.`,
source_trace_id: sourceTrace?.source_trace_id || trace.source_trace_id || `!${trace.pcb_trace_id}`,
error_type: "pcb_trace_error",
pcb_trace_id: trace.pcb_trace_id,
pcb_trace_error_id: `misaligned_via_${trace.pcb_trace_id}_${i}`,
pcb_component_ids: [],
pcb_port_ids: []
});
}
}
}
}
const traceName = getReadableNameForPcbTrace(
circuitJson,
trace.pcb_trace_id
);
if (sourceTrace && expectedPorts.length > 0) {
if (checkedSourceTraceIds.has(sourceTrace.source_trace_id)) continue;
checkedSourceTraceIds.add(sourceTrace.source_trace_id);
}
for (const port of expectedPorts) {
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)
);
if (isConnectedByRoutedSourceTrace || getPourConnectivity().traceConnectedToPortThroughPour(
trace.pcb_trace_id,
port.pcb_port_id
))
continue;
const isFirstPointConnected = pads.some(
(pad) => routePointTouchesPad(firstPoint, pad)
);
const isLastPointConnected = pads.some(
(pad) => routePointTouchesPad(lastPoint, pad)
);
if (!isFirstPointConnected && !isLastPointConnected) {
const portName = getReadableNameForPcbPort2(
circuitJson,
port.pcb_port_id
).replace("pcb_port", "");
const padType = pads[0].type.replace(/pcb_/, "");
const errorCenter = getMissingConnectionErrorCenter({
firstPoint,
lastPoint,
port,
expectedPorts,
padMap
});
errors.push({
type: "pcb_trace_error",
message: `Trace [${traceName}] is missing a connection to ${padType}${portName}`,
source_trace_id: sourceTrace?.source_trace_id || trace.source_trace_id || `!${trace.pcb_trace_id}`,
error_type: "pcb_trace_error",
pcb_trace_id: trace.pcb_trace_id,
pcb_trace_error_id: `missing_connection_${trace.pcb_trace_id}_${port.pcb_port_id}`,
center: errorCenter,
pcb_component_ids: [],
pcb_port_ids: [port.pcb_port_id]
});
}
}
if (expectedPorts.length === 0) {
let firstConnectsToAnyPad = false;
let lastConnectsToAnyPad = false;
for (const pads of padMap.values()) {
if (pads.some((pad) => routePointTouchesPad(firstPoint, pad))) {
firstConnectsToAnyPad = true;
}
if (pads.some((pad) => routePointTouchesPad(lastPoint, pad))) {
lastConnectsToAnyPad = true;
}
}
const firstEndpointTraceCopperWidth = !firstConnectsToAnyPad ? getEndpointTraceCopperWidth(trace, "start") : void 0;
const lastEndpointTraceCopperWidth = !lastConnectsToAnyPad ? getEndpointTraceCopperWidth(trace, "end") : void 0;
const firstConnectsToLogicallyConnectedTraceCopper = firstEndpointTraceCopperWidth !== void 0 && routePointTouchesLogicallyConnectedTraceCopper({
point: firstPoint,
endpointTraceCopperWidth: firstEndpointTraceCopperWidth,
ownerTrace: trace,
traceWireSegmentsByNetAndLayer: getTraceWireSegmentIndex(),
fullConnectivityMap: getFullConnectivityMap()
});
const lastConnectsToLogicallyConnectedTraceCopper = lastEndpointTraceCopperWidth !== void 0 && routePointTouchesLogicallyConnectedTraceCopper({
point: lastPoint,
endpointTraceCopperWidth: lastEndpointTraceCopperWidth,
ownerTrace: trace,
traceWireSegmentsByNetAndLayer: getTraceWireSegmentIndex(),
fullConnectivityMap: getFullConnectivityMap()
});
const firstIsConnected = firstConnectsToAnyPad || firstConnectsToLogicallyConnectedTraceCopper || getPourConnectivity().endpointTouchesConnectedPour(
firstPoint,
trace.pcb_trace_id,
firstEndpointTraceCopperWidth ?? 0
) || firstEndpointTraceCopperWidth !== void 0 && endpointTouchesVia({
point: firstPoint,
width: firstEndpointTraceCopperWidth,
ownerTrace: trace,
index: getViaIndex(),
connectivity: getFullConnectivityMap()
});
const lastIsConnected = lastConnectsToAnyPad || lastConnectsToLogicallyConnectedTraceCopper || getPourConnectivity().endpointTouchesConnectedPour(
lastPoint,
trace.pcb_trace_id,
lastEndpointTraceCopperWidth ?? 0
) || lastEndpointTraceCopperWidth !== void 0 && endpointTouchesVia({
point: lastPoint,
width: lastEndpointTraceCopperWidth,
ownerTrace: trace,
index: getViaIndex(),
connectivity: getFullConnectivityMap()
});
const endpointsAreSame = firstPoint.route_type === "wire" && lastPoint.route_type === "wire" && firstPoint.layer === lastPoint.layer && Math.hypot(firstPoint.x - lastPoint.x, firstPoint.y - lastPoint.y) <= ENDPOINT_CONTACT_EPSILON;
if (!firstIsConnected && firstPoint.route_type === "wire") {
errors.push({
type: "pcb_trace_error",
message: `Trace [${traceName}] has disconnected endpoint at (${firstPoint.x.toFixed(2)}, ${firstPoint.y.toFixed(2)})`,
source_trace_id: sourceTrace?.source_trace_id || trace.source_trace_id || `!${trace.pcb_trace_id}`,
error_type: "pcb_trace_error",
pcb_trace_id: trace.pcb_trace_id,
pcb_trace_error_id: `disconnected_endpoint_${trace.pcb_trace_id}_start`,
center: { x: firstPoint.x, y: firstPoint.y },
pcb_component_ids: [],
pcb_port_ids: []
});
}
if (!lastIsConnected && lastPoint.route_type === "wire" && !(endpointsAreSame && !firstIsConnected)) {
errors.push({
type: "pcb_trace_error",
message: `Trace [${traceName}] has disconnected endpoint at (${lastPoint.x.toFixed(2)}, ${lastPoint.y.toFixed(2)})`,
source_trace_id: sourceTrace?.source_trace_id || trace.source_trace_id || `!${trace.pcb_trace_id}`,
error_type: "pcb_trace_error",
pcb_trace_id: trace.pcb_trace_id,
pcb_trace_error_id: `disconnected_endpoint_${trace.pcb_trace_id}_end`,
center: { x: lastPoint.x, y: lastPoint.y },
pcb_component_ids: [],
pcb_port_ids: []
});
}
}
}
return errors;
}
// lib/check-trace-out-of-board/checkTraceOutOfBoard.ts
import { cju as cju5 } from "@tscircuit/circuit-json-util";
import { segmentToSegmentMinDistance as segmentToSegmentMinDistance4 } from "@tscircuit/math-utils";
function getBoardPolygonPoints(board) {
if (board.outline && board.outline.length > 0) {
return board.outline.map((p) => ({ x: p.x, y: p.y }));
}
if (board.center && typeof board.width === "number" && typeof board.height === "number") {
const cx = board.center.x;
const cy = board.center.y;
const hw = board.width / 2;
const hh = board.height / 2;
return [
{ x: cx - hw, y: cy - hh },
// bottom-left
{ x: cx + hw, y: cy - hh },
// bottom-right
{ x: cx + hw, y: cy + hh },
// top-right
{ x: cx - hw, y: cy + hh }
// top-left
];
}
return null;
}
function checkPcbTracesOutOfBoard(circuitJson, config = {}) {
const errors = [];
const board = getPcbBoard(circuitJson);
if (!board) return errors;
const margin = config.margin ?? getBoardDrcValue(board, "min_board_edge_clearance") ?? jlcMinTolerances.min_board_edge_clearance;
const boardPoints = getBoardPolygonPoints(board);
if (!boardPoints) return errors;
const pcbTraces = cju5(circuitJson).pcb_trace.list();
for (const trace of pcbTraces) {
if (trace.route.length < 2) continue;
for (let i = 0; i < trace.route.length - 1; i++) {
const p1 = trace.route[i];
const p2 = trace.route[i + 1];
if (p1.route_type !== "wire" || p2.route_type !== "wire") continue;
const traceWidth = "width" in p1 ? p1.width : "width" in p2 ? p2.width : 0.1;
const segmentStart = { x: p1.x, y: p1.y };
const segmentEnd = { x: p2.x, y: p2.y };
let minDistance = Number.POSITIVE_INFINITY;
for (let j = 0; j < boardPoints.length; j++) {
const edgeStart = boardPoints[j];
const edgeEnd = boardPoints[(j + 1) % boardPoints.length];
const distance3 = segmentToSegmentMinDistance4(
segmentStart,
segmentEnd,
edgeStart,
edgeEnd
);
if (distance3 < minDistance) {
minDistance = distance3;
}
}
const minimumDistance = traceWidth / 2 + margin;
if (minDistance < minimumDistance) {
const error = {
type: "pcb_trace_error",
error_type: "pcb_trace_error",
pcb_trace_error_id: `trace_too_close_to_board_${trace.pcb_trace_id}_segment_${i}`,
message: `Trace too close to board edge (${minDistance.toFixed(3)}mm < ${minimumDistance.toFixed(3)}mm required, margin: ${margin}mm)`,
pcb_trace_id: trace.pcb_trace_id,
source_trace_id: trace.source_trace_id || "",
center: {
x: (segmentStart.x + segmentEnd.x) / 2,
y: (segmentStart.y + segmentEnd.y) / 2
},
pcb_component_ids: [],
pcb_port_ids: []
};
errors.push(error);
}
}
}
return errors;
}
// lib/check-pcb-components-overlap/checkPcbComponentOverlap.ts
import {
cju as cju6,
getBoundsOfPcbElements as getBoundsOfPcbElements5,
getPrimaryId as getPrimaryId5
} from "@tscircuit/circuit-json-util";
import { doBoundsOverlap as doBoundsOverlap3 } from "@tscircuit/math-utils";
import { getFullConnectivityMapFromCircuitJson as getFullConnectivityMapFromCircuitJson8 } from "circuit-json-to-connectivity-map";
// lib/check-pcb-components-overlap/doPcbElementsOverlap.ts
import { getBoundsOfPcbElements as getBoundsOfPcbElements4 } from "@tscircuit/circuit-json-util";
import { doBoundsOverlap as doBoundsOverlap2 } from "@tscircuit/math-utils";
function getElementLayers(elem) {
if (elem.type === "pcb_courtyard_circle" || elem.type === "pcb_courtyard_outline" || elem.type === "pcb_courtyard_polygon" || elem.type === "pcb_courtyard_rect") {
return [elem.layer];
}
return getLayersOfPcbElement(elem);
}
function doLayersOverlap(layers1, layers2) {
if (layers1.length === 0 || layers2.length === 0) return true;
return layers1.some((l) => layers2.includes(l));
}
function doPcbElementsOverlap(elem1, elem2) {
const layers1 = getElementLayers(elem1);
const layers2 = getElementLayers(elem2);
if (!doLayersOverlap(layers1, layers2)) return false;
if (elem1.type === "pcb_smtpad" && elem2.type === "pcb_smtpad") {
return getPadToPadGap(elem1, elem2) <= 0;
}
const bounds1 = getBoundsOfPcbElements4([elem1]);
const bounds2 = getBoundsOfPcbElements4([elem2]);
return doBoundsOverlap2(bounds1, bounds2);
}
// lib/check-pcb-components-overlap/checkPcbComponentOverlap.ts
var isCourtyardElement = (element) => element.type === "pcb_courtyard_circle" || element.type === "pcb_courtyard_outline" || element.type === "pcb_courtyard_polygon" || element.type === "pcb_courtyard_rect";
var formatOverlapElementDescription = (circuitJson, element) => {
if ("pcb_port_id" in element && element.pcb_port_id) {
return getReadableNameForPort(circuitJson, element.pcb_port_id);
}
const id = getPrimaryId5(element);
const readableName = getReadableNameForElementId(circuitJson, id);
return readableName === "element" ? `[${id}]` : readableName;
};
function checkPcbComponentOverlap(circuitJson) {
const errors = [];
const connMap = getFullConnectivityMapFromCircuitJson8(circuitJson);
const smtPads = cju6(circuitJson).pcb_smtpad.list();
const platedHoles = cju6(circuitJson).pcb_plated_hole.list();
const holes = cju6(circuitJson).pcb_hole.list();
const courtyards = circuitJson.filter(isCourtyardElement);
const componentMap = /* @__PURE__ */ new Map();
for (const pad of smtPads) {
const componentId = pad.pcb_component_id || `standalone_pad_${getPrimaryId5(pad)}`;
if (!componentMap.has(componentId)) {
componentMap.set(componentId, {
component_id: componentId,
elements: []
});
}
componentMap.get(componentId).elements.push(pad);
}
for (const hole of platedHoles) {
const componentId = hole.pcb_component_id || `standalone_plated_hole_${getPrimaryId5(hole)}`;
if (!componentMap.has(componentId)) {
componentMap.set(componentId, {
component_id: componentId,
elements: []
});
}
componentMap.get(componentId).elements.push(hole);
}
for (const hole of holes) {
const componentId = hole.pcb_component_id || `standalone_hole_${getPrimaryId5(hole)}`;
if (!componentMap.has(componentId)) {
componentMap.set(componentId, {
component_id: componentId,
elements: [hole]
});
}
}
for (const courtyard of courtyards) {
const componentId = courtyard.pcb_component_id;
if (!componentMap.has(componentId)) {
componentMap.set(componentId, {
component_id: componentId,
elements: []
});
}
componentMap.get(componentId).elements.push(courtyard);
}
for (const [componentId, componentData] of componentMap) {
if (componentData.elements.length > 0) {
componentData.bounds = getBoundsOfPcbElements5(componentData.elements);
}
}
const componentsWithElements = Array.from(componentMap.values());
for (let i = 0; i < componentsWithElements.length; i++) {
for (let j = i + 1; j < componentsWithElements.length; j++) {
const comp1 = componentsWithElements[i];
const comp2 = componentsWithElements[j];
if (!comp1.bounds || !comp2.bounds) {
continue;
}
if (!doBoundsOverlap3(comp1.bounds, comp2.bounds)) {
continue;
}
for (const elem1 of comp1.elements) {
for (const elem2 of comp2.elements) {
const id1 = getPrimaryId5(elem1);
const id2 = getPrimaryId5(elem2);
if ((isCourtyardElement(elem1) || isCourtyardElement(elem2)) && elem1.type !== "pcb_hole" && elem2.type !== "pcb_hole") {
continue;
}
if (elem1.type === "pcb_smtpad" && elem2.type === "pcb_smtpad" && connMap.areIdsConnected(id1, id2)) {
continue;
}
if (doPcbElementsOverlap(elem1, elem2)) {
const elem1Description = formatOverlapElementDescription(
circuitJson,
elem1
);
const elem2Description = formatOverlapElementDescription(
circuitJson,
elem2
);
const error = {
type: "pcb_footprint_overlap_error",
pcb_error_id: `pcb_footprint_overlap_${id1}_${id2}`,
error_type: "pcb_footprint_overlap_error",
message: `${elem1.type} ${elem1Description} overlaps with ${elem2.type} ${elem2Description}`
};
if (elem1.type === "pcb_smtpad" || elem2.type === "pcb_smtpad") {
error.pcb_smtpad_ids = [];
if (elem1.type === "pcb_smtpad") error.pcb_smtpad_ids.push(id1);
if (elem2.type === "pcb_smtpad") error.pcb_smtpad_ids.push(id2);
}
if (elem1.type === "pcb_plated_hole" || elem2.type === "pcb_plated_hole") {
error.pcb_plated_hole_ids = [];
if (elem1.type === "pcb_plated_hole")
error.pcb_plated_hole_ids.push(id1);
if (elem2.type === "pcb_plated_hole")
error.pcb_plated_hole_ids.push(id2);
}
if (elem1.type === "pcb_hole" || elem2.type === "pcb_hole") {
error.pcb_hole_ids = [];
if (elem1.type === "pcb_hole") error.pcb_hole_ids.push(id1);
if (elem2.type === "pcb_hole") error.pcb_hole_ids.push(id2);
}
errors.push(error);
}
}
}
}
}
return errors;
}
// lib/check-pcb-components-missing-courtyard.ts
var courtyardTypes = /* @__PURE__ */ new Set([
"pcb_courtyard_circle",
"pcb_courtyard_outline",
"pcb_courtyard_polygon",
"pcb_courtyard_pill",
"pcb_courtyard_rect"
]);
function checkPcbComponentsMissingCourtyard(circuitJson) {
const componentIdsWithCourtyards = new Set(
circuitJson.filter((element) => courtyardTypes.has(element.type)).flatMap(
(element) => "pcb_component_id" in element && element.pcb_component_id ? [element.pcb_component_id] : []
)
);
return circuitJson.filter(
(element) => element.type === "pcb_component"
).filter(
(component) => !componentIdsWithCourtyards.has(component.pcb_component_id)
).map((component) => {
const sourceComponent = component.source_component_id ? circuitJson.find(
(element) => element.type === "source_component" && element.source_component_id === component.source_component_id
) : void 0;
const componentName = sourceComponent?.type === "source_component" ? sourceComponent.name : getReadableNameForComponent(circuitJson, component.pcb_component_id);
return {
type: "pcb_component_missing_courtyard_warning",
pcb_component_missing_courtyard_warning_id: `pcb_component_missing_courtyard_warning_${component.pcb_component_id}`,
warning_type: "pcb_component_missing_courtyard_warning",
message: `${componentName} has no courtyard`,
pcb_component_id: component.pcb_component_id,
source_component_id: component.source_component_id,
subcircuit_id: component.subcircuit_id
};
});
}
// lib/check-pcb-trace-lengths.ts
var DEFAULT_VIA_LENGTH_MM = 1.6;
var getRoutePointPosition = (routePoint) => routePoint.route_type === "through_pad" ? routePoint.start : { x: routePoint.x, y: routePoint.y };
var getPcbTraceLength = (pcbTrace) => {
if (pcbTrace.trace_length !== void 0) return pcbTrace.trace_length;
let traceLength = 0;
for (let routePointIndex = 0; routePointIndex < pcbTrace.route.length; routePointIndex++) {
const routePoint = pcbTrace.route[routePointIndex];
if (!routePoint) continue;
if (routePoint.route_type === "via") {
traceLength += DEFAULT_VIA_LENGTH_MM;
continue;
}
const nextRoutePoint = pcbTrace.route[routePointIndex + 1];
if (!nextRoutePoint) continue;
const routePointPosition = getRoutePointPosition(routePoint);
const nextRoutePointPosition = getRoutePointPosition(nextRoutePoint);
traceLength += Math.hypot(
nextRoutePointPosition.x - routePointPosition.x,
nextRoutePointPosition.y - routePointPosition.y
);
}
return traceLength;
};
var getReferencedPcbPortIds = (pcbTrace) => {
const pcbPortIds = /* @__PURE__ */ new Set();
for (const routePoint of pcbTrace.route) {
if (routePoint.route_type !== "wire") continue;
if (routePoint.start_pcb_port_id) {
pcbPortIds.add(routePoint.start_pcb_port_id);
}
if (routePoint.end_pcb_port_id) {
pcbPortIds.add(routePoint.end_pcb_port_id);
}
}
return pcbPortIds;
};
var checkPcbTraceLengths = (circuitJson) => {
const sourceTraces = circuitJson.filter(
(element) => element.type === "source_trace"
);
const pcbTraces = circuitJson.filter(
(element) => element.type === "pcb_trace"
);
const pcbPorts = circuitJson.filter(
(element) => element.type === "pcb_port"
);
const sourceTracesById = new Map(
sourceTraces.map((sourceTrace) => [
sourceTrace.source_trace_id,
sourceTrace
])
);
const pcbPortIdsBySourcePortId = /* @__PURE__ */ new Map();
for (const pcbPort of pcbPorts) {
if (!pcbPort.source_port_id) continue;
const pcbPortIds = pcbPortIdsBySourcePortId.get(pcbPort.source_port_id) ?? /* @__PURE__ */ new Set();
pcbPortIds.add(pcbPort.pcb_port_id);
pcbPortIdsBySourcePortId.set(pcbPort.source_port_id, pcbPortIds);
}
const pcbTracesBySourceTraceId = /* @__PURE__ */ new Map();
for (const pcbTrace of pcbTraces) {
if (!pcbTrace.source_trace_id) continue;
const matchingPcbTraces = pcbTracesBySourceTraceId.get(pcbTrace.source_trace_id) ?? [];
matchingPcbTraces.push(pcbTrace);
pcbTracesBySourceTraceId.set(pcbTrace.source_trace_id, matchingPcbTraces);
}
const exactEndpointPcbTraceIdsBySourceTraceId = /* @__PURE__ */ new Map();
for (const sourceTrace of sourceTraces) {
if (sourceTrace.connected_source_port_ids.length !== 2) continue;
const endpointPcbPortIds = sourceTrace.connected_source_port_ids.map(
(sourcePortId) => pcbPortIdsBySourcePortId.get(sourcePortId)
);
if (endpointPcbPortIds.some((pcbPortIds) => !pcbPortIds?.size)) continue;
const exactEndpointPcbTraceIds = new Set(
(pcbTracesBySourceTraceId.get(sourceTrace.source_trace_id) ?? []).filter((pcbTrace) => {
const referencedPcbPortIds = getReferencedPcbPortIds(pcbTrace);
return endpointPcbPortIds.every(
(pcbPortIds) => [...pcbPortIds].some(
(pcbPortId) => referencedPcbPortIds.has(pcbPortId)
)
);
}).map((pcbTrace) => pcbTrace.pcb_trace_id)
);
if (exactEndpointPcbTraceIds.size > 0) {
exactEndpointPcbTraceIdsBySourceTraceId.set(
sourceTrace.source_trace_id,
exactEndpointPcbTraceIds
);
}
}
const warnings = [];
for (const pcbTrace of pcbTraces) {
if (!pcbTrace.source_trace_id) continue;
const sourceTrace = sourceTracesById.get(pcbTrace.source_trace_id);
if (!sourceTrace) continue;
const maximumTraceLength = sourceTrace.max_length;
if (typeof maximumTraceLength !== "number") continue;
const exactEndpointPcbTraceIds = exactEndpointPcbTraceIdsBySourceTraceId.get(sourceTrace.source_trace_id);
if (exactEndpointPcbTraceIds && !exactEndpointPcbTraceIds.has(pcbTrace.pcb_trace_id)) {
continue;
}
const actualTraceLength = getPcbTraceLength(pcbTrace);
if (actualTraceLength <= maximumTraceLength) continue;
warnings.push({
type: "pcb_trace_too_long_warning",
pcb_trace_too_long_warning_id: `pcb_trace_too_long_warning_${pcbTrace.pcb_trace_id}`,
warning_type: "pcb_trace_too_long_warning",
message: `PCB trace is ${actualTraceLength.toFixed(2)}mm long, exceeding the ${maximumTraceLength}mm maximum`,
pcb_trace_id: pcbTrace.pcb_trace_id,
source_trace_id: sourceTrace.source_trace_id,
source_net_id: sourceTrace.connected_source_net_ids[0],
actual_trace_length: actualTraceLength,
maximum_trace_length: maximumTraceLength,
subcircuit_id: pcbTrace.subcircuit_id ?? sourceTrace.subcircuit_id
});
}
return warnings;
};
// lib/check-pcb-trace-via-counts.ts
var checkPcbTraceViaCounts = (circuitJson) => {
const sourceTraces = circuitJson.filter(
(element) => element.type === "source_trace"
);
const pcbTraces = circuitJson.filter(
(element) => element.type === "pcb_trace"
);
const pcbPorts = circuitJson.filter(
(element) => element.type === "pcb_port"
);
const errors = [];
for (const sourceTrace of sourceTraces) {
const maximumViaCount = sourceTrace.max_via_count;
if (typeof maximumViaCount !== "number") continue;
const routedPcbTraces = pcbTraces.filter(
(pcbTrace) => pcbTrace.source_trace_id === sourceTrace.source_trace_id
);
if (routedPcbTraces.length === 0) continue;
const actualViaCount = routedPcbTraces.reduce(
(viaCount, pcbTrace) => viaCount + pcbTrace.route.filter((routePoint) => routePoint.route_type === "via").length,
0
);
if (actualViaCount <= maximumViaCount) continue;
const connectedPcbPorts = pcbPorts.filter(
(pcbPort) => pcbPort.source_port_id !== void 0 && sourceTrace.connected_source_port_ids.includes(pcbPort.source_port_id)
);
errors.push({
type: "pcb_trace_error",
pcb_trace_error_id: `max_via_count_exceeded_${sourceTrace.source_trace_id}`,
error_type: "pcb_trace_error",
message: `PCB trace uses ${actualViaCount} vias, exceeding the ${maximumViaCount} maximum`,
pcb_trace_id: routedPcbTraces[0].pcb_trace_id,
source_trace_id: sourceTrace.source_trace_id,
pcb_component_ids: [
...new Set(
connectedPcbPorts.map((pcbPort) => pcbPort.pcb_component_id).filter(
(pcbComponentId) => pcbComponentId !== void 0
)
)
],
pcb_port_ids: connectedPcbPorts.map((pcbPort) => pcbPort.pcb_port_id),
subcircuit_id: routedPcbTraces[0].subcircuit_id ?? sourceTrace.subcircuit_id
});
}
return errors;
};
// lib/check-pad-pad-clearance.ts
import {
getPrimaryId as getPrimaryId6,
getReadableNameForElement as getReadableNameForElement6
} from "@tscircuit/circuit-json-util";
import { formatMm } from "format-si-unit";
import {
getFullConnectivityMapFromCircuitJson as getFullConnectivityMapFromCircuitJson9
} from "circuit-json-to-connectivity-map";
function checkPadPadClearance(circuitJson, {
connMap,
minClearance
} = {}) {
const pads = getPads(circuitJson);
if (pads.length < 2) return [];
const board = getPcbBoard(circuitJson);
minClearance ??= getBoardDrcValue(board, "min_pad_edge_to_pad_edge_clearance") ?? jlcMinTolerances.min_pad_edge_to_pad_edge_clearance;
connMap ??= getFullConnectivityMapFromCircuitJson9(circuitJson);
const spatialIndex = new SpatialObjectIndex({
objects: pads,
getBounds: getPadBounds,
getId: (pad) => getPrimaryId6(pad)
});
const errors = /* @__PURE__ */ new Map();
for (const padA of pads) {
const padAId = getPrimaryId6(padA);
const nearbyPads = spatialIndex.getObjectsInBounds(
getPadBounds(padA),
minClearance
);
for (const padB of nearbyPads) {
const padBId = getPrimaryId6(padB);
if (padAId === padBId) continue;
if (!getLayersOfPcbElement(padA).some(
(layer) => getLayersOfPcbElement(padB).includes(layer)
)) {
continue;
}
if (connMap.areIdsConnected(padAId, padBId)) continue;
const pairId = [padAId, padBId].sort().join("_");
const gap = getPadToPadGap(padA, padB);
if (gap + EPSILON >= minClearance) continue;
const centerA = getPadCenter(padA);
const centerB = getPadCenter(padB);
const nextError = {
type: "pcb_pad_pad_clearance_error",
pcb_pad_pad_clearance_error_id: `pad_pad_clearance_${pairId}`,
error_type: "pcb_pad_pad_clearance_error",
message: `Pads ${getReadableNameForElement6(circuitJson, padAId)} and ${getReadableNameForElement6(circuitJson, padBId)} are too close (clearance: ${formatMm(gap)}, minimum: ${formatMm(minClearance)})`,
pcb_pad_ids: [padAId, padBId],
minimum_clearance: minClearance,
actual_clearance: gap,
center: {
x: (centerA.x + centerB.x) / 2,
y: (centerA.y + centerB.y) / 2
}
};
if (!errors.has(pairId)) {
errors.set(pairId, nextError);
}
}
}
return Array.from(errors.values());
}
// lib/check-pad-trace-clearance.ts
import {
getPrimaryId as getPrimaryId7,
getReadableNameForElement as getReadableNameForElement7
} from "@tscircuit/circuit-json-util";
import { formatMm as formatMm2 } from "format-si-unit";
import {
getFullConnectivityMapFromCircuitJson as getFullConnectivityMapFromCircuitJson10
} from "circuit-json-to-connectivity-map";
function checkPadTraceClearance(circuitJson, {
connMap,
minClearance
} = {}) {
const pads = getPads(circuitJson);
const segments = getTraceSegments(circuitJson);
if (pads.length === 0 || segments.length === 0) return [];
const board = getPcbBoard(circuitJson);
minClearance ??= getBoardDrcValue(board, "min_trace_to_pad_edge_clearance") ?? jlcMinTolerances.min_trace_to_pad_edge_clearance;
connMap ??= getFullConnectivityMapFromCircuitJson10(circuitJson);
const spatialIndex = new SpatialObjectIndex({
objects: pads,
getBounds: getPadBounds,
getId: (pad) => getPrimaryId7(pad)
});
const errors = /* @__PURE__ */ new Map();
const overlappingPairIds = /* @__PURE__ */ new Set();
for (const segment of segments) {
const nearbyPads = spatialIndex.getObjectsInBounds(
getCollidableBounds(segment),
minClearance + segment.thickness / 2
);
for (const pad of nearbyPads) {
const padId = getPrimaryId7(pad);
if (!getLayersOfPcbElement(pad).includes(segment.layer)) continue;
if (connMap.areIdsConnected(segment.pcb_trace_id, padId)) continue;
const pairId = `${padId}_${segment.pcb_trace_id}`;
const { gap } = getTraceObstacleClearance(segment, pad);
if (isTraceObstacleOverlap(gap)) {
errors.delete(pairId);
overlappingPairIds.add(pairId);
continue;
}
if (overlappingPairIds.has(pairId)) continue;
if (gap + EPSILON >= minClearance) continue;
const nextError = {
type: "pcb_pad_trace_clearance_error",
pcb_pad_trace_clearance_error_id: `pad_trace_clearance_${pairId}`,
error_type: "pcb_pad_trace_clearance_error",
message: `Pad ${getReadableNameForElement7(circuitJson, padId)} and trace ${getReadableNameForElement7(circuitJson, segment.pcb_trace_id)} are too close (clearance: ${formatMm2(gap)}, minimum: ${formatMm2(minClearance)})`,
pcb_pad_id: padId,
pcb_trace_id: segment.pcb_trace_id,
minimum_clearance: minClearance,
actual_clearance: gap,
center: getTraceCenter(segment)
};
const current = errors.get(pairId);
if (!current || gap < current.gap) {
errors.set(pairId, { error: nextError, gap });
}
}
}
return Array.from(errors.values()).map(({ error }) => error);
}
// lib/check-via-trace-clearance.ts
import { getReadableNameForElement as getReadableNameForElement8 } from "@tscircuit/circuit-json-util";
import { formatMm as formatMm3 } from "format-si-unit";
import {
getFullConnectivityMapFromCircuitJson as getFullConnectivityMapFromCircuitJson11
} from "circuit-json-to-connectivity-map";
function checkViaTraceClearance(circuitJson, {
connMap,
minClearance
} = {}) {
const vias = circuitJson.filter((el) => el.type === "pcb_via");
const segments = getTraceSegments(circuitJson);
if (vias.length === 0 || segments.length === 0) return [];
const board = getPcbBoard(circuitJson);
minClearance ??= getBoardDrcValue(board, "min_trace_to_pad_edge_clearance") ?? jlcMinTolerances.min_trace_to_pad_edge_clearance;
connMap ??= getFullConnectivityMapFromCircuitJson11(circuitJson);
const errors = /* @__PURE__ */ new Map();
const overlappingPairIds = /* @__PURE__ */ new Set();
for (const via of vias) {
for (const segment of segments) {
if (!getLayersOfPcbElement(via).includes(segment.layer)) continue;
if (connMap.areIdsConnected(segment.pcb_trace_id, via.pcb_via_id))
continue;
const pairId = `${via.pcb_via_id}_${segment.pcb_trace_id}`;
const { gap } = getTraceObstacleClearance(segment, via);
if (isTraceObstacleOverlap(gap)) {
errors.delete(pairId);
overlappingPairIds.add(pairId);
continue;
}
if (overlappingPairIds.has(pairId)) continue;
if (gap + EPSILON >= minClearance) continue;
const nextError = {
type: "pcb_via_trace_clearance_error",
pcb_via_trace_clearance_error_id: `via_trace_clearance_${pairId}`,
error_type: "pcb_via_trace_clearance_error",
message: `Via ${getReadableNameForElement8(circuitJson, via.pcb_via_id)} and trace ${getReadableNameForElement8(circuitJson, segment.pcb_trace_id)} are too close (clearance: ${formatMm3(gap)}, minimum: ${formatMm3(minClearance)})`,
pcb_via_id: via.pcb_via_id,
pcb_trace_id: segment.pcb_trace_id,
minimum_clearance: minClearance,
actual_clearance: gap,
center: getTraceCenter(segment)
};
const current = errors.get(pairId);
if (!current || gap < current.gap) {
errors.set(pairId, { error: nextError, gap });
}
}
}
return Array.from(errors.values()).map(({ error }) => error);
}
// lib/check-via-pad-clearance.ts
import {
getPrimaryId as getPrimaryId8,
getReadableNameForElement as getReadableNameForElement9
} from "@tscircuit/circuit-json-util";
import { formatMm as formatMm4 } from "format-si-unit";
import {
getFullConnectivityMapFromCircuitJson as getFullConnectivityMapFromCircuitJson12
} from "circuit-json-to-connectivity-map";
function checkViaPadClearance(circuitJson, {
connMap,
minClearance
} = {}) {
const vias = circuitJson.filter(
(element) => element.type === "pcb_via"
);
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;
connMap ??= getFullConnectivityMapFromCircuitJson12(circuitJson);
const padIndex = new SpatialObjectIndex({
objects: pads,
getBounds: getPadBounds,
getId: getPrimaryId8
});
const errors = [];
for (const via of vias) {
const nearbyPads = padIndex.getObjectsInBounds(
getPadBounds(via),
requiredClearance
);
for (const pad of nearbyPads) {
const padId = getPrimaryId8(pad);
if (!getLayersOfPcbElement(via).some(
(layer) => getLayersOfPcbElement(pad).includes(layer)
)) {
continue;
}
if (connMap.areIdsConnected(via.pcb_via_id, padId)) continue;
const gap = getPadToPadGap(via, pad);
if (gap + EPSILON >= requiredClearance) continue;
const viaCenter = getPadCenter(via);
const padCenter = getPadCenter(pad);
errors.push({
type: "pcb_pad_pad_clearance_error",
pcb_pad_pad_clearance_error_id: `via_pad_clearance_${via.pcb_via_id}_${padId}`,
error_type: "pcb_pad_pad_clearance_error",
message: `Via ${getReadableNameForElement9(circuitJson, via.pcb_via_id)} and pad ${getReadableNameForElement9(circuitJson, padId)} are too close (clearance: ${formatMm4(gap)}, minimum: ${formatMm4(requiredClearance)})`,
pcb_pad_ids: [via.pcb_via_id, padId],
minimum_clearance: requiredClearance,
actual_clearance: gap,
center: {
x: (viaCenter.x + padCenter.x) / 2,
y: (viaCenter.y + padCenter.y) / 2
}
});
}
}
return errors;
}
// lib/check-vias-in-pads.ts
import { getPrimaryId as getPrimaryId9 } from "@tscircuit/circuit-json-util";
import { getFullConnectivityMapFromCircuitJson as getFullConnectivityMapFromCircuitJson13 } from "circuit-json-to-connectivity-map";
function checkViasInPads(circuitJson) {
const board = getPcbBoard(circuitJson);
if (board && "is_via_in_pad_allowed" in board && board.is_via_in_pad_allowed === true) {
return [];
}
const vias = circuitJson.filter(
(element) => element.type === "pcb_via"
);
const pads = getPads(circuitJson);
if (vias.length === 0 || pads.length === 0) return [];
const connMap = getFullConnectivityMapFromCircuitJson13(circuitJson);
const padOrdinals = new Map(
pads.map((pad, index) => [getPrimaryId9(pad), index])
);
const padIndex = new SpatialObjectIndex({
objects: pads,
getBounds: getPadBounds,
getId: getPrimaryId9
});
const errors = [];
for (const via of vias) {
const nearbyPads = padIndex.getObjectsInBounds(getPadBounds(via));
for (const pad of nearbyPads) {
const padId = getPrimaryId9(pad);
const viaLayers = getLayersOfPcbElement(via);
const padLayers = getLayersOfPcbElement(pad);
if (!viaLayers.some((layer) => padLayers.includes(layer))) continue;
if (connMap.areIdsConnected(via.pcb_via_id, padId)) continue;
if (getPadToPadGap(via, pad) > 0) continue;
const padOrdinal = padOrdinals.get(padId) ?? 0;
const padName = getReadableNameForFootprintPad(
circuitJson,
pad,
padOrdinal
);
errors.push({
type: "pcb_placement_error",
pcb_placement_error_id: `via_in_pad_${via.pcb_via_id}_${padId}`,
error_type: "pcb_placement_error",
message: `Via copper at (${via.x.toFixed(2)}mm, ${via.y.toFixed(2)}mm) overlaps ${padName}`,
subcircuit_id: via.subcircuit_id ?? pad.subcircuit_id
});
}
}
return errors;
}
// lib/dedupe-pcb-drc-errors.ts
var dedupePcbDrcErrors = (errors) => {
const specificallyReportedPairIds = /* @__PURE__ */ new Set();
for (const error of errors) {
if (error.type === "pcb_pad_trace_clearance_error" && typeof error.pcb_trace_id === "string" && typeof error.pcb_pad_id === "string") {
specificallyReportedPairIds.add(
`overlap_${error.pcb_trace_id}_${error.pcb_pad_id}`
);
}
if (error.type === "pcb_via_trace_clearance_error" && typeof error.pcb_trace_id === "string" && typeof error.pcb_via_id === "string") {
specificallyReportedPairIds.add(
`overlap_${error.pcb_trace_id}_${error.pcb_via_id}`
);
}
}
return errors.filter((element) => {
const error = element;
return !(error.type === "pcb_trace_error" && typeof error.pcb_trace_error_id === "string" && specificallyReportedPairIds.has(error.pcb_trace_error_id));
});
};
// lib/check-pin-must-be-connected.ts
function checkPinMustBeConnected(circuitJson) {
const errors = [];
const sourceComponents = circuitJson.filter(
(el) => "source_component_id" in el && (el.type === "source_component" || el.type.startsWith("source_simple_"))
);
const sourcePorts = circuitJson.filter(
(el) => el.type === "source_port"
);
const sourceTraces = circuitJson.filter(
(el) => el.type === "source_trace"
);
const connectedPortIds = /* @__PURE__ */ new Set();
for (const trace of sourceTraces) {
for (const portId of trace.connected_source_port_ids ?? []) {
connectedPortIds.add(portId);
}
}
const componentInternalConnections = /* @__PURE__ */ new Map();
for (const component of sourceComponents) {
if ("internally_connected_source_port_ids" in component && component.internally_connected_source_port_ids) {
componentInternalConnections.set(
component.source_component_id,
component.internally_connected_source_port_ids
);
}
}
for (const internalGroups of componentInternalConnections.values()) {
for (const group of internalGroups) {
if (group.some((portId) => connectedPortIds.has(portId))) {
for (const portId of group) {
connectedPortIds.add(portId);
}
}
}
}
for (const port of sourcePorts) {
if (port.must_be_connected === true) {
if (!connectedPortIds.has(port.source_port_id)) {
const component = sourceComponents.find(
(c) => c.source_component_id === port.source_component_id
);
const componentName = component?.name ?? "Unknown";
errors.push({
type: "source_pin_must_be_connected_error",
source_pin_must_be_connected_error_id: `source_pin_must_be_connected_error_${port.source_port_id}`,
error_type: "source_pin_must_be_connected_error",
message: `Port ${port.name} on ${componentName} must be connected but is floating`,
source_component_id: port.source_component_id ?? "",
source_port_id: port.source_port_id,
subcircuit_id: port.subcircuit_id
});
}
}
}
return errors;
}
// lib/check-two-terminal-switch-contacts-on-different-nets.ts
import {
source_component_misconfigured_error
} from "circuit-json";
function checkTwoTerminalSwitchContactsOnDifferentNets(circuitJson) {
const switchingComponents = circuitJson.filter(
(element) => element.type === "source_component" && (element.ftype === "simple_push_button" || element.ftype === "simple_switch")
);
const sourcePorts = circuitJson.filter(
(element) => element.type === "source_port"
);
const schematicPorts = circuitJson.filter(
(element) => element.type === "schematic_port"
);
const errors = [];
for (const switchingComponent of switchingComponents) {
const schematicContactSourcePorts = schematicPorts.flatMap(
(schematicPort) => {
const sourcePort = sourcePorts.find(
(sourcePort2) => sourcePort2.source_port_id === schematicPort.source_port_id && sourcePort2.source_component_id === switchingComponent.source_component_id
);
if (!sourcePort) return [];
return [sourcePort];
}
);
if (schematicContactSourcePorts.length !== 2) continue;
const firstContactConnectivityKey = schematicContactSourcePorts[0].subcircuit_connectivity_map_key;
const secondContactConnectivityKey = schematicContactSourcePorts[1].subcircuit_connectivity_map_key;
if (!firstContactConnectivityKey) continue;
if (firstContactConnectivityKey !== secondContactConnectivityKey) continue;
errors.push(
source_component_misconfigured_error.parse({
type: "source_component_misconfigured_error",
message: `Switch ${switchingComponent.name} has both schematic contacts connected to the same net. Check internallyConnectedPins and the footprint pin mapping.`,
source_component_ids: [switchingComponent.source_component_id],
source_port_ids: schematicContactSourcePorts.map(
(sourcePort) => sourcePort.source_port_id
),
is_fatal: true
})
);
}
return errors;
}
// lib/check-all-pins-in-component-are-underspecified.ts
import { cju as cju7 } from "@tscircuit/circuit-json-util";
var PIN_ATTRIBUTE_KEYS = [
"must_be_connected",
"provides_power",
"requires_power",
"provides_ground",
"requires_ground",
"provides_voltage",
"requires_voltage",
"do_not_connect",
"include_in_board_pinout",
"can_use_internal_pullup",
"is_using_internal_pullup",
"needs_external_pullup",
"can_use_internal_pulldown",
"is_using_internal_pulldown",
"needs_external_pulldown",
"can_use_open_drain",
"is_using_open_drain",
"can_use_push_pull",
"is_using_push_pull",
"should_have_decoupling_capacitor",
"recommended_decoupling_capacitor_capacitance",
"is_configured_for_i2c_sda",
"is_configured_for_i2c_scl",
"is_configured_for_spi_mosi",
"is_configured_for_spi_miso",
"is_configured_for_spi_sck",
"is_configured_for_spi_cs",
"is_configured_for_uart_tx",
"is_configured_for_uart_rx",
"supports_i2c_sda",
"supports_i2c_scl",
"supports_spi_mosi",
"supports_spi_miso",
"supports_spi_sck",
"supports_spi_cs",
"supports_uart_tx",
"supports_uart_rx"
];
function hasAnyPinAttribute(port) {
return PIN_ATTRIBUTE_KEYS.some((key) => port[key] !== void 0);
}
function checkAllPinsInComponentAreUnderspecified(circuitJson) {
const warnings = [];
const db = cju7(circuitJson);
const sourceComponents = db.source_component.list();
const sourcePorts = db.source_port.list();
const portsByComponent = /* @__PURE__ */ new Map();
for (const port of sourcePorts) {
if (!port.source_component_id) continue;
const existing = portsByComponent.get(port.source_component_id) ?? [];
existing.push(port);
portsByComponent.set(port.source_component_id, existing);
}
for (const component of sourceComponents) {
if (component.ftype !== "simple_chip") continue;
const componentPorts = portsByComponent.get(component.source_component_id) ?? [];
if (componentPorts.length === 0) continue;
const hasAnySpecifiedAttributes = componentPorts.some(
(port) => hasAnyPinAttribute(port)
);
if (hasAnySpecifiedAttributes) continue;
warnings.push({
type: "source_component_pins_underspecified_warning",
source_component_pins_underspecified_warning_id: `source_component_pins_underspecified_warning_${component.source_component_id}`,
warning_type: "source_component_pins_underspecified_warning",
message: `All pins on ${component.name} are underspecified (no pinAttributes set)`,
source_component_id: component.source_component_id,
source_port_ids: componentPorts.map((port) => port.source_port_id),
subcircuit_id: componentPorts[0]?.subcircuit_id
});
}
return warnings;
}
// lib/check-no-power-pin-defined.ts
import { cju as cju8 } from "@tscircuit/circuit-json-util";
// lib/util/should-check-chip-power-ground-pins.ts
var shouldCheckChipPowerGroundPins = (component, ports) => component.ftype === "simple_chip" && ports.filter((port) => port.do_not_connect !== true).length >= 2;
// lib/check-no-power-pin-defined.ts
function checkNoPowerPinDefined(circuitJson) {
const warnings = [];
const db = cju8(circuitJson);
const sourceComponents = db.source_component.list();
const sourcePorts = db.source_port.list();
const portsByComponent = /* @__PURE__ */ new Map();
for (const port of sourcePorts) {
if (!port.source_component_id) continue;
const existing = portsByComponent.get(port.source_component_id) ?? [];
existing.push(port);
portsByComponent.set(port.source_component_id, existing);
}
for (const component of sourceComponents) {
const componentPorts = portsByComponent.get(component.source_component_id) ?? [];
if (!shouldCheckChipPowerGroundPins(component, componentPorts)) continue;
const hasRequiredPowerPin = componentPorts.some(
(port) => port.requires_power === true
);
if (hasRequiredPowerPin) continue;
warnings.push({
type: "source_no_power_pin_defined_warning",
source_no_power_pin_defined_warning_id: `source_no_power_pin_defined_warning_${component.source_component_id}`,
warning_type: "source_no_power_pin_defined_warning",
message: `${component.name} has no pin with requires_power=true`,
source_component_id: component.source_component_id,
source_port_ids: componentPorts.map((port) => port.source_port_id),
subcircuit_id: componentPorts[0]?.subcircuit_id
});
}
return warnings;
}
// lib/check-no-ground-pin-defined.ts
import { cju as cju9 } from "@tscircuit/circuit-json-util";
function checkNoGroundPinDefined(circuitJson) {
const warnings = [];
const db = cju9(circuitJson);
const sourceComponents = db.source_component.list();
const sourcePorts = db.source_port.list();
const portsByComponent = /* @__PURE__ */ new Map();
for (const port of sourcePorts) {
if (!port.source_component_id) continue;
const existing = portsByComponent.get(port.source_component_id) ?? [];
existing.push(port);
portsByComponent.set(port.source_component_id, existing);
}
for (const component of sourceComponents) {
const componentPorts = portsByComponent.get(component.source_component_id) ?? [];
if (!shouldCheckChipPowerGroundPins(component, componentPorts)) continue;
const hasRequiredGroundPin = componentPorts.some(
(port) => port.requires_ground === true
);
if (hasRequiredGroundPin) continue;
warnings.push({
type: "source_no_ground_pin_defined_warning",
source_no_ground_pin_defined_warning_id: `source_no_ground_pin_defined_warning_${component.source_component_id}`,
warning_type: "source_no_ground_pin_defined_warning",
message: `${component.name} has no pin with requires_ground=true`,
source_component_id: component.source_component_id,
source_port_ids: componentPorts.map((port) => port.source_port_id),
subcircuit_id: componentPorts[0]?.subcircuit_id
});
}
return warnings;
}
// lib/check-schematic-component-excessive-vertical-padding.ts
var DEFAULT_PIN_SPACING = 0.2;
var MAX_VERTICAL_PADDING_IN_PIN_SPACINGS = 3;
var FLOATING_POINT_TOLERANCE = 1e-9;
function checkSchematicComponentExcessiveVerticalPadding(circuitJson) {
const schematicComponents = circuitJson.filter(
(element) => element.type === "schematic_component"
);
const schematicPorts = circuitJson.filter(
(element) => element.type === "schematic_port"
);
const sourceComponents = circuitJson.filter(
(element) => element.type === "source_component"
);
const sourceComponentById = new Map(
sourceComponents.map((component) => [
component.source_component_id,
component
])
);
const portsByComponentId = /* @__PURE__ */ new Map();
for (const port of schematicPorts) {
if (!port.schematic_component_id) continue;
const componentPorts = portsByComponentId.get(port.schematic_component_id) ?? [];
componentPorts.push(port);
portsByComponentId.set(port.schematic_component_id, componentPorts);
}
const warnings = [];
for (const component of schematicComponents) {
if (!component.is_box_with_pins || component.size.height <= 0) continue;
const sidePorts = (portsByComponentId.get(component.schematic_component_id) ?? []).filter(
(port) => port.side_of_component === "left" || port.side_of_component === "right"
);
if (sidePorts.length < 2) continue;
const pinYs = sidePorts.map((port) => port.center.y);
const highestPinY = Math.max(...pinYs);
const lowestPinY = Math.min(...pinYs);
const componentTopY = component.center.y + component.size.height / 2;
const componentBottomY = component.center.y - component.size.height / 2;
const pinSpacing = component.pin_spacing ?? DEFAULT_PIN_SPACING;
const maximumPadding = pinSpacing * MAX_VERTICAL_PADDING_IN_PIN_SPACINGS;
const paddingBySide = {
top: componentTopY - highestPinY,
bottom: lowestPinY - componentBottomY
};
const sourceComponent = component.source_component_id ? sourceComponentById.get(component.source_component_id) : void 0;
const componentName = sourceComponent?.name ?? component.schematic_component_id;
for (const side of ["top", "bottom"]) {
const padding = paddingBySide[side];
if (padding <= maximumPadding + FLOATING_POINT_TOLERANCE || padding <= 0) {
continue;
}
const relativePosition = side === "top" ? "above" : "below";
const stylingIssueType = `excessive_${side}_padding`;
warnings.push({
type: "schematic_component_styling_warning",
schematic_component_styling_warning_id: `schematic_component_styling_warning_${component.schematic_component_id}_${stylingIssueType}`,
warning_type: "schematic_component_styling_warning",
message: `${componentName} has excessive empty space ${relativePosition} its pins (${padding.toFixed(2)}mm, more than ${MAX_VERTICAL_PADDING_IN_PIN_SPACINGS} pin spacings)`,
schematic_component_id: component.schematic_component_id,
styling_issue_type: stylingIssueType,
schematic_port_ids: sidePorts.map((port) => port.schematic_port_id),
source_component_id: component.source_component_id,
schematic_sheet_id: component.schematic_sheet_id,
subcircuit_id: component.subcircuit_id
});
}
}
return warnings;
}
// lib/check-schematic-component-missing-reference-designator-text.ts
var isFallbackReferenceDesignator = (name) => /^unnamed_[a-z0-9_-]+\d+$/i.test(name);
var isTextWithinComponentBounds = (schematicText, schematicComponent) => {
const { center, size } = schematicComponent;
const tolerance = 1e-9;
return schematicText.position.x >= center.x - size.width / 2 - tolerance && schematicText.position.x <= center.x + size.width / 2 + tolerance && schematicText.position.y >= center.y - size.height / 2 - tolerance && schematicText.position.y <= center.y + size.height / 2 + tolerance;
};
function checkSchematicComponentMissingReferenceDesignatorText(circuitJson) {
const schematicComponents = circuitJson.filter(
(element) => element.type === "schematic_component"
);
const sourceComponents = circuitJson.filter(
(element) => element.type === "source_component"
);
const schematicTexts = circuitJson.filter(
(element) => element.type === "schematic_text"
);
const sourceComponentById = new Map(
sourceComponents.map((component) => [
component.source_component_id,
component
])
);
const textBySchematicComponentId = /* @__PURE__ */ new Map();
const customSymbolTexts = [];
for (const schematicText of schematicTexts) {
if (!schematicText.schematic_component_id) {
if (schematicText.schematic_symbol_id) {
customSymbolTexts.push(schematicText);
}
continue;
}
const componentTexts = textBySchematicComponentId.get(schematicText.schematic_component_id) ?? /* @__PURE__ */ new Set();
componentTexts.add(schematicText.text.trim());
textBySchematicComponentId.set(
schematicText.schematic_component_id,
componentTexts
);
}
const warnings = [];
for (const schematicComponent of schematicComponents) {
if (!schematicComponent.source_component_id) continue;
const sourceComponent = sourceComponentById.get(
schematicComponent.source_component_id
);
if (!sourceComponent) continue;
const referenceDesignators = new Set(
[sourceComponent.name, sourceComponent.display_name].map((name) => name?.trim()).filter((name) => Boolean(name))
);
const nonFallbackReferenceDesignator = [...referenceDesignators].find(
(referenceDesignator) => !isFallbackReferenceDesignator(referenceDesignator)
);
const componentTexts = textBySchematicComponentId.get(
schematicComponent.schematic_component_id
);
const hasReferenceDesignatorText = [...referenceDesignators].some(
(referenceDesignator) => componentTexts?.has(referenceDesignator)
) || customSymbolTexts.some(
(schematicText) => referenceDesignators.has(schematicText.text.trim()) && isTextWithinComponentBounds(schematicText, schematicComponent)
);
if (nonFallbackReferenceDesignator && hasReferenceDesignatorText) {
continue;
}
const readableComponentName = nonFallbackReferenceDesignator ?? "Schematic component";
warnings.push({
type: "schematic_component_styling_warning",
schematic_component_styling_warning_id: `schematic_component_styling_warning_${schematicComponent.schematic_component_id}_missing_reference_designator_text`,
warning_type: "schematic_component_styling_warning",
message: `${readableComponentName} is missing schematic reference designator text. For a custom symbol, add name="{REFDES}" inside the symbol.`,
schematic_component_id: schematicComponent.schematic_component_id,
styling_issue_type: "missing_reference_designator_text",
source_component_id: schematicComponent.source_component_id,
schematic_sheet_id: schematicComponent.schematic_sheet_id,
subcircuit_id: schematicComponent.subcircuit_id
});
}
return warnings;
}
// lib/check-schematic-component-ports-outside-body.ts
var FLOATING_POINT_TOLERANCE2 = 1e-9;
var getPortLabel = (port) => port.display_pin_label ?? `pin ${port.pin_number}`;
function checkSchematicComponentPortsOutsideBody(circuitJson) {
const schematicComponents = circuitJson.filter(
(element) => element.type === "schematic_component"
);
const schematicPorts = circuitJson.filter(
(element) => element.type === "schematic_port"
);
const sourceComponents = circuitJson.filter(
(element) => element.type === "source_component"
);
const sourceComponentById = new Map(
sourceComponents.map((component) => [
component.source_component_id,
component
])
);
const portsByComponentId = /* @__PURE__ */ new Map();
for (const port of schematicPorts) {
if (!port.schematic_component_id) continue;
const componentPorts = portsByComponentId.get(port.schematic_component_id) ?? [];
componentPorts.push(port);
portsByComponentId.set(port.schematic_component_id, componentPorts);
}
const warnings = [];
for (const component of schematicComponents) {
if (component.is_box_with_pins === false || component.size.width <= 0 || component.size.height <= 0) {
continue;
}
const componentLeftX = component.center.x - component.size.width / 2;
const componentRightX = component.center.x + component.size.width / 2;
const componentBottomY = component.center.y - component.size.height / 2;
const componentTopY = component.center.y + component.size.height / 2;
const componentPorts = portsByComponentId.get(component.schematic_component_id) ?? [];
const portsOutsideBody = componentPorts.filter((port) => {
if (port.side_of_component === "left" || port.side_of_component === "right") {
return port.center.y < componentBottomY - FLOATING_POINT_TOLERANCE2 || port.center.y > componentTopY + FLOATING_POINT_TOLERANCE2;
}
if (port.side_of_component === "top" || port.side_of_component === "bottom") {
return port.center.x < componentLeftX - FLOATING_POINT_TOLERANCE2 || port.center.x > componentRightX + FLOATING_POINT_TOLERANCE2;
}
return false;
}).sort((portA, portB) => {
const portAIsVertical = portA.side_of_component === "left" || portA.side_of_component === "right";
const portBIsVertical = portB.side_of_component === "left" || portB.side_of_component === "right";
if (portAIsVertical && portBIsVertical) {
return portB.center.y - portA.center.y;
}
if (!portAIsVertical && !portBIsVertical) {
return portA.center.x - portB.center.x;
}
return portAIsVertical ? -1 : 1;
});
if (portsOutsideBody.length === 0) continue;
const requiredHeight = Math.max(
component.size.height,
...portsOutsideBody.filter(
(port) => port.side_of_component === "left" || port.side_of_component === "right"
).map((port) => 2 * Math.abs(port.center.y - component.center.y))
);
const requiredWidth = Math.max(
component.size.width,
...portsOutsideBody.filter(
(port) => port.side_of_component === "top" || port.side_of_component === "bottom"
).map((port) => 2 * Math.abs(port.center.x - component.center.x))
);
const suggestedDimensionChanges = [];
if (requiredHeight > component.size.height + FLOATING_POINT_TOLERANCE2) {
suggestedDimensionChanges.push(
`increase schHeight to at least ${requiredHeight.toFixed(2)}mm`
);
}
if (requiredWidth > component.size.width + FLOATING_POINT_TOLERANCE2) {
suggestedDimensionChanges.push(
`increase schWidth to at least ${requiredWidth.toFixed(2)}mm`
);
}
const sourceComponent = component.source_component_id ? sourceComponentById.get(component.source_component_id) : void 0;
const componentName = sourceComponent?.name ?? component.schematic_component_id;
const portLabels = portsOutsideBody.map(getPortLabel).join(", ");
warnings.push({
type: "schematic_component_styling_warning",
schematic_component_styling_warning_id: `schematic_component_styling_warning_${component.schematic_component_id}_ports_outside_body`,
warning_type: "schematic_component_styling_warning",
message: `${componentName} has schematic pins outside its body (${portLabels}); ${suggestedDimensionChanges.join(" and ")}`,
schematic_component_id: component.schematic_component_id,
styling_issue_type: "ports_outside_body",
schematic_port_ids: portsOutsideBody.map(
(port) => port.schematic_port_id
),
source_component_id: component.source_component_id,
schematic_sheet_id: component.schematic_sheet_id,
subcircuit_id: component.subcircuit_id
});
}
return warnings;
}
// lib/check-same-name-nets-are-connected.ts
function checkSameNameNetsAreConnected(circuitJson) {
const parents = /* @__PURE__ */ new Map();
const find = (id) => {
let root = id;
while (parents.has(root) && parents.get(root) !== root) {
root = parents.get(root);
}
while (parents.has(id) && parents.get(id) !== root) {
const next = parents.get(id);
parents.set(id, root);
id = next;
}
return root;
};
const connect = (ids) => {
if (ids.length < 2) return;
const root = find(ids[0]);
for (const id of ids.slice(1)) parents.set(find(id), root);
};
const netsByName = /* @__PURE__ */ new Map();
for (const element of circuitJson) {
if (element.type === "source_net" && element.name.trim()) {
const nets = netsByName.get(element.name) ?? [];
nets.push(element);
netsByName.set(element.name, nets);
}
if (element.type === "source_trace") {
connect([
...element.connected_source_net_ids.map((id) => `net:${id}`),
...element.connected_source_port_ids.map((id) => `port:${id}`)
]);
}
if (element.type === "source_component_internal_connection") {
connect(element.source_port_ids.map((id) => `port:${id}`));
}
if (element.type === "source_component") {
for (const ids of element.internally_connected_source_port_ids ?? []) {
connect(ids.map((id) => `port:${id}`));
}
}
}
const warnings = [];
for (const [name, nets] of netsByName) {
const islands = new Set(nets.map((net) => find(`net:${net.source_net_id}`)));
if (islands.size < 2) continue;
const ids = nets.map((net) => net.source_net_id).sort();
warnings.push({
type: "source_confusing_net_name_warning",
source_confusing_net_name_warning_id: `source_confusing_net_name_warning_${ids[0]}`,
warning_type: "source_confusing_net_name_warning",
message: `Nets named "${name}" are not all connected (${islands.size} separate electrical networks). Connect them or use distinct names to avoid confusion.`,
source_net_ids: ids,
net_name: name,
...nets.every((net) => net.subcircuit_id === nets[0].subcircuit_id) ? { subcircuit_id: nets[0].subcircuit_id } : {}
});
}
return warnings;
}
// lib/check-connector-accessible-orientation.ts
import { getBoardBounds } from "@tscircuit/circuit-json-util";
function getFacingDirectionFromInsertionDirection(component) {
switch (component.insertion_direction) {
case "from_left":
return "x-";
case "from_right":
return "x+";
case "from_top":
return "y+";
case "from_bottom":
return "y-";
case "from_above":
case "from_below":
return null;
default:
return null;
}
}
function getFacingDirection(component) {
if (component.insertion_direction) {
return getFacingDirectionFromInsertionDirection(component);
}
if (!component.center || !component.cable_insertion_center) return null;
const dx = component.cable_insertion_center.x - component.center.x;
const dy = component.cable_insertion_center.y - component.center.y;
if (Math.abs(dx) < 1e-6 && Math.abs(dy) < 1e-6) return null;
if (Math.abs(dx) >= Math.abs(dy)) {
return dx >= 0 ? "x+" : "x-";
}
return dy >= 0 ? "y+" : "y-";
}
function getRecommendedFacingDirection(component, bounds2) {
if (!component.center) return null;
const distances = [
{ direction: "x-", distance: component.center.x - bounds2.minX },
{ direction: "x+", distance: bounds2.maxX - component.center.x },
{ direction: "y-", distance: component.center.y - bounds2.minY },
{ direction: "y+", distance: bounds2.maxY - component.center.y }
];
distances.sort((a, b) => a.distance - b.distance);
return distances[0]?.direction ?? null;
}
function checkConnectorAccessibleOrientation(circuitJson) {
const board = circuitJson.find(
(el) => el.type === "pcb_board"
);
if (!board) return [];
const bounds2 = (() => {
try {
return getBoardBounds(board);
} catch {
return null;
}
})();
if (!bounds2) return [];
const warnings = [];
const components = circuitJson.filter(
(el) => el.type === "pcb_component"
);
for (const component of components) {
const facingDirection = getFacingDirection(component);
const recommendedFacingDirection = getRecommendedFacingDirection(
component,
bounds2
);
if (!facingDirection || !recommendedFacingDirection) continue;
if (facingDirection === recommendedFacingDirection) continue;
const componentName = getReadableNameForComponent(
circuitJson,
component.pcb_component_id
);
warnings.push({
type: "pcb_connector_not_in_accessible_orientation_warning",
warning_type: "pcb_connector_not_in_accessible_orientation_warning",
pcb_connector_not_in_accessible_orientation_warning_id: `pcb_connector_not_in_accessible_orientation_warning_${component.pcb_component_id}`,
message: `${componentName} is facing ${facingDirection} but should face ${recommendedFacingDirection} so the connector is accessible from the board edge`,
pcb_component_id: component.pcb_component_id,
source_component_id: component.source_component_id,
pcb_board_id: board.pcb_board_id,
facing_direction: facingDirection,
recommended_facing_direction: recommendedFacingDirection,
subcircuit_id: component.subcircuit_id
});
}
return warnings;
}
// lib/check-courtyard-overlap/checkCourtyardOverlap.ts
import {
doSegmentsIntersect,
isPointInsidePolygon as isPointInsidePolygon2
} from "@tscircuit/math-utils";
function getCourtyardPolygon(el) {
if (el.type === "pcb_courtyard_rect") {
const hw = el.width / 2;
const hh = el.height / 2;
const corners = [
{ x: -hw, y: -hh },
{ x: +hw, y: -hh },
{ x: +hw, y: +hh },
{ x: -hw, y: +hh }
];
const angle = (el.ccw_rotation ?? 0) * Math.PI / 180;
const cos = Math.cos(angle);
const sin = Math.sin(angle);
return corners.map(({ x, y }) => ({
x: el.center.x + x * cos - y * sin,
y: el.center.y + x * sin + y * cos
}));
}
if (el.type === "pcb_courtyard_circle") {
const N = 32;
return Array.from({ length: N }, (_, i) => {
const a = 2 * Math.PI * i / N;
return {
x: el.center.x + el.radius * Math.cos(a),
y: el.center.y + el.radius * Math.sin(a)
};
});
}
return el.outline;
}
function getComponentName2(circuitJson, pcbComponentId) {
const pcbComponent = circuitJson.find(
(el) => el.type === "pcb_component" && el.pcb_component_id === pcbComponentId
);
if (pcbComponent?.type !== "pcb_component") return pcbComponentId;
const sourceComponent = circuitJson.find(
(el) => el.type === "source_component" && el.source_component_id === pcbComponent.source_component_id
);
if (sourceComponent?.type === "source_component" && sourceComponent.name) {
return sourceComponent.name;
}
return pcbComponentId;
}
function polygonsOverlap(polyA, polyB) {
if (polyA.some((p) => isPointInsidePolygon2(p, polyB))) return true;
if (polyB.some((p) => isPointInsidePolygon2(p, polyA))) return true;
for (let i = 0; i < polyA.length; i++) {
const a1 = polyA[i];
const a2 = polyA[(i + 1) % polyA.length];
for (let j = 0; j < polyB.length; j++) {
const b1 = polyB[j];
const b2 = polyB[(j + 1) % polyB.length];
if (doSegmentsIntersect(a1, a2, b1, b2)) return true;
}
}
return false;
}
function checkCourtyardOverlap(circuitJson) {
const courtyards = circuitJson.filter(
(el) => el.type === "pcb_courtyard_rect" || el.type === "pcb_courtyard_circle" || el.type === "pcb_courtyard_outline"
);
const byComponent = /* @__PURE__ */ new Map();
for (const el of courtyards) {
const id = el.pcb_component_id;
if (!byComponent.has(id)) byComponent.set(id, []);
byComponent.get(id).push(el);
}
const componentIds = Array.from(byComponent.keys());
const errors = [];
for (let i = 0; i < componentIds.length; i++) {
for (let j = i + 1; j < componentIds.length; j++) {
const idA = componentIds[i];
const idB = componentIds[j];
let overlapping = false;
outer: for (const a of byComponent.get(idA)) {
for (const b of byComponent.get(idB)) {
if ("layer" in a && "layer" in b && a.layer !== b.layer) {
continue;
}
const polyA = getCourtyardPolygon(a);
const polyB = getCourtyardPolygon(b);
if (polygonsOverlap(polyA, polyB)) {
overlapping = true;
break outer;
}
}
}
if (overlapping) {
errors.push({
type: "pcb_courtyard_overlap_error",
pcb_error_id: `pcb_courtyard_overlap_${idA}_${idB}`,
error_type: "pcb_courtyard_overlap_error",
message: `Courtyard of ${getComponentName2(circuitJson, idA)} overlaps with courtyard of ${getComponentName2(circuitJson, idB)}`,
pcb_component_ids: [idA, idB]
});
}
}
}
return errors;
}
// lib/check-testpoint-accessibility.ts
import { isPointInsidePolygon as isPointInsidePolygon3 } from "@tscircuit/math-utils";
var isCourtyardElement2 = (element) => element.type === "pcb_courtyard_circle" || element.type === "pcb_courtyard_outline" || element.type === "pcb_courtyard_polygon" || element.type === "pcb_courtyard_rect";
var isPointInsideCourtyard = (point, courtyard) => {
if (courtyard.type === "pcb_courtyard_circle") {
const dx = point.x - courtyard.center.x;
const dy = point.y - courtyard.center.y;
return dx * dx + dy * dy <= courtyard.radius * courtyard.radius;
}
if (courtyard.type === "pcb_courtyard_rect") {
const angle = -1 * (courtyard.ccw_rotation ?? 0) * Math.PI / 180;
const dx = point.x - courtyard.center.x;
const dy = point.y - courtyard.center.y;
const localX = dx * Math.cos(angle) - dy * Math.sin(angle);
const localY = dx * Math.sin(angle) + dy * Math.cos(angle);
return Math.abs(localX) <= courtyard.width / 2 && Math.abs(localY) <= courtyard.height / 2;
}
const polygon = courtyard.type === "pcb_courtyard_polygon" ? courtyard.points : courtyard.outline;
return isPointInsidePolygon3(point, polygon);
};
var getPcbComponentName = (circuitJson, pcbComponentId) => {
const pcbComponent = circuitJson.find(
(element) => element.type === "pcb_component" && element.pcb_component_id === pcbComponentId
);
const sourceComponent = circuitJson.find(
(element) => element.type === "source_component" && element.source_component_id === pcbComponent?.source_component_id
);
return (sourceComponent && "name" in sourceComponent ? sourceComponent.name : void 0) ?? getReadableNameForComponent(circuitJson, pcbComponentId);
};
function checkTestPointAccessibility(circuitJson) {
const sourceTestPoints = circuitJson.filter(
(element) => element.type === "source_component" && element.ftype === "simple_test_point"
);
const sourceTestPointIds = new Set(
sourceTestPoints.map((testPoint) => testPoint.source_component_id)
);
const testPointNames = new Map(
sourceTestPoints.map((testPoint) => [
testPoint.source_component_id,
testPoint.name
])
);
const testPointComponents = circuitJson.filter(
(element) => element.type === "pcb_component" && sourceTestPointIds.has(element.source_component_id)
);
const courtyards = circuitJson.filter(isCourtyardElement2);
const errors = [];
const reportedComponentPairs = /* @__PURE__ */ new Set();
for (const testPoint of testPointComponents) {
for (const courtyard of courtyards) {
if (courtyard.pcb_component_id === testPoint.pcb_component_id) continue;
if (courtyard.layer !== testPoint.layer) continue;
if (!isPointInsideCourtyard(testPoint.center, courtyard)) continue;
const componentPair = `${testPoint.pcb_component_id}:${courtyard.pcb_component_id}`;
if (reportedComponentPairs.has(componentPair)) continue;
reportedComponentPairs.add(componentPair);
const testPointName = testPointNames.get(testPoint.source_component_id) ?? "Test point";
const obstructingComponentName = getPcbComponentName(
circuitJson,
courtyard.pcb_component_id
);
errors.push({
type: "pcb_placement_error",
pcb_placement_error_id: `testpoint_in_courtyard_${testPoint.pcb_component_id}_${courtyard.pcb_component_id}`,
error_type: "pcb_placement_error",
message: `Test point ${testPointName} is not accessible because it is inside the courtyard of ${obstructingComponentName}`,
subcircuit_id: testPoint.subcircuit_id
});
}
}
return errors;
}
// lib/run-all-checks.ts
async function runAllPlacementChecks(circuitJson) {
return [
...checkCopperToBoardEdgeClearance(circuitJson),
...checkViasInPads(circuitJson),
...checkPcbComponentsOutOfBoard(circuitJson),
...checkPcbComponentOverCutout(circuitJson),
...checkPcbCopperOverKeepout(circuitJson),
...checkPcbComponentOverlap(circuitJson),
...checkPcbComponentsMissingCourtyard(circuitJson),
...checkPadPadClearance(circuitJson),
...checkCourtyardOverlap(circuitJson),
...checkConnectorAccessibleOrientation(circuitJson),
...checkTestPointAccessibility(circuitJson)
];
}
async function runAllNetlistChecks(circuitJson) {
return [
...checkPinMustBeConnected(circuitJson),
...checkSameNameNetsAreConnected(circuitJson),
...checkTwoTerminalSwitchContactsOnDifferentNets(circuitJson)
];
}
async function runAllSchematicChecks(circuitJson) {
return [
...checkSchematicComponentExcessiveVerticalPadding(circuitJson),
...checkSchematicComponentMissingReferenceDesignatorText(circuitJson),
...checkSchematicComponentPortsOutsideBody(circuitJson)
];
}
async function runAllPinSpecificationChecks(circuitJson) {
return [
...checkAllPinsInComponentAreUnderspecified(circuitJson),
...checkNoPowerPinDefined(circuitJson),
...checkNoGroundPinDefined(circuitJson)
];
}
async function runAllRoutingChecks(circuitJson) {
return [
...checkEachPcbPortConnectedToPcbTraces(circuitJson),
...checkSourceTracesHavePcbTraces(circuitJson),
...checkPcbTraceLengths(circuitJson),
...checkPcbTraceViaCounts(circuitJson),
...checkEachPcbTraceNonOverlapping(circuitJson),
...checkPadTraceClearance(circuitJson),
...checkViaTraceClearance(circuitJson),
...checkViaPadClearance(circuitJson),
...checkSameNetViaSpacing(circuitJson),
...checkDifferentNetViaSpacing(circuitJson),
...checkTracesAreContiguous(circuitJson),
...checkPcbTracesOutOfBoard(circuitJson)
];
}
async function runAllChecks(circuitJson) {
return [
...await runAllPlacementChecks(circuitJson),
...await runAllSchematicChecks(circuitJson),
...await runAllNetlistChecks(circuitJson),
...await runAllPinSpecificationChecks(circuitJson),
...await runAllRoutingChecks(circuitJson)
];
}
export {
NetManager,
checkAllPinsInComponentAreUnderspecified,
checkConnectorAccessibleOrientation,
checkCopperToBoardEdgeClearance,
checkDifferentNetViaSpacing,
checkEachPcbPortConnectedToPcbTraces,
checkEachPcbTraceNonOverlapping,
checkNoGroundPinDefined,
checkNoPowerPinDefined,
checkPadPadClearance,
checkPadTraceClearance,
checkPcbComponentOverCutout,
checkPcbComponentOverlap,
checkPcbComponentsMissingCourtyard,
checkPcbComponentsOutOfBoard,
checkPcbCopperOverKeepout,
checkPcbTraceLengths,
checkPcbTraceViaCounts,
checkPcbTracesOutOfBoard,
checkPinMustBeConnected,
checkSameNameNetsAreConnected,
checkSameNetViaSpacing,
checkSchematicComponentExcessiveVerticalPadding,
checkSchematicComponentMissingReferenceDesignatorText,
checkSchematicComponentPortsOutsideBody,
checkSourceTracesHavePcbTraces,
checkSourceTracesMatchPcbTraceThickness,
checkTestPointAccessibility,
checkTracesAreContiguous,
checkTwoTerminalSwitchContactsOnDifferentNets,
checkViaPadClearance,
checkViaTraceClearance,
checkViasInPads,
checkViasOffBoard,
dedupePcbDrcErrors,
runAllChecks,
runAllNetlistChecks,
runAllPinSpecificationChecks,
runAllPlacementChecks,
runAllRoutingChecks,
runAllSchematicChecks
};
//# sourceMappingURL=index.js.map