Improve layout stability, tie-net metrics, and annotation readability
Some checks are pending
CI / test (push) Waiting to run

This commit is contained in:
Rbanh 2026-02-20 01:05:28 -05:00
parent cf3e64e836
commit 1a9c7b56ff
9 changed files with 70 additions and 20 deletions

View File

@ -750,12 +750,16 @@ function compactPlacementByConnectivity(model, placedMap, options = {}) {
repelY += ddy >= 0 ? py : -py;
}
tx = tx * 0.62 + rankTargetX * 0.24 + currentCenter.x * 0.14 + repelX;
ty = ty * 0.72 + laneY * 0.28;
tx = tx * 0.54 + rankTargetX * 0.31 + currentCenter.x * 0.15 + repelX * 0.55;
ty = ty * 0.62 + laneY * 0.3 + currentCenter.y * 0.08 + repelY * 0.35;
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));
const nextX = toGrid(Math.max(MARGIN_X, inst.placement.x + dx));
const rankCorridor = toGrid(COLUMN_GAP * 0.62);
const minRankX = toGrid(Math.max(MARGIN_X, rankTargetX - rankCorridor));
const maxRankX = toGrid(rankTargetX + rankCorridor);
inst.placement.x = Math.max(minRankX, Math.min(maxRankX, nextX));
inst.placement.y = toGrid(Math.max(MARGIN_Y, inst.placement.y + dy));
placedMap.set(ref, inst);
}
@ -822,17 +826,32 @@ function tightenPassiveAdjacency(model, placedMap, options = {}) {
if (!other) continue;
anchors.push(centerForPlacement(model, other));
}
if (!anchors.length) {
if (anchors.length < 2) {
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 spread =
anchors.length > 1
? Math.max(...anchors.map((p) => Math.hypot(p.x - cx, p.y - cy)))
: 0;
if (spread < GRID * 5) {
continue;
}
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));
const rawX = tx - (sym?.body?.width ?? 120) / 2;
const rawY = ty - (sym?.body?.height ?? 80) / 2;
const maxShift = GRID * 7;
const jitter = (((ref.charCodeAt(0) ?? 65) + (ref.charCodeAt(ref.length - 1) ?? 90)) % 3) - 1;
const nx = toGrid(
Math.max(MARGIN_X, Math.min(inst.placement.x + maxShift, Math.max(inst.placement.x - maxShift, rawX + jitter * GRID)))
);
const ny = toGrid(
Math.max(MARGIN_Y, Math.min(inst.placement.y + maxShift, Math.max(inst.placement.y - maxShift, rawY)))
);
inst.placement.x = nx;
inst.placement.y = ny;
placedMap.set(ref, inst);
@ -1001,9 +1020,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 + 64;
yCursor = y + sym.body.height + 48;
}
yCursor += 28;
yCursor += 12;
}
}
@ -1945,6 +1964,8 @@ function routeLabelTieNet(net, pinNodes, context, fallbackReason = null) {
}
}
const tieLength = routeLengthFromSegments(routes);
return {
mode: "label_tie",
routes,
@ -1952,8 +1973,8 @@ function routeLabelTieNet(net, pinNodes, context, fallbackReason = null) {
tiePoints,
junctionPoints: [],
route_stats: {
total_length: routeLengthFromSegments(routes),
direct_length: 0,
total_length: tieLength,
direct_length: tieLength,
total_bends: 0,
detour_ratio: 1,
used_label_tie: true,
@ -2228,12 +2249,12 @@ function shouldUseLabelTie(net, pinNodes, context) {
}
// Prefer readable schematic stubs for dense multi-node nets.
if (pinNodes.length >= 5) {
if (pinNodes.length >= 4) {
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) {
if ((net.class === "analog" || net.class === "signal") && pinNodes.length >= 3 && span > GRID * 34) {
return true;
}
@ -2255,7 +2276,7 @@ function shouldFallbackToTieByQuality(net, pinNodes, routed) {
const span = Math.abs(Math.max(...xs) - Math.min(...xs)) + Math.abs(Math.max(...ys) - Math.min(...ys));
const hasSpread = span >= GRID * 24;
if (pinNodes.length >= 4 && hasSpread && (detour > 2.25 || bends >= 7)) {
if (pinNodes.length >= 4 && hasSpread && (detour > 2.05 || bends >= 6)) {
return true;
}
if (

View File

@ -66,6 +66,31 @@ function truncate(text, max) {
return `${s.slice(0, Math.max(1, max - 1))}...`;
}
function wrapTextLine(text, maxChars) {
const words = String(text ?? "").trim().split(/\s+/).filter(Boolean);
if (!words.length) {
return [];
}
const lines = [];
let current = "";
for (const word of words) {
if (!current) {
current = word;
continue;
}
if (`${current} ${word}`.length <= maxChars) {
current = `${current} ${word}`;
continue;
}
lines.push(current);
current = word;
}
if (current) {
lines.push(current);
}
return lines;
}
function netColor(netClass) {
return NET_COLORS[netClass] ?? NET_COLORS.signal;
}
@ -383,7 +408,7 @@ function renderLegend(offsetY = 14) {
</g>`;
}
function renderAnnotationPanel(entries) {
function renderAnnotationPanel(entries, maxWidth = 380) {
if (!entries.length) {
return { svg: "", width: 0, height: 0 };
}
@ -391,9 +416,13 @@ function renderAnnotationPanel(entries) {
const panelX = 14;
const panelY = 14;
const rowHeight = 14;
const panelWidth = 420;
const panelHeight = 12 + entries.length * rowHeight;
const textRows = entries
const panelWidth = Math.max(260, Math.min(420, Math.round(maxWidth)));
const charsPerRow = Math.max(28, Math.floor((panelWidth - 16) / 6.2));
const wrapped = entries
.flatMap((line) => wrapTextLine(line, charsPerRow).map((seg) => truncate(seg, charsPerRow)))
.slice(0, 8);
const panelHeight = 12 + wrapped.length * rowHeight;
const textRows = wrapped
.map(
(line, idx) =>
`<text x="${panelX + 8}" y="${panelY + 18 + idx * rowHeight}" font-size="10.5" fill="#6b7280">${esc(line)}</text>`
@ -598,9 +627,9 @@ export function renderSvgFromLayout(model, layout, options = {}) {
const labels = [];
const tieLabels = [];
const annotationLines = showAnnotations
? (model.annotations ?? []).map((a) => truncate(String(a.text ?? ""), 86)).filter(Boolean).slice(0, 6)
? (model.annotations ?? []).map((a) => truncate(String(a.text ?? ""), 140)).filter(Boolean).slice(0, 6)
: [];
const annotationPanel = renderAnnotationPanel(annotationLines);
const annotationPanel = renderAnnotationPanel(annotationLines, (layout.width ?? 1200) * 0.36);
const legendY = annotationPanel.height > 0 ? annotationPanel.y + annotationPanel.height + 8 : 14;
const blockedLabelRects = [
...(annotationPanel.height > 0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 195 KiB

View File

@ -15,5 +15,5 @@ test("render output for reference fixture remains deterministic", () => {
assert.equal(outA.ok, true);
assert.equal(outB.ok, true);
assert.equal(outA.svg, outB.svg);
assert.equal(svgHash(outA.svg), "a793f82594e4aff3e898db85cd7e984e86ad568d58aecd33661e8bbb1eff1856");
assert.equal(svgHash(outA.svg), "fe431f62be90da91fbd707e1d2dae9286a04d61442893fa54fbebb048c748841");
});