Improve layout stability, tie-net metrics, and annotation readability
Some checks are pending
CI / test (push) Waiting to run
@ -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 (
|
||||
|
||||
@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 232 KiB After Width: | Height: | Size: 229 KiB |
|
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 201 KiB |
|
Before Width: | Height: | Size: 206 KiB After Width: | Height: | Size: 209 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 202 KiB After Width: | Height: | Size: 202 KiB |
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 195 KiB |
@ -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");
|
||||
});
|
||||
|
||||