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-one-layer.mjs

import fs from "node:fs"

const [traceId, layer = "bottom", outputPath = "/private/tmp/grid-routed.circuit.json"] = process.argv.slice(2)
if (!traceId) throw new Error("usage: node scripts/route-one-layer.mjs <pcb_trace_id> [layer] [output]")

const circuit = JSON.parse(fs.readFileSync("dist/index/circuit.json", "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 customStart = process.env.ROUTE_START ? JSON.parse(process.env.ROUTE_START) : null
const customEnd = process.env.ROUTE_END ? JSON.parse(process.env.ROUTE_END) : null
const start = customStart ? { route_type: "wire", x: customStart[0], y: customStart[1], width: 0.15, layer } : target.route[0]
const end = customEnd ? { route_type: "wire", x: customEnd[0], y: customEnd[1], width: 0.15, layer } : 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.1)
const step = Number(process.env.ROUTE_GRID ?? 0.1)
const endpointPorts = new Set(target.connectsTo ?? [])
const endpointEscapeRadius = Number(process.env.ROUTE_ENDPOINT_ESCAPE ?? 0.55)
const ignoreTraceObstacles = process.env.ROUTE_IGNORE_TRACES === "1"

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 obstacles = []
for (const element of circuit) {
  if (element.type === "pcb_trace") {
    if (element.connection_name === target.connection_name) continue
    if (ignoreTraceObstacles) continue
    const points = element.route
    for (let index = 1; index < points.length; index++) {
      const a = points[index - 1]
      const b = points[index]
      if (a.route_type !== "wire" || b.route_type !== "wire" || a.layer !== layer || b.layer !== layer) continue
      obstacles.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
    obstacles.push({ type: "circle", x: element.x, y: element.y,
      radius: element.outer_diameter / 2 + width / 2 + clearance })
    continue
  }
  if (element.type === "pcb_smtpad" && element.layer === 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)
    obstacles.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) obstacles.push({ type: "rect", x: element.x, y: element.y,
      width: obstacleWidth, height: obstacleHeight, rotation: element.ccw_rotation ?? 0,
      margin: width / 2 + clearance })
  }
}

const isBlocked = (x, y) => {
  if (Math.abs(x) > 9.75 || Math.abs(y) > 9.75) return true
  if (Math.hypot(x - start.x, y - start.y) <= endpointEscapeRadius ||
    Math.hypot(x - end.x, y - end.y) <= endpointEscapeRadius) return false
  for (const obstacle of obstacles) {
    if (obstacle.type === "circle" && Math.hypot(x - obstacle.x, y - obstacle.y) <= obstacle.radius) return true
    if (obstacle.type === "segment" && distanceToSegment(x, y, obstacle.a, obstacle.b) <= obstacle.radius) return true
    if (obstacle.type === "rect" && pointInInflatedRotatedRect(x, y, obstacle)) return true
  }
  return false
}

const min = -9.7
const size = Math.round((19.4 / step)) + 1
const toIndex = (coordinate) => Math.max(0, Math.min(size - 1, Math.round((coordinate - min) / step)))
const toCoordinate = (index) => min + index * step
const startNode = [toIndex(start.x), toIndex(start.y)]
const endNode = [toIndex(end.x), toIndex(end.y)]
const keyOf = (ix, iy) => iy * size + ix
const startKey = keyOf(...startNode)
const endKey = keyOf(...endNode)
const blocked = new Uint8Array(size * size)
for (let iy = 0; iy < size; iy++) for (let ix = 0; ix < size; ix++) {
  const key = keyOf(ix, iy)
  blocked[key] = isBlocked(toCoordinate(ix), toCoordinate(iy)) ? 1 : 0
}
blocked[startKey] = 0
blocked[endKey] = 0

const distance = new Float64Array(size * size)
distance.fill(Number.POSITIVE_INFINITY)
distance[startKey] = 0
const previous = new Int32Array(size * size)
previous.fill(-1)
const open = [{ key: startKey, score: 0 }]
const moves = [[1, 0], [-1, 0], [0, 1], [0, -1], [1, 1], [1, -1], [-1, 1], [-1, -1]]
while (open.length > 0) {
  let bestIndex = 0
  for (let index = 1; index < open.length; index++) if (open[index].score < open[bestIndex].score) bestIndex = index
  const [{ key }] = open.splice(bestIndex, 1)
  if (key === endKey) break
  const ix = key % size
  const iy = Math.floor(key / size)
  for (const [dx, dy] of moves) {
    const nx = ix + dx
    const ny = iy + dy
    if (nx < 0 || nx >= size || ny < 0 || ny >= size) continue
    const nextKey = keyOf(nx, ny)
    if (blocked[nextKey]) continue
    const nextDistance = distance[key] + Math.hypot(dx, dy)
    if (nextDistance >= distance[nextKey]) continue
    distance[nextKey] = nextDistance
    previous[nextKey] = key
    const heuristic = Math.hypot(nx - endNode[0], ny - endNode[1])
    open.push({ key: nextKey, score: nextDistance + heuristic })
  }
}
if (previous[endKey] === -1) throw new Error(`no ${layer} route found for ${traceId}`)

const path = []
for (let key = endKey; key !== -1; key = previous[key]) {
  path.push({ x: toCoordinate(key % size), y: toCoordinate(Math.floor(key / size)) })
  if (key === startKey) break
}
path.reverse()
const simplified = []
for (const point of path) {
  simplified.push(point)
  while (simplified.length >= 3) {
    const a = simplified[simplified.length - 3]
    const b = simplified[simplified.length - 2]
    const c = simplified[simplified.length - 1]
    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)
  }
}
target.route = [
  { route_type: "wire", x: start.x, y: start.y, width, layer },
  ...simplified.slice(1, -1).map((point) => ({ route_type: "wire", ...point, width, layer })),
  { route_type: "wire", x: end.x, y: end.y, width, layer },
]
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)
}
fs.writeFileSync(outputPath, `${JSON.stringify(circuit)}\n`)
console.log(JSON.stringify(target.route))