ShiboSoftwareDev/linux-gameboy-advance
Restores PCB assembly metadata by assigning part numbers and 3D CAD models, removing test-point bodies, and validating top-side SMT solder-paste features without altering copper or pads.
- Version
- 1.0.17
- License
- unset
- Stars
- 0
scripts/reroute-carrier-trace.mjs
import fs from "node:fs"
import {
MultilayerIjump,
getObstaclesFromCircuitJson,
markObstaclesAsConnected,
} from "@tscircuit/infgrid-ijump-astar"
import {
getFullConnectivityMapFromCircuitJson,
PcbConnectivityMap,
} from "circuit-json-to-connectivity-map"
const targetId = process.argv[2]
if (!targetId) throw new Error("Usage: node scripts/reroute-carrier-trace.mjs <pcb_trace_id> [routes.json]")
const routeFile = process.argv[3] ?? "work/optimized-carrier-traces.json"
const tailStartIndex = process.argv[4] === undefined ? null : Number(process.argv[4])
const segmentEndIndex = process.argv[5] === undefined ? null : Number(process.argv[5])
const circuit = JSON.parse(fs.readFileSync("dist/index/circuit.json", "utf8"))
const cache = JSON.parse(fs.readFileSync("carrier-route-cache.json", "utf8"))
const traces = JSON.parse(fs.readFileSync(routeFile, "utf8"))
const carrierIds = new Set(cache.traces.map((trace) => trace.pcb_trace_id))
const routeById = new Map(traces.map((trace) => [trace.pcb_trace_id, trace.route]))
const target = circuit.find((element) =>
element.type === "pcb_trace" && element.pcb_trace_id === targetId)
if (!target) throw new Error(`Missing target pcb trace ${targetId}`)
const editableTarget = traces.find((trace) => trace.pcb_trace_id === targetId)
if (!editableTarget) throw new Error(`Missing editable route ${targetId} in ${routeFile}`)
// Tail repairs intentionally seed the route file with a reviewed prefix. Use
// that prefix instead of the last generated Circuit JSON's route, which may be
// the failed candidate that prompted this repair.
if (tailStartIndex !== null) target.route = structuredClone(editableTarget.route)
// Install the frozen carrier solution into the full board so every existing
// pad, via, and routed segment becomes an obstacle to this one-trace repair.
for (const trace of circuit.filter((element) => element.type === "pcb_trace")) {
const route = routeById.get(trace.pcb_trace_id)
if (route) trace.route = structuredClone(route)
}
const synthetic = circuit.filter((element) =>
element.type !== "pcb_copper_pour" &&
!(element.type === "pcb_trace" && element.pcb_trace_id === targetId) &&
!(element.type === "pcb_via" && carrierIds.has(element.pcb_trace_id)))
let viaIndex = 0
for (const trace of circuit.filter((element) =>
element.type === "pcb_trace" && carrierIds.has(element.pcb_trace_id) &&
element.pcb_trace_id !== targetId)) {
for (const point of trace.route.filter((point) => point.route_type === "via")) {
synthetic.push({
type: "pcb_via",
pcb_via_id: `reroute_obstacle_via_${viaIndex++}`,
pcb_trace_id: trace.pcb_trace_id,
x: point.x,
y: point.y,
hole_diameter: point.via_hole_diameter ?? 0.3,
outer_diameter: point.via_diameter ?? 0.45,
layers: ["top", "inner1", "inner2", "bottom"],
from_layer: point.from_layer,
to_layer: point.to_layer,
subcircuit_id: trace.subcircuit_id,
})
}
}
const connMap = getFullConnectivityMapFromCircuitJson(synthetic)
// The iJump package's stock obstacle converter rejects diagonal copper. Build
// accurate-enough axis-aligned envelopes for routed wire segments, while its
// converter handles pads, holes, vias, and component bodies.
const obstacleSoup = synthetic.filter((element) => element.type !== "pcb_trace")
const obstacles = getObstaclesFromCircuitJson(obstacleSoup, connMap)
for (const trace of synthetic.filter((element) => element.type === "pcb_trace")) {
for (let index = 0; index < trace.route.length - 1; index++) {
const a = trace.route[index]
const b = trace.route[index + 1]
if (a.route_type !== "wire" || b.route_type !== "wire" || a.layer !== b.layer) continue
const width = Math.max(a.width ?? 0.12, b.width ?? 0.12)
obstacles.push({
type: "rect",
center: { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 },
width: Math.abs(a.x - b.x) + width,
height: Math.abs(a.y - b.y) + width,
layers: [a.layer],
connectedTo: [trace.source_trace_id, trace.pcb_trace_id].filter(Boolean),
})
}
}
let pointsToConnect = target.connectsTo.map((pcbPortId) => {
const port = circuit.find((element) =>
element.type === "pcb_port" && element.pcb_port_id === pcbPortId)
if (!port) throw new Error(`Missing endpoint ${pcbPortId}`)
return {
x: port.x,
y: port.y,
layer: port.layers?.[0] ?? "top",
pcb_port_id: port.pcb_port_id,
}
})
if (tailStartIndex !== null) {
const start = target.route[tailStartIndex]
if (!start || start.route_type !== "wire")
throw new Error(`Tail start ${tailStartIndex} must be a wire route point`)
pointsToConnect = [
{ x: start.x, y: start.y, layer: start.layer },
segmentEndIndex === null
? pointsToConnect.at(-1)
: (() => {
const end = target.route[segmentEndIndex]
if (!end || end.route_type !== "wire")
throw new Error(`Segment end ${segmentEndIndex} must be a wire route point`)
return { x: end.x, y: end.y, layer: end.layer }
})(),
]
}
markObstaclesAsConnected(obstacles, pointsToConnect, target.source_trace_id)
const input = {
layerCount: 4,
minTraceWidth: 0.12,
obstacles,
connections: [{ name: target.source_trace_id, pointsToConnect }],
bounds: { minX: -72.25, maxX: 72.25, minY: -41, maxY: 41 },
}
console.log(JSON.stringify({
targetId,
connection: input.connections[0],
obstacles: input.obstacles.length,
bounds: input.bounds,
}, null, 2))
const autorouter = new MultilayerIjump({
input,
connMap,
pcbConnMap: new PcbConnectivityMap(synthetic),
GRID_STEP: 0.05,
OBSTACLE_MARGIN: 0.22,
MAX_ITERATIONS: 2_000_000,
VIA_COST: 8,
isRemovePathLoopsEnabled: true,
isShortenPathWithShortcutsEnabled: true,
optimizeWithGoalBoxes: false,
})
autorouter.VIA_DIAMETER = 0.45
const solved = autorouter.solveAndMapToTraces()
if (solved.length !== 1) throw new Error(`Router returned ${solved.length} traces: ${autorouter.debugMessage}`)
let route = solved[0].route.map((point) => point.route_type === "via"
? { ...point, via_diameter: 0.45, via_hole_diameter: 0.3 }
: { ...point, width: 0.12 })
// Circuit JSON requires a wire vertex at the via coordinate on both sides.
for (let index = 0; index < route.length; index++) {
const point = route[index]
if (point.route_type !== "via") continue
const previous = route[index - 1]
if (!previous || previous.x !== point.x || previous.y !== point.y ||
previous.layer !== point.from_layer) {
route.splice(index, 0, {
route_type: "wire", x: point.x, y: point.y,
width: 0.12, layer: point.from_layer,
})
index++
}
const next = route[index + 1]
if (!next || next.x !== point.x || next.y !== point.y ||
next.layer !== point.to_layer) {
route.splice(index + 1, 0, {
route_type: "wire", x: point.x, y: point.y,
width: 0.12, layer: point.to_layer,
})
}
}
const replacement = editableTarget
replacement.route = tailStartIndex === null
? route
: segmentEndIndex === null
? [...target.route.slice(0, tailStartIndex), ...route]
: [
...target.route.slice(0, tailStartIndex),
...route,
...target.route.slice(segmentEndIndex + 1),
]
fs.writeFileSync(routeFile, JSON.stringify(traces, null, 2) + "\n")
console.log(`Replaced ${targetId} with ${route.length} route points (${route.filter((p) => p.route_type === "via").length} vias)`)