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; 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) { function renderGroundSymbol(x, y, netName) {
const y0 = y + 3; const y0 = y + 3;
const y1 = y + 7; const y1 = y + 7;
@ -339,7 +360,7 @@ function representativePoint(routeInfo, netAnchor) {
return netAnchor; return netAnchor;
} }
function renderLegend() { function renderLegend(offsetY = 14) {
const entries = [ const entries = [
["power", "Power"], ["power", "Power"],
["ground", "Ground"], ["ground", "Ground"],
@ -356,14 +377,41 @@ function renderLegend() {
.join(""); .join("");
return ` 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" /> <rect x="-8" y="-8" width="120" height="82" rx="6" fill="#ffffffd8" stroke="#d0d5dd" />
${rows} ${rows}
</g>`; </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 = {}) { export function renderSvgFromLayout(model, layout, options = {}) {
const showLabels = options.show_labels !== false; const showLabels = options.show_labels !== false;
const showAnnotations = options.show_annotations !== false;
const pinNets = pinNetMap(model); const pinNets = pinNetMap(model);
const netClassByName = new Map((model.nets ?? []).map((n) => [n.name, n.class])); const netClassByName = new Map((model.nets ?? []).map((n) => [n.name, n.class]));
const allPinPoints = []; const allPinPoints = [];
@ -394,6 +442,8 @@ export function renderSvgFromLayout(model, layout, options = {}) {
const pinCircles = []; const pinCircles = [];
const pinLabels = []; const pinLabels = [];
const instanceNetLabels = []; const instanceNetLabels = [];
const pinLabelOccupied = { left: [], right: [], top: [], bottom: [] };
const netLabelOccupied = { left: [], right: [], top: [], bottom: [] };
for (const pin of sym.pins) { for (const pin of sym.pins) {
let px = x; let px = x;
let py = y; let py = y;
@ -440,6 +490,10 @@ export function renderSvgFromLayout(model, layout, options = {}) {
textAnchor = "start"; 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); const showPinLabel = !templateKind || !/^\d+$/.test(pin.name);
if (showPinLabel) { if (showPinLabel) {
pinLabels.push( pinLabels.push(
@ -478,6 +532,10 @@ export function renderSvgFromLayout(model, layout, options = {}) {
netAnchor = "start"; netAnchor = "start";
} }
const netTextPos = placePinText(rotatedSide, { x: netX, y: netY }, netLabelOccupied, 13);
netX = netTextPos.x;
netY = netTextPos.y;
instanceNetLabels.push( 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>` `<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 usedLabelRects = [];
const labels = []; const labels = [];
const tieLabels = []; const tieLabels = [];
const blockedLabelRects = [{ x: 6, y: 6, width: 126, height: 86 }, ...componentRects]; const annotationLines = showAnnotations
const annotationEntries = (model.annotations ?? []).map((a, idx) => { ? (model.annotations ?? []).map((a) => truncate(String(a.text ?? ""), 86)).filter(Boolean).slice(0, 6)
const x = a.x ?? 16; : [];
const y = a.y ?? 24 + idx * 16; const annotationPanel = renderAnnotationPanel(annotationLines);
const yAdjusted = x < 170 && y < 110 ? 110 + idx * 16 : y; const legendY = annotationPanel.height > 0 ? annotationPanel.y + annotationPanel.height + 8 : 14;
const text = String(a.text ?? ""); const blockedLabelRects = [
return { x, y: yAdjusted, text, width: Math.max(90, Math.min(640, text.length * 6.2)), height: 14 }; ...(annotationPanel.height > 0
}); ? [{ x: annotationPanel.x - 2, y: annotationPanel.y - 2, width: annotationPanel.width + 4, height: annotationPanel.height + 4 }]
for (const ann of annotationEntries) { : []),
blockedLabelRects.push({ x: ann.x - 4, y: ann.y - 12, width: ann.width + 8, height: ann.height + 2 }); { x: 6, y: legendY - 8, width: 126, height: 86 },
} ...componentRects
];
for (const net of model.nets) { for (const net of model.nets) {
if (isGroundLikeNet(net)) { if (isGroundLikeNet(net)) {
@ -655,12 +714,6 @@ export function renderSvgFromLayout(model, layout, options = {}) {
}) })
.join("\n"); .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") : ""; const labelLayer = showLabels ? [...labels, ...tieLabels].join("\n") : "";
return `<?xml version="1.0" encoding="UTF-8"?> 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="ties">${tiePoints}</g>
<g data-layer="net-labels">${labelLayer}</g> <g data-layer="net-labels">${labelLayer}</g>
<g data-layer="bus-groups">${busLabels}</g> <g data-layer="bus-groups">${busLabels}</g>
<g data-layer="annotations">${annotations}</g> ${annotationPanel.svg}
${renderLegend()} ${renderLegend(legendY)}
</svg>`; </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(outA.ok, true);
assert.equal(outB.ok, true); assert.equal(outB.ok, true);
assert.equal(outA.svg, outB.svg); assert.equal(outA.svg, outB.svg);
assert.equal(svgHash(outA.svg), "8dc4f0722829a68136cb237373a8d3e26669c693ec7f4287c92d22772488b99f"); assert.equal(svgHash(outA.svg), "80ab4c279caf29b2a14096346d6e993ef677f41bf1b3bc226eefa7b069b6487d");
}); });