rushabhcodes/ESP32-E-Reader
A compact ESP32-C3 e-reader PCB integrating LiPo charging/protection, USB-C, 3.3 V regulation, e-paper display bias generation, microSD storage, and tactile controls.
- Version
- 1.0.85
- License
- unset
- Stars
- 3
scripts/check-pcb-mounts.mjs
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import mounts from "../assets/enclosure/pcb-mounts.json";
const near = (a, b) => Math.abs(a - b) < 1e-6;
const distanceToSegment = (point, a, b) => {
const dx = b.x - a.x;
const dy = b.y - a.y;
const lengthSquared = dx * dx + dy * dy;
const t = lengthSquared
? Math.max(
0,
Math.min(
1,
((point.x - a.x) * dx + (point.y - a.y) * dy) / lengthSquared,
),
)
: 0;
return Math.hypot(point.x - a.x - t * dx, point.y - a.y - t * dy);
};
const insideRing = (point, vertices) => {
let inside = false;
for (let i = 0, j = vertices.length - 1; i < vertices.length; j = i++) {
const a = vertices[i],
b = vertices[j];
if (
a.y > point.y !== b.y > point.y &&
point.x < ((b.x - a.x) * (point.y - a.y)) / (b.y - a.y) + a.x
) {
inside = !inside;
}
}
return inside;
};
const ringDistance = (point, vertices) =>
Math.min(
...vertices.map((a, i) =>
distanceToSegment(point, a, vertices[(i + 1) % vertices.length]),
),
);
const distanceToPour = (point, pour) => {
assert.equal(pour.shape, "brep", "Unsupported copper pour geometry");
const { outer_ring, inner_rings } = pour.brep_shape;
if (!insideRing(point, outer_ring.vertices))
return ringDistance(point, outer_ring.vertices);
const containingHoles = inner_rings.filter((ring) =>
insideRing(point, ring.vertices),
);
return containingHoles.length
? Math.min(
...containingHoles.map((ring) => ringDistance(point, ring.vertices)),
)
: 0;
};
export function checkPcbMounts(circuit) {
assert(
circuit.some((e) => e.type === "pcb_trace"),
"Missing routed PCB copper",
);
const board = circuit.find((e) => e.type === "pcb_board");
assert(board?.outline, "Missing PCB outline");
assert.equal(mounts.mounts.length, 4, "Both ends require two screw points");
const pours = circuit.filter((e) => e.type === "pcb_copper_pour");
if (pours.length)
assert.deepEqual([...new Set(pours.map((e) => e.layer))].sort(), [
"bottom",
"inner1",
"inner2",
"top",
]);
const report = [];
for (const mount of mounts.mounts) {
const holes = circuit.filter(
(e) => e.type === "pcb_hole" && near(e.x, mount.x) && near(e.y, mount.y),
);
assert.equal(holes.length, 1, `Missing or duplicate ${mount.name} hole`);
assert(
near(holes[0].hole_diameter, mounts.holeDiameter),
`Wrong ${mount.name} diameter`,
);
const keepout = circuit.find(
(e) =>
e.type === "pcb_keepout" &&
near(e.center.x, mount.x) &&
near(e.center.y, mount.y),
);
assert(
keepout?.shape === "circle" &&
near(keepout.radius, mounts.copperKeepoutRadius),
`Missing ${mount.name} screw keepout`,
);
assert.deepEqual([...keepout.layers].sort(), [
"bottom",
"inner1",
"inner2",
"top",
]);
assert(insideRing(mount, board.outline), `${mount.name} lies outside PCB`);
const outlineDistance = Math.min(
...board.outline.map((a, i) =>
distanceToSegment(
mount,
a,
board.outline[(i + 1) % board.outline.length],
),
),
);
assert(
outlineDistance >=
mounts.holeDiameter / 2 + board.min_board_edge_clearance,
`${mount.name} drill too close to edge`,
);
let traceDistance = Infinity;
let padDistance = Infinity;
for (const element of circuit) {
if (element.type === "pcb_trace") {
for (let i = 1; i < element.route.length; i++) {
const a = element.route[i - 1],
b = element.route[i];
traceDistance = Math.min(
traceDistance,
distanceToSegment(mount, a, b) -
Math.max(a.width ?? 0, b.width ?? 0) / 2,
);
}
} else if (
element.type === "pcb_via" ||
element.type === "pcb_plated_hole"
) {
const diameter =
element.outer_diameter ??
Math.hypot(element.outer_width ?? 0, element.outer_height ?? 0);
padDistance = Math.min(
padDistance,
Math.hypot(mount.x - element.x, mount.y - element.y) - diameter / 2,
);
} else if (element.type === "pcb_smtpad") {
let distance;
if (element.x !== undefined) {
// A circumscribed circle also covers rotated rectangular pads.
const radius =
element.radius ??
Math.hypot(element.width ?? 0, element.height ?? 0) / 2;
distance =
Math.hypot(mount.x - element.x, mount.y - element.y) - radius;
} else {
const xs = element.points.map((p) => p.x),
ys = element.points.map((p) => p.y);
distance = Math.hypot(
Math.max(Math.min(...xs) - mount.x, 0, mount.x - Math.max(...xs)),
Math.max(Math.min(...ys) - mount.y, 0, mount.y - Math.max(...ys)),
);
}
padDistance = Math.min(padDistance, distance);
}
}
assert(
traceDistance >= keepout.radius,
`${mount.name} overlaps routed copper`,
);
assert(
padDistance >= keepout.radius,
`${mount.name} overlaps pad/via envelope`,
);
const pourDistance = pours.length
? Math.min(...pours.map((pour) => distanceToPour(mount, pour)))
: null;
// The exported circular keepout uses a 32-sided polygon; its apothem
// is slightly smaller than the specified radius.
if (pourDistance !== null)
assert(
pourDistance >= keepout.radius * Math.cos(Math.PI / 32) - 1e-5,
`${mount.name} overlaps copper pour keepout`,
);
report.push({
name: mount.name,
holeEdgeToBoardEdge: outlineDistance - mounts.holeDiameter / 2,
traceClearanceFromKeepout: traceDistance - keepout.radius,
conservativePadClearanceFromKeepout: padDistance - keepout.radius,
copperPourClearanceFromHole:
pourDistance === null ? null : pourDistance - mounts.holeDiameter / 2,
});
}
return report;
}
if (import.meta.main) {
const circuit = JSON.parse(
await readFile(process.argv[2] ?? "dist/index/circuit.json", "utf8"),
);
console.log(JSON.stringify(checkPcbMounts(circuit), null, 2));
}