diff --git a/src/render.js b/src/render.js index 12c1de5..9a5fd3e 100644 --- a/src/render.js +++ b/src/render.js @@ -285,6 +285,27 @@ function pickLabelPoints(points, maxCount, used, usedRects, minSpacing, avoidPoi return accepted; } +function placePinText(side, desired, occupiedBySide, minStep) { + const occupied = occupiedBySide[side] ?? []; + if (side === "left" || side === "right") { + let y = desired.y; + while (occupied.some((value) => Math.abs(value - y) < minStep)) { + y += minStep; + } + occupied.push(y); + occupiedBySide[side] = occupied; + return { x: desired.x, y }; + } + + let x = desired.x; + while (occupied.some((value) => Math.abs(value - x) < minStep)) { + x += minStep; + } + occupied.push(x); + occupiedBySide[side] = occupied; + return { x, y: desired.y }; +} + function renderGroundSymbol(x, y, netName) { const y0 = y + 3; const y1 = y + 7; @@ -339,7 +360,7 @@ function representativePoint(routeInfo, netAnchor) { return netAnchor; } -function renderLegend() { +function renderLegend(offsetY = 14) { const entries = [ ["power", "Power"], ["ground", "Ground"], @@ -356,14 +377,41 @@ function renderLegend() { .join(""); return ` - + ${rows} `; } +function renderAnnotationPanel(entries) { + if (!entries.length) { + return { svg: "", width: 0, height: 0 }; + } + + const panelX = 14; + const panelY = 14; + const rowHeight = 14; + const panelWidth = 420; + const panelHeight = 12 + entries.length * rowHeight; + const textRows = entries + .map( + (line, idx) => + `${esc(line)}` + ) + .join("\n"); + + const svg = ` + + + ${textRows} +`; + + return { svg, x: panelX, y: panelY, width: panelWidth, height: panelHeight }; +} + export function renderSvgFromLayout(model, layout, options = {}) { const showLabels = options.show_labels !== false; + const showAnnotations = options.show_annotations !== false; const pinNets = pinNetMap(model); const netClassByName = new Map((model.nets ?? []).map((n) => [n.name, n.class])); const allPinPoints = []; @@ -394,6 +442,8 @@ export function renderSvgFromLayout(model, layout, options = {}) { const pinCircles = []; const pinLabels = []; const instanceNetLabels = []; + const pinLabelOccupied = { left: [], right: [], top: [], bottom: [] }; + const netLabelOccupied = { left: [], right: [], top: [], bottom: [] }; for (const pin of sym.pins) { let px = x; let py = y; @@ -440,6 +490,10 @@ export function renderSvgFromLayout(model, layout, options = {}) { textAnchor = "start"; } + const pinTextPos = placePinText(rotatedSide, { x: labelX, y: labelY }, pinLabelOccupied, 11); + labelX = pinTextPos.x; + labelY = pinTextPos.y; + const showPinLabel = !templateKind || !/^\d+$/.test(pin.name); if (showPinLabel) { pinLabels.push( @@ -478,6 +532,10 @@ export function renderSvgFromLayout(model, layout, options = {}) { netAnchor = "start"; } + const netTextPos = placePinText(rotatedSide, { x: netX, y: netY }, netLabelOccupied, 13); + netX = netTextPos.x; + netY = netTextPos.y; + instanceNetLabels.push( `${esc(displayNet)}` ); @@ -539,17 +597,18 @@ export function renderSvgFromLayout(model, layout, options = {}) { const usedLabelRects = []; const labels = []; const tieLabels = []; - const blockedLabelRects = [{ x: 6, y: 6, width: 126, height: 86 }, ...componentRects]; - const annotationEntries = (model.annotations ?? []).map((a, idx) => { - const x = a.x ?? 16; - const y = a.y ?? 24 + idx * 16; - const yAdjusted = x < 170 && y < 110 ? 110 + idx * 16 : y; - const text = String(a.text ?? ""); - return { x, y: yAdjusted, text, width: Math.max(90, Math.min(640, text.length * 6.2)), height: 14 }; - }); - for (const ann of annotationEntries) { - blockedLabelRects.push({ x: ann.x - 4, y: ann.y - 12, width: ann.width + 8, height: ann.height + 2 }); - } + const annotationLines = showAnnotations + ? (model.annotations ?? []).map((a) => truncate(String(a.text ?? ""), 86)).filter(Boolean).slice(0, 6) + : []; + const annotationPanel = renderAnnotationPanel(annotationLines); + const legendY = annotationPanel.height > 0 ? annotationPanel.y + annotationPanel.height + 8 : 14; + const blockedLabelRects = [ + ...(annotationPanel.height > 0 + ? [{ x: annotationPanel.x - 2, y: annotationPanel.y - 2, width: annotationPanel.width + 4, height: annotationPanel.height + 4 }] + : []), + { x: 6, y: legendY - 8, width: 126, height: 86 }, + ...componentRects + ]; for (const net of model.nets) { if (isGroundLikeNet(net)) { @@ -655,12 +714,6 @@ export function renderSvgFromLayout(model, layout, options = {}) { }) .join("\n"); - const annotations = annotationEntries - .map((a) => { - return `${esc(a.text)}`; - }) - .join("\n"); - const labelLayer = showLabels ? [...labels, ...tieLabels].join("\n") : ""; return ` @@ -674,8 +727,8 @@ export function renderSvgFromLayout(model, layout, options = {}) { ${tiePoints} ${labelLayer} ${busLabels} - ${annotations} - ${renderLegend()} + ${annotationPanel.svg} + ${renderLegend(legendY)} `; } diff --git a/tests/baselines/ui/dense-analog.png b/tests/baselines/ui/dense-analog.png index 2e9de6f..583d51c 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 11f1709..2ebbbb9 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 2ba955b..1f6314a 100644 Binary files a/tests/baselines/ui/initial.png and b/tests/baselines/ui/initial.png differ diff --git a/tests/baselines/ui/post-migration-apply.png b/tests/baselines/ui/post-migration-apply.png index dffd89f..7db3f32 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 bf961cf..253d620 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 6fc053c..eb7c03a 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), "8dc4f0722829a68136cb237373a8d3e26669c693ec7f4287c92d22772488b99f"); + assert.equal(svgHash(outA.svg), "80ab4c279caf29b2a14096346d6e993ef677f41bf1b3bc226eefa7b069b6487d"); });