Improve label readability with collision suppression and zoom density

This commit is contained in:
Rbanh 2026-02-16 22:03:43 -05:00
parent 85e5a345f1
commit f938c6dcbc
2 changed files with 120 additions and 2 deletions

View File

@ -381,6 +381,11 @@ function saveSnapshot() {
function updateTransform() { function updateTransform() {
el.canvasInner.style.transform = `translate(${state.panX}px, ${state.panY}px) scale(${state.scale})`; el.canvasInner.style.transform = `translate(${state.panX}px, ${state.panY}px) scale(${state.scale})`;
el.zoomResetBtn.textContent = `${Math.round(state.scale * 100)}%`; el.zoomResetBtn.textContent = `${Math.round(state.scale * 100)}%`;
const svg = el.canvasInner.querySelector("svg");
if (svg) {
applyLabelDensityByZoom(svg);
resolveLabelCollisions(svg);
}
} }
function fitView(layout) { function fitView(layout) {
@ -589,6 +594,114 @@ function setLabelLayerVisibility() {
if (layer) { if (layer) {
layer.style.display = state.showLabels ? "" : "none"; layer.style.display = state.showLabels ? "" : "none";
} }
applyLabelDensityByZoom(svg);
resolveLabelCollisions(svg);
}
function boxesOverlap(a, b, pad = 2) {
return !(a.right + pad < b.left || a.left - pad > b.right || a.bottom + pad < b.top || a.top - pad > b.bottom);
}
function labelPriority(node) {
if (node.hasAttribute("data-net-label")) return 4;
if (node.hasAttribute("data-ref-label")) return 3;
if (node.hasAttribute("data-pin-label")) return 2;
if (node.hasAttribute("data-value-label")) return 1;
return 0;
}
function applyLabelDensityByZoom(svg) {
const pinLabels = svg.querySelectorAll("[data-pin-label]");
const valueLabels = svg.querySelectorAll("[data-value-label]");
const refLabels = svg.querySelectorAll("[data-ref-label]");
const dense = state.scale < 0.85;
const veryDense = state.scale < 0.65;
pinLabels.forEach((n) => {
n.style.display = dense ? "none" : "";
});
valueLabels.forEach((n) => {
n.style.display = veryDense ? "none" : "";
});
refLabels.forEach((n) => {
n.style.display = veryDense ? "none" : "";
});
}
function resolveLabelCollisions(svg) {
if (!state.showLabels) {
return;
}
const labels = [
...svg.querySelectorAll("[data-net-label], [data-pin-label], [data-ref-label], [data-value-label]")
].filter((n) => n.style.display !== "none");
for (const n of labels) {
n.style.visibility = "";
}
const netSeen = [];
for (const node of labels) {
const net = node.getAttribute("data-net-label");
if (!net) {
continue;
}
const box = node.getBoundingClientRect();
let dup = false;
for (const prev of netSeen) {
if (prev.net !== net) {
continue;
}
const dx = Math.abs((box.left + box.right) / 2 - prev.cx);
const dy = Math.abs((box.top + box.bottom) / 2 - prev.cy);
if (dx < 30 && dy < 18) {
dup = true;
break;
}
}
if (dup) {
node.style.visibility = "hidden";
continue;
}
netSeen.push({
net,
cx: (box.left + box.right) / 2,
cy: (box.top + box.bottom) / 2
});
}
const active = labels.filter((n) => n.style.display !== "none" && n.style.visibility !== "hidden");
const entries = active.map((node) => ({
node,
priority: labelPriority(node),
box: node.getBoundingClientRect()
}));
for (let i = 0; i < entries.length; i += 1) {
const a = entries[i];
if (a.node.style.visibility === "hidden") {
continue;
}
for (let j = i + 1; j < entries.length; j += 1) {
const b = entries[j];
if (b.node.style.visibility === "hidden") {
continue;
}
if (!boxesOverlap(a.box, b.box, 1)) {
continue;
}
if (a.priority === b.priority) {
b.node.style.visibility = "hidden";
} else if (a.priority > b.priority) {
b.node.style.visibility = "hidden";
} else {
a.node.style.visibility = "hidden";
break;
}
}
}
} }
function renderInstances() { function renderInstances() {
@ -1197,6 +1310,11 @@ function renderCanvas() {
bindSvgInteractions(); bindSvgInteractions();
applyVisualHighlight(); applyVisualHighlight();
updateTransform(); updateTransform();
const svg = el.canvasInner.querySelector("svg");
if (svg) {
applyLabelDensityByZoom(svg);
resolveLabelCollisions(svg);
}
} }
function renderAll() { function renderAll() {

View File

@ -437,8 +437,8 @@ export function renderSvgFromLayout(model, layout, options = {}) {
${pinCoreSvg} ${pinCoreSvg}
</g> </g>
${pinLabelsSvg} ${pinLabelsSvg}
<text x="${x + 8}" y="${refY}" font-size="${compactLabel ? 11 : 12}" font-weight="700" fill="#111827" stroke="#f8fafc" stroke-width="2.2" paint-order="stroke fill">${esc(refLabel)}</text> <text x="${x + 8}" y="${refY}" font-size="${compactLabel ? 11 : 12}" font-weight="700" fill="#111827" stroke="#f8fafc" stroke-width="2.2" paint-order="stroke fill" data-ref-label="${esc(inst.ref)}">${esc(refLabel)}</text>
<text x="${x + 8}" y="${valueY}" font-size="${compactLabel ? 9 : 10}" fill="#667085" stroke="#f8fafc" stroke-width="2" paint-order="stroke fill">${esc(valueLabel)}</text> <text x="${x + 8}" y="${valueY}" font-size="${compactLabel ? 9 : 10}" fill="#667085" stroke="#f8fafc" stroke-width="2" paint-order="stroke fill" data-value-label="${esc(inst.ref)}">${esc(valueLabel)}</text>
</g>`; </g>`;
}) })
.join("\n"); .join("\n");