ShiboSoftwareDev/nema8-20mm-dual-usbc-stepper-controller
A tscircuit package created by ShiboSoftwareDev.
- Version
- 1.0.47
- License
- unset
- Stars
- 0
scripts/check-trace-cutout-clearance.mjs
import fs from "node:fs"
const circuitPath = process.argv[2] ?? "dist/index/circuit.json"
const requiredClearance = Number(process.env.CUTOUT_CLEARANCE_MM ?? 0.1)
const circuit = JSON.parse(fs.readFileSync(circuitPath, "utf8"))
const cutouts = circuit.filter((element) => element.type === "pcb_cutout")
const holes = circuit.filter((element) => element.type === "pcb_hole")
const holeClearance = Number(process.env.HOLE_CLEARANCE_MM ?? circuit
.find((element) => element.type === "pcb_board")?.min_trace_to_hole_edge_clearance ?? 0.2)
const obstacles = [...cutouts, ...holes]
const traces = circuit.filter((element) => element.type === "pcb_trace")
const pointSegmentDistance = (point, start, end) => {
const dx = end.x - start.x
const dy = end.y - start.y
const lengthSquared = dx * dx + dy * dy
const t = lengthSquared === 0 ? 0 : Math.max(0, Math.min(1,
((point.x - start.x) * dx + (point.y - start.y) * dy) / lengthSquared))
return Math.hypot(point.x - (start.x + t * dx), point.y - (start.y + t * dy))
}
const orientation = (a, b, c) =>
Math.sign((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x))
const segmentsIntersect = (a, b, c, d) => {
const o1 = orientation(a, b, c)
const o2 = orientation(a, b, d)
const o3 = orientation(c, d, a)
const o4 = orientation(c, d, b)
if (o1 !== o2 && o3 !== o4) return true
const onSegment = (p, q, r) => orientation(p, q, r) === 0 &&
q.x >= Math.min(p.x, r.x) - 1e-9 && q.x <= Math.max(p.x, r.x) + 1e-9 &&
q.y >= Math.min(p.y, r.y) - 1e-9 && q.y <= Math.max(p.y, r.y) + 1e-9
return onSegment(a, c, b) || onSegment(a, d, b) ||
onSegment(c, a, d) || onSegment(c, b, d)
}
const pointInPolygon = (point, polygon) => {
let inside = false
for (let index = 0, previous = polygon.length - 1;
index < polygon.length; previous = index++) {
const a = polygon[index]
const b = polygon[previous]
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 polygonEdges = (points) => points.map((point, index) =>
[point, points[(index + 1) % points.length]])
const segmentPolygonDistance = (start, end, polygon) => {
if (pointInPolygon(start, polygon) || pointInPolygon(end, polygon)) return 0
const edges = polygonEdges(polygon)
if (edges.some(([a, b]) => segmentsIntersect(start, end, a, b))) return 0
return Math.min(...edges.flatMap(([a, b]) => [
pointSegmentDistance(start, a, b),
pointSegmentDistance(end, a, b),
pointSegmentDistance(a, start, end),
pointSegmentDistance(b, start, end),
]))
}
const pointCutoutDistance = (point, cutout) => {
if (cutout.shape === "circle") {
return Math.max(0, Math.hypot(point.x - cutout.center.x,
point.y - cutout.center.y) - cutout.radius)
}
if (cutout.shape === "rect") {
const angle = -((cutout.rotation ?? 0) * Math.PI) / 180
const dx = point.x - cutout.center.x
const dy = point.y - cutout.center.y
const x = dx * Math.cos(angle) - dy * Math.sin(angle)
const y = dx * Math.sin(angle) + dy * Math.cos(angle)
return Math.hypot(Math.max(Math.abs(x) - cutout.width / 2, 0),
Math.max(Math.abs(y) - cutout.height / 2, 0))
}
if (cutout.shape === "polygon") {
if (pointInPolygon(point, cutout.points)) return 0
return Math.min(...polygonEdges(cutout.points)
.map(([a, b]) => pointSegmentDistance(point, a, b)))
}
return Infinity
}
const segmentCutoutDistance = (start, end, cutout) => {
if (cutout.shape === "polygon") return segmentPolygonDistance(start, end, cutout.points)
const distance = Math.hypot(end.x - start.x, end.y - start.y)
const samples = Math.max(2, Math.ceil(distance / 0.02))
let minimum = Infinity
for (let index = 0; index <= samples; index++) {
const t = index / samples
minimum = Math.min(minimum, pointCutoutDistance({
x: start.x + (end.x - start.x) * t,
y: start.y + (end.y - start.y) * t,
}, cutout))
}
return minimum
}
const holeCenterline = (hole) => {
if (hole.hole_shape === "circle") {
return { start: hole, end: hole, radius: hole.hole_diameter / 2 }
}
if (!["pill", "rotated_pill"].includes(hole.hole_shape)) {
throw new Error(`Unsupported NPTH shape: ${hole.hole_shape}`)
}
const angle = ((hole.ccw_rotation ?? 0) * Math.PI) / 180 +
(hole.hole_height > hole.hole_width ? Math.PI / 2 : 0)
const halfLength = Math.abs(hole.hole_width - hole.hole_height) / 2
const dx = halfLength * Math.cos(angle)
const dy = halfLength * Math.sin(angle)
return {
start: { x: hole.x - dx, y: hole.y - dy },
end: { x: hole.x + dx, y: hole.y + dy },
radius: Math.min(hole.hole_width, hole.hole_height) / 2,
}
}
const pointObstacleDistance = (point, obstacle) => {
if (obstacle.type !== "pcb_hole") return pointCutoutDistance(point, obstacle)
const { start, end, radius } = holeCenterline(obstacle)
return Math.max(0, pointSegmentDistance(point, start, end) - radius)
}
const segmentObstacleDistance = ({ start, end }, obstacle) => {
if (obstacle.type !== "pcb_hole") return segmentCutoutDistance(start, end, obstacle)
const hole = holeCenterline(obstacle)
if (segmentsIntersect(start, end, hole.start, hole.end)) return 0
return Math.max(0, Math.min(
pointSegmentDistance(start, hole.start, hole.end),
pointSegmentDistance(end, hole.start, hole.end),
pointSegmentDistance(hole.start, start, end),
pointSegmentDistance(hole.end, start, end),
) - hole.radius)
}
const clearanceFor = (obstacle) => obstacle.type === "pcb_hole" ? holeClearance : requiredClearance
const obstacleId = (obstacle) => obstacle.pcb_hole_id ?? obstacle.pcb_cutout_id
const violations = []
for (const trace of traces) {
let previous
for (const point of trace.route ?? []) {
if (point.route_type === "via") {
const radius = (point.via_diameter ?? 0.45) / 2
for (const obstacle of obstacles) {
const distance = pointObstacleDistance(point, obstacle)
const clearance = clearanceFor(obstacle)
if (distance + 1e-9 < radius + clearance) {
violations.push({ trace: trace.pcb_trace_id, kind: "via", distance,
obstacle: obstacleId(obstacle), required: radius + clearance,
point: { x: point.x, y: point.y } })
}
}
previous = undefined
continue
}
if (point.route_type !== "wire") continue
if (previous?.layer === point.layer) {
const radius = Math.max(previous.width ?? 0.15, point.width ?? 0.15) / 2
for (const obstacle of obstacles) {
const distance = segmentObstacleDistance({ start: previous, end: point }, obstacle)
const clearance = clearanceFor(obstacle)
if (distance + 1e-9 < radius + clearance) {
violations.push({ trace: trace.pcb_trace_id, kind: "wire", layer: point.layer,
obstacle: obstacleId(obstacle), distance, required: radius + clearance,
start: { x: previous.x, y: previous.y }, end: { x: point.x, y: point.y } })
}
}
}
previous = point
}
}
if (violations.length) {
console.error(`Trace/hole/cutout clearance check failed: ${violations.length} violation(s)`)
for (const violation of violations) console.error(JSON.stringify(violation))
process.exit(1)
}
console.log(`Trace/hole/cutout clearance check passed: ${traces.length} traces, ${holes.length} holes, ${cutouts.length} cutouts; NPTH clearance ${holeClearance.toFixed(2)} mm`)