diff --git a/frontend/app.js b/frontend/app.js index b99ac24..eb526d6 100644 --- a/frontend/app.js +++ b/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() { diff --git a/src/render.js b/src/render.js index c763786..486585f 100644 --- a/src/render.js +++ b/src/render.js @@ -437,8 +437,8 @@ export function renderSvgFromLayout(model, layout, options = {}) { ${pinCoreSvg} ${pinLabelsSvg} - ${esc(refLabel)} - ${esc(valueLabel)} + ${esc(refLabel)} + ${esc(valueLabel)} `; }) .join("\n");