ShiboSoftwareDev/nema8-20mm-usbc-pd-controller
A set of PCB components including Schottky/TVS diodes, low-value resistor, voltage regulator, MCU, stepper-driver IC, USB-C connector, and USB ESD-protection device with defined SMT footprints and 3D models.
- Version
- 1.0.6
- License
- unset
- Stars
- 0
scripts/route-through-vias.mjs
import fs from "node:fs"
const [traceId, inputPath = "dist/index/circuit.json", outputPath = "/private/tmp/through-routed.circuit.json"] = process.argv.slice(2)
if (!traceId) throw new Error("usage: node scripts/route-through-vias.mjs <pcb_trace_id> [input] [output]")
const circuit = JSON.parse(fs.readFileSync(inputPath, "utf8"))
const target = circuit.find((element) => element.type === "pcb_trace" && element.pcb_trace_id === traceId)
if (!target) throw new Error(`trace not found: ${traceId}`)
const start = target.route[0]
const end = target.route[target.route.length - 1]
const width = Number(process.env.ROUTE_WIDTH ?? start.width ?? 0.15)
const clearance = Number(process.env.ROUTE_CLEARANCE ?? 0.08)
const gridStep = Number(process.env.ROUTE_GRID ?? 0.15)
const viaCost = Number(process.env.ROUTE_VIA_COST ?? 8)
const endpointEscapeRadius = Number(process.env.ROUTE_ENDPOINT_ESCAPE ?? 0.45)
const endpointPorts = new Set(target.connectsTo ?? [])
const board = circuit.find((element) => element.type === "pcb_board")
const innerCount = Math.max(0, (board?.num_layers ?? 2) - 2)
const layers = ["top", ...Array.from({ length: innerCount }, (_, index) => `inner${index + 1}`), "bottom"]
const distanceToSegment = (x, y, a, b) => {
const vx = b.x - a.x
const vy = b.y - a.y
const denominator = vx * vx + vy * vy
const t = denominator === 0 ? 0 : Math.max(0, Math.min(1, ((x - a.x) * vx + (y - a.y) * vy) / denominator))
return Math.hypot(x - (a.x + t * vx), y - (a.y + t * vy))
}
const pointInInflatedRotatedRect = (x, y, obstacle) => {
const angle = -(obstacle.rotation ?? 0) * Math.PI / 180
const dx = x - obstacle.x
const dy = y - obstacle.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) <= obstacle.width / 2 + obstacle.margin &&
Math.abs(localY) <= obstacle.height / 2 + obstacle.margin
}
const obstaclesByLayer = Object.fromEntries(layers.map((layer) => [layer, []]))
for (const element of circuit) {
if (element.type === "pcb_trace") {
if (element.connection_name === target.connection_name) continue
for (let index = 1; index < element.route.length; index++) {
const a = element.route[index - 1]
const b = element.route[index]
if (a.route_type !== "wire" || b.route_type !== "wire" || a.layer !== b.layer || !obstaclesByLayer[a.layer]) continue
obstaclesByLayer[a.layer].push({ type: "segment", a, b,
radius: (a.width ?? 0.15) / 2 + width / 2 + clearance })
}
continue
}
if (element.type === "pcb_via") {
if (element.pcb_trace_id === traceId) continue
for (const layer of layers) obstaclesByLayer[layer].push({ type: "circle", x: element.x, y: element.y,
radius: element.outer_diameter / 2 + width / 2 + clearance })
continue
}
if (element.type === "pcb_smtpad" && obstaclesByLayer[element.layer]) {
if (endpointPorts.has(element.pcb_port_id)) continue
const padWidth = element.width ?? (element.radius ? element.radius * 2 : 0.3)
const padHeight = element.height ?? (element.radius ? element.radius * 2 : padWidth)
obstaclesByLayer[element.layer].push({ type: "rect", x: element.x, y: element.y,
width: padWidth, height: padHeight, rotation: element.ccw_rotation ?? 0,
margin: width / 2 + clearance })
continue
}
if (element.type === "pcb_plated_hole" || element.type === "pcb_hole") {
const obstacleWidth = element.outer_width ?? element.outer_diameter ?? element.hole_width ?? element.hole_diameter
const obstacleHeight = element.outer_height ?? element.outer_diameter ?? element.hole_height ?? element.hole_diameter
if (!obstacleWidth || !obstacleHeight) continue
for (const layer of layers) obstaclesByLayer[layer].push({ type: "rect", x: element.x, y: element.y,
width: obstacleWidth, height: obstacleHeight, rotation: element.ccw_rotation ?? 0,
margin: width / 2 + clearance })
}
}
const isBlockedBy = (x, y, obstacle) =>
obstacle.type === "circle" ? Math.hypot(x - obstacle.x, y - obstacle.y) <= obstacle.radius
: obstacle.type === "segment" ? distanceToSegment(x, y, obstacle.a, obstacle.b) <= obstacle.radius
: pointInInflatedRotatedRect(x, y, obstacle)
const isBlocked = (x, y, layer) => {
if (Math.abs(x) > 9.7 || Math.abs(y) > 9.7) return true
if (layer === start.layer && Math.hypot(x - start.x, y - start.y) <= endpointEscapeRadius) return false
if (layer === end.layer && Math.hypot(x - end.x, y - end.y) <= endpointEscapeRadius) return false
return obstaclesByLayer[layer].some((obstacle) => isBlockedBy(x, y, obstacle))
}
const minimum = -9.6
const size = Math.round(19.2 / gridStep) + 1
const area = size * size
const toGrid = (coordinate) => Math.max(0, Math.min(size - 1, Math.round((coordinate - minimum) / gridStep)))
const toCoordinate = (index) => minimum + index * gridStep
const keyOf = (layerIndex, xIndex, yIndex) => layerIndex * area + yIndex * size + xIndex
const decode = (key) => {
const layerIndex = Math.floor(key / area)
const planar = key % area
return { layerIndex, xIndex: planar % size, yIndex: Math.floor(planar / size) }
}
const blocked = new Uint8Array(area * layers.length)
for (let layerIndex = 0; layerIndex < layers.length; layerIndex++) {
for (let yIndex = 0; yIndex < size; yIndex++) for (let xIndex = 0; xIndex < size; xIndex++) {
blocked[keyOf(layerIndex, xIndex, yIndex)] = isBlocked(
toCoordinate(xIndex), toCoordinate(yIndex), layers[layerIndex],
) ? 1 : 0
}
}
const startLayerIndex = layers.indexOf(start.layer)
const endLayerIndex = layers.indexOf(end.layer)
const startKey = keyOf(startLayerIndex, toGrid(start.x), toGrid(start.y))
const endKey = keyOf(endLayerIndex, toGrid(end.x), toGrid(end.y))
blocked[startKey] = 0
blocked[endKey] = 0
class MinHeap {
values = []
push(value) {
this.values.push(value)
for (let index = this.values.length - 1; index > 0;) {
const parent = Math.floor((index - 1) / 2)
if (this.values[parent].score <= value.score) break
this.values[index] = this.values[parent]
index = parent
this.values[index] = value
}
}
pop() {
if (this.values.length === 1) return this.values.pop()
const first = this.values[0]
const last = this.values.pop()
this.values[0] = last
for (let index = 0;;) {
const left = index * 2 + 1
const right = left + 1
if (left >= this.values.length) break
let child = right < this.values.length && this.values[right].score < this.values[left].score ? right : left
if (this.values[child].score >= this.values[index].score) break
;[this.values[index], this.values[child]] = [this.values[child], this.values[index]]
index = child
}
return first
}
}
const distances = new Float64Array(blocked.length)
distances.fill(Number.POSITIVE_INFINITY)
distances[startKey] = 0
const previous = new Int32Array(blocked.length)
previous.fill(-1)
const heap = new MinHeap()
heap.push({ key: startKey, score: 0 })
const planarMoves = [[1, 0], [-1, 0], [0, 1], [0, -1], [1, 1], [1, -1], [-1, 1], [-1, -1]]
while (heap.values.length > 0) {
const current = heap.pop()
const { key } = current
if (key === endKey) break
const { layerIndex, xIndex, yIndex } = decode(key)
const currentDistance = distances[key]
for (const [dx, dy] of planarMoves) {
const nextX = xIndex + dx
const nextY = yIndex + dy
if (nextX < 0 || nextX >= size || nextY < 0 || nextY >= size) continue
if (dx !== 0 && dy !== 0 && (blocked[keyOf(layerIndex, xIndex + dx, yIndex)] || blocked[keyOf(layerIndex, xIndex, yIndex + dy)])) continue
const nextKey = keyOf(layerIndex, nextX, nextY)
if (blocked[nextKey]) continue
const nextDistance = currentDistance + Math.hypot(dx, dy)
if (nextDistance >= distances[nextKey]) continue
distances[nextKey] = nextDistance
previous[nextKey] = key
const heuristic = Math.hypot(nextX - toGrid(end.x), nextY - toGrid(end.y)) +
(layerIndex === endLayerIndex ? 0 : viaCost)
heap.push({ key: nextKey, score: nextDistance + heuristic })
}
const x = toCoordinate(xIndex)
const y = toCoordinate(yIndex)
let viaIsClear = Math.hypot(x - start.x, y - start.y) > endpointEscapeRadius + 0.35 &&
Math.hypot(x - end.x, y - end.y) > endpointEscapeRadius + 0.35
const viaInflationCells = Math.ceil(0.18 / gridStep)
for (let candidateLayer = 0; candidateLayer < layers.length && viaIsClear; candidateLayer++) {
for (let deltaY = -viaInflationCells; deltaY <= viaInflationCells && viaIsClear; deltaY++) {
for (let deltaX = -viaInflationCells; deltaX <= viaInflationCells; deltaX++) {
if (Math.hypot(deltaX, deltaY) * gridStep > 0.2) continue
const checkX = xIndex + deltaX
const checkY = yIndex + deltaY
if (checkX < 0 || checkX >= size || checkY < 0 || checkY >= size ||
blocked[keyOf(candidateLayer, checkX, checkY)]) { viaIsClear = false; break }
}
}
}
if (!viaIsClear) continue
for (let nextLayer = 0; nextLayer < layers.length; nextLayer++) {
if (nextLayer === layerIndex) continue
const nextKey = keyOf(nextLayer, xIndex, yIndex)
const nextDistance = currentDistance + viaCost
if (nextDistance >= distances[nextKey]) continue
distances[nextKey] = nextDistance
previous[nextKey] = key
const heuristic = Math.hypot(xIndex - toGrid(end.x), yIndex - toGrid(end.y)) +
(nextLayer === endLayerIndex ? 0 : viaCost)
heap.push({ key: nextKey, score: nextDistance + heuristic })
}
}
if (previous[endKey] === -1) throw new Error(`no through-via route found for ${traceId}`)
const nodes = []
for (let key = endKey; key !== -1; key = previous[key]) {
const node = decode(key)
nodes.push({ x: toCoordinate(node.xIndex), y: toCoordinate(node.yIndex), layer: layers[node.layerIndex] })
if (key === startKey) break
}
nodes.reverse()
const simplified = []
for (const node of nodes) {
simplified.push(node)
while (simplified.length >= 3) {
const a = simplified[simplified.length - 3]
const b = simplified[simplified.length - 2]
const c = simplified[simplified.length - 1]
if (a.layer !== b.layer || b.layer !== c.layer) break
if (Math.abs((b.x - a.x) * (c.y - b.y) - (b.y - a.y) * (c.x - b.x)) > 1e-9) break
simplified.splice(simplified.length - 2, 1)
}
}
const route = [{ route_type: "wire", x: start.x, y: start.y, width, layer: start.layer }]
for (let index = 1; index < simplified.length; index++) {
const previousNode = simplified[index - 1]
const node = simplified[index]
if (previousNode.layer !== node.layer) {
route.push({ route_type: "via", x: node.x, y: node.y, from_layer: previousNode.layer,
to_layer: node.layer, via_diameter: 0.45, via_hole_diameter: 0.3 })
}
route.push({ route_type: "wire", x: node.x, y: node.y, width, layer: node.layer })
}
route.push({ route_type: "wire", x: end.x, y: end.y, width, layer: end.layer })
target.route = route
for (let index = circuit.length - 1; index >= 0; index--) {
if (circuit[index].type === "pcb_via" && circuit[index].pcb_trace_id === traceId) circuit.splice(index, 1)
}
let viaIndex = 20000
for (const point of route) if (point.route_type === "via") circuit.push({
type: "pcb_via", pcb_via_id: `pcb_via_grid_${viaIndex++}`, pcb_trace_id: traceId,
x: point.x, y: point.y, hole_diameter: 0.3, outer_diameter: 0.45,
layers, from_layer: "top", to_layer: "bottom", subcircuit_id: "subcircuit_source_group_0",
})
fs.writeFileSync(outputPath, `${JSON.stringify(circuit)}\n`)
console.log(JSON.stringify(route))