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); }