Improve auto-layout with semantic lanes and overlap resolution
123
src/layout.js
@ -19,6 +19,7 @@ const LABEL_TIE_CLASSES = new Set(["power", "ground", "bus"]);
|
||||
const DEFAULT_RENDER_MODE = "schematic_stub";
|
||||
const ROTATION_STEPS = [0, 90, 180, 270];
|
||||
const MIN_CHANNEL_SPACING_STEPS = 2;
|
||||
const LANE_ORDER = ["power", "clock", "signal", "analog", "ground", "bus", "differential"];
|
||||
|
||||
function toGrid(value) {
|
||||
return Math.round(value / GRID) * GRID;
|
||||
@ -375,8 +376,42 @@ function connectivityDegree(model) {
|
||||
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) {
|
||||
const { rank, degree, instanceByRef, respectLocks } = 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);
|
||||
@ -409,6 +444,11 @@ function placeGroup(model, group, start, context) {
|
||||
|
||||
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) {
|
||||
@ -417,8 +457,19 @@ function placeGroup(model, group, start, context) {
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
let yCursor = start.y;
|
||||
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;
|
||||
@ -449,7 +500,9 @@ function placeGroup(model, group, start, context) {
|
||||
maxX = Math.max(maxX, x + sym.body.width);
|
||||
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) {
|
||||
const map = new Map();
|
||||
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 { rank } = computeRanks(model);
|
||||
const degree = connectivityDegree(model);
|
||||
const laneProfiles = refLaneProfiles(model);
|
||||
const instanceByRef = buildInstanceMap(instances);
|
||||
const groups = buildConstraintGroups(model, rank);
|
||||
|
||||
@ -602,6 +717,7 @@ function placeInstances(model, options = {}) {
|
||||
const out = placeGroup(model, group, origin, {
|
||||
rank,
|
||||
degree,
|
||||
laneProfiles,
|
||||
instanceByRef,
|
||||
respectLocks
|
||||
});
|
||||
@ -618,6 +734,7 @@ function placeInstances(model, options = {}) {
|
||||
|
||||
applyAlignmentConstraints(placedMap, model.constraints);
|
||||
applyNearConstraints(model, placedMap, model.constraints);
|
||||
resolvePlacementOverlaps(model, placedMap, { respectLocks });
|
||||
|
||||
return { placed, placedMap };
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 230 KiB After Width: | Height: | Size: 232 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 255 KiB After Width: | Height: | Size: 255 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 166 KiB |