diff --git a/src/layout.js b/src/layout.js index 649a4b6..be7353d 100644 --- a/src/layout.js +++ b/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 }; } diff --git a/tests/baselines/ui/dense-analog.png b/tests/baselines/ui/dense-analog.png index 91bfc6b..e7bdb73 100644 Binary files a/tests/baselines/ui/dense-analog.png and b/tests/baselines/ui/dense-analog.png differ diff --git a/tests/baselines/ui/explicit-mode-auto-tidy.png b/tests/baselines/ui/explicit-mode-auto-tidy.png index ef346db..1602532 100644 Binary files a/tests/baselines/ui/explicit-mode-auto-tidy.png and b/tests/baselines/ui/explicit-mode-auto-tidy.png differ diff --git a/tests/baselines/ui/laptop-viewport.png b/tests/baselines/ui/laptop-viewport.png index 2c50715..899f554 100644 Binary files a/tests/baselines/ui/laptop-viewport.png and b/tests/baselines/ui/laptop-viewport.png differ diff --git a/tests/baselines/ui/post-migration-apply.png b/tests/baselines/ui/post-migration-apply.png index 9d64503..4ba43a6 100644 Binary files a/tests/baselines/ui/post-migration-apply.png and b/tests/baselines/ui/post-migration-apply.png differ diff --git a/tests/baselines/ui/selected-u2.png b/tests/baselines/ui/selected-u2.png index 7fae770..e52a329 100644 Binary files a/tests/baselines/ui/selected-u2.png and b/tests/baselines/ui/selected-u2.png differ