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");