Improve text readability with pin label spacing and annotation panel
Some checks are pending
CI / test (push) Waiting to run

This commit is contained in:
Rbanh 2026-02-19 23:52:12 -05:00
parent 5f31542cb6
commit cc20c0cc25
7 changed files with 75 additions and 22 deletions

View File

@ -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 `
<g data-layer="legend" transform="translate(14 14)">
<g data-layer="legend" transform="translate(14 ${offsetY})">
<rect x="-8" y="-8" width="120" height="82" rx="6" fill="#ffffffd8" stroke="#d0d5dd" />
${rows}
</g>`;
}
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) =>
`<text x="${panelX + 8}" y="${panelY + 18 + idx * rowHeight}" font-size="10.5" fill="#6b7280">${esc(line)}</text>`
)
.join("\n");
const svg = `
<g data-layer="annotation-panel">
<rect x="${panelX}" y="${panelY}" width="${panelWidth}" height="${panelHeight}" rx="6" fill="#ffffffde" stroke="#d0d5dd" />
${textRows}
</g>`;
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(
`<text x="${netX}" y="${netY}" text-anchor="${netAnchor}" font-size="10" font-weight="700" fill="${netColor(netClassByName.get(displayNet))}" stroke="#f8fafc" stroke-width="2.6" paint-order="stroke fill" data-net-label="${esc(displayNet)}" data-ref-net-label="${esc(inst.ref)}">${esc(displayNet)}</text>`
);
@ -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 `<text x="${a.x}" y="${a.y}" font-size="11" fill="#6b7280">${esc(a.text)}</text>`;
})
.join("\n");
const labelLayer = showLabels ? [...labels, ...tieLabels].join("\n") : "";
return `<?xml version="1.0" encoding="UTF-8"?>
@ -674,8 +727,8 @@ export function renderSvgFromLayout(model, layout, options = {}) {
<g data-layer="ties">${tiePoints}</g>
<g data-layer="net-labels">${labelLayer}</g>
<g data-layer="bus-groups">${busLabels}</g>
<g data-layer="annotations">${annotations}</g>
${renderLegend()}
${annotationPanel.svg}
${renderLegend(legendY)}
</svg>`;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 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), "8dc4f0722829a68136cb237373a8d3e26669c693ec7f4287c92d22772488b99f");
assert.equal(svgHash(outA.svg), "80ab4c279caf29b2a14096346d6e993ef677f41bf1b3bc226eefa7b069b6487d");
});