2489 lines
68 KiB
JavaScript
2489 lines
68 KiB
JavaScript
import { resolveElkRuntime } from "./layout-elk.js";
|
|
|
|
const GRID = 20;
|
|
const MARGIN_X = 140;
|
|
const MARGIN_Y = 140;
|
|
const COLUMN_GAP = 320;
|
|
const ROW_GAP = 190;
|
|
const OBSTACLE_PADDING = 14;
|
|
const CONNECTIVITY_COMPACT_PASSES = 8;
|
|
const CONNECTIVITY_MOVE_LIMIT = GRID * 6;
|
|
|
|
const NET_CLASS_PRIORITY = {
|
|
power: 0,
|
|
ground: 1,
|
|
clock: 2,
|
|
signal: 3,
|
|
analog: 4,
|
|
bus: 5,
|
|
differential: 6
|
|
};
|
|
|
|
const LABEL_TIE_CLASSES = new Set(["power", "ground", "bus"]);
|
|
const DEFAULT_RENDER_MODE = "schematic_stub";
|
|
export const DEFAULT_LAYOUT_ENGINE = "schemeta-v2";
|
|
const ROTATION_STEPS = [0, 90, 180, 270];
|
|
const MIN_CHANNEL_SPACING_STEPS = 3;
|
|
const LANE_ORDER = ["power", "clock", "signal", "analog", "ground", "bus", "differential"];
|
|
|
|
function toGrid(value) {
|
|
return Math.round(value / GRID) * GRID;
|
|
}
|
|
|
|
function pointKey(p) {
|
|
return `${p.x},${p.y}`;
|
|
}
|
|
|
|
function edgeKey(a, b) {
|
|
const ka = pointKey(a);
|
|
const kb = pointKey(b);
|
|
return ka < kb ? `${ka}|${kb}` : `${kb}|${ka}`;
|
|
}
|
|
|
|
function clone(obj) {
|
|
return JSON.parse(JSON.stringify(obj));
|
|
}
|
|
|
|
function pinTypeFor(model, ref, pinName) {
|
|
const inst = model.instances.find((x) => x.ref === ref);
|
|
if (!inst) {
|
|
return "passive";
|
|
}
|
|
const sym = model.symbols[inst.symbol];
|
|
const pin = sym?.pins.find((p) => p.name === pinName);
|
|
return pin?.type ?? "passive";
|
|
}
|
|
|
|
function pinPoint(inst, pin, width, height) {
|
|
const x0 = inst.placement.x;
|
|
const y0 = inst.placement.y;
|
|
const rotation = normalizeRotation(inst.placement.rotation ?? 0);
|
|
let base = { x: x0, y: y0 };
|
|
|
|
switch (pin.side) {
|
|
case "left":
|
|
base = { x: x0, y: y0 + pin.offset };
|
|
break;
|
|
case "right":
|
|
base = { x: x0 + width, y: y0 + pin.offset };
|
|
break;
|
|
case "top":
|
|
base = { x: x0 + pin.offset, y: y0 };
|
|
break;
|
|
case "bottom":
|
|
base = { x: x0 + pin.offset, y: y0 + height };
|
|
break;
|
|
default:
|
|
base = { x: x0, y: y0 };
|
|
}
|
|
|
|
if (!rotation) {
|
|
return base;
|
|
}
|
|
|
|
const cx = x0 + width / 2;
|
|
const cy = y0 + height / 2;
|
|
return rotatePoint(base, { x: cx, y: cy }, rotation);
|
|
}
|
|
|
|
function normalizeRotation(value) {
|
|
const n = Number(value ?? 0);
|
|
if (!Number.isFinite(n)) {
|
|
return 0;
|
|
}
|
|
const snapped = Math.round(n / 90) * 90;
|
|
let rot = snapped % 360;
|
|
if (rot < 0) {
|
|
rot += 360;
|
|
}
|
|
return rot;
|
|
}
|
|
|
|
function rotatePoint(point, center, rotation) {
|
|
const rad = (rotation * Math.PI) / 180;
|
|
const cos = Math.round(Math.cos(rad));
|
|
const sin = Math.round(Math.sin(rad));
|
|
const dx = point.x - center.x;
|
|
const dy = point.y - center.y;
|
|
return {
|
|
x: Math.round(center.x + dx * cos - dy * sin),
|
|
y: Math.round(center.y + dx * sin + dy * cos)
|
|
};
|
|
}
|
|
|
|
function rotateSide(side, rotation) {
|
|
const steps = normalizeRotation(rotation) / 90;
|
|
const order = ["top", "right", "bottom", "left"];
|
|
const idx = order.indexOf(side);
|
|
if (idx < 0) {
|
|
return side;
|
|
}
|
|
return order[(idx + steps) % 4];
|
|
}
|
|
|
|
function hasValidPlacement(inst) {
|
|
return Number.isFinite(inst?.placement?.x) && Number.isFinite(inst?.placement?.y);
|
|
}
|
|
|
|
function preservePlacedInstances(model, options = {}) {
|
|
const respectLocks = options.respectLocks ?? true;
|
|
const autoRotate = options.autoRotate ?? false;
|
|
const fallback = placeInstances(model, { respectLocks, autoRotate });
|
|
const fallbackByRef = new Map((fallback?.placed ?? []).map((inst) => [inst.ref, inst]));
|
|
const instances = [...model.instances].sort((a, b) => a.ref.localeCompare(b.ref));
|
|
|
|
return instances.map((inst) => {
|
|
if (hasValidPlacement(inst)) {
|
|
return {
|
|
...inst,
|
|
placement: {
|
|
x: toGrid(Number(inst.placement.x)),
|
|
y: toGrid(Number(inst.placement.y)),
|
|
rotation: normalizeRotation(inst.placement.rotation ?? 0),
|
|
locked: respectLocks ? Boolean(inst.placement.locked) : false
|
|
}
|
|
};
|
|
}
|
|
return fallbackByRef.get(inst.ref) ?? {
|
|
...inst,
|
|
placement: {
|
|
x: toGrid(MARGIN_X),
|
|
y: toGrid(MARGIN_Y),
|
|
rotation: normalizeRotation(inst.placement?.rotation ?? 0),
|
|
locked: respectLocks ? Boolean(inst.placement?.locked) : false
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
function getNodePin(model, placedMap, node) {
|
|
const inst = placedMap.get(node.ref);
|
|
if (!inst) {
|
|
return null;
|
|
}
|
|
|
|
const sym = model.symbols[inst.symbol];
|
|
const pin = sym.pins.find((x) => x.name === node.pin);
|
|
if (!pin) {
|
|
return null;
|
|
}
|
|
|
|
const point = pinPoint(inst, pin, sym.body.width, sym.body.height);
|
|
const exit = {
|
|
left: { x: point.x - GRID, y: point.y },
|
|
right: { x: point.x + GRID, y: point.y },
|
|
top: { x: point.x, y: point.y - GRID },
|
|
bottom: { x: point.x, y: point.y + GRID }
|
|
}[rotateSide(pin.side, inst.placement.rotation ?? 0)];
|
|
|
|
return {
|
|
ref: node.ref,
|
|
pin: node.pin,
|
|
pinType: pin.type,
|
|
side: pin.side,
|
|
point,
|
|
exit: { x: toGrid(exit.x), y: toGrid(exit.y) }
|
|
};
|
|
}
|
|
|
|
function buildDirectedEdges(model) {
|
|
const edges = [];
|
|
|
|
for (const net of model.nets) {
|
|
const sources = net.nodes.filter((n) => {
|
|
const t = pinTypeFor(model, n.ref, n.pin);
|
|
return t === "output" || t === "power_out";
|
|
});
|
|
|
|
const sinks = net.nodes.filter((n) => {
|
|
const t = pinTypeFor(model, n.ref, n.pin);
|
|
return t === "input" || t === "power_in" || t === "analog" || t === "bidirectional";
|
|
});
|
|
|
|
for (const s of sources) {
|
|
for (const d of sinks) {
|
|
if (s.ref !== d.ref) {
|
|
edges.push([s.ref, d.ref]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const dedup = new Map();
|
|
for (const [a, b] of edges) {
|
|
dedup.set(`${a}->${b}`, [a, b]);
|
|
}
|
|
|
|
return [...dedup.values()];
|
|
}
|
|
|
|
function isLikelySourceSymbol(sym) {
|
|
const category = String(sym?.category ?? "").toLowerCase();
|
|
return (
|
|
category.includes("power") ||
|
|
category.includes("mcu") ||
|
|
category.includes("microcontroller") ||
|
|
category.includes("processor") ||
|
|
category.includes("clock")
|
|
);
|
|
}
|
|
|
|
function fallbackUndirectedRanks(model, directedRank) {
|
|
const refs = model.instances.map((x) => x.ref).sort();
|
|
if (!refs.length) {
|
|
return directedRank;
|
|
}
|
|
|
|
const graph = connectivityGraph(model);
|
|
const degree = new Map(refs.map((ref) => [ref, 0]));
|
|
for (const ref of refs) {
|
|
let sum = 0;
|
|
for (const w of graph.get(ref)?.values() ?? []) {
|
|
sum += w;
|
|
}
|
|
degree.set(ref, sum);
|
|
}
|
|
|
|
const seeds = [];
|
|
for (const inst of model.instances) {
|
|
const sym = model.symbols[inst.symbol];
|
|
if (!sym) {
|
|
continue;
|
|
}
|
|
if (sym.pins.some((p) => p.type === "power_out" || p.type === "output")) {
|
|
seeds.push(inst.ref);
|
|
continue;
|
|
}
|
|
if (isLikelySourceSymbol(sym)) {
|
|
seeds.push(inst.ref);
|
|
}
|
|
}
|
|
|
|
if (!seeds.length) {
|
|
const strongest = refs
|
|
.slice()
|
|
.sort((a, b) => (degree.get(b) ?? 0) - (degree.get(a) ?? 0) || a.localeCompare(b))[0];
|
|
if (strongest) {
|
|
seeds.push(strongest);
|
|
}
|
|
}
|
|
|
|
const dist = new Map(refs.map((ref) => [ref, Number.POSITIVE_INFINITY]));
|
|
const queue = [];
|
|
for (const s of seeds) {
|
|
dist.set(s, 0);
|
|
queue.push(s);
|
|
}
|
|
|
|
while (queue.length) {
|
|
const ref = queue.shift();
|
|
const base = dist.get(ref) ?? Number.POSITIVE_INFINITY;
|
|
for (const [nbr, weight] of graph.get(ref)?.entries() ?? []) {
|
|
const step = Math.max(0.35, 1.4 - Math.min(1.1, weight * 0.55));
|
|
const next = base + step;
|
|
if (next + 1e-6 < (dist.get(nbr) ?? Number.POSITIVE_INFINITY)) {
|
|
dist.set(nbr, next);
|
|
queue.push(nbr);
|
|
}
|
|
}
|
|
}
|
|
|
|
let minFinite = Number.POSITIVE_INFINITY;
|
|
let maxFinite = 0;
|
|
for (const d of dist.values()) {
|
|
if (!Number.isFinite(d)) {
|
|
continue;
|
|
}
|
|
minFinite = Math.min(minFinite, d);
|
|
maxFinite = Math.max(maxFinite, d);
|
|
}
|
|
|
|
const normalized = new Map(directedRank);
|
|
for (const ref of refs) {
|
|
const d = dist.get(ref);
|
|
if (!Number.isFinite(d)) {
|
|
continue;
|
|
}
|
|
const shifted = d - (Number.isFinite(minFinite) ? minFinite : 0);
|
|
const fallbackRank = Math.max(0, Math.round(shifted * 1.6));
|
|
const directed = directedRank.get(ref) ?? 1;
|
|
normalized.set(ref, Math.max(directed, fallbackRank));
|
|
}
|
|
|
|
if (maxFinite - minFinite < 0.6) {
|
|
refs
|
|
.slice()
|
|
.sort((a, b) => {
|
|
const da = degree.get(a) ?? 0;
|
|
const db = degree.get(b) ?? 0;
|
|
if (da !== db) {
|
|
return db - da;
|
|
}
|
|
return a.localeCompare(b);
|
|
})
|
|
.forEach((ref, idx) => {
|
|
const directed = normalized.get(ref) ?? 1;
|
|
normalized.set(ref, Math.max(directed, Math.floor(idx / 3)));
|
|
});
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
function computeRanks(model) {
|
|
const refs = model.instances.map((x) => x.ref).sort();
|
|
const rank = new Map(refs.map((r) => [r, 1]));
|
|
|
|
const powerRefs = new Set(
|
|
model.instances
|
|
.filter((inst) => {
|
|
const sym = model.symbols[inst.symbol];
|
|
return (
|
|
sym.category.toLowerCase().includes("power") ||
|
|
sym.pins.some((p) => p.type === "power_out")
|
|
);
|
|
})
|
|
.map((x) => x.ref)
|
|
);
|
|
|
|
for (const r of powerRefs) {
|
|
rank.set(r, 0);
|
|
}
|
|
|
|
const edges = buildDirectedEdges(model);
|
|
|
|
for (let i = 0; i < refs.length; i += 1) {
|
|
let changed = false;
|
|
for (const [from, to] of edges) {
|
|
const next = (rank.get(from) ?? 1) + 1;
|
|
if (!powerRefs.has(to) && next > (rank.get(to) ?? 1)) {
|
|
rank.set(to, next);
|
|
changed = true;
|
|
}
|
|
}
|
|
if (!changed) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (const r of powerRefs) {
|
|
rank.set(r, 0);
|
|
}
|
|
|
|
const uniqueDirectedRanks = new Set(refs.map((ref) => rank.get(ref) ?? 1));
|
|
const needsFallback =
|
|
edges.length < Math.max(2, Math.floor(refs.length * 0.25)) ||
|
|
uniqueDirectedRanks.size <= 2;
|
|
const ranked = needsFallback ? fallbackUndirectedRanks(model, rank) : rank;
|
|
|
|
return { rank: ranked, edges, powerRefs };
|
|
}
|
|
|
|
function computeBaryOrder(columns, edges) {
|
|
const predecessors = new Map();
|
|
for (const [a, b] of edges) {
|
|
const list = predecessors.get(b) ?? [];
|
|
list.push(a);
|
|
predecessors.set(b, list);
|
|
}
|
|
|
|
const orderByRef = new Map();
|
|
for (const refs of columns.values()) {
|
|
refs.forEach((ref, idx) => orderByRef.set(ref, idx));
|
|
}
|
|
|
|
const sortedColumns = [...columns.entries()].sort((a, b) => Number(a[0]) - Number(b[0]));
|
|
for (const [, refs] of sortedColumns) {
|
|
refs.sort((a, b) => {
|
|
const pa = predecessors.get(a) ?? [];
|
|
const pb = predecessors.get(b) ?? [];
|
|
const ba = pa.length
|
|
? pa.reduce((sum, r) => sum + (orderByRef.get(r) ?? 0), 0) / pa.length
|
|
: Number.MAX_SAFE_INTEGER;
|
|
const bb = pb.length
|
|
? pb.reduce((sum, r) => sum + (orderByRef.get(r) ?? 0), 0) / pb.length
|
|
: Number.MAX_SAFE_INTEGER;
|
|
|
|
if (ba !== bb) {
|
|
return ba - bb;
|
|
}
|
|
return a.localeCompare(b);
|
|
});
|
|
|
|
refs.forEach((ref, idx) => orderByRef.set(ref, idx));
|
|
}
|
|
}
|
|
|
|
function applyAlignmentConstraints(placedMap, constraints) {
|
|
const alignments = constraints?.alignment ?? [];
|
|
for (const rule of alignments) {
|
|
const left = placedMap.get(rule.left_of);
|
|
const right = placedMap.get(rule.right_of);
|
|
if (!left || !right) {
|
|
continue;
|
|
}
|
|
|
|
const targetRightX = left.placement.x + COLUMN_GAP;
|
|
if (right.placement.x < targetRightX) {
|
|
right.placement.x = toGrid(targetRightX);
|
|
}
|
|
}
|
|
}
|
|
|
|
function applyNearConstraints(model, placedMap, constraints) {
|
|
const rules = constraints?.near ?? [];
|
|
for (const rule of rules) {
|
|
const comp = placedMap.get(rule.component);
|
|
const target = placedMap.get(rule.target_pin?.ref);
|
|
if (!comp || !target || comp.placement.locked) {
|
|
continue;
|
|
}
|
|
|
|
const targetSym = model.symbols[target.symbol];
|
|
const targetPin = targetSym.pins.find((p) => p.name === rule.target_pin.pin);
|
|
if (!targetPin) {
|
|
continue;
|
|
}
|
|
|
|
const tp = pinPoint(target, targetPin, targetSym.body.width, targetSym.body.height);
|
|
comp.placement.x = toGrid(tp.x + GRID * 4);
|
|
comp.placement.y = toGrid(tp.y - GRID * 3);
|
|
}
|
|
}
|
|
|
|
function buildInstanceMap(instances) {
|
|
return new Map(instances.map((inst) => [inst.ref, inst]));
|
|
}
|
|
|
|
function buildConstraintGroups(model, rank) {
|
|
const allRefs = new Set(model.instances.map((i) => i.ref));
|
|
const consumed = new Set();
|
|
const out = [];
|
|
|
|
for (const g of model.constraints?.groups ?? []) {
|
|
const members = (g.members ?? []).filter((ref) => allRefs.has(ref) && !consumed.has(ref));
|
|
if (!members.length) {
|
|
continue;
|
|
}
|
|
for (const ref of members) {
|
|
consumed.add(ref);
|
|
}
|
|
out.push({
|
|
name: g.name ?? `group_${out.length + 1}`,
|
|
members,
|
|
synthetic: false
|
|
});
|
|
}
|
|
|
|
const leftovers = model.instances
|
|
.map((i) => i.ref)
|
|
.filter((ref) => !consumed.has(ref))
|
|
.sort((a, b) => {
|
|
const ra = rank.get(a) ?? 1;
|
|
const rb = rank.get(b) ?? 1;
|
|
if (ra !== rb) {
|
|
return ra - rb;
|
|
}
|
|
return a.localeCompare(b);
|
|
});
|
|
|
|
const autoGroups = autoClusterLeftovers(model, leftovers, rank);
|
|
for (const g of autoGroups) {
|
|
out.push(g);
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
function autoClusterLeftovers(model, leftovers, rank) {
|
|
if (!leftovers.length) {
|
|
return [];
|
|
}
|
|
const graph = connectivityGraph(model);
|
|
const leftoverSet = new Set(leftovers);
|
|
const visited = new Set();
|
|
const clusters = [];
|
|
const EDGE_THRESHOLD = 0.35;
|
|
|
|
for (const start of leftovers) {
|
|
if (visited.has(start)) {
|
|
continue;
|
|
}
|
|
const queue = [start];
|
|
visited.add(start);
|
|
const comp = [];
|
|
while (queue.length) {
|
|
const ref = queue.shift();
|
|
comp.push(ref);
|
|
for (const [nbr, w] of graph.get(ref)?.entries() ?? []) {
|
|
if (!leftoverSet.has(nbr) || visited.has(nbr)) {
|
|
continue;
|
|
}
|
|
if (w < EDGE_THRESHOLD) {
|
|
continue;
|
|
}
|
|
visited.add(nbr);
|
|
queue.push(nbr);
|
|
}
|
|
}
|
|
clusters.push(comp);
|
|
}
|
|
|
|
clusters.sort((a, b) => {
|
|
const ar = Math.min(...a.map((r) => rank.get(r) ?? 1));
|
|
const br = Math.min(...b.map((r) => rank.get(r) ?? 1));
|
|
if (ar !== br) {
|
|
return ar - br;
|
|
}
|
|
return a[0].localeCompare(b[0]);
|
|
});
|
|
|
|
return clusters.map((members, idx) => {
|
|
const sorted = [...members].sort((a, b) => {
|
|
const ra = rank.get(a) ?? 1;
|
|
const rb = rank.get(b) ?? 1;
|
|
if (ra !== rb) {
|
|
return ra - rb;
|
|
}
|
|
return a.localeCompare(b);
|
|
});
|
|
if (sorted.length === 1) {
|
|
return { name: `solo_${sorted[0]}`, members: sorted, synthetic: true };
|
|
}
|
|
return { name: `auto_cluster_${idx + 1}`, members: sorted, synthetic: true };
|
|
});
|
|
}
|
|
|
|
function rankColumnsForRefs(refs, rank) {
|
|
const minRank = Math.min(...refs.map((r) => rank.get(r) ?? 1));
|
|
const cols = new Map();
|
|
|
|
for (const ref of refs) {
|
|
const localRank = Math.max(0, (rank.get(ref) ?? 1) - minRank);
|
|
const list = cols.get(localRank) ?? [];
|
|
list.push(ref);
|
|
cols.set(localRank, list);
|
|
}
|
|
|
|
const uniqueCols = [...cols.keys()].sort((a, b) => a - b);
|
|
if (uniqueCols.length === 1 && refs.length >= 4) {
|
|
cols.clear();
|
|
const targetCols = Math.min(3, Math.max(2, Math.ceil(refs.length / 3)));
|
|
refs.forEach((ref, idx) => {
|
|
const col = idx % targetCols;
|
|
const list = cols.get(col) ?? [];
|
|
list.push(ref);
|
|
cols.set(col, list);
|
|
});
|
|
}
|
|
|
|
return cols;
|
|
}
|
|
|
|
function connectivityDegree(model) {
|
|
const deg = new Map(model.instances.map((i) => [i.ref, 0]));
|
|
for (const net of model.nets) {
|
|
const refs = [...new Set((net.nodes ?? []).map((n) => n.ref))];
|
|
for (const ref of refs) {
|
|
deg.set(ref, (deg.get(ref) ?? 0) + Math.max(1, refs.length - 1));
|
|
}
|
|
}
|
|
return deg;
|
|
}
|
|
|
|
function connectivityGraph(model) {
|
|
const graph = new Map(model.instances.map((i) => [i.ref, new Map()]));
|
|
const classWeight = (netClass) => {
|
|
if (netClass === "clock") return 4.6;
|
|
if (netClass === "signal") return 4.2;
|
|
if (netClass === "analog") return 3.8;
|
|
if (netClass === "bus") return 3.4;
|
|
if (netClass === "differential") return 3.2;
|
|
if (netClass === "power") return 1.6;
|
|
if (netClass === "ground") return 1.4;
|
|
return 2.4;
|
|
};
|
|
|
|
for (const net of model.nets ?? []) {
|
|
const refs = [...new Set((net.nodes ?? []).map((n) => n.ref))].filter((ref) => graph.has(ref));
|
|
if (refs.length < 2) {
|
|
continue;
|
|
}
|
|
const weight = classWeight(String(net.class ?? "signal")) / Math.max(1, refs.length - 1);
|
|
for (let i = 0; i < refs.length; i += 1) {
|
|
for (let j = i + 1; j < refs.length; j += 1) {
|
|
const a = refs[i];
|
|
const b = refs[j];
|
|
const aAdj = graph.get(a);
|
|
const bAdj = graph.get(b);
|
|
aAdj.set(b, (aAdj.get(b) ?? 0) + weight);
|
|
bAdj.set(a, (bAdj.get(a) ?? 0) + weight);
|
|
}
|
|
}
|
|
}
|
|
|
|
return graph;
|
|
}
|
|
|
|
function centerForPlacement(model, inst) {
|
|
const sym = model.symbols[inst.symbol];
|
|
const w = sym?.body?.width ?? 120;
|
|
const h = sym?.body?.height ?? 80;
|
|
return {
|
|
x: inst.placement.x + w / 2,
|
|
y: inst.placement.y + h / 2
|
|
};
|
|
}
|
|
|
|
function clampStep(delta, maxStep) {
|
|
if (delta > maxStep) {
|
|
return maxStep;
|
|
}
|
|
if (delta < -maxStep) {
|
|
return -maxStep;
|
|
}
|
|
return delta;
|
|
}
|
|
|
|
function compactPlacementByConnectivity(model, placedMap, options = {}) {
|
|
const respectLocks = options.respectLocks ?? true;
|
|
const laneProfiles = options.laneProfiles ?? new Map();
|
|
const rank = options.rank ?? new Map();
|
|
const graph = connectivityGraph(model);
|
|
const refs = [...placedMap.keys()].sort();
|
|
if (!refs.length) {
|
|
return;
|
|
}
|
|
const passCount = refs.length > 140 ? 2 : refs.length > 80 ? 4 : CONNECTIVITY_COMPACT_PASSES;
|
|
const minRank = Math.min(...refs.map((r) => rank.get(r) ?? 1), 1);
|
|
const rankBuckets = new Map();
|
|
for (const ref of refs) {
|
|
const r = rank.get(ref) ?? 1;
|
|
const list = rankBuckets.get(r) ?? [];
|
|
list.push(ref);
|
|
rankBuckets.set(r, list);
|
|
}
|
|
const rankOffsetByRef = new Map();
|
|
for (const [, bucketRefs] of rankBuckets.entries()) {
|
|
bucketRefs.sort();
|
|
const cols = Math.min(4, Math.max(1, Math.ceil(bucketRefs.length / 2)));
|
|
const stride = GRID * 6;
|
|
for (let i = 0; i < bucketRefs.length; i += 1) {
|
|
const slot = i % cols;
|
|
const centered = slot - (cols - 1) / 2;
|
|
rankOffsetByRef.set(bucketRefs[i], centered * stride);
|
|
}
|
|
}
|
|
|
|
const centersByRef = () => {
|
|
const out = new Map();
|
|
for (const ref of refs) {
|
|
const inst = placedMap.get(ref);
|
|
if (!inst) continue;
|
|
out.set(ref, centerForPlacement(model, inst));
|
|
}
|
|
return out;
|
|
};
|
|
|
|
for (let pass = 0; pass < passCount; pass += 1) {
|
|
const centers = centersByRef();
|
|
for (const ref of refs) {
|
|
const inst = placedMap.get(ref);
|
|
if (!inst) {
|
|
continue;
|
|
}
|
|
if (respectLocks && inst.placement.locked) {
|
|
continue;
|
|
}
|
|
const sym = model.symbols[inst.symbol];
|
|
const w = sym?.body?.width ?? 120;
|
|
const h = sym?.body?.height ?? 80;
|
|
const currentCenter = centers.get(ref);
|
|
if (!currentCenter) {
|
|
continue;
|
|
}
|
|
|
|
const neighbors = [...(graph.get(ref)?.entries() ?? [])];
|
|
if (!neighbors.length) {
|
|
continue;
|
|
}
|
|
|
|
let sumW = 0;
|
|
let tx = 0;
|
|
let ty = 0;
|
|
for (const [nbr, weight] of neighbors) {
|
|
const c = centers.get(nbr);
|
|
if (!c) {
|
|
continue;
|
|
}
|
|
sumW += weight;
|
|
tx += c.x * weight;
|
|
ty += c.y * weight;
|
|
}
|
|
if (!sumW) {
|
|
continue;
|
|
}
|
|
tx /= sumW;
|
|
ty /= sumW;
|
|
|
|
const localRank = Math.max(0, (rank.get(ref) ?? 1) - minRank);
|
|
const rankTargetX =
|
|
MARGIN_X + localRank * (COLUMN_GAP * 0.82) + (rankOffsetByRef.get(ref) ?? 0);
|
|
const lane = laneProfiles.get(ref)?.laneIndex ?? 2;
|
|
const laneY = MARGIN_Y + lane * (ROW_GAP * 0.65);
|
|
let repelX = 0;
|
|
let repelY = 0;
|
|
for (const [otherRef, otherCenter] of centers.entries()) {
|
|
if (otherRef === ref) {
|
|
continue;
|
|
}
|
|
const ddx = currentCenter.x - otherCenter.x;
|
|
const ddy = currentCenter.y - otherCenter.y;
|
|
const ax = Math.abs(ddx);
|
|
const ay = Math.abs(ddy);
|
|
if (ax > GRID * 10 || ay > GRID * 10) {
|
|
continue;
|
|
}
|
|
const px = (GRID * 10 - ax) * 0.035;
|
|
const py = (GRID * 10 - ay) * 0.03;
|
|
repelX += ddx >= 0 ? px : -px;
|
|
repelY += ddy >= 0 ? py : -py;
|
|
}
|
|
|
|
tx = tx * 0.62 + rankTargetX * 0.24 + currentCenter.x * 0.14 + repelX;
|
|
ty = ty * 0.72 + laneY * 0.28;
|
|
|
|
const dx = clampStep(tx - currentCenter.x, CONNECTIVITY_MOVE_LIMIT);
|
|
const dy = clampStep(ty - currentCenter.y, CONNECTIVITY_MOVE_LIMIT);
|
|
inst.placement.x = toGrid(Math.max(MARGIN_X, inst.placement.x + dx));
|
|
inst.placement.y = toGrid(Math.max(MARGIN_Y, inst.placement.y + dy));
|
|
placedMap.set(ref, inst);
|
|
}
|
|
|
|
const shouldResolve =
|
|
pass === passCount - 1 || (refs.length <= 80 && pass % 2 === 1);
|
|
if (shouldResolve) {
|
|
resolvePlacementOverlaps(model, placedMap, { respectLocks });
|
|
}
|
|
}
|
|
}
|
|
|
|
function isRailNetName(name) {
|
|
const n = String(name ?? "").toUpperCase();
|
|
return n === "GND" || n === "GROUND" || n === "3V3" || n === "5V" || n === "VCC" || n === "VIN";
|
|
}
|
|
|
|
function symbolKind(sym) {
|
|
const t = String(sym?.template_name ?? "").toLowerCase();
|
|
if (t) {
|
|
return t;
|
|
}
|
|
const c = String(sym?.category ?? "").toLowerCase();
|
|
if (c.includes("resistor")) return "resistor";
|
|
if (c.includes("capacitor")) return "capacitor";
|
|
if (c.includes("inductor")) return "inductor";
|
|
if (c.includes("diode")) return "diode";
|
|
if (c.includes("led")) return "led";
|
|
return c;
|
|
}
|
|
|
|
function tightenPassiveAdjacency(model, placedMap, options = {}) {
|
|
const respectLocks = options.respectLocks ?? true;
|
|
const refToNets = new Map();
|
|
for (const net of model.nets ?? []) {
|
|
for (const node of net.nodes ?? []) {
|
|
const list = refToNets.get(node.ref) ?? [];
|
|
list.push(net);
|
|
refToNets.set(node.ref, list);
|
|
}
|
|
}
|
|
|
|
const refs = [...placedMap.keys()].sort();
|
|
for (const ref of refs) {
|
|
const inst = placedMap.get(ref);
|
|
if (!inst) continue;
|
|
if (respectLocks && inst.placement.locked) continue;
|
|
const sym = model.symbols[inst.symbol];
|
|
const kind = symbolKind(sym);
|
|
if (!["resistor", "capacitor", "inductor", "diode", "led"].includes(kind)) {
|
|
continue;
|
|
}
|
|
|
|
const nets = refToNets.get(ref) ?? [];
|
|
const preferred = nets.find((n) => !["power", "ground"].includes(String(n.class ?? "")) && !isRailNetName(n.name));
|
|
if (!preferred) {
|
|
continue;
|
|
}
|
|
|
|
const anchors = [];
|
|
for (const node of preferred.nodes ?? []) {
|
|
if (node.ref === ref) continue;
|
|
const other = placedMap.get(node.ref);
|
|
if (!other) continue;
|
|
anchors.push(centerForPlacement(model, other));
|
|
}
|
|
if (!anchors.length) {
|
|
continue;
|
|
}
|
|
|
|
const cx = anchors.reduce((s, p) => s + p.x, 0) / anchors.length;
|
|
const cy = anchors.reduce((s, p) => s + p.y, 0) / anchors.length;
|
|
const current = centerForPlacement(model, inst);
|
|
const tx = cx * 0.86 + current.x * 0.14;
|
|
const ty = cy * 0.86 + current.y * 0.14;
|
|
const nx = toGrid(Math.max(MARGIN_X, tx - (sym?.body?.width ?? 120) / 2));
|
|
const ny = toGrid(Math.max(MARGIN_Y, ty - (sym?.body?.height ?? 80) / 2));
|
|
inst.placement.x = nx;
|
|
inst.placement.y = ny;
|
|
placedMap.set(ref, inst);
|
|
}
|
|
}
|
|
|
|
function tightenConstraintGroups(model, placedMap, options = {}) {
|
|
const respectLocks = options.respectLocks ?? true;
|
|
for (const g of model.constraints?.groups ?? []) {
|
|
const members = (g.members ?? []).map((ref) => placedMap.get(ref)).filter(Boolean);
|
|
if (members.length < 2) {
|
|
continue;
|
|
}
|
|
|
|
const centers = members.map((inst) => centerForPlacement(model, inst));
|
|
const cx = centers.reduce((s, p) => s + p.x, 0) / centers.length;
|
|
const cy = centers.reduce((s, p) => s + p.y, 0) / centers.length;
|
|
const maxRadius = 320;
|
|
|
|
for (const inst of members) {
|
|
if (respectLocks && inst.placement.locked) {
|
|
continue;
|
|
}
|
|
const sym = model.symbols[inst.symbol];
|
|
const c = centerForPlacement(model, inst);
|
|
const dx = c.x - cx;
|
|
const dy = c.y - cy;
|
|
const dist = Math.hypot(dx, dy);
|
|
if (dist <= maxRadius) {
|
|
continue;
|
|
}
|
|
|
|
const pull = Math.min(CONNECTIVITY_MOVE_LIMIT, (dist - maxRadius) * 0.55);
|
|
const ux = dist > 0 ? dx / dist : 0;
|
|
const uy = dist > 0 ? dy / dist : 0;
|
|
const tx = c.x - ux * pull;
|
|
const ty = c.y - uy * pull;
|
|
inst.placement.x = toGrid(Math.max(MARGIN_X, tx - (sym?.body?.width ?? 120) / 2));
|
|
inst.placement.y = toGrid(Math.max(MARGIN_Y, ty - (sym?.body?.height ?? 80) / 2));
|
|
placedMap.set(inst.ref, inst);
|
|
}
|
|
}
|
|
}
|
|
|
|
function refLaneProfiles(model) {
|
|
const profiles = new Map(model.instances.map((inst) => [inst.ref, { total: 0, byClass: {} }]));
|
|
for (const net of model.nets ?? []) {
|
|
const netClass = String(net.class ?? "signal");
|
|
const refs = [...new Set((net.nodes ?? []).map((n) => n.ref))];
|
|
for (const ref of refs) {
|
|
const p = profiles.get(ref);
|
|
if (!p) {
|
|
continue;
|
|
}
|
|
p.total += 1;
|
|
p.byClass[netClass] = (p.byClass[netClass] ?? 0) + 1;
|
|
}
|
|
}
|
|
|
|
const out = new Map();
|
|
for (const [ref, profile] of profiles.entries()) {
|
|
const ranked = Object.entries(profile.byClass).sort((a, b) => {
|
|
if (a[1] !== b[1]) {
|
|
return b[1] - a[1];
|
|
}
|
|
return (NET_CLASS_PRIORITY[a[0]] ?? 99) - (NET_CLASS_PRIORITY[b[0]] ?? 99);
|
|
});
|
|
const dominantClass = ranked[0]?.[0] ?? "signal";
|
|
const laneIndex = Math.max(0, LANE_ORDER.indexOf(dominantClass));
|
|
out.set(ref, {
|
|
dominantClass,
|
|
laneIndex,
|
|
profile
|
|
});
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function placeGroup(model, group, start, context) {
|
|
const { rank, degree, instanceByRef, respectLocks, laneProfiles } = context;
|
|
const refs = [...group.members].sort((a, b) => a.localeCompare(b));
|
|
const cols = rankColumnsForRefs(refs, rank);
|
|
const colOrder = [...cols.keys()].sort((a, b) => a - b);
|
|
|
|
const colWidths = new Map();
|
|
for (const col of colOrder) {
|
|
const w = Math.max(
|
|
...cols.get(col).map((ref) => {
|
|
const inst = instanceByRef.get(ref);
|
|
const sym = inst ? model.symbols[inst.symbol] : null;
|
|
return sym?.body?.width ?? 120;
|
|
}),
|
|
120
|
|
);
|
|
colWidths.set(col, w);
|
|
}
|
|
|
|
const colX = new Map();
|
|
let xCursor = start.x;
|
|
for (const col of colOrder) {
|
|
colX.set(col, toGrid(xCursor));
|
|
xCursor += (colWidths.get(col) ?? 120) + 110;
|
|
}
|
|
|
|
const placed = [];
|
|
let minX = Number.POSITIVE_INFINITY;
|
|
let minY = Number.POSITIVE_INFINITY;
|
|
let maxX = Number.NEGATIVE_INFINITY;
|
|
let maxY = Number.NEGATIVE_INFINITY;
|
|
|
|
for (const col of colOrder) {
|
|
const refsInCol = [...(cols.get(col) ?? [])].sort((a, b) => {
|
|
const la = laneProfiles.get(a)?.laneIndex ?? 2;
|
|
const lb = laneProfiles.get(b)?.laneIndex ?? 2;
|
|
if (la !== lb) {
|
|
return la - lb;
|
|
}
|
|
const da = degree.get(a) ?? 0;
|
|
const db = degree.get(b) ?? 0;
|
|
if (da !== db) {
|
|
return db - da;
|
|
}
|
|
return a.localeCompare(b);
|
|
});
|
|
|
|
const byLane = new Map();
|
|
for (const ref of refsInCol) {
|
|
const lane = laneProfiles.get(ref)?.laneIndex ?? 2;
|
|
const list = byLane.get(lane) ?? [];
|
|
list.push(ref);
|
|
byLane.set(lane, list);
|
|
}
|
|
|
|
let yCursor = start.y;
|
|
const laneSequence = [...byLane.keys()].sort((a, b) => a - b);
|
|
for (const lane of laneSequence) {
|
|
const laneRefs = byLane.get(lane) ?? [];
|
|
for (const ref of laneRefs) {
|
|
const inst = instanceByRef.get(ref);
|
|
if (!inst) {
|
|
continue;
|
|
}
|
|
const sym = model.symbols[inst.symbol];
|
|
const locked = respectLocks ? (inst.placement.locked ?? false) : false;
|
|
let x = inst.placement.x;
|
|
let y = inst.placement.y;
|
|
|
|
if (x == null || y == null || !locked) {
|
|
x = toGrid(colX.get(col) ?? start.x);
|
|
y = toGrid(yCursor);
|
|
}
|
|
|
|
const next = {
|
|
...inst,
|
|
placement: {
|
|
x,
|
|
y,
|
|
rotation: normalizeRotation(inst.placement.rotation ?? 0),
|
|
locked
|
|
}
|
|
};
|
|
placed.push(next);
|
|
|
|
minX = Math.min(minX, x);
|
|
minY = Math.min(minY, y);
|
|
maxX = Math.max(maxX, x + sym.body.width);
|
|
maxY = Math.max(maxY, y + sym.body.height);
|
|
|
|
yCursor = y + sym.body.height + 64;
|
|
}
|
|
yCursor += 28;
|
|
}
|
|
}
|
|
|
|
if (!placed.length) {
|
|
return {
|
|
placed,
|
|
bbox: { x: start.x, y: start.y, w: 320, h: 240 }
|
|
};
|
|
}
|
|
|
|
return {
|
|
placed,
|
|
bbox: {
|
|
x: minX,
|
|
y: minY,
|
|
w: maxX - minX,
|
|
h: maxY - minY
|
|
}
|
|
};
|
|
}
|
|
|
|
function rectForPlacement(model, inst) {
|
|
const sym = model.symbols[inst.symbol];
|
|
return {
|
|
x: inst.placement.x,
|
|
y: inst.placement.y,
|
|
w: sym?.body?.width ?? 120,
|
|
h: sym?.body?.height ?? 80
|
|
};
|
|
}
|
|
|
|
function boxesOverlap(a, b, pad = 12) {
|
|
return !(
|
|
a.x + a.w + pad <= b.x ||
|
|
b.x + b.w + pad <= a.x ||
|
|
a.y + a.h + pad <= b.y ||
|
|
b.y + b.h + pad <= a.y
|
|
);
|
|
}
|
|
|
|
function resolvePlacementOverlaps(model, placedMap, options = {}) {
|
|
const respectLocks = options.respectLocks ?? true;
|
|
const refs = [...placedMap.keys()].sort();
|
|
const iterations = Math.max(1, refs.length * 3);
|
|
|
|
for (let pass = 0; pass < iterations; pass += 1) {
|
|
let moved = false;
|
|
for (let i = 0; i < refs.length; i += 1) {
|
|
const aRef = refs[i];
|
|
const aInst = placedMap.get(aRef);
|
|
if (!aInst) {
|
|
continue;
|
|
}
|
|
const aLocked = respectLocks ? Boolean(aInst.placement.locked) : false;
|
|
let aBox = rectForPlacement(model, aInst);
|
|
for (let j = i + 1; j < refs.length; j += 1) {
|
|
const bRef = refs[j];
|
|
const bInst = placedMap.get(bRef);
|
|
if (!bInst) {
|
|
continue;
|
|
}
|
|
const bLocked = respectLocks ? Boolean(bInst.placement.locked) : false;
|
|
const bBox = rectForPlacement(model, bInst);
|
|
if (!boxesOverlap(aBox, bBox, 14)) {
|
|
continue;
|
|
}
|
|
|
|
const target = !aLocked ? aInst : !bLocked ? bInst : null;
|
|
if (!target) {
|
|
continue;
|
|
}
|
|
const oldX = target.placement.x;
|
|
const oldY = target.placement.y;
|
|
const overlapX = Math.max(
|
|
0,
|
|
Math.min(aBox.x + aBox.w, bBox.x + bBox.w) - Math.max(aBox.x, bBox.x)
|
|
);
|
|
const overlapY = Math.max(
|
|
0,
|
|
Math.min(aBox.y + aBox.h, bBox.y + bBox.h) - Math.max(aBox.y, bBox.y)
|
|
);
|
|
const targetBox = rectForPlacement(model, target);
|
|
const otherBox = target === aInst ? bBox : aBox;
|
|
const targetCx = targetBox.x + targetBox.w / 2;
|
|
const targetCy = targetBox.y + targetBox.h / 2;
|
|
const otherCx = otherBox.x + otherBox.w / 2;
|
|
const otherCy = otherBox.y + otherBox.h / 2;
|
|
|
|
if (overlapX <= overlapY) {
|
|
const dir = targetCx >= otherCx ? 1 : -1;
|
|
const push = toGrid(overlapX + 64) * dir;
|
|
target.placement.x = Math.max(MARGIN_X, toGrid(target.placement.x + push));
|
|
} else {
|
|
const dir = targetCy >= otherCy ? 1 : -1;
|
|
const push = toGrid(overlapY + 64) * dir;
|
|
target.placement.y = Math.max(MARGIN_Y, toGrid(target.placement.y + push));
|
|
}
|
|
if (target.placement.x === oldX && target.placement.y === oldY) {
|
|
const fallbackDir = targetCy >= otherCy ? 1 : -1;
|
|
const fallbackPush = toGrid(Math.max(overlapY, overlapX) + 84) * fallbackDir;
|
|
target.placement.y = Math.max(MARGIN_Y, toGrid(target.placement.y + fallbackPush));
|
|
}
|
|
if (target === aInst) {
|
|
aBox = rectForPlacement(model, aInst);
|
|
}
|
|
moved = true;
|
|
}
|
|
}
|
|
if (!moved) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
function enforceFinalComponentSeparation(model, placedMap, options = {}) {
|
|
const respectLocks = options.respectLocks ?? true;
|
|
const refs = [...placedMap.keys()].sort();
|
|
const maxPasses = Math.max(1, refs.length * 2);
|
|
|
|
for (let pass = 0; pass < maxPasses; pass += 1) {
|
|
let changed = false;
|
|
for (let i = 0; i < refs.length; i += 1) {
|
|
const aRef = refs[i];
|
|
const aInst = placedMap.get(aRef);
|
|
if (!aInst) continue;
|
|
const aLocked = respectLocks ? Boolean(aInst.placement.locked) : false;
|
|
const aBox = rectForPlacement(model, aInst);
|
|
for (let j = i + 1; j < refs.length; j += 1) {
|
|
const bRef = refs[j];
|
|
const bInst = placedMap.get(bRef);
|
|
if (!bInst) continue;
|
|
const bLocked = respectLocks ? Boolean(bInst.placement.locked) : false;
|
|
const bBox = rectForPlacement(model, bInst);
|
|
if (!boxesOverlap(aBox, bBox, 8)) {
|
|
continue;
|
|
}
|
|
|
|
let mover = null;
|
|
let anchor = null;
|
|
if (!aLocked && bLocked) {
|
|
mover = aInst;
|
|
anchor = bInst;
|
|
} else if (aLocked && !bLocked) {
|
|
mover = bInst;
|
|
anchor = aInst;
|
|
} else if (!aLocked && !bLocked) {
|
|
mover = bRef.localeCompare(aRef) >= 0 ? bInst : aInst;
|
|
anchor = mover === bInst ? aInst : bInst;
|
|
} else {
|
|
continue;
|
|
}
|
|
|
|
const mBox = rectForPlacement(model, mover);
|
|
const kBox = rectForPlacement(model, anchor);
|
|
const moveRight = mBox.x >= kBox.x;
|
|
const stepX = toGrid(Math.max(mBox.w, kBox.w) + 70) * (moveRight ? 1 : -1);
|
|
const oldX = mover.placement.x;
|
|
mover.placement.x = Math.max(MARGIN_X, toGrid(mover.placement.x + stepX));
|
|
if (mover.placement.x === oldX) {
|
|
mover.placement.y = Math.max(MARGIN_Y, toGrid(mover.placement.y + toGrid(Math.max(mBox.h, kBox.h) + 70)));
|
|
}
|
|
changed = true;
|
|
}
|
|
}
|
|
if (!changed) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
function buildNodeNetMap(model) {
|
|
const map = new Map();
|
|
for (const net of model.nets) {
|
|
for (const node of net.nodes ?? []) {
|
|
const key = `${node.ref}.${node.pin}`;
|
|
const list = map.get(key) ?? [];
|
|
list.push(net);
|
|
map.set(key, list);
|
|
}
|
|
}
|
|
return map;
|
|
}
|
|
|
|
function scoreInstanceRotation(model, placedMap, inst, rotation, nodeNetMap) {
|
|
const sym = model.symbols[inst.symbol];
|
|
if (!sym || !Array.isArray(sym.pins)) {
|
|
return Number.POSITIVE_INFINITY;
|
|
}
|
|
|
|
const temp = {
|
|
...inst,
|
|
placement: {
|
|
...inst.placement,
|
|
rotation
|
|
}
|
|
};
|
|
|
|
let score = 0;
|
|
for (const pin of sym.pins) {
|
|
const key = `${inst.ref}.${pin.name}`;
|
|
const nets = nodeNetMap.get(key) ?? [];
|
|
if (!nets.length) {
|
|
continue;
|
|
}
|
|
|
|
const p = pinPoint(temp, pin, sym.body.width, sym.body.height);
|
|
for (const net of nets) {
|
|
const others = (net.nodes ?? []).filter((n) => !(n.ref === inst.ref && n.pin === pin.name));
|
|
if (!others.length) {
|
|
continue;
|
|
}
|
|
|
|
let cx = 0;
|
|
let cy = 0;
|
|
let count = 0;
|
|
|
|
for (const n of others) {
|
|
const oi = placedMap.get(n.ref);
|
|
if (!oi) {
|
|
continue;
|
|
}
|
|
const os = model.symbols[oi.symbol];
|
|
const op = os?.pins?.find((pp) => pp.name === n.pin);
|
|
if (!os || !op) {
|
|
continue;
|
|
}
|
|
const q = pinPoint(oi, op, os.body.width, os.body.height);
|
|
cx += q.x;
|
|
cy += q.y;
|
|
count += 1;
|
|
}
|
|
|
|
if (!count) {
|
|
continue;
|
|
}
|
|
|
|
const tx = cx / count;
|
|
const ty = cy / count;
|
|
score += Math.abs(p.x - tx) + Math.abs(p.y - ty);
|
|
}
|
|
}
|
|
|
|
return score;
|
|
}
|
|
|
|
function applyAutoRotation(model, placedMap, options = {}) {
|
|
const allow = options.autoRotate !== false;
|
|
if (!allow) {
|
|
return;
|
|
}
|
|
|
|
const nodeNetMap = buildNodeNetMap(model);
|
|
const refs = [...placedMap.keys()].sort();
|
|
for (const ref of refs) {
|
|
const inst = placedMap.get(ref);
|
|
if (!inst || inst.placement.locked) {
|
|
continue;
|
|
}
|
|
|
|
let bestRot = normalizeRotation(inst.placement.rotation ?? 0);
|
|
let bestScore = scoreInstanceRotation(model, placedMap, inst, bestRot, nodeNetMap);
|
|
|
|
for (const rot of ROTATION_STEPS) {
|
|
const s = scoreInstanceRotation(model, placedMap, inst, rot, nodeNetMap);
|
|
if (s < bestScore - 0.001) {
|
|
bestScore = s;
|
|
bestRot = rot;
|
|
}
|
|
}
|
|
|
|
inst.placement.rotation = bestRot;
|
|
placedMap.set(ref, inst);
|
|
}
|
|
}
|
|
|
|
function placeInstances(model, options = {}) {
|
|
const respectLocks = options.respectLocks ?? true;
|
|
const instances = [...model.instances].sort((a, b) => a.ref.localeCompare(b.ref));
|
|
const { rank } = computeRanks(model);
|
|
const degree = connectivityDegree(model);
|
|
const laneProfiles = refLaneProfiles(model);
|
|
const instanceByRef = buildInstanceMap(instances);
|
|
const groups = buildConstraintGroups(model, rank);
|
|
|
|
const placed = [];
|
|
const placedMap = new Map();
|
|
const groupsPerRow = groups.length <= 3 ? groups.length : groups.length >= 6 ? 3 : 2;
|
|
const groupCellW = 620;
|
|
const groupCellH = 420;
|
|
|
|
for (let i = 0; i < groups.length; i += 1) {
|
|
const group = groups[i];
|
|
const row = groupsPerRow ? Math.floor(i / groupsPerRow) : 0;
|
|
const col = groupsPerRow ? i % groupsPerRow : 0;
|
|
const origin = {
|
|
x: toGrid(MARGIN_X + col * groupCellW),
|
|
y: toGrid(MARGIN_Y + row * groupCellH)
|
|
};
|
|
|
|
const out = placeGroup(model, group, origin, {
|
|
rank,
|
|
degree,
|
|
laneProfiles,
|
|
instanceByRef,
|
|
respectLocks
|
|
});
|
|
|
|
for (const inst of out.placed) {
|
|
placed.push(inst);
|
|
placedMap.set(inst.ref, inst);
|
|
}
|
|
}
|
|
|
|
applyAutoRotation(model, placedMap, {
|
|
autoRotate: options.autoRotate ?? true
|
|
});
|
|
|
|
compactPlacementByConnectivity(model, placedMap, {
|
|
respectLocks,
|
|
laneProfiles,
|
|
rank
|
|
});
|
|
if (model.instances.length >= 8 && model.instances.length <= 220) {
|
|
tightenPassiveAdjacency(model, placedMap, { respectLocks });
|
|
tightenConstraintGroups(model, placedMap, { respectLocks });
|
|
}
|
|
|
|
applyAlignmentConstraints(placedMap, model.constraints);
|
|
applyNearConstraints(model, placedMap, model.constraints);
|
|
resolvePlacementOverlaps(model, placedMap, { respectLocks });
|
|
enforceFinalComponentSeparation(model, placedMap, { respectLocks });
|
|
|
|
const normalizedPlaced = instances
|
|
.map((inst) => placedMap.get(inst.ref) ?? inst)
|
|
.sort((a, b) => a.ref.localeCompare(b.ref));
|
|
|
|
return { placed: normalizedPlaced, placedMap };
|
|
}
|
|
|
|
function buildObstacles(model, placed) {
|
|
return placed.map((inst) => {
|
|
const sym = model.symbols[inst.symbol];
|
|
return {
|
|
ref: inst.ref,
|
|
x: inst.placement.x - OBSTACLE_PADDING,
|
|
y: inst.placement.y - OBSTACLE_PADDING,
|
|
w: sym.body.width + OBSTACLE_PADDING * 2,
|
|
h: sym.body.height + OBSTACLE_PADDING * 2
|
|
};
|
|
});
|
|
}
|
|
|
|
function between(value, min, max) {
|
|
return value >= min && value <= max;
|
|
}
|
|
|
|
function segmentIntersectsBox(a, b, box) {
|
|
if (a.x === b.x) {
|
|
if (!between(a.x, box.x, box.x + box.w)) {
|
|
return false;
|
|
}
|
|
const minY = Math.min(a.y, b.y);
|
|
const maxY = Math.max(a.y, b.y);
|
|
return !(maxY < box.y || minY > box.y + box.h);
|
|
}
|
|
|
|
if (a.y === b.y) {
|
|
if (!between(a.y, box.y, box.y + box.h)) {
|
|
return false;
|
|
}
|
|
const minX = Math.min(a.x, b.x);
|
|
const maxX = Math.max(a.x, b.x);
|
|
return !(maxX < box.x || minX > box.x + box.w);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function buildBounds(model, placed) {
|
|
const maxX = Math.max(...placed.map((p) => p.placement.x + model.symbols[p.symbol].body.width), MARGIN_X * 3);
|
|
const maxY = Math.max(...placed.map((p) => p.placement.y + model.symbols[p.symbol].body.height), MARGIN_Y * 3);
|
|
|
|
return {
|
|
minX: 0,
|
|
minY: 0,
|
|
maxX: toGrid(maxX + MARGIN_X * 2),
|
|
maxY: toGrid(maxY + MARGIN_Y * 2)
|
|
};
|
|
}
|
|
|
|
function segmentBlocked(a, b, obstacles, allowedRefs) {
|
|
for (const box of obstacles) {
|
|
if (allowedRefs.has(box.ref)) {
|
|
continue;
|
|
}
|
|
if (segmentIntersectsBox(a, b, box)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function heuristic(a, b) {
|
|
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
|
|
}
|
|
|
|
function reconstructPath(cameFrom, currentKey) {
|
|
const out = [];
|
|
let key = currentKey;
|
|
while (key) {
|
|
const node = cameFrom.get(key)?.node;
|
|
if (!node) {
|
|
const [x, y] = key.split(",").map(Number);
|
|
out.push({ x, y });
|
|
break;
|
|
}
|
|
out.push(node);
|
|
key = cameFrom.get(key)?.prev;
|
|
}
|
|
out.reverse();
|
|
return out;
|
|
}
|
|
|
|
function hasForeignEdgeUsage(edgeUsage, netName, a, b) {
|
|
const usage = edgeUsage.get(edgeKey(a, b));
|
|
if (!usage) {
|
|
return false;
|
|
}
|
|
|
|
for (const existingNet of usage.byNet.keys()) {
|
|
if (existingNet !== netName) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function hasForeignPointUsage(pointUsage, netName, point) {
|
|
const usage = pointUsage.get(pointKey(point));
|
|
if (!usage) {
|
|
return false;
|
|
}
|
|
|
|
for (const existingNet of usage.keys()) {
|
|
if (existingNet !== netName) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function foreignUsageCount(usageMap, key, ownNet) {
|
|
const usage = usageMap.get(key);
|
|
if (!usage) {
|
|
return 0;
|
|
}
|
|
|
|
let count = 0;
|
|
for (const [net, n] of usage.entries()) {
|
|
if (net !== ownNet) {
|
|
count += n;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
function channelCrowdingPenalty(current, next, context) {
|
|
const { netName, hLineUsage, vLineUsage } = context;
|
|
if (current.x !== next.x && current.y !== next.y) {
|
|
return 0;
|
|
}
|
|
|
|
const isHorizontal = current.y === next.y;
|
|
const lineUsage = isHorizontal ? hLineUsage : vLineUsage;
|
|
const lineCoord = isHorizontal ? current.y : current.x;
|
|
|
|
let penalty = 0;
|
|
for (let step = -MIN_CHANNEL_SPACING_STEPS; step <= MIN_CHANNEL_SPACING_STEPS; step += 1) {
|
|
const coord = lineCoord + step * GRID;
|
|
const foreign = foreignUsageCount(lineUsage, coord, netName);
|
|
if (!foreign) {
|
|
continue;
|
|
}
|
|
|
|
const distance = Math.abs(step);
|
|
const weight = distance === 0 ? 34 : distance === 1 ? 17 : 7;
|
|
penalty += foreign * weight;
|
|
}
|
|
|
|
return penalty;
|
|
}
|
|
|
|
function pointCongestionPenalty(point, context) {
|
|
const { pointUsage, netName } = context;
|
|
let penalty = 0;
|
|
for (const nb of [
|
|
{ x: point.x, y: point.y },
|
|
{ x: point.x + GRID, y: point.y },
|
|
{ x: point.x - GRID, y: point.y },
|
|
{ x: point.x, y: point.y + GRID },
|
|
{ x: point.x, y: point.y - GRID }
|
|
]) {
|
|
const foreign = foreignUsageCount(pointUsage, pointKey(nb), netName);
|
|
if (foreign) {
|
|
penalty += foreign * 10;
|
|
}
|
|
}
|
|
return penalty;
|
|
}
|
|
|
|
function proximityPenalty(point, obstacles, allowedRefs) {
|
|
let penalty = 0;
|
|
for (const box of obstacles) {
|
|
if (allowedRefs.has(box.ref)) {
|
|
continue;
|
|
}
|
|
|
|
const dx = Math.max(box.x - point.x, 0, point.x - (box.x + box.w));
|
|
const dy = Math.max(box.y - point.y, 0, point.y - (box.y + box.h));
|
|
const dist = dx + dy;
|
|
if (dist < GRID * 4) {
|
|
penalty += (GRID * 4 - dist) * 0.7;
|
|
}
|
|
}
|
|
return penalty;
|
|
}
|
|
|
|
function aStar(start, goal, context) {
|
|
const { bounds, obstacles, allowedRefs, edgeUsage, pointUsage, netName } = context;
|
|
|
|
const startNode = { x: toGrid(start.x), y: toGrid(start.y) };
|
|
const goalNode = { x: toGrid(goal.x), y: toGrid(goal.y) };
|
|
|
|
const open = new Map();
|
|
const gScore = new Map();
|
|
const cameFrom = new Map();
|
|
|
|
const startKey = pointKey(startNode);
|
|
open.set(startKey, { ...startNode, dir: null, f: heuristic(startNode, goalNode) });
|
|
gScore.set(startKey, 0);
|
|
cameFrom.set(startKey, { prev: null, node: startNode, dir: null });
|
|
|
|
const maxIterations = 30000;
|
|
let iterations = 0;
|
|
|
|
while (open.size > 0 && iterations < maxIterations) {
|
|
iterations += 1;
|
|
|
|
let current = null;
|
|
for (const cand of open.values()) {
|
|
if (!current || cand.f < current.f) {
|
|
current = cand;
|
|
}
|
|
}
|
|
|
|
if (!current) {
|
|
break;
|
|
}
|
|
|
|
const currentKey = pointKey(current);
|
|
if (current.x === goalNode.x && current.y === goalNode.y) {
|
|
return reconstructPath(cameFrom, currentKey);
|
|
}
|
|
|
|
open.delete(currentKey);
|
|
|
|
const neighbors = [
|
|
{ x: current.x + GRID, y: current.y, dir: "h" },
|
|
{ x: current.x - GRID, y: current.y, dir: "h" },
|
|
{ x: current.x, y: current.y + GRID, dir: "v" },
|
|
{ x: current.x, y: current.y - GRID, dir: "v" }
|
|
];
|
|
|
|
for (const nb of neighbors) {
|
|
if (nb.x < bounds.minX || nb.x > bounds.maxX || nb.y < bounds.minY || nb.y > bounds.maxY) {
|
|
continue;
|
|
}
|
|
|
|
if (segmentBlocked(current, nb, obstacles, allowedRefs)) {
|
|
continue;
|
|
}
|
|
|
|
if (hasForeignEdgeUsage(edgeUsage, netName, current, nb)) {
|
|
continue;
|
|
}
|
|
|
|
const isGoal = nb.x === goalNode.x && nb.y === goalNode.y;
|
|
if (!isGoal && hasForeignPointUsage(pointUsage, netName, nb)) {
|
|
continue;
|
|
}
|
|
|
|
const nbKey = pointKey(nb);
|
|
const prevCost = gScore.get(currentKey) ?? Number.POSITIVE_INFINITY;
|
|
const turnPenalty = current.dir && current.dir !== nb.dir ? 24 : 0;
|
|
const obstaclePenalty = proximityPenalty(nb, obstacles, allowedRefs);
|
|
const channelPenalty = channelCrowdingPenalty(current, nb, context);
|
|
const congestionPenalty = pointCongestionPenalty(nb, context);
|
|
const tentative = prevCost + GRID + turnPenalty + obstaclePenalty + channelPenalty + congestionPenalty;
|
|
|
|
if (tentative >= (gScore.get(nbKey) ?? Number.POSITIVE_INFINITY)) {
|
|
continue;
|
|
}
|
|
|
|
gScore.set(nbKey, tentative);
|
|
open.set(nbKey, {
|
|
...nb,
|
|
f: tentative + heuristic(nb, goalNode)
|
|
});
|
|
cameFrom.set(nbKey, {
|
|
prev: currentKey,
|
|
node: { x: nb.x, y: nb.y },
|
|
dir: nb.dir
|
|
});
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function pointsToSegments(points) {
|
|
const segments = [];
|
|
for (let i = 1; i < points.length; i += 1) {
|
|
const a = points[i - 1];
|
|
const b = points[i];
|
|
if (a.x === b.x && a.y === b.y) {
|
|
continue;
|
|
}
|
|
segments.push({ a: { ...a }, b: { ...b } });
|
|
}
|
|
return simplifySegments(segments);
|
|
}
|
|
|
|
function simplifySegments(segments) {
|
|
const out = [];
|
|
|
|
for (const seg of segments) {
|
|
const prev = out[out.length - 1];
|
|
if (!prev) {
|
|
out.push(seg);
|
|
continue;
|
|
}
|
|
|
|
const prevVertical = prev.a.x === prev.b.x;
|
|
const currVertical = seg.a.x === seg.b.x;
|
|
if (prevVertical === currVertical && prev.b.x === seg.a.x && prev.b.y === seg.a.y) {
|
|
prev.b = { ...seg.b };
|
|
continue;
|
|
}
|
|
|
|
out.push(seg);
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
function segmentStepPoints(a, b) {
|
|
const points = [];
|
|
|
|
if (a.x === b.x) {
|
|
const step = a.y < b.y ? GRID : -GRID;
|
|
for (let y = a.y; step > 0 ? y <= b.y : y >= b.y; y += step) {
|
|
points.push({ x: a.x, y });
|
|
}
|
|
return points;
|
|
}
|
|
|
|
if (a.y === b.y) {
|
|
const step = a.x < b.x ? GRID : -GRID;
|
|
for (let x = a.x; step > 0 ? x <= b.x : x >= b.x; x += step) {
|
|
points.push({ x, y: a.y });
|
|
}
|
|
return points;
|
|
}
|
|
|
|
return [a, b];
|
|
}
|
|
|
|
function addLineUsage(lineUsage, coord, netName) {
|
|
const usage = lineUsage.get(coord) ?? new Map();
|
|
usage.set(netName, (usage.get(netName) ?? 0) + 1);
|
|
lineUsage.set(coord, usage);
|
|
}
|
|
|
|
function addUsageForSegments(edgeUsage, pointUsage, hLineUsage, vLineUsage, netName, segments) {
|
|
for (const seg of segments) {
|
|
const stepPoints = segmentStepPoints(seg.a, seg.b);
|
|
|
|
for (let i = 1; i < stepPoints.length; i += 1) {
|
|
const p0 = stepPoints[i - 1];
|
|
const p1 = stepPoints[i];
|
|
const eKey = edgeKey(p0, p1);
|
|
const edge = edgeUsage.get(eKey) ?? { total: 0, byNet: new Map() };
|
|
edge.total += 1;
|
|
edge.byNet.set(netName, (edge.byNet.get(netName) ?? 0) + 1);
|
|
edgeUsage.set(eKey, edge);
|
|
}
|
|
|
|
if (seg.a.y === seg.b.y) {
|
|
addLineUsage(hLineUsage, seg.a.y, netName);
|
|
} else if (seg.a.x === seg.b.x) {
|
|
addLineUsage(vLineUsage, seg.a.x, netName);
|
|
}
|
|
|
|
for (const p of stepPoints) {
|
|
const pKey = pointKey(p);
|
|
const usage = pointUsage.get(pKey) ?? new Map();
|
|
usage.set(netName, (usage.get(netName) ?? 0) + 1);
|
|
pointUsage.set(pKey, usage);
|
|
}
|
|
}
|
|
}
|
|
|
|
function computeJunctionPoints(routes) {
|
|
const degree = new Map();
|
|
|
|
for (const route of routes) {
|
|
for (const seg of route) {
|
|
const aKey = pointKey(seg.a);
|
|
const bKey = pointKey(seg.b);
|
|
degree.set(aKey, (degree.get(aKey) ?? 0) + 1);
|
|
degree.set(bKey, (degree.get(bKey) ?? 0) + 1);
|
|
}
|
|
}
|
|
|
|
const out = [];
|
|
for (const [key, deg] of degree.entries()) {
|
|
if (deg >= 3) {
|
|
const [x, y] = key.split(",").map(Number);
|
|
out.push({ x, y });
|
|
}
|
|
}
|
|
|
|
out.sort((a, b) => a.x - b.x || a.y - b.y);
|
|
return out;
|
|
}
|
|
|
|
function labelPointForPin(pin) {
|
|
if (pin.side === "left") {
|
|
return { x: pin.exit.x - GRID * 2.2, y: pin.exit.y - GRID * 0.4 };
|
|
}
|
|
if (pin.side === "right") {
|
|
return { x: pin.exit.x + GRID * 0.6, y: pin.exit.y - GRID * 0.4 };
|
|
}
|
|
if (pin.side === "top") {
|
|
return { x: pin.exit.x + GRID * 0.3, y: pin.exit.y - GRID * 0.8 };
|
|
}
|
|
return { x: pin.exit.x + GRID * 0.3, y: pin.exit.y + GRID * 0.9 };
|
|
}
|
|
|
|
function chooseEndpoints(pinNodes) {
|
|
if (pinNodes.length < 2) {
|
|
return null;
|
|
}
|
|
|
|
const priority = (pinType) => {
|
|
if (pinType === "power_out") return 0;
|
|
if (pinType === "output") return 1;
|
|
if (pinType === "bidirectional") return 2;
|
|
if (pinType === "analog") return 3;
|
|
return 4;
|
|
};
|
|
|
|
const sorted = [...pinNodes].sort((a, b) => {
|
|
const pa = priority(a.pinType);
|
|
const pb = priority(b.pinType);
|
|
if (pa !== pb) {
|
|
return pa - pb;
|
|
}
|
|
return a.exit.x - b.exit.x || a.exit.y - b.exit.y;
|
|
});
|
|
|
|
return { source: sorted[0], target: sorted[1] };
|
|
}
|
|
|
|
function pinRoutePriority(pin) {
|
|
if (pin.pinType === "power_out") return 0;
|
|
if (pin.pinType === "output") return 1;
|
|
if (pin.pinType === "bidirectional") return 2;
|
|
if (pin.pinType === "analog") return 3;
|
|
return 4;
|
|
}
|
|
|
|
function uniquePoints(points) {
|
|
const map = new Map();
|
|
for (const p of points) {
|
|
map.set(pointKey(p), p);
|
|
}
|
|
return [...map.values()];
|
|
}
|
|
|
|
function routeLabelTieNet(net, pinNodes, context, fallbackReason = null) {
|
|
const routes = [];
|
|
const tiePoints = [];
|
|
|
|
for (const pin of pinNodes) {
|
|
const stub = pointsToSegments([pin.point, pin.exit]);
|
|
if (stub.length) {
|
|
routes.push(stub);
|
|
addUsageForSegments(
|
|
context.edgeUsage,
|
|
context.pointUsage,
|
|
context.hLineUsage,
|
|
context.vLineUsage,
|
|
net.name,
|
|
stub
|
|
);
|
|
}
|
|
tiePoints.push({ x: pin.exit.x, y: pin.exit.y });
|
|
}
|
|
|
|
const labelPoints = [];
|
|
if (tiePoints.length) {
|
|
const centroid = {
|
|
x: tiePoints.reduce((sum, p) => sum + p.x, 0) / tiePoints.length,
|
|
y: tiePoints.reduce((sum, p) => sum + p.y, 0) / tiePoints.length
|
|
};
|
|
labelPoints.push({
|
|
x: toGrid(centroid.x + GRID * 0.6),
|
|
y: toGrid(centroid.y - GRID * 0.8)
|
|
});
|
|
|
|
const anchorPin = [...pinNodes].sort((a, b) => a.exit.x - b.exit.x || a.exit.y - b.exit.y)[0];
|
|
if (anchorPin) {
|
|
labelPoints.push(labelPointForPin(anchorPin));
|
|
}
|
|
}
|
|
|
|
return {
|
|
mode: "label_tie",
|
|
routes,
|
|
labelPoints,
|
|
tiePoints,
|
|
junctionPoints: [],
|
|
route_stats: {
|
|
total_length: routeLengthFromSegments(routes),
|
|
direct_length: 0,
|
|
total_bends: 0,
|
|
detour_ratio: 1,
|
|
used_label_tie: true,
|
|
fallback_reason: fallbackReason
|
|
}
|
|
};
|
|
}
|
|
|
|
function pathLength(points) {
|
|
let length = 0;
|
|
for (let i = 1; i < points.length; i += 1) {
|
|
length += Math.abs(points[i].x - points[i - 1].x) + Math.abs(points[i].y - points[i - 1].y);
|
|
}
|
|
return length;
|
|
}
|
|
|
|
function routeLengthFromSegments(routes) {
|
|
let length = 0;
|
|
for (const route of routes) {
|
|
for (const seg of route) {
|
|
length += Math.abs(seg.a.x - seg.b.x) + Math.abs(seg.a.y - seg.b.y);
|
|
}
|
|
}
|
|
return length;
|
|
}
|
|
|
|
function countBendsInRoute(routes) {
|
|
let bends = 0;
|
|
for (const route of routes) {
|
|
for (let i = 1; i < route.length; i += 1) {
|
|
const prev = route[i - 1];
|
|
const curr = route[i];
|
|
const prevH = prev.a.y === prev.b.y;
|
|
const currH = curr.a.y === curr.b.y;
|
|
if (prevH !== currH) {
|
|
bends += 1;
|
|
}
|
|
}
|
|
}
|
|
return bends;
|
|
}
|
|
|
|
function routePointToPointNet(net, pinNodes, context) {
|
|
if (pinNodes.length < 2) {
|
|
return {
|
|
mode: "routed",
|
|
routes: [],
|
|
labelPoints: [],
|
|
tiePoints: [],
|
|
junctionPoints: [],
|
|
route_stats: {
|
|
total_length: 0,
|
|
direct_length: 0,
|
|
total_bends: 0,
|
|
detour_ratio: 1,
|
|
used_label_tie: false,
|
|
fallback_reason: null
|
|
}
|
|
};
|
|
}
|
|
|
|
const sorted = [...pinNodes].sort((a, b) => {
|
|
const pa = pinRoutePriority(a);
|
|
const pb = pinRoutePriority(b);
|
|
if (pa !== pb) {
|
|
return pa - pb;
|
|
}
|
|
return a.exit.x - b.exit.x || a.exit.y - b.exit.y;
|
|
});
|
|
|
|
const source = sorted[0];
|
|
const routes = [];
|
|
const sourceStub = pointsToSegments([source.point, source.exit]);
|
|
if (sourceStub.length) {
|
|
routes.push(sourceStub);
|
|
addUsageForSegments(
|
|
context.edgeUsage,
|
|
context.pointUsage,
|
|
context.hLineUsage,
|
|
context.vLineUsage,
|
|
net.name,
|
|
sourceStub
|
|
);
|
|
}
|
|
|
|
const treePoints = new Map();
|
|
treePoints.set(pointKey(source.exit), source.exit);
|
|
|
|
const allowedRefs = new Set(sorted.map((p) => p.ref));
|
|
const remaining = sorted.slice(1);
|
|
let accumulatedPath = 0;
|
|
let accumulatedDirect = 0;
|
|
|
|
for (const target of remaining) {
|
|
const candidates = uniquePoints([...treePoints.values()])
|
|
.sort((a, b) => heuristic(target.exit, a) - heuristic(target.exit, b))
|
|
.slice(0, 12);
|
|
|
|
let best = null;
|
|
for (const attach of candidates) {
|
|
const path = aStar(target.exit, attach, {
|
|
...context,
|
|
allowedRefs,
|
|
netName: net.name
|
|
});
|
|
if (!path) {
|
|
continue;
|
|
}
|
|
|
|
const cost = pathLength(path);
|
|
if (!best || cost < best.cost) {
|
|
best = { path, attach, cost };
|
|
}
|
|
}
|
|
|
|
if (!best) {
|
|
return {
|
|
...routeLabelTieNet(net, sorted, context),
|
|
route_stats: {
|
|
total_length: 0,
|
|
direct_length: 0,
|
|
total_bends: 0,
|
|
detour_ratio: 1,
|
|
used_label_tie: true,
|
|
fallback_reason: "no_path"
|
|
}
|
|
};
|
|
}
|
|
|
|
const direct = heuristic(target.exit, best.attach);
|
|
accumulatedPath += best.cost;
|
|
accumulatedDirect += Math.max(GRID, direct);
|
|
if (context.renderMode === "schematic_stub" && best.cost > direct * 1.6) {
|
|
return {
|
|
...routeLabelTieNet(net, sorted, context),
|
|
route_stats: {
|
|
total_length: accumulatedPath,
|
|
direct_length: accumulatedDirect,
|
|
total_bends: 0,
|
|
detour_ratio: accumulatedPath / Math.max(GRID, accumulatedDirect),
|
|
used_label_tie: true,
|
|
fallback_reason: "branch_detour"
|
|
}
|
|
};
|
|
}
|
|
|
|
const branchPoints = [target.point, target.exit, ...best.path.slice(1)];
|
|
const branchSegments = pointsToSegments(branchPoints);
|
|
if (branchSegments.length) {
|
|
routes.push(branchSegments);
|
|
addUsageForSegments(
|
|
context.edgeUsage,
|
|
context.pointUsage,
|
|
context.hLineUsage,
|
|
context.vLineUsage,
|
|
net.name,
|
|
branchSegments
|
|
);
|
|
for (const seg of branchSegments) {
|
|
for (const p of segmentStepPoints(seg.a, seg.b)) {
|
|
treePoints.set(pointKey(p), p);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const allPathPoints = routes.flatMap((route) => route.flatMap((seg) => [seg.a, seg.b]));
|
|
const centroid = allPathPoints.length
|
|
? {
|
|
x: allPathPoints.reduce((sum, p) => sum + p.x, 0) / allPathPoints.length,
|
|
y: allPathPoints.reduce((sum, p) => sum + p.y, 0) / allPathPoints.length
|
|
}
|
|
: labelPointForPin(source);
|
|
|
|
const labelPoints = [labelPointForPin(source)];
|
|
if (sorted.length >= 3) {
|
|
labelPoints.push({ x: toGrid(centroid.x + GRID * 0.4), y: toGrid(centroid.y - GRID * 0.6) });
|
|
}
|
|
const junctionPoints = computeJunctionPoints(routes);
|
|
if (sorted.length >= 3 && junctionPoints.length === 0) {
|
|
junctionPoints.push({ x: source.exit.x, y: source.exit.y });
|
|
}
|
|
|
|
const totalLength = routeLengthFromSegments(routes);
|
|
const totalBends = countBendsInRoute(routes);
|
|
const directLength = Math.max(GRID, accumulatedDirect || heuristic(source.exit, sorted[sorted.length - 1].exit));
|
|
const detourRatio = totalLength / directLength;
|
|
const maxAllowedBends = Math.max(6, sorted.length * 3);
|
|
if (
|
|
context.renderMode === "schematic_stub" &&
|
|
(detourRatio > 1.9 || totalBends > maxAllowedBends || totalLength > GRID * 180)
|
|
) {
|
|
return {
|
|
...routeLabelTieNet(net, sorted, context),
|
|
route_stats: {
|
|
total_length: totalLength,
|
|
direct_length: directLength,
|
|
total_bends: totalBends,
|
|
detour_ratio: detourRatio,
|
|
used_label_tie: true,
|
|
fallback_reason: "global_quality"
|
|
}
|
|
};
|
|
}
|
|
|
|
return {
|
|
mode: "routed",
|
|
routes,
|
|
labelPoints,
|
|
tiePoints: [],
|
|
junctionPoints,
|
|
route_stats: {
|
|
total_length: totalLength,
|
|
direct_length: directLength,
|
|
total_bends: totalBends,
|
|
detour_ratio: detourRatio,
|
|
used_label_tie: false,
|
|
fallback_reason: null
|
|
}
|
|
};
|
|
}
|
|
|
|
function detectBusGroups(nets) {
|
|
const groups = new Map();
|
|
for (const net of nets) {
|
|
const match = /^([A-Za-z0-9]+)_/.exec(net.name);
|
|
if (!match) {
|
|
continue;
|
|
}
|
|
const key = match[1].toUpperCase();
|
|
const list = groups.get(key) ?? [];
|
|
list.push(net.name);
|
|
groups.set(key, list);
|
|
}
|
|
|
|
const out = [];
|
|
for (const [name, members] of groups.entries()) {
|
|
if (members.length >= 2) {
|
|
out.push({ name, nets: members.sort() });
|
|
}
|
|
}
|
|
|
|
out.sort((a, b) => a.name.localeCompare(b.name));
|
|
return out;
|
|
}
|
|
|
|
function shouldUseLabelTie(net, pinNodes, context) {
|
|
if (!pinNodes.length) {
|
|
return false;
|
|
}
|
|
const minX = Math.min(...pinNodes.map((p) => p.exit.x), 0);
|
|
const maxX = Math.max(...pinNodes.map((p) => p.exit.x), 0);
|
|
const minY = Math.min(...pinNodes.map((p) => p.exit.y), 0);
|
|
const maxY = Math.max(...pinNodes.map((p) => p.exit.y), 0);
|
|
const span = Math.abs(maxX - minX) + Math.abs(maxY - minY);
|
|
|
|
if (context.renderMode === "explicit") {
|
|
if (context.busNetNames.has(net.name) && (pinNodes.length >= 3 || span > GRID * 36)) {
|
|
return true;
|
|
}
|
|
return LABEL_TIE_CLASSES.has(net.class) && pinNodes.length > 2;
|
|
}
|
|
|
|
if (LABEL_TIE_CLASSES.has(net.class)) {
|
|
return true;
|
|
}
|
|
|
|
if (context.busNetNames.has(net.name)) {
|
|
return true;
|
|
}
|
|
|
|
// Prefer readable schematic stubs for dense multi-node nets.
|
|
if (pinNodes.length >= 5) {
|
|
return true;
|
|
}
|
|
|
|
// Long-distributed analog/signal nets become noisy when fully routed.
|
|
if ((net.class === "analog" || net.class === "signal") && pinNodes.length >= 3 && span > GRID * 56) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function shouldFallbackToTieByQuality(net, pinNodes, routed) {
|
|
if (!routed || routed.mode !== "routed") {
|
|
return false;
|
|
}
|
|
const stats = routed.route_stats ?? {};
|
|
const detour = Number(stats.detour_ratio ?? 1);
|
|
const bends = Number(stats.total_bends ?? 0);
|
|
const totalLength = Number(stats.total_length ?? 0);
|
|
const directLength = Number(stats.direct_length ?? totalLength);
|
|
const spanRatio = directLength > 0 ? totalLength / directLength : 1;
|
|
|
|
if (pinNodes.length >= 4 && (detour > 2.25 || bends >= 7)) {
|
|
return true;
|
|
}
|
|
if ((net.class === "analog" || net.class === "signal") && pinNodes.length >= 3 && spanRatio > 2.7) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function routeAllNets(model, placed, placedMap, bounds, options) {
|
|
const obstacles = buildObstacles(model, placed);
|
|
const edgeUsage = new Map();
|
|
const pointUsage = new Map();
|
|
const hLineUsage = new Map();
|
|
const vLineUsage = new Map();
|
|
const busGroups = detectBusGroups(model.nets);
|
|
const busNetNames = new Set(busGroups.flatMap((g) => g.nets));
|
|
|
|
const nets = [...model.nets].sort((a, b) => {
|
|
const pa = NET_CLASS_PRIORITY[a.class] ?? 99;
|
|
const pb = NET_CLASS_PRIORITY[b.class] ?? 99;
|
|
if (pa !== pb) {
|
|
return pa - pb;
|
|
}
|
|
return a.name.localeCompare(b.name);
|
|
});
|
|
|
|
const routedByName = new Map();
|
|
|
|
for (const net of nets) {
|
|
const pinNodes = net.nodes.map((node) => getNodePin(model, placedMap, node)).filter(Boolean);
|
|
|
|
const routeContext = {
|
|
bounds,
|
|
obstacles,
|
|
edgeUsage,
|
|
pointUsage,
|
|
hLineUsage,
|
|
vLineUsage,
|
|
renderMode: options.renderMode,
|
|
busNetNames
|
|
};
|
|
|
|
let routed = shouldUseLabelTie(net, pinNodes, routeContext)
|
|
? routeLabelTieNet(net, pinNodes, routeContext)
|
|
: routePointToPointNet(net, pinNodes, routeContext);
|
|
|
|
if (shouldFallbackToTieByQuality(net, pinNodes, routed)) {
|
|
routed = routeLabelTieNet(net, pinNodes, routeContext, "quality_policy");
|
|
}
|
|
|
|
routedByName.set(net.name, {
|
|
net,
|
|
isBusMember: busNetNames.has(net.name),
|
|
...routed
|
|
});
|
|
}
|
|
|
|
const routed = model.nets.map(
|
|
(net) =>
|
|
routedByName.get(net.name) ?? {
|
|
net,
|
|
mode: "routed",
|
|
routes: [],
|
|
labelPoints: [],
|
|
tiePoints: [],
|
|
junctionPoints: [],
|
|
route_stats: {
|
|
total_length: 0,
|
|
direct_length: 0,
|
|
total_bends: 0,
|
|
detour_ratio: 1,
|
|
used_label_tie: false,
|
|
fallback_reason: null
|
|
}
|
|
}
|
|
);
|
|
|
|
return { routed, busGroups };
|
|
}
|
|
|
|
function lineIntersection(a1, a2, b1, b2) {
|
|
const aVertical = a1.x === a2.x;
|
|
const bVertical = b1.x === b2.x;
|
|
|
|
if (aVertical === bVertical) {
|
|
return null;
|
|
}
|
|
|
|
const v = aVertical ? [a1, a2] : [b1, b2];
|
|
const h = aVertical ? [b1, b2] : [a1, a2];
|
|
|
|
const vx = v[0].x;
|
|
const hy = h[0].y;
|
|
const vMinY = Math.min(v[0].y, v[1].y);
|
|
const vMaxY = Math.max(v[0].y, v[1].y);
|
|
const hMinX = Math.min(h[0].x, h[1].x);
|
|
const hMaxX = Math.max(h[0].x, h[1].x);
|
|
|
|
if (vx >= hMinX && vx <= hMaxX && hy >= vMinY && hy <= vMaxY) {
|
|
return { x: vx, y: hy };
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function collectSegmentsByNet(routed) {
|
|
const out = [];
|
|
for (const rn of routed) {
|
|
for (const route of rn.routes) {
|
|
for (const seg of route) {
|
|
out.push({ net: rn.net.name, seg });
|
|
}
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function countOverlapEdges(routed) {
|
|
const edgeNets = new Map();
|
|
for (const rn of routed) {
|
|
for (const route of rn.routes) {
|
|
for (const seg of route) {
|
|
const steps = segmentStepPoints(seg.a, seg.b);
|
|
for (let i = 1; i < steps.length; i += 1) {
|
|
const k = edgeKey(steps[i - 1], steps[i]);
|
|
const set = edgeNets.get(k) ?? new Set();
|
|
set.add(rn.net.name);
|
|
edgeNets.set(k, set);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let overlaps = 0;
|
|
for (const nets of edgeNets.values()) {
|
|
if (nets.size > 1) {
|
|
overlaps += 1;
|
|
}
|
|
}
|
|
return overlaps;
|
|
}
|
|
|
|
function countCrossings(routed) {
|
|
const segments = collectSegmentsByNet(routed);
|
|
let crossings = 0;
|
|
|
|
for (let i = 0; i < segments.length; i += 1) {
|
|
const a = segments[i];
|
|
for (let j = i + 1; j < segments.length; j += 1) {
|
|
const b = segments[j];
|
|
if (a.net === b.net) {
|
|
continue;
|
|
}
|
|
const hit = lineIntersection(a.seg.a, a.seg.b, b.seg.a, b.seg.b);
|
|
if (!hit) {
|
|
continue;
|
|
}
|
|
const atEndpoint =
|
|
pointKey(hit) === pointKey(a.seg.a) ||
|
|
pointKey(hit) === pointKey(a.seg.b) ||
|
|
pointKey(hit) === pointKey(b.seg.a) ||
|
|
pointKey(hit) === pointKey(b.seg.b);
|
|
if (!atEndpoint) {
|
|
crossings += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
return crossings;
|
|
}
|
|
|
|
function countLabelCollisions(routed) {
|
|
const labels = routed.flatMap((rn) => {
|
|
if (rn.isBusMember && rn.mode === "label_tie") {
|
|
return [];
|
|
}
|
|
|
|
const points = rn.labelPoints?.length ? [rn.labelPoints[0]] : [];
|
|
return points.map((p) => ({ ...p, net: rn.net.name }));
|
|
});
|
|
let collisions = 0;
|
|
|
|
for (let i = 0; i < labels.length; i += 1) {
|
|
for (let j = i + 1; j < labels.length; j += 1) {
|
|
const a = labels[i];
|
|
const b = labels[j];
|
|
if (Math.abs(a.x - b.x) < GRID * 2 && Math.abs(a.y - b.y) < GRID * 1.2) {
|
|
collisions += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
return collisions;
|
|
}
|
|
|
|
function computeLayoutMetrics(routed, busGroups) {
|
|
const segmentCount = routed.reduce(
|
|
(total, rn) => total + rn.routes.reduce((sum, route) => sum + route.length, 0),
|
|
0
|
|
);
|
|
const tiePoints = routed.reduce((total, rn) => total + rn.tiePoints.length, 0);
|
|
const totalBends = routed.reduce((total, rn) => total + Number(rn.route_stats?.total_bends ?? 0), 0);
|
|
const totalLength = routed.reduce((total, rn) => total + Number(rn.route_stats?.total_length ?? 0), 0);
|
|
const totalDirect = routed.reduce((total, rn) => total + Number(rn.route_stats?.direct_length ?? 0), 0);
|
|
const labelTieFallbacks = routed.reduce(
|
|
(total, rn) => total + (rn.route_stats?.used_label_tie && rn.route_stats?.fallback_reason ? 1 : 0),
|
|
0
|
|
);
|
|
const labelTieRoutes = routed.reduce((total, rn) => total + (rn.route_stats?.used_label_tie ? 1 : 0), 0);
|
|
|
|
return {
|
|
segment_count: segmentCount,
|
|
overlap_edges: countOverlapEdges(routed),
|
|
crossings: countCrossings(routed),
|
|
label_collisions: countLabelCollisions(routed),
|
|
tie_points_used: tiePoints,
|
|
bus_groups: busGroups.length,
|
|
total_bends: totalBends,
|
|
total_length: totalLength,
|
|
direct_length: totalDirect,
|
|
detour_ratio: totalDirect > 0 ? totalLength / totalDirect : 1,
|
|
label_tie_fallbacks: labelTieFallbacks,
|
|
label_tie_routes: labelTieRoutes
|
|
};
|
|
}
|
|
|
|
export function applyLayoutToModel(model, options = {}) {
|
|
const working = clone(model);
|
|
const respectLocks = options.respectLocks ?? true;
|
|
const autoRotate = options.autoRotate ?? true;
|
|
|
|
if (!respectLocks) {
|
|
for (const inst of working.instances) {
|
|
inst.placement.x = null;
|
|
inst.placement.y = null;
|
|
inst.placement.locked = false;
|
|
}
|
|
}
|
|
|
|
const { placed } = placeInstances(working, { respectLocks, autoRotate });
|
|
const byRef = new Map(placed.map((inst) => [inst.ref, inst]));
|
|
|
|
for (const inst of working.instances) {
|
|
const p = byRef.get(inst.ref);
|
|
if (!p) {
|
|
continue;
|
|
}
|
|
inst.placement.x = p.placement.x;
|
|
inst.placement.y = p.placement.y;
|
|
inst.placement.rotation = p.placement.rotation;
|
|
inst.placement.locked = p.placement.locked;
|
|
}
|
|
|
|
return working;
|
|
}
|
|
|
|
export function requestedLayoutEngine(options = {}) {
|
|
const explicitEngine = typeof options.layout_engine === "string" ? options.layout_engine.trim().toLowerCase() : "";
|
|
if (explicitEngine === "elk" || options.use_elk_layout === true) {
|
|
return "elk";
|
|
}
|
|
return DEFAULT_LAYOUT_ENGINE;
|
|
}
|
|
|
|
function layoutAndRouteNative(model, options = {}) {
|
|
const renderMode = options.render_mode === "explicit" ? "explicit" : DEFAULT_RENDER_MODE;
|
|
const respectLocks = options.respect_locks ?? true;
|
|
const autoRotate = options.auto_rotate ?? true;
|
|
const preservePlacement = options.preserve_placement === true;
|
|
const placed = preservePlacement
|
|
? preservePlacedInstances(model, { respectLocks, autoRotate: false })
|
|
: placeInstances(model, { respectLocks, autoRotate }).placed;
|
|
const placedMap = new Map(placed.map((inst) => [inst.ref, inst]));
|
|
const bounds = buildBounds(model, placed);
|
|
const { routed, busGroups } = routeAllNets(model, placed, placedMap, bounds, {
|
|
renderMode
|
|
});
|
|
|
|
return {
|
|
placed,
|
|
routed,
|
|
width: bounds.maxX,
|
|
height: bounds.maxY,
|
|
bus_groups: busGroups,
|
|
metrics: computeLayoutMetrics(routed, busGroups),
|
|
render_mode_used: renderMode
|
|
};
|
|
}
|
|
|
|
export function layoutAndRoute(model, options = {}) {
|
|
const requestedEngine = requestedLayoutEngine(options);
|
|
const nativeLayout = layoutAndRouteNative(model, options);
|
|
|
|
if (requestedEngine !== "elk") {
|
|
return {
|
|
...nativeLayout,
|
|
layout_engine_requested: requestedEngine,
|
|
layout_engine_used: DEFAULT_LAYOUT_ENGINE,
|
|
layout_warnings: []
|
|
};
|
|
}
|
|
|
|
const elkRuntime = resolveElkRuntime(options.elk_runtime_module);
|
|
if (!elkRuntime.ok) {
|
|
return {
|
|
...nativeLayout,
|
|
layout_engine_requested: "elk",
|
|
layout_engine_used: DEFAULT_LAYOUT_ENGINE,
|
|
layout_warnings: [
|
|
{
|
|
code: "elk_layout_unavailable_fallback",
|
|
message: elkRuntime.message
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
return {
|
|
...nativeLayout,
|
|
layout_engine_requested: "elk",
|
|
layout_engine_used: DEFAULT_LAYOUT_ENGINE,
|
|
layout_warnings: [
|
|
{
|
|
code: "elk_layout_boundary_fallback",
|
|
message:
|
|
"ELK runtime resolved, but backend ELK placement is not yet enabled. Using default layout engine."
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
export function netAnchorPoint(net, model, placed) {
|
|
const first = net.nodes[0];
|
|
if (!first) {
|
|
return null;
|
|
}
|
|
|
|
const inst = placed.find((x) => x.ref === first.ref);
|
|
if (!inst) {
|
|
return null;
|
|
}
|
|
|
|
const sym = model.symbols[inst.symbol];
|
|
const p = sym.pins.find((x) => x.name === first.pin);
|
|
if (!p) {
|
|
return null;
|
|
}
|
|
|
|
return pinPoint(inst, p, sym.body.width, sym.body.height);
|
|
}
|