diff --git a/src/render.js b/src/render.js
index 486585f..12c1de5 100644
--- a/src/render.js
+++ b/src/render.js
@@ -191,7 +191,39 @@ function distance(a, b) {
return Math.hypot(a.x - b.x, a.y - b.y);
}
-function pickLabelPoints(points, maxCount, used, minSpacing, avoidPoints = []) {
+function rectFromLabelPoint(point, width = 78, height = 14) {
+ return {
+ x: point.x - 2,
+ y: point.y - height + 2,
+ width,
+ height
+ };
+}
+
+function rectsOverlap(a, b) {
+ return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
+}
+
+function pointInRect(point, rect) {
+ return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height;
+}
+
+function withCandidateOffsets(point) {
+ const offsets = [
+ [0, 0],
+ [0, -10],
+ [0, 10],
+ [10, 0],
+ [-10, 0],
+ [14, -10],
+ [-14, -10],
+ [14, 10],
+ [-14, 10]
+ ];
+ return offsets.map(([dx, dy]) => ({ x: point.x + dx, y: point.y + dy }));
+}
+
+function pickLabelPoints(points, maxCount, used, usedRects, minSpacing, avoidPoints = [], blockedRects = []) {
const accepted = [];
for (const p of points) {
@@ -199,29 +231,55 @@ function pickLabelPoints(points, maxCount, used, minSpacing, avoidPoints = []) {
break;
}
- let blocked = false;
- for (const prev of used) {
- if (distance(p, prev) < minSpacing) {
- blocked = true;
- break;
+ const candidates = withCandidateOffsets(p);
+ for (const candidate of candidates) {
+ let blocked = false;
+ for (const prev of used) {
+ if (distance(candidate, prev) < minSpacing) {
+ blocked = true;
+ break;
+ }
}
- }
- if (blocked) {
- continue;
- }
-
- for (const pin of avoidPoints) {
- if (distance(p, pin) < minSpacing * 0.9) {
- blocked = true;
- break;
+ if (blocked) {
+ continue;
}
- }
- if (blocked) {
- continue;
- }
- accepted.push(p);
- used.push(p);
+ for (const pin of avoidPoints) {
+ if (distance(candidate, pin) < minSpacing * 0.9) {
+ blocked = true;
+ break;
+ }
+ }
+ if (blocked) {
+ continue;
+ }
+
+ const labelRect = rectFromLabelPoint(candidate);
+ for (const usedRect of usedRects) {
+ if (rectsOverlap(labelRect, usedRect)) {
+ blocked = true;
+ break;
+ }
+ }
+ if (blocked) {
+ continue;
+ }
+
+ for (const blockedRect of blockedRects) {
+ if (rectsOverlap(labelRect, blockedRect) || pointInRect(candidate, blockedRect)) {
+ blocked = true;
+ break;
+ }
+ }
+ if (blocked) {
+ continue;
+ }
+
+ accepted.push(candidate);
+ used.push(candidate);
+ usedRects.push(labelRect);
+ break;
+ }
}
return accepted;
@@ -309,6 +367,7 @@ export function renderSvgFromLayout(model, layout, options = {}) {
const pinNets = pinNetMap(model);
const netClassByName = new Map((model.nets ?? []).map((n) => [n.name, n.class]));
const allPinPoints = [];
+ const componentRects = [];
const components = layout.placed
.map((inst) => {
@@ -318,6 +377,12 @@ export function renderSvgFromLayout(model, layout, options = {}) {
const rotation = normalizeRotation(inst.placement.rotation ?? 0);
const cx = x + sym.body.width / 2;
const cy = y + sym.body.height / 2;
+ componentRects.push({
+ x: x - 6,
+ y: y - 6,
+ width: sym.body.width + 12,
+ height: sym.body.height + 12
+ });
const templateKind = symbolTemplateKind(sym);
const compactLabel = templateKind || sym.body.width <= 140 || sym.body.height <= 90;
const legacyShowInstanceNetLabels = Boolean(inst.properties?.show_net_labels);
@@ -471,8 +536,20 @@ export function renderSvgFromLayout(model, layout, options = {}) {
const routedByName = new Map(layout.routed.map((r) => [r.net.name, r]));
const usedLabelPoints = [];
+ const usedLabelRects = [];
const labels = [];
const tieLabels = [];
+ const blockedLabelRects = [{ x: 6, y: 6, width: 126, height: 86 }, ...componentRects];
+ const annotationEntries = (model.annotations ?? []).map((a, idx) => {
+ const x = a.x ?? 16;
+ const y = a.y ?? 24 + idx * 16;
+ const yAdjusted = x < 170 && y < 110 ? 110 + idx * 16 : y;
+ const text = String(a.text ?? "");
+ return { x, y: yAdjusted, text, width: Math.max(90, Math.min(640, text.length * 6.2)), height: 14 };
+ });
+ for (const ann of annotationEntries) {
+ blockedLabelRects.push({ x: ann.x - 4, y: ann.y - 12, width: ann.width + 8, height: ann.height + 2 });
+ }
for (const net of model.nets) {
if (isGroundLikeNet(net)) {
@@ -489,7 +566,15 @@ export function renderSvgFromLayout(model, layout, options = {}) {
if (routeInfo?.mode === "label_tie") {
candidates.push(...(routeInfo?.labelPoints ?? []));
- const selected = pickLabelPoints(candidates, 1, usedLabelPoints, GRID * 2.4, allPinPoints);
+ const selected = pickLabelPoints(
+ candidates,
+ 1,
+ usedLabelPoints,
+ usedLabelRects,
+ GRID * 2.4,
+ allPinPoints,
+ blockedLabelRects
+ );
for (const p of selected) {
labels.push(renderNetLabel(p.x, p.y, net.name, net.class, true));
}
@@ -503,7 +588,15 @@ export function renderSvgFromLayout(model, layout, options = {}) {
candidates.push({ x: netAnchor.x + 8, y: netAnchor.y - 8 });
}
- const selected = pickLabelPoints(candidates, 1, usedLabelPoints, GRID * 2.4, allPinPoints);
+ const selected = pickLabelPoints(
+ candidates,
+ 1,
+ usedLabelPoints,
+ usedLabelRects,
+ GRID * 2.4,
+ allPinPoints,
+ blockedLabelRects
+ );
for (const p of selected) {
labels.push(renderNetLabel(p.x, p.y, net.name, net.class));
}
@@ -511,6 +604,7 @@ export function renderSvgFromLayout(model, layout, options = {}) {
if (showLabels) {
const usedTieLabels = [];
+ const usedTieRects = [];
for (const rn of layout.routed) {
if (rn.mode !== "label_tie" || isGroundLikeNet(rn.net)) {
continue;
@@ -522,7 +616,15 @@ export function renderSvgFromLayout(model, layout, options = {}) {
}
const maxPerNet = rn.net.class === "power" ? Math.min(6, candidates.length) : Math.min(2, candidates.length);
- const selected = pickLabelPoints(candidates, maxPerNet, usedTieLabels, GRID * 1.5, allPinPoints);
+ const selected = pickLabelPoints(
+ candidates,
+ maxPerNet,
+ usedTieLabels,
+ usedTieRects,
+ GRID * 1.5,
+ allPinPoints,
+ blockedLabelRects
+ );
for (const p of selected) {
tieLabels.push(renderNetLabel(p.x, p.y, rn.net.name, rn.net.class, true));
}
@@ -553,11 +655,9 @@ export function renderSvgFromLayout(model, layout, options = {}) {
})
.join("\n");
- const annotations = (model.annotations ?? [])
- .map((a, idx) => {
- const x = a.x ?? 16;
- const y = a.y ?? 24 + idx * 16;
- return `${esc(a.text)}`;
+ const annotations = annotationEntries
+ .map((a) => {
+ return `${esc(a.text)}`;
})
.join("\n");
diff --git a/tests/baselines/ui/dense-analog.png b/tests/baselines/ui/dense-analog.png
index ba8574c..09245a8 100644
Binary files a/tests/baselines/ui/dense-analog.png and b/tests/baselines/ui/dense-analog.png differ
diff --git a/tests/baselines/ui/explicit-mode-auto-tidy.png b/tests/baselines/ui/explicit-mode-auto-tidy.png
index 5d22d32..a476e08 100644
Binary files a/tests/baselines/ui/explicit-mode-auto-tidy.png and b/tests/baselines/ui/explicit-mode-auto-tidy.png differ
diff --git a/tests/baselines/ui/initial.png b/tests/baselines/ui/initial.png
index 70a6c5d..2ba955b 100644
Binary files a/tests/baselines/ui/initial.png and b/tests/baselines/ui/initial.png differ
diff --git a/tests/baselines/ui/laptop-viewport.png b/tests/baselines/ui/laptop-viewport.png
index 41e4c43..ae2aea7 100644
Binary files a/tests/baselines/ui/laptop-viewport.png and b/tests/baselines/ui/laptop-viewport.png differ
diff --git a/tests/baselines/ui/post-migration-apply.png b/tests/baselines/ui/post-migration-apply.png
index a0ce2e0..97b09b7 100644
Binary files a/tests/baselines/ui/post-migration-apply.png and b/tests/baselines/ui/post-migration-apply.png differ
diff --git a/tests/baselines/ui/selected-u2.png b/tests/baselines/ui/selected-u2.png
index c214e1f..bf961cf 100644
Binary files a/tests/baselines/ui/selected-u2.png and b/tests/baselines/ui/selected-u2.png differ
diff --git a/tests/render-regression.test.js b/tests/render-regression.test.js
index f4e0127..6fc053c 100644
--- a/tests/render-regression.test.js
+++ b/tests/render-regression.test.js
@@ -15,5 +15,5 @@ test("render output for reference fixture remains deterministic", () => {
assert.equal(outA.ok, true);
assert.equal(outB.ok, true);
assert.equal(outA.svg, outB.svg);
- assert.equal(svgHash(outA.svg), "8cdeb27f324decbd375fc9b127c7361f204c4e167551076178d6ad52dee66f94");
+ assert.equal(svgHash(outA.svg), "8dc4f0722829a68136cb237373a8d3e26669c693ec7f4287c92d22772488b99f");
});