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() {
|
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() {
|
||||||
|
|||||||
@ -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");
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user