From 4b7b77928c9c61158f991bb7c3bb4d11751afc35 Mon Sep 17 00:00:00 2001 From: Exceluyi Date: Fri, 7 Nov 2025 17:28:12 +0100 Subject: [PATCH 1/4] Add trace segment moving to avoid net label overlap --- .../rerouteCollidingTrace.ts | 13 +- .../tryMoveTraceSegments.ts | 43 ++++ tests/assets/repro.input.json | 57 +++++ .../__snapshots__/repro.snap.svg | 207 ++++++++++++++++++ .../repro.test.ts | 18 ++ 5 files changed, 337 insertions(+), 1 deletion(-) create mode 100644 lib/solvers/TraceLabelOverlapAvoidanceSolver/tryMoveTraceSegments.ts create mode 100644 tests/assets/repro.input.json create mode 100644 tests/solvers/TraceLabelOverlapAvoidanceSolver/__snapshots__/repro.snap.svg create mode 100644 tests/solvers/TraceLabelOverlapAvoidanceSolver/repro.test.ts diff --git a/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts b/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts index 7d1e4d4..16c5605 100644 --- a/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts +++ b/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts @@ -8,9 +8,12 @@ import { generateSnipAndReconnectCandidates } from "./trySnipAndReconnect" import { generateFourPointDetourCandidates } from "./tryFourPointDetour" import { simplifyPath } from "../TraceCleanupSolver/simplifyPath" +import { generateMoveTraceSegmentsCandidates } from "./tryMoveTraceSegments" + export const generateRerouteCandidates = ({ trace, label, + problem, paddingBuffer, detourCount, }: { @@ -58,5 +61,13 @@ export const generateRerouteCandidates = ({ detourCount, }) - return [...fourPointCandidates, ...snipReconnectCandidates] + const moveTraceSegmentsCandidates = generateMoveTraceSegmentsCandidates({ + initialTrace, + label, + labelBounds, + paddingBuffer, + detourCount, + }) + + return [...fourPointCandidates, ...snipReconnectCandidates, ...moveTraceSegmentsCandidates] } diff --git a/lib/solvers/TraceLabelOverlapAvoidanceSolver/tryMoveTraceSegments.ts b/lib/solvers/TraceLabelOverlapAvoidanceSolver/tryMoveTraceSegments.ts new file mode 100644 index 0000000..b5c6d9b --- /dev/null +++ b/lib/solvers/TraceLabelOverlapAvoidanceSolver/tryMoveTraceSegments.ts @@ -0,0 +1,43 @@ +import type { Point } from "@tscircuit/math-utils" +import type { SolvedTracePath } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver" +import type { Bounds } from "@tscircuit/math-utils" + +export const generateMoveTraceSegmentsCandidates = ({ + initialTrace, + label, + labelBounds, + paddingBuffer, + detourCount, +}: { + initialTrace: SolvedTracePath + label: NetLabelPlacement + labelBounds: Bounds & { chipId: string } + paddingBuffer: number + detourCount: number +}): Point[][] => { + const candidates: Point[][] = [] + const path = initialTrace.tracePath + + for (let i = 0; i < path.length - 1; i++) { + const p1 = path[i] + const p2 = path[i+1] + + const isHorizontal = p1.y === p2.y + + if (isHorizontal) { + const newY = labelBounds.maxY + paddingBuffer + label.height / 2 + const newPath = JSON.parse(JSON.stringify(path)); + newPath[i].y = newY + newPath[i+1].y = newY + candidates.push(newPath) + } else { + const newX = labelBounds.maxX + paddingBuffer + label.width / 2 + const newPath = JSON.parse(JSON.stringify(path)); + newPath[i].x = newX + newPath[i+1].x = newX + candidates.push(newPath) + } + } + return candidates +} diff --git a/tests/assets/repro.input.json b/tests/assets/repro.input.json new file mode 100644 index 0000000..d22a1c2 --- /dev/null +++ b/tests/assets/repro.input.json @@ -0,0 +1,57 @@ +{ + "netLabelPlacements": [ + { + "globalConnNetId": "connectivity_net1", + "dcConnNetId": "connectivity_net1", + "netId": ".L1 > .pin2 to .M1 > .drain", + "mspConnectionPairIds": ["L1.2-D1.1"], + "pinIds": ["L1.2", "D1.1"], + "orientation": "y+", + "anchorPoint": { + "x": 0.78, + "y": 2.97 + }, + "width": 0.2, + "height": 0.45, + "center": { + "x": 1.53, + "y": 2.97 + } + } + ], + "traces": [ + { + "mspPairId": "V1.1-L1.1", + "dcConnNetId": "connectivity_net0", + "globalConnNetId": "connectivity_net0", + "pins": [ + { + "pinId": "V1.1", + "x": -5.005, + "y": 2.54, + "chipId": "schematic_component_0", + "_facingDirection": "y+" + }, + { + "pinId": "L1.1", + "x": -0.58, + "y": 2.98, + "_facingDirection": "x-", + "chipId": "schematic_component_1" + } + ], + "tracePath": [ + { + "x": 0, + "y": 2.97 + }, + { + "x": 2, + "y": 2.97 + } + ], + "mspConnectionPairIds": ["V1.1-L1.1"], + "pinIds": ["V1.1", "L1.1"] + } + ] +} diff --git a/tests/solvers/TraceLabelOverlapAvoidanceSolver/__snapshots__/repro.snap.svg b/tests/solvers/TraceLabelOverlapAvoidanceSolver/__snapshots__/repro.snap.svg new file mode 100644 index 0000000..5450c31 --- /dev/null +++ b/tests/solvers/TraceLabelOverlapAvoidanceSolver/__snapshots__/repro.snap.svg @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/solvers/TraceLabelOverlapAvoidanceSolver/repro.test.ts b/tests/solvers/TraceLabelOverlapAvoidanceSolver/repro.test.ts new file mode 100644 index 0000000..2095016 --- /dev/null +++ b/tests/solvers/TraceLabelOverlapAvoidanceSolver/repro.test.ts @@ -0,0 +1,18 @@ +import { expect } from "bun:test" +import { test } from "bun:test" +import { TraceLabelOverlapAvoidanceSolver } from "lib/solvers/TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver" +import inputProblem from "tests/assets/example25.json" +import "tests/fixtures/matcher" +import testInput from "tests/assets/repro.input.json" + +test("TraceLabelOverlapAvoidanceSolver snapshot", () => { + const solver = new TraceLabelOverlapAvoidanceSolver({ + inputProblem: inputProblem as any, + netLabelPlacements: testInput.netLabelPlacements as any, + traces: testInput.traces as any, + }) + + solver.solve() + + expect(solver).toMatchSolverSnapshot(import.meta.path) +}) \ No newline at end of file From de711a313376d2451a541ec08f560398219b180c Mon Sep 17 00:00:00 2001 From: Exceluyi Date: Sat, 8 Nov 2025 04:29:29 +0100 Subject: [PATCH 2/4] format --- .../rerouteCollidingTrace.ts | 6 +++++- .../tryMoveTraceSegments.ts | 10 +++++----- .../TraceLabelOverlapAvoidanceSolver/repro.test.ts | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts b/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts index 16c5605..fd8bc47 100644 --- a/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts +++ b/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts @@ -69,5 +69,9 @@ export const generateRerouteCandidates = ({ detourCount, }) - return [...fourPointCandidates, ...snipReconnectCandidates, ...moveTraceSegmentsCandidates] + return [ + ...fourPointCandidates, + ...snipReconnectCandidates, + ...moveTraceSegmentsCandidates, + ] } diff --git a/lib/solvers/TraceLabelOverlapAvoidanceSolver/tryMoveTraceSegments.ts b/lib/solvers/TraceLabelOverlapAvoidanceSolver/tryMoveTraceSegments.ts index b5c6d9b..4cc260a 100644 --- a/lib/solvers/TraceLabelOverlapAvoidanceSolver/tryMoveTraceSegments.ts +++ b/lib/solvers/TraceLabelOverlapAvoidanceSolver/tryMoveTraceSegments.ts @@ -21,21 +21,21 @@ export const generateMoveTraceSegmentsCandidates = ({ for (let i = 0; i < path.length - 1; i++) { const p1 = path[i] - const p2 = path[i+1] + const p2 = path[i + 1] const isHorizontal = p1.y === p2.y if (isHorizontal) { const newY = labelBounds.maxY + paddingBuffer + label.height / 2 - const newPath = JSON.parse(JSON.stringify(path)); + const newPath = JSON.parse(JSON.stringify(path)) newPath[i].y = newY - newPath[i+1].y = newY + newPath[i + 1].y = newY candidates.push(newPath) } else { const newX = labelBounds.maxX + paddingBuffer + label.width / 2 - const newPath = JSON.parse(JSON.stringify(path)); + const newPath = JSON.parse(JSON.stringify(path)) newPath[i].x = newX - newPath[i+1].x = newX + newPath[i + 1].x = newX candidates.push(newPath) } } diff --git a/tests/solvers/TraceLabelOverlapAvoidanceSolver/repro.test.ts b/tests/solvers/TraceLabelOverlapAvoidanceSolver/repro.test.ts index 2095016..00663bb 100644 --- a/tests/solvers/TraceLabelOverlapAvoidanceSolver/repro.test.ts +++ b/tests/solvers/TraceLabelOverlapAvoidanceSolver/repro.test.ts @@ -15,4 +15,4 @@ test("TraceLabelOverlapAvoidanceSolver snapshot", () => { solver.solve() expect(solver).toMatchSolverSnapshot(import.meta.path) -}) \ No newline at end of file +}) From 7ec079e120af47d6ce964450381c5bb25b903402 Mon Sep 17 00:00:00 2001 From: Exceluyi Date: Sat, 8 Nov 2025 04:53:09 +0100 Subject: [PATCH 3/4] make sure no regressions in snapshots --- .../rerouteCollidingTrace.ts | 26 ++++++++++- .../tryMoveTraceSegments.ts | 44 ++++++++++++------- .../OverlapAvoidanceStepSolver.snap.svg | 4 +- 3 files changed, 54 insertions(+), 20 deletions(-) diff --git a/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts b/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts index fd8bc47..1759615 100644 --- a/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts +++ b/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts @@ -10,6 +10,16 @@ import { simplifyPath } from "../TraceCleanupSolver/simplifyPath" import { generateMoveTraceSegmentsCandidates } from "./tryMoveTraceSegments" +const getPathLength = (pts: Point[]) => { + let len = 0 + for (let i = 0; i < pts.length - 1; i++) { + const dx = pts[i + 1].x - pts[i].x + const dy = pts[i + 1].y - pts[i].y + len += Math.sqrt(dx * dx + dy * dy) + } + return len +} + export const generateRerouteCandidates = ({ trace, label, @@ -69,9 +79,23 @@ export const generateRerouteCandidates = ({ detourCount, }) - return [ + // Return candidates in order of preference: four-point, snip-reconnect, then move trace segments + const allCandidates = [ ...fourPointCandidates, ...snipReconnectCandidates, ...moveTraceSegmentsCandidates, ] + + // Sort by path length within each group, but keep move trace segments at the end + const sortedFourPoint = fourPointCandidates.sort( + (a, b) => getPathLength(a) - getPathLength(b), + ) + const sortedSnipReconnect = snipReconnectCandidates.sort( + (a, b) => getPathLength(a) - getPathLength(b), + ) + const sortedMoveTrace = moveTraceSegmentsCandidates.sort( + (a, b) => getPathLength(a) - getPathLength(b), + ) + + return [...sortedFourPoint, ...sortedSnipReconnect, ...sortedMoveTrace] } diff --git a/lib/solvers/TraceLabelOverlapAvoidanceSolver/tryMoveTraceSegments.ts b/lib/solvers/TraceLabelOverlapAvoidanceSolver/tryMoveTraceSegments.ts index 4cc260a..e2c8842 100644 --- a/lib/solvers/TraceLabelOverlapAvoidanceSolver/tryMoveTraceSegments.ts +++ b/lib/solvers/TraceLabelOverlapAvoidanceSolver/tryMoveTraceSegments.ts @@ -2,6 +2,7 @@ import type { Point } from "@tscircuit/math-utils" import type { SolvedTracePath } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver" import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver" import type { Bounds } from "@tscircuit/math-utils" +import { segmentIntersectsRect } from "../SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions" export const generateMoveTraceSegmentsCandidates = ({ initialTrace, @@ -19,25 +20,34 @@ export const generateMoveTraceSegmentsCandidates = ({ const candidates: Point[][] = [] const path = initialTrace.tracePath - for (let i = 0; i < path.length - 1; i++) { - const p1 = path[i] - const p2 = path[i + 1] + // Only apply to simple traces (2 points = straight line) + if (path.length !== 2) { + return [] + } - const isHorizontal = p1.y === p2.y + const p1 = path[0] + const p2 = path[1] - if (isHorizontal) { - const newY = labelBounds.maxY + paddingBuffer + label.height / 2 - const newPath = JSON.parse(JSON.stringify(path)) - newPath[i].y = newY - newPath[i + 1].y = newY - candidates.push(newPath) - } else { - const newX = labelBounds.maxX + paddingBuffer + label.width / 2 - const newPath = JSON.parse(JSON.stringify(path)) - newPath[i].x = newX - newPath[i + 1].x = newX - candidates.push(newPath) - } + // Only move segments that actually intersect with the label bounds + if (!segmentIntersectsRect(p1, p2, labelBounds)) { + return [] } + + const isHorizontal = p1.y === p2.y + + if (isHorizontal) { + const newY = labelBounds.maxY + paddingBuffer + label.height / 2 + const newPath = JSON.parse(JSON.stringify(path)) + newPath[0].y = newY + newPath[1].y = newY + candidates.push(newPath) + } else { + const newX = labelBounds.maxX + paddingBuffer + label.width / 2 + const newPath = JSON.parse(JSON.stringify(path)) + newPath[0].x = newX + newPath[1].x = newX + candidates.push(newPath) + } + return candidates } diff --git a/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/OverlapAvoidanceStepSolver.snap.svg b/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/OverlapAvoidanceStepSolver.snap.svg index e86c89d..134ae62 100644 --- a/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/OverlapAvoidanceStepSolver.snap.svg +++ b/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/OverlapAvoidanceStepSolver.snap.svg @@ -112,7 +112,7 @@ y+" data-x="1.2000000000000002" data-y="2.25" cx="425" cy="114.375" r="3" fill=" - + @@ -124,7 +124,7 @@ y+" data-x="1.2000000000000002" data-y="2.25" cx="425" cy="114.375" r="3" fill=" - + From f84ae539369af6017d051055f98030631f7851b0 Mon Sep 17 00:00:00 2001 From: Exceluyi Date: Sat, 8 Nov 2025 05:46:43 +0100 Subject: [PATCH 4/4] resolve example14 --- .../examples/__snapshots__/example14.snap.svg | 135 +++++++++++------- tests/examples/example14.test.tsx | 2 +- 2 files changed, 88 insertions(+), 49 deletions(-) diff --git a/tests/examples/__snapshots__/example14.snap.svg b/tests/examples/__snapshots__/example14.snap.svg index 6ed2077..0e7de58 100644 --- a/tests/examples/__snapshots__/example14.snap.svg +++ b/tests/examples/__snapshots__/example14.snap.svg @@ -2,156 +2,195 @@ +x+" data-x="1.2000000000000002" data-y="-0.30000000000000004" cx="425" cy="337.5" r="3" fill="hsl(319, 100%, 50%, 0.8)" /> +x-" data-x="-1.2000000000000002" data-y="-0.30000000000000004" cx="215" cy="337.5" r="3" fill="hsl(320, 100%, 50%, 0.8)" /> +x+" data-x="1.2000000000000002" data-y="0.09999999999999998" cx="425" cy="302.5" r="3" fill="hsl(321, 100%, 50%, 0.8)" /> +x-" data-x="-1.2000000000000002" data-y="0.30000000000000004" cx="215" cy="285" r="3" fill="hsl(322, 100%, 50%, 0.8)" /> +x-" data-x="-1.2000000000000002" data-y="0.10000000000000003" cx="215" cy="302.5" r="3" fill="hsl(323, 100%, 50%, 0.8)" /> +x-" data-x="-1.2000000000000002" data-y="-0.09999999999999998" cx="215" cy="320" r="3" fill="hsl(324, 100%, 50%, 0.8)" /> +x+" data-x="1.2000000000000002" data-y="-0.10000000000000003" cx="425" cy="320" r="3" fill="hsl(325, 100%, 50%, 0.8)" /> +x+" data-x="1.2000000000000002" data-y="0.30000000000000004" cx="425" cy="285" r="3" fill="hsl(326, 100%, 50%, 0.8)" /> +x+" data-x="3" data-y="-0.10000000000000016" cx="582.5" cy="320" r="3" fill="hsl(226, 100%, 50%, 0.8)" /> +x-" data-x="1.9000000000000004" data-y="-0.10000000000000002" cx="486.25" cy="320" r="3" fill="hsl(227, 100%, 50%, 0.8)" /> +x+" data-x="1.2000000000000002" data-y="-1.2944553500000002" cx="425" cy="424.514843125" r="3" fill="hsl(107, 100%, 50%, 0.8)" /> +x-" data-x="0.10000000000000009" data-y="-1.2944553500000002" cx="328.75" cy="424.514843125" r="3" fill="hsl(108, 100%, 50%, 0.8)" /> +y+" data-x="-1.2000000000000002" data-y="-1.1500000000000001" cx="215" cy="411.875" r="3" fill="hsl(121, 100%, 50%, 0.8)" /> +y-" data-x="-1.2000000000000002" data-y="-2.25" cx="215" cy="508.125" r="3" fill="hsl(122, 100%, 50%, 0.8)" /> +x+" data-x="-1.9000000000000004" data-y="0.10000000000000002" cx="153.74999999999997" cy="302.5" r="3" fill="hsl(2, 100%, 50%, 0.8)" /> +x-" data-x="-3" data-y="0.10000000000000016" cx="57.5" cy="302.5" r="3" fill="hsl(3, 100%, 50%, 0.8)" /> +y-" data-x="1.2000000000000002" data-y="1.1500000000000001" cx="425" cy="210.625" r="3" fill="hsl(348, 100%, 50%, 0.8)" /> +y+" data-x="1.2000000000000002" data-y="2.25" cx="425" cy="114.375" r="3" fill="hsl(349, 100%, 50%, 0.8)" /> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +