Improve auto-layout with semantic lanes and overlap resolution

This commit is contained in:
Rbanh 2026-02-18 22:03:44 -05:00
parent 570e89cf4e
commit 538d137d35
6 changed files with 120 additions and 3 deletions

View File

@ -19,6 +19,7 @@ const LABEL_TIE_CLASSES = new Set(["power", "ground", "bus"]);
const DEFAULT_RENDER_MODE = "schematic_stub"; const DEFAULT_RENDER_MODE = "schematic_stub";
const ROTATION_STEPS = [0, 90, 180, 270]; const ROTATION_STEPS = [0, 90, 180, 270];
const MIN_CHANNEL_SPACING_STEPS = 2; const MIN_CHANNEL_SPACING_STEPS = 2;
const LANE_ORDER = ["power", "clock", "signal", "analog", "ground", "bus", "differential"];
function toGrid(value) { function toGrid(value) {
return Math.round(value / GRID) * GRID; return Math.round(value / GRID) * GRID;
@ -375,8 +376,42 @@ function connectivityDegree(model) {
return deg; return deg;
} }
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) { function placeGroup(model, group, start, context) {
const { rank, degree, instanceByRef, respectLocks } = context; const { rank, degree, instanceByRef, respectLocks, laneProfiles } = context;
const refs = [...group.members].sort((a, b) => a.localeCompare(b)); const refs = [...group.members].sort((a, b) => a.localeCompare(b));
const cols = rankColumnsForRefs(refs, rank); const cols = rankColumnsForRefs(refs, rank);
const colOrder = [...cols.keys()].sort((a, b) => a - b); const colOrder = [...cols.keys()].sort((a, b) => a - b);
@ -409,6 +444,11 @@ function placeGroup(model, group, start, context) {
for (const col of colOrder) { for (const col of colOrder) {
const refsInCol = [...(cols.get(col) ?? [])].sort((a, b) => { 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 da = degree.get(a) ?? 0;
const db = degree.get(b) ?? 0; const db = degree.get(b) ?? 0;
if (da !== db) { if (da !== db) {
@ -417,8 +457,19 @@ function placeGroup(model, group, start, context) {
return a.localeCompare(b); return a.localeCompare(b);
}); });
let yCursor = start.y; const byLane = new Map();
for (const ref of refsInCol) { 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); const inst = instanceByRef.get(ref);
if (!inst) { if (!inst) {
continue; continue;
@ -449,7 +500,9 @@ function placeGroup(model, group, start, context) {
maxX = Math.max(maxX, x + sym.body.width); maxX = Math.max(maxX, x + sym.body.width);
maxY = Math.max(maxY, y + sym.body.height); maxY = Math.max(maxY, y + sym.body.height);
yCursor = y + sym.body.height + 110; yCursor = y + sym.body.height + 96;
}
yCursor += 48;
} }
} }
@ -471,6 +524,67 @@ function placeGroup(model, group, start, context) {
}; };
} }
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;
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, 14)) {
continue;
}
const target = !aLocked ? aInst : !bLocked ? bInst : null;
if (!target) {
continue;
}
const pushY = toGrid(Math.max(aBox.y + aBox.h + 56, bBox.y + bBox.h + 56));
target.placement.y = Math.max(target.placement.y, pushY);
moved = true;
}
}
if (!moved) {
break;
}
}
}
function buildNodeNetMap(model) { function buildNodeNetMap(model) {
const map = new Map(); const map = new Map();
for (const net of model.nets) { for (const net of model.nets) {
@ -581,6 +695,7 @@ function placeInstances(model, options = {}) {
const instances = [...model.instances].sort((a, b) => a.ref.localeCompare(b.ref)); const instances = [...model.instances].sort((a, b) => a.ref.localeCompare(b.ref));
const { rank } = computeRanks(model); const { rank } = computeRanks(model);
const degree = connectivityDegree(model); const degree = connectivityDegree(model);
const laneProfiles = refLaneProfiles(model);
const instanceByRef = buildInstanceMap(instances); const instanceByRef = buildInstanceMap(instances);
const groups = buildConstraintGroups(model, rank); const groups = buildConstraintGroups(model, rank);
@ -602,6 +717,7 @@ function placeInstances(model, options = {}) {
const out = placeGroup(model, group, origin, { const out = placeGroup(model, group, origin, {
rank, rank,
degree, degree,
laneProfiles,
instanceByRef, instanceByRef,
respectLocks respectLocks
}); });
@ -618,6 +734,7 @@ function placeInstances(model, options = {}) {
applyAlignmentConstraints(placedMap, model.constraints); applyAlignmentConstraints(placedMap, model.constraints);
applyNearConstraints(model, placedMap, model.constraints); applyNearConstraints(model, placedMap, model.constraints);
resolvePlacementOverlaps(model, placedMap, { respectLocks });
return { placed, placedMap }; return { placed, placedMap };
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 KiB

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 255 KiB

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 166 KiB