Improve label readability with collision suppression and zoom density
This commit is contained in:
parent
85e5a345f1
commit
f938c6dcbc
118
frontend/app.js
118
frontend/app.js
@ -381,6 +381,11 @@ function saveSnapshot() {
|
||||
function updateTransform() {
|
||||
el.canvasInner.style.transform = `translate(${state.panX}px, ${state.panY}px) scale(${state.scale})`;
|
||||
el.zoomResetBtn.textContent = `${Math.round(state.scale * 100)}%`;
|
||||
const svg = el.canvasInner.querySelector("svg");
|
||||
if (svg) {
|
||||
applyLabelDensityByZoom(svg);
|
||||
resolveLabelCollisions(svg);
|
||||
}
|
||||
}
|
||||
|
||||
function fitView(layout) {
|
||||
@ -589,6 +594,114 @@ function setLabelLayerVisibility() {
|
||||
if (layer) {
|
||||
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() {
|
||||
@ -1197,6 +1310,11 @@ function renderCanvas() {
|
||||
bindSvgInteractions();
|
||||
applyVisualHighlight();
|
||||
updateTransform();
|
||||
const svg = el.canvasInner.querySelector("svg");
|
||||
if (svg) {
|
||||
applyLabelDensityByZoom(svg);
|
||||
resolveLabelCollisions(svg);
|
||||
}
|
||||
}
|
||||
|
||||
function renderAll() {
|
||||
|
||||
@ -437,8 +437,8 @@ export function renderSvgFromLayout(model, layout, options = {}) {
|
||||
${pinCoreSvg}
|
||||
</g>
|
||||
${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="${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="${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" data-value-label="${esc(inst.ref)}">${esc(valueLabel)}</text>
|
||||
</g>`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user