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