Improve text readability with pin label spacing and annotation panel
Some checks are pending
CI / test (push) Waiting to run
@ -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>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 244 KiB After Width: | Height: | Size: 244 KiB |
|
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 209 KiB After Width: | Height: | Size: 208 KiB |
|
Before Width: | Height: | Size: 191 KiB After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 195 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(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");
|
||||||
});
|
});
|
||||||
|
|||||||