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