diff --git a/src/layout.js b/src/layout.js index 4fab98e..d189068 100644 --- a/src/layout.js +++ b/src/layout.js @@ -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 ( diff --git a/src/render.js b/src/render.js index 016a538..5cf72f5 100644 --- a/src/render.js +++ b/src/render.js @@ -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) { `; } -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) => `${esc(line)}` @@ -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 diff --git a/tests/baselines/ui/dense-analog.png b/tests/baselines/ui/dense-analog.png index 7231d91..b4177c0 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 14e3b91..d8eb2bd 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/initial.png b/tests/baselines/ui/initial.png index 9697d7e..8f37fa4 100644 Binary files a/tests/baselines/ui/initial.png and b/tests/baselines/ui/initial.png differ diff --git a/tests/baselines/ui/laptop-viewport.png b/tests/baselines/ui/laptop-viewport.png index 43253f2..7149680 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 c020258..097614e 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 a7faa8c..e1fb983 100644 Binary files a/tests/baselines/ui/selected-u2.png and b/tests/baselines/ui/selected-u2.png differ diff --git a/tests/render-regression.test.js b/tests/render-regression.test.js index b4f7900..f5c6b78 100644 --- a/tests/render-regression.test.js +++ b/tests/render-regression.test.js @@ -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"); });