astra/f1c100s
The code defines the physical pin layout, labels, and footprint for the F1C100S system-on-chip (SoC) component used in electronic devices.
- Version
- 0.9.2
- License
- unset
- Stars
- 0
scripts/grid-router.ts
import type { SimpleRouteJson, SimplifiedPcbTrace } from "tscircuit";
import { MODULE_SIZE, TERMINAL_EDGE } from "../src/profiles";
type P = { x: number; y: number; layer: string };
const LAYERS = ["top", "inner1", "inner2", "bottom"];
class Heap {
ids: number[] = [];
costs: number[] = [];
push(id: number, cost: number) {
let i = this.ids.length;
this.ids.push(id);
this.costs.push(cost);
while (i) {
const p = (i - 1) >> 1;
if (this.costs[p]! <= cost) break;
this.ids[i] = this.ids[p]!;
this.costs[i] = this.costs[p]!;
i = p;
}
this.ids[i] = id;
this.costs[i] = cost;
}
pop() {
const id = this.ids[0]!,
v = this.ids.pop()!,
c = this.costs.pop()!;
if (this.ids.length) {
let i = 0;
while (i * 2 + 1 < this.ids.length) {
let q = i * 2 + 1;
if (q + 1 < this.ids.length && this.costs[q + 1]! < this.costs[q]!) q++;
if (this.costs[q]! >= c) break;
this.ids[i] = this.ids[q]!;
this.costs[i] = this.costs[q]!;
i = q;
}
this.ids[i] = v;
this.costs[i] = c;
}
return id;
}
}
/** Offline grid search. Through-via keepouts always span all four layers.
* Rasterization includes a small additional clearance margin.
* Every result is subsequently checked against exact Circuit JSON geometry.
*/
export function routeGrid(
input: SimpleRouteJson,
options: { first?: string[]; onProgress?: (s: string) => void } = {},
) {
const step = 0.05,
half = MODULE_SIZE / 2 - 0.45,
W = Math.round((half * 2) / step) + 1,
N = W * W,
total = N * 4;
const width = 0.12,
clearance = 0.1,
viaRadius = 0.225,
extra = 0.008;
const wire = new Int16Array(total),
via = new Int16Array(total);
const holeBlocked = new Int16Array(N),
viaCenters = new Int16Array(N);
const toGrid = (x: number) => Math.round((x + half) / step);
const pos = (i: number) => ({
x: (i % W) * step - half,
y: Math.floor((i % N) / W) * step - half,
layer: LAYERS[Math.floor(i / N)]!,
});
const index = (p: P) =>
LAYERS.indexOf(p.layer) * N + toGrid(p.y) * W + toGrid(p.x);
const stamp = (
map: Int16Array,
layer: number,
x: number,
y: number,
hx: number,
hy: number,
r: number,
owner: number,
) => {
const x0 = Math.max(0, Math.floor((x - hx - r + half) / step)),
x1 = Math.min(W - 1, Math.ceil((x + hx + r + half) / step));
const y0 = Math.max(0, Math.floor((y - hy - r + half) / step)),
y1 = Math.min(W - 1, Math.ceil((y + hy + r + half) / step));
for (let iy = y0; iy <= y1; iy++)
for (let ix = x0; ix <= x1; ix++) {
const dx = Math.max(0, Math.abs(ix * step - half - x) - hx),
dy = Math.max(0, Math.abs(iy * step - half - y) - hy);
if (dx * dx + dy * dy > r * r) continue;
const id = layer * N + iy * W + ix,
old = map[id]!;
map[id] = old === 0 || old === owner ? owner : -1;
}
};
const owners = new Map<string, number>();
input.connections.forEach((c, i) => {
for (const key of [
c.name,
c.source_trace_id,
...c.pointsToConnect.flatMap((p) => [p.pcb_port_id, p.pointId]),
])
if (key) owners.set(key, i + 1);
});
for (const o of input.obstacles) {
const owner =
o.connectedTo.map((k) => owners.get(k)).find((x) => x !== undefined) ??
-1;
// Current package consists only of axis-aligned pad rectangles.
const rot = (o as any).ccwRotationDegrees ?? 0;
if (Math.abs(rot % 90) > 1e-6)
throw new Error("Grid generator requires axis-aligned pads");
const swap = Math.abs(Math.round(rot / 90)) % 2 === 1;
const hx = (swap ? o.height : o.width) / 2,
hy = (swap ? o.width : o.height) / 2;
for (const l of o.layers) {
const z = LAYERS.indexOf(l);
if (z < 0) throw new Error(`Unexpected layer ${l}`);
stamp(
wire,
z,
o.center.x,
o.center.y,
hx,
hy,
width / 2 + clearance + extra,
owner,
);
// Even same-net pads forbid via-in-pad for ordinary fabrication.
stamp(
via,
z,
o.center.x,
o.center.y,
hx,
hy,
viaRadius + clearance + extra,
-1,
);
}
}
// Every exposed top-side terminal needs an unobstructed outward connection
// to the carrier. Reserve its exit corridor before routing internal nets.
for (const [ci, c] of input.connections.entries()) {
for (const p of c.pointsToConnect) {
if (
p.layer !== "top" ||
Math.max(Math.abs(p.x), Math.abs(p.y)) < TERMINAL_EDGE - 1e-4
)
continue;
const vertical = Math.abs(p.y) > Math.abs(p.x);
const outward = vertical
? { x: p.x, y: Math.sign(p.y) * half }
: { x: Math.sign(p.x) * half, y: p.y };
const inward = vertical
? { x: p.x, y: p.y - Math.sign(p.y) * 0.25 }
: { x: p.x - Math.sign(p.x) * 0.25, y: p.y };
const x = (inward.x + outward.x) / 2,
y = (inward.y + outward.y) / 2;
const hx = Math.abs(inward.x - outward.x) / 2,
hy = Math.abs(inward.y - outward.y) / 2;
stamp(wire, 0, x, y, hx, hy, width + clearance + extra, ci + 1);
stamp(
via,
0,
x,
y,
hx,
hy,
width / 2 + viaRadius + clearance + extra,
ci + 1,
);
}
}
const scores = new Float32Array(total),
parents = new Int32Array(total),
seen = new Int32Array(total),
closed = new Int32Array(total);
let serial = 0;
const traces: SimplifiedPcbTrace[] = [];
const free = (map: Int16Array, id: number, owner: number) =>
map[id] === 0 || map[id] === owner;
const canVia = (q: number, owner: number) =>
(holeBlocked[q] === 0 || viaCenters[q] === owner) &&
[0, 1, 2, 3].every((l) => free(via, l * N + q, owner));
function solve(a: P, b: P, owner: number, allowVias = true) {
const start = index(a),
end = index(b),
endZ = Math.floor(end / N),
endXY = end % N,
ex = endXY % W,
ey = Math.floor(endXY / W);
if (!free(wire, start, owner) || !free(wire, end, owner))
throw new Error(
`Blocked terminal for net ${owner}: ${JSON.stringify({ a, b, aw: wire[start], bw: wire[end] })}`,
);
const h = (id: number) => {
const q = id % N;
return (
Math.abs((q % W) - ex) +
Math.abs(Math.floor(q / W) - ey) +
(Math.floor(id / N) === endZ ? 0 : 45)
);
};
const heap = new Heap();
serial++;
seen[start] = serial;
scores[start] = 0;
parents[start] = -1;
heap.push(start, h(start));
let expanded = 0;
while (heap.ids.length) {
const id = heap.pop();
if (closed[id] === serial) continue;
closed[id] = serial;
if (id === end) {
const result: number[] = [];
let p = id;
while (p !== -1) {
result.push(p);
p = parents[p]!;
}
return result.reverse();
}
if (++expanded > total) throw new Error("Search exhausted");
const z = Math.floor(id / N),
q = id % N,
x = q % W,
y = Math.floor(q / W),
g = scores[id]!;
const add = (next: number, cost: number) => {
if (closed[next] === serial || !free(wire, next, owner)) return;
const ng = g + cost;
if (seen[next] === serial && scores[next]! <= ng) return;
seen[next] = serial;
scores[next] = ng;
parents[next] = id;
heap.push(next, ng + h(next));
};
if (x > 0) add(id - 1, 1);
if (x + 1 < W) add(id + 1, 1);
if (y > 0) add(id - W, 1);
if (y + 1 < W) add(id + W, 1);
if (allowVias && canVia(q, owner)) {
let prev = id,
nearVia = false;
for (let k = 0; k < 15; k++) {
const parent = parents[prev]!;
if (parent < 0) break;
if (Math.floor(parent / N) !== Math.floor(prev / N)) {
const v = prev % N,
dx = ((v % W) - x) * step,
dy = (Math.floor(v / W) - y) * step;
if (dx * dx + dy * dy < 0.31 ** 2 && v !== q) nearVia = true;
}
prev = parent;
}
if (!nearVia)
for (let l = 0; l < 4; l++) if (l !== z) add(l * N + q, 45);
}
}
throw new Error(`No route ${JSON.stringify({ a, b, owner, expanded })}`);
}
// Pull same-layer paths taut using clear straight/45-degree shortcuts.
// Via positions and endpoints are fixed. Check all cells touched by a
// shortcut against the clearance-inflated occupancy before committing it.
function simplify(ids: number[], owner: number): number[] {
const clearLine = (a: number, b: number) => {
const z = Math.floor(a / N);
if (Math.floor(b / N) !== z) return false;
const ax = a % W,
ay = Math.floor((a % N) / W);
const dx = (b % W) - ax,
dy = Math.floor((b % N) / W) - ay;
if (dx && dy && Math.abs(dx) !== Math.abs(dy)) return false;
const samples = Math.max(Math.abs(dx), Math.abs(dy)) * 2;
for (let k = 0; k <= samples; k++) {
const x = ax + (dx * k) / (samples || 1),
y = ay + (dy * k) / (samples || 1);
for (const gx of [Math.floor(x), Math.ceil(x)])
for (const gy of [Math.floor(y), Math.ceil(y)])
if (!free(wire, z * N + gy * W + gx, owner)) return false;
}
return true;
};
const result: number[] = [];
let i = 0;
while (i < ids.length) {
result.push(ids[i]!);
let end = i;
while (
end + 1 < ids.length &&
Math.floor(ids[end + 1]! / N) === Math.floor(ids[i]! / N)
)
end++;
let next = i + 1;
for (let j = end; j > i + 1; j--) {
if (clearLine(ids[i]!, ids[j]!)) {
next = j;
break;
}
}
i = next;
}
return result;
}
function record(
ids: number[],
a: P,
b: P,
owner: number,
connection: SimpleRouteJson["connections"][number],
branch: number,
waypoints?: P[],
) {
const points: P[] = [a, ...(waypoints ?? simplify(ids, owner).map(pos)), b];
const compact: P[] = [];
for (const p of points) {
const last = compact.at(-1);
if (
last &&
Math.hypot(p.x - last.x, p.y - last.y) < 1e-9 &&
last.layer === p.layer
)
continue;
const prev = compact.at(-2);
if (
last &&
prev &&
prev.layer === last.layer &&
last.layer === p.layer &&
Math.abs(
(last.x - prev.x) * (p.y - last.y) -
(last.y - prev.y) * (p.x - last.x),
) < 1e-10
)
compact.pop();
compact.push(p);
}
const route: SimplifiedPcbTrace["route"] = [];
for (let i = 0; i < compact.length; i++) {
const p = compact[i]!,
prev = compact[i - 1];
if (prev && prev.layer !== p.layer) {
route.push({
route_type: "via",
x: p.x,
y: p.y,
from_layer: prev.layer,
to_layer: p.layer,
layers: LAYERS,
via_diameter: viaRadius * 2,
via_hole_diameter: 0.2,
});
for (let l = 0; l < 4; l++) {
stamp(
wire,
l,
p.x,
p.y,
0,
0,
viaRadius + width / 2 + clearance + extra,
owner,
);
stamp(
via,
l,
p.x,
p.y,
0,
0,
2 * viaRadius + clearance + extra,
owner,
);
}
stamp(holeBlocked, 0, p.x, p.y, 0, 0, 0.31, -1);
viaCenters[index(p) % N] = owner;
}
route.push({ route_type: "wire", ...p, width });
if (prev && prev.layer === p.layer) {
const length = Math.hypot(p.x - prev.x, p.y - prev.y),
samples = Math.max(1, Math.ceil(length / (step / 2)));
for (let j = 0; j <= samples; j++) {
const x = prev.x + ((p.x - prev.x) * j) / samples,
y = prev.y + ((p.y - prev.y) * j) / samples,
l = LAYERS.indexOf(p.layer);
stamp(wire, l, x, y, 0, 0, width + clearance + extra, owner);
stamp(
via,
l,
x,
y,
0,
0,
width / 2 + viaRadius + clearance + extra,
owner,
);
}
}
}
traces.push({
type: "pcb_trace",
pcb_trace_id: `${connection.name}_grid${branch}`,
connection_name: connection.name,
source_trace_id: connection.source_trace_id,
route,
} as SimplifiedPcbTrace);
}
// Reserve package-normal dogbones and staggered through-vias before routing
// any bus. Without this, an early bus can cut off its neighbor's pad escape.
const escaped = new Map<string, P>();
const accessRoutes = new Map<P, SimplifiedPcbTrace["route"]>();
const directNets = new Set<string>();
for (const [ci, c] of input.connections.entries()) {
if (
!c.pointsToConnect.some(
(p) =>
p.port_selector === "U1.USB_DP" || p.port_selector === "U1.USB_DM",
)
)
continue;
if (c.pointsToConnect.length !== 2)
throw new Error("USB must remain point-to-point");
const pair = [...c.pointsToConnect].sort(
(a, b) =>
Number(!a.port_selector?.startsWith("U1.")) -
Number(!b.port_selector?.startsWith("U1.")),
);
const [a, b] = pair as [P, P];
const vertical = Math.abs(b.y) > Math.abs(b.x);
const tangent = vertical ? "x" : "y",
normal = vertical ? "y" : "x";
const bend = { ...a };
bend[normal] =
b[normal] -
Math.sign(b[normal] - a[normal]) * Math.abs(b[tangent] - a[tangent]);
// Keep the pair parallel on the top copper, without a layer transition.
// Exact terminal geometry and pair skew are checked after rendering.
record([], a, b, ci + 1, c, 0, [bend]);
directNets.add(c.name);
}
for (const [ci, c] of input.connections.entries())
for (const [pi, p] of c.pointsToConnect.entries()) {
if (
directNets.has(c.name) ||
c.pointsToConnect.some(
(p) =>
p.port_selector?.startsWith("Y1.") &&
!p.port_selector?.match(/pin[24]|gnd/),
)
)
continue;
if (
!p.port_selector?.startsWith("U1.") ||
Math.max(Math.abs(p.x), Math.abs(p.y)) < 4.5
)
continue;
const vertical = Math.abs(p.y) > Math.abs(p.x),
tangent = vertical ? p.x : p.y;
const parity = Math.round((tangent + 4.2) / 0.4) % 2;
const normal = 6.15 + parity * 0.65;
const e: P = {
x: vertical ? p.x : Math.sign(p.x) * normal,
y: vertical ? Math.sign(p.y) * normal : p.y,
layer: "top",
};
const ep = pos(index(e));
ep.layer = "top";
const q = index(ep) % N;
if (!canVia(q, ci + 1))
throw new Error(`Reserved dogbone via obstructed: ${p.port_selector}`);
const dest = { ...ep, layer: LAYERS[1 + ((pi + ci) % 3)]! };
const ids: number[] = [];
let cur = index(p),
end = index(ep);
ids.push(cur);
const delta = vertical ? (end > cur ? W : -W) : end > cur ? 1 : -1;
while (cur !== end) {
cur += delta;
if (!free(wire, cur, ci + 1))
throw new Error(`Reserved dogbone obstructed: ${p.port_selector}`);
ids.push(cur);
}
ids.push(index(dest));
record(ids, p, dest, ci + 1, c, -pi - 1);
escaped.set(`${ci}:${pi}`, dest);
accessRoutes.set(dest, traces.at(-1)!.route);
}
// Crystal signal nets are short, completely top-side trees. Route them
// after neighboring QFN escapes and before general support/bus routing.
for (const [ci, c] of input.connections.entries()) {
if (
!c.pointsToConnect.some(
(p) =>
p.port_selector?.startsWith("Y1.") &&
!p.port_selector?.match(/pin[24]|gnd/),
)
)
continue;
const remaining = [...c.pointsToConnect];
const connected = [remaining.shift()!];
let branch = 0;
while (remaining.length) {
let best = Infinity,
ai = 0,
bi = 0;
for (let i = 0; i < connected.length; i++)
for (let j = 0; j < remaining.length; j++) {
const a = connected[i]!,
b = remaining[j]!,
d = Math.hypot(a.x - b.x, a.y - b.y);
if (d < best) {
best = d;
ai = i;
bi = j;
}
}
const a = connected[ai]!,
b = remaining[bi]!;
record(solve(a, b, ci + 1, false), a, b, ci + 1, c, branch++);
connected.push(b);
remaining.splice(bi, 1);
}
directNets.add(c.name);
}
// Reserve an access via for each support-component pad and boundary terminal
// as well: no later route is allowed to isolate an as-yet unrouted terminal.
const padAccessOrder = input.connections
.flatMap((c, ci) => c.pointsToConnect.map((p, pi) => ({ c, ci, p, pi })))
.sort((a, b) => {
const ai = options.first?.indexOf(a.c.name) ?? -1,
bi = options.first?.indexOf(b.c.name) ?? -1;
if (ai >= 0 || bi >= 0) return (ai < 0 ? 999 : ai) - (bi < 0 ? 999 : bi);
const boundary = (p: P) =>
Math.max(Math.abs(p.x), Math.abs(p.y)) >= TERMINAL_EDGE - 1e-4;
return Number(boundary(b.p)) - Number(boundary(a.p));
});
for (const { c, ci, p, pi } of padAccessOrder) {
if (directNets.has(c.name)) continue;
if (p.port_selector?.startsWith("U1.")) continue;
if (
input.obstacles.some(
(o) =>
o.circuitJsonMetadata?.pcb_plated_hole_id &&
o.circuitJsonMetadata.pcb_port_id === p.pcb_port_id &&
o.layers.length === 4,
)
) {
const preferred = 1 + ((ci + pi) % 3);
const choices = [
LAYERS[preferred]!,
...LAYERS.filter((l) => l !== LAYERS[preferred]),
];
const vertical = Math.abs(p.y) > Math.abs(p.x);
let reserved = false;
for (const layer of choices) {
for (const depth of [0.75, 1.25, 1.8]) {
const contact = { ...p, layer };
const target = pos(
index({
...contact,
x: vertical ? p.x : p.x - Math.sign(p.x) * depth,
y: vertical ? p.y - Math.sign(p.y) * depth : p.y,
}),
);
try {
const ids = solve(contact, target, ci + 1, false);
if (ids.length > 100) continue;
record(ids, contact, target, ci + 1, c, -pi - 1);
escaped.set(`${ci}:${pi}`, target);
accessRoutes.set(target, traces.at(-1)!.route);
reserved = true;
break;
} catch {}
}
if (reserved) break;
}
if (!reserved)
throw Object.assign(
new Error(`Cannot reserve plated exit: ${p.port_selector}`),
{ connection: c.name },
);
continue;
}
const candidates: P[] = [];
if (Math.max(Math.abs(p.x), Math.abs(p.y)) >= TERMINAL_EDGE - 1e-4) {
const vertical = Math.abs(p.y) > Math.abs(p.x);
for (const depth of [0.75, 1.25, 1.8, 2.4])
for (const offset of [0, 0.3, -0.3, 0.65, -0.65, 1.3, -1.3])
candidates.push({
...p,
x: vertical ? p.x + offset : p.x - Math.sign(p.x) * depth,
y: vertical ? p.y - Math.sign(p.y) * depth : p.y + offset,
});
} else {
const componentName = p.port_selector?.split(".")[0];
const otherPad =
/^[CR]_/.test(componentName ?? "") &&
input.connections
.flatMap((c) => c.pointsToConnect)
.find(
(q) =>
q.pcb_port_id !== p.pcb_port_id &&
q.port_selector?.split(".")[0] === componentName,
);
const separation = otherPad
? Math.hypot(p.x - otherPad.x, p.y - otherPad.y)
: 1;
const normal = otherPad
? {
x: (p.x - otherPad.x) / separation,
y: (p.y - otherPad.y) / separation,
}
: { x: p.port_selector?.endsWith("pin1") ? -1 : 1, y: 0 };
const sign = 1;
for (const dx of [
sign * 0.7,
sign * 1.0,
0,
sign * 1.4,
sign * 1.8,
-sign * 0.7,
-sign * 1.0,
-sign * 1.4,
-sign * 1.8,
])
for (const dy of [
0, 0.7, -0.7, 1.05, -1.05, 1.4, -1.4, 1.8, -1.8, 2.2, -2.2,
]) {
if (dx === 0 && dy === 0) continue;
candidates.push({
x: p.x + dx * normal.x - dy * normal.y,
y: p.y + dx * normal.y + dy * normal.x,
layer: p.layer,
});
}
}
let found = false;
for (const e of candidates) {
if (Math.max(Math.abs(e.x), Math.abs(e.y)) > half - 0.15) continue;
const ep = pos(index(e)),
q = index(ep) % N;
if (!canVia(q, ci + 1)) continue;
const dest = {
...ep,
layer: LAYERS.filter((l) => l !== p.layer)[(ci + pi) % 3]!,
};
try {
const ids = solve(p, ep, ci + 1);
// A local access stub must stay short and on its pad's layer.
if (
ids.length > 120 ||
ids.some((id) => Math.floor(id / N) !== LAYERS.indexOf(p.layer))
)
continue;
ids.push(index(dest));
record(ids, p, dest, ci + 1, c, -pi - 1);
escaped.set(`${ci}:${pi}`, dest);
found = true;
break;
} catch {}
}
if (!found)
throw Object.assign(
new Error(`Cannot reserve pad access: ${p.port_selector}`),
{ connection: c.name },
);
accessRoutes.set(escaped.get(`${ci}:${pi}`)!, traces.at(-1)!.route);
}
const ordered = input.connections
.map((c, i) => ({ c, owner: i + 1 }))
.sort((a, b) => {
const ai = options.first?.indexOf(a.c.name) ?? -1,
bi = options.first?.indexOf(b.c.name) ?? -1;
if (ai >= 0 || bi >= 0) return (ai < 0 ? 999 : ai) - (bi < 0 ? 999 : bi);
return a.c.pointsToConnect.length - b.c.pointsToConnect.length;
});
for (const [i, { c, owner }] of ordered.entries()) {
if (directNets.has(c.name)) continue;
options.onProgress?.(
`${i + 1}/${ordered.length} ${c.name} (${c.pointsToConnect.length} terminals)`,
);
const endpoints = c.pointsToConnect.map(
(p, pi) => escaped.get(`${owner - 1}:${pi}`) ?? p,
);
const connected = [endpoints[0]!],
remaining = endpoints.slice(1);
let branch = 0;
while (remaining.length) {
let best = Infinity,
ai = 0,
bi = 0;
for (let a = 0; a < connected.length; a++)
for (let b = 0; b < remaining.length; b++) {
const p = connected[a]!,
q = remaining[b]!,
d = Math.abs(p.x - q.x) + Math.abs(p.y - q.y);
if (d < best) {
best = d;
ai = a;
bi = b;
}
}
const a = connected[ai]!,
b = remaining[bi]!;
try {
record(solve(a, b, owner), a, b, owner, c, branch++);
const reverse = (r: SimplifiedPcbTrace["route"]) =>
r
.toReversed()
.map((p) =>
p.route_type === "via"
? { ...p, from_layer: p.to_layer, to_layer: p.from_layer }
: { ...p },
);
const trace = traces.at(-1)!;
// Emit complete pad-to-pad branches, retaining the pre-reserved copper.
// This also makes each route consumable by Circuit JSON's endpoint checks.
trace.route = [
...(accessRoutes.get(a) ?? []),
...trace.route,
...reverse(accessRoutes.get(b) ?? []),
].filter((p, i, all) => {
const prev = all[i - 1];
return !(
prev?.route_type === "wire" &&
p.route_type === "wire" &&
prev.layer === p.layer &&
Math.hypot(prev.x - p.x, prev.y - p.y) < 1e-8
);
});
} catch (e) {
throw Object.assign(new Error(`${c.name}: ${(e as Error).message}`), {
connection: c.name,
partialTraces: traces,
});
}
connected.push(b);
remaining.splice(bi, 1);
}
}
return traces.filter((t) => !t.pcb_trace_id.includes("_grid-"));
}