@tscircuit/schematic-trace-solver 0.0.107 → 0.0.109
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +31 -3
- package/dist/index.js +784 -247
- package/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts +3 -1
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +58 -27
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/generateEndpointCollisionDetours.ts +1 -10
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/generateInternalSegmentCollisionDetours.ts +94 -0
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/pathOps.ts +10 -0
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect.ts +5 -1
- package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +18 -3
- package/lib/solvers/UnroutedTraceRecoverySolver/UnroutedTraceRecoverySolver.ts +592 -0
- package/package.json +1 -1
- package/site/bug-reports/bug-report-20260724T175257Z.page.tsx +4 -0
- package/tests/bug-reports/bug-report-20260707T092615Z/__snapshots__/bug-report-20260707T092615Z.snap.svg +5 -9
- package/tests/bug-reports/bug-report-20260708T095725Z/__snapshots__/bug-report-20260708T095725Z.snap.svg +5 -5
- package/tests/bug-reports/bug-report-20260708T095725Z/bug-report-20260708T095725Z.test.ts +15 -0
- package/tests/bug-reports/bug-report-20260724T175257Z/__snapshots__/bug-report-20260724T175257Z.snap.svg +58 -0
- package/tests/bug-reports/bug-report-20260724T175257Z/bug-report-20260724T175257Z.json +138 -0
- package/tests/bug-reports/bug-report-20260724T175257Z/bug-report-20260724T175257Z.test.ts +12 -0
- package/tests/examples/__snapshots__/example04.snap.svg +7 -9
- package/tests/examples/__snapshots__/example08.snap.svg +16 -20
- package/tests/examples/__snapshots__/example13.snap.svg +2 -4
- package/tests/examples/__snapshots__/example32.snap.svg +64 -66
- package/tests/examples/__snapshots__/example46.snap.svg +1 -1
- package/tests/repros/__snapshots__/repro-netlabel-collision-687.snap.svg +81 -0
- package/tests/repros/__snapshots__/repro-netlabel-overlap-trace.snap.svg +16 -18
- package/tests/repros/__snapshots__/repro-tps61222-trace-intersection.snap.svg +63 -220
- package/tests/repros/assets/repro-netlabel-collision-687.input.json +212 -0
- package/tests/repros/repro-netlabel-collision-687.test.ts +59 -0
- package/tests/repros/repro-tps61222-trace-intersection.test.ts +27 -0
- package/tests/solvers/SchematicTraceSingleLineSolver2/generate-internal-segment-collision-detours.test.ts +49 -0
- package/tests/solvers/UnroutedTraceRecoverySolver/paired-junction-recovery.test.ts +49 -0
- package/tests/solvers/UnroutedTraceRecoverySolver/skip-ground.test.ts +30 -0
- package/tests/solvers/UnroutedTraceRecoverySolver/skip-long-connection.test.ts +24 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
|
|
3
|
+
import inputProblem from "./assets/repro-netlabel-collision-687.input.json"
|
|
4
|
+
import "tests/fixtures/matcher"
|
|
5
|
+
|
|
6
|
+
const boundsOf = (label: {
|
|
7
|
+
center: { x: number; y: number }
|
|
8
|
+
width: number
|
|
9
|
+
height: number
|
|
10
|
+
}) => ({
|
|
11
|
+
minX: label.center.x - label.width / 2,
|
|
12
|
+
maxX: label.center.x + label.width / 2,
|
|
13
|
+
minY: label.center.y - label.height / 2,
|
|
14
|
+
maxY: label.center.y + label.height / 2,
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const overlapArea = (
|
|
18
|
+
a: ReturnType<typeof boundsOf>,
|
|
19
|
+
b: ReturnType<typeof boundsOf>,
|
|
20
|
+
) => {
|
|
21
|
+
const w = Math.min(a.maxX, b.maxX) - Math.max(a.minX, b.minX)
|
|
22
|
+
const h = Math.min(a.maxY, b.maxY) - Math.max(a.minY, b.minY)
|
|
23
|
+
return w > 0 && h > 0 ? w * h : 0
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Reproduction for #687, minimized from bug-report-20260721T221026Z.
|
|
27
|
+
//
|
|
28
|
+
// U1's right-edge pin column is fully walled in: a solid column of port
|
|
29
|
+
// labels (QD0..QD3, QCLK) to the east, chips C1/C2 above and below, and
|
|
30
|
+
// chip U3 closing off the corridor. The V1V1 trace (U1.1 -> U1.7) wraps
|
|
31
|
+
// around the label column, and its net label has NO strictly collision-free
|
|
32
|
+
// candidate anywhere along the trace. NetLabelNetLabelCollisionSolver gives
|
|
33
|
+
// up and leaves the V1V1 label stacked directly on top of the QD3 and QCLK
|
|
34
|
+
// port labels — the same pile-up as the original board.
|
|
35
|
+
//
|
|
36
|
+
// This test asserts the CURRENT (buggy) behavior so the bug is pinned by CI.
|
|
37
|
+
// A fix should flip the two non-zero overlap assertions to .toBe(0).
|
|
38
|
+
test("repro #687: V1V1 net label left stacked on QD3/QCLK port labels", () => {
|
|
39
|
+
const solver = new SchematicTracePipelineSolver(inputProblem as any)
|
|
40
|
+
solver.solve()
|
|
41
|
+
|
|
42
|
+
const labels =
|
|
43
|
+
solver.netLabelNetLabelCollisionSolver!.outputNetLabelPlacements
|
|
44
|
+
|
|
45
|
+
const v1v1 = labels.find((l) => l.netId === "V1V1")!
|
|
46
|
+
const qd3 = labels.find((l) => l.netId === "QD3" && l.center.x < 3)!
|
|
47
|
+
const qclk = labels.find((l) => l.netId === "QCLK" && l.center.x < 3)!
|
|
48
|
+
expect(v1v1).toBeDefined()
|
|
49
|
+
expect(qd3).toBeDefined()
|
|
50
|
+
expect(qclk).toBeDefined()
|
|
51
|
+
|
|
52
|
+
// BUG: the V1V1 label still overlaps both port labels after the collision
|
|
53
|
+
// solver has run (total stacked overlap ≈ 0.16)
|
|
54
|
+
expect(overlapArea(boundsOf(v1v1), boundsOf(qd3))).toBeGreaterThan(0)
|
|
55
|
+
expect(overlapArea(boundsOf(v1v1), boundsOf(qclk))).toBeGreaterThan(0)
|
|
56
|
+
|
|
57
|
+
// visual snapshot: the V1V1 label sits on the QD3/QCLK pile-up
|
|
58
|
+
expect(solver).toMatchSolverSnapshot(import.meta.path)
|
|
59
|
+
})
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { expect, test } from "bun:test"
|
|
2
|
+
import { isPathCollidingWithObstacles } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
|
|
2
3
|
import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
|
|
4
|
+
import { getTextBoxBounds } from "lib/utils/textBoxBounds"
|
|
3
5
|
import "tests/fixtures/matcher"
|
|
4
6
|
import inputProblem from "./assets/repro-tps61222-trace-intersection.input.json"
|
|
5
7
|
|
|
@@ -8,7 +10,32 @@ import inputProblem from "./assets/repro-tps61222-trace-intersection.input.json"
|
|
|
8
10
|
test("repro TPS61222 schematic trace intersection", () => {
|
|
9
11
|
const solver = new SchematicTracePipelineSolver(inputProblem as any)
|
|
10
12
|
|
|
13
|
+
const manufacturerPartNumberTextBox = inputProblem.textBoxes.find(
|
|
14
|
+
(textBox) => textBox.text === "TPS61222DCKT",
|
|
15
|
+
)
|
|
16
|
+
expect(manufacturerPartNumberTextBox).toBeDefined()
|
|
17
|
+
|
|
18
|
+
const textBounds = getTextBoxBounds(manufacturerPartNumberTextBox!)
|
|
19
|
+
solver.solveUntilPhase("longDistancePairSolver")
|
|
20
|
+
|
|
21
|
+
const initialGroundTrace =
|
|
22
|
+
solver.schematicTraceLinesSolver!.solvedTracePaths.find(
|
|
23
|
+
(trace) => trace.mspPairId === "U2.3-C25.2",
|
|
24
|
+
)
|
|
25
|
+
expect(initialGroundTrace).toBeDefined()
|
|
26
|
+
expect(
|
|
27
|
+
isPathCollidingWithObstacles(initialGroundTrace!.tracePath, [textBounds]),
|
|
28
|
+
).toBe(false)
|
|
29
|
+
|
|
11
30
|
solver.solve()
|
|
12
31
|
|
|
32
|
+
const crossingTraceIds = solver
|
|
33
|
+
.netLabelTraceCollisionSolver!.getOutput()
|
|
34
|
+
.traces.filter((trace) =>
|
|
35
|
+
isPathCollidingWithObstacles(trace.tracePath, [textBounds]),
|
|
36
|
+
)
|
|
37
|
+
.map((trace) => trace.mspPairId)
|
|
38
|
+
|
|
39
|
+
expect(crossingTraceIds).toEqual([])
|
|
13
40
|
expect(solver).toMatchSolverSnapshot(import.meta.path)
|
|
14
41
|
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { isPathCollidingWithObstacles } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
|
|
3
|
+
import { generateInternalSegmentCollisionDetours } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/generateInternalSegmentCollisionDetours"
|
|
4
|
+
|
|
5
|
+
test("internal segment detours preserve endpoint legs and clear the obstacle", () => {
|
|
6
|
+
const path = [
|
|
7
|
+
{ x: 0, y: 0 },
|
|
8
|
+
{ x: 1, y: 0 },
|
|
9
|
+
{ x: 9, y: 0 },
|
|
10
|
+
{ x: 10, y: 0 },
|
|
11
|
+
]
|
|
12
|
+
const obstacle = { minX: 4, minY: -0.5, maxX: 6, maxY: 0.5 }
|
|
13
|
+
|
|
14
|
+
const detours = generateInternalSegmentCollisionDetours({
|
|
15
|
+
path,
|
|
16
|
+
collidingSegmentIndex: 1,
|
|
17
|
+
obstacle,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
expect(detours).toHaveLength(2)
|
|
21
|
+
for (const detour of detours) {
|
|
22
|
+
expect(detour.slice(0, 2)).toEqual(path.slice(0, 2))
|
|
23
|
+
expect(detour.slice(-2)).toEqual(path.slice(-2))
|
|
24
|
+
expect(isPathCollidingWithObstacles(detour, [obstacle])).toBe(false)
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test("internal segment detours support vertical paths", () => {
|
|
29
|
+
const path = [
|
|
30
|
+
{ x: 0, y: 0 },
|
|
31
|
+
{ x: 0, y: 1 },
|
|
32
|
+
{ x: 0, y: 9 },
|
|
33
|
+
{ x: 0, y: 10 },
|
|
34
|
+
]
|
|
35
|
+
const obstacle = { minX: -0.5, minY: 4, maxX: 0.5, maxY: 6 }
|
|
36
|
+
|
|
37
|
+
const detours = generateInternalSegmentCollisionDetours({
|
|
38
|
+
path,
|
|
39
|
+
collidingSegmentIndex: 1,
|
|
40
|
+
obstacle,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
expect(detours).toHaveLength(2)
|
|
44
|
+
expect(
|
|
45
|
+
detours.every(
|
|
46
|
+
(detour) => !isPathCollidingWithObstacles(detour, [obstacle]),
|
|
47
|
+
),
|
|
48
|
+
).toBe(true)
|
|
49
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { countPathIntersections } from "lib/solvers/Example28Solver/geometry"
|
|
3
|
+
import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
|
|
4
|
+
import { segmentOverlapsRectBoundary } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
|
|
5
|
+
import { chipToRect } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
|
|
6
|
+
import inputProblem from "tests/bug-reports/bug-report-20260724T175257Z/bug-report-20260724T175257Z.json"
|
|
7
|
+
|
|
8
|
+
test("recovers paired connections through clear same-net junctions", () => {
|
|
9
|
+
const solver = new SchematicTracePipelineSolver(inputProblem as any)
|
|
10
|
+
|
|
11
|
+
solver.solve()
|
|
12
|
+
|
|
13
|
+
const recoveredTraces =
|
|
14
|
+
solver.unroutedTraceRecoverySolver!.solvedUnroutedTraces
|
|
15
|
+
const alreadySolvedTraces =
|
|
16
|
+
solver.longDistancePairSolver!.getOutput().allTracesMerged
|
|
17
|
+
|
|
18
|
+
expect(recoveredTraces.map((trace) => trace.mspPairId)).toEqual([
|
|
19
|
+
"R1.1-C1.1",
|
|
20
|
+
"R1.2-C1.2",
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
for (const recoveredTrace of recoveredTraces) {
|
|
24
|
+
for (
|
|
25
|
+
let pointIndex = 0;
|
|
26
|
+
pointIndex < recoveredTrace.tracePath.length - 1;
|
|
27
|
+
pointIndex++
|
|
28
|
+
) {
|
|
29
|
+
const startPoint = recoveredTrace.tracePath[pointIndex]!
|
|
30
|
+
const endPoint = recoveredTrace.tracePath[pointIndex + 1]!
|
|
31
|
+
for (const chip of solver.inputProblem.chips) {
|
|
32
|
+
expect(
|
|
33
|
+
segmentOverlapsRectBoundary(startPoint, endPoint, chipToRect(chip)),
|
|
34
|
+
).toBe(false)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
for (const existingTrace of alreadySolvedTraces) {
|
|
38
|
+
const intersectionCount = countPathIntersections(
|
|
39
|
+
recoveredTrace.tracePath,
|
|
40
|
+
existingTrace.tracePath,
|
|
41
|
+
)
|
|
42
|
+
if (recoveredTrace.globalConnNetId === existingTrace.globalConnNetId) {
|
|
43
|
+
expect(intersectionCount).toBeGreaterThan(0)
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
expect(intersectionCount).toBe(0)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
|
|
3
|
+
import { UnroutedTraceRecoverySolver } from "lib/solvers/UnroutedTraceRecoverySolver/UnroutedTraceRecoverySolver"
|
|
4
|
+
import inputProblem from "tests/assets/example12.json"
|
|
5
|
+
|
|
6
|
+
test("does not recover the canonical ground connectivity net", () => {
|
|
7
|
+
const pipelineSolver = new SchematicTracePipelineSolver(inputProblem as any)
|
|
8
|
+
pipelineSolver.solveUntilPhase("unroutedTraceRecoverySolver")
|
|
9
|
+
|
|
10
|
+
const failedConnectionPairs =
|
|
11
|
+
pipelineSolver.schematicTraceLinesSolver!.failedConnectionPairs.map(
|
|
12
|
+
(connectionPair) => {
|
|
13
|
+
return {
|
|
14
|
+
...connectionPair,
|
|
15
|
+
userNetId: undefined,
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
)
|
|
19
|
+
const recoverySolver = new UnroutedTraceRecoverySolver({
|
|
20
|
+
inputProblem: pipelineSolver.inputProblem,
|
|
21
|
+
failedConnectionPairs,
|
|
22
|
+
alreadySolvedTraces:
|
|
23
|
+
pipelineSolver.longDistancePairSolver!.getOutput().allTracesMerged,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
recoverySolver.solve()
|
|
27
|
+
|
|
28
|
+
expect(failedConnectionPairs).not.toHaveLength(0)
|
|
29
|
+
expect(recoverySolver.solvedUnroutedTraces).toHaveLength(0)
|
|
30
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
|
|
3
|
+
import { UnroutedTraceRecoverySolver } from "lib/solvers/UnroutedTraceRecoverySolver/UnroutedTraceRecoverySolver"
|
|
4
|
+
import inputProblem from "tests/bug-reports/bug-report-20260724T175257Z/bug-report-20260724T175257Z.json"
|
|
5
|
+
|
|
6
|
+
test("does not recover connection pairs beyond maxMspPairDistance", () => {
|
|
7
|
+
const pipelineSolver = new SchematicTracePipelineSolver(inputProblem as any)
|
|
8
|
+
pipelineSolver.solveUntilPhase("unroutedTraceRecoverySolver")
|
|
9
|
+
|
|
10
|
+
const recoverySolver = new UnroutedTraceRecoverySolver({
|
|
11
|
+
inputProblem: {
|
|
12
|
+
...pipelineSolver.inputProblem,
|
|
13
|
+
maxMspPairDistance: 1,
|
|
14
|
+
},
|
|
15
|
+
failedConnectionPairs:
|
|
16
|
+
pipelineSolver.schematicTraceLinesSolver!.failedConnectionPairs,
|
|
17
|
+
alreadySolvedTraces:
|
|
18
|
+
pipelineSolver.longDistancePairSolver!.getOutput().allTracesMerged,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
recoverySolver.solve()
|
|
22
|
+
|
|
23
|
+
expect(recoverySolver.solvedUnroutedTraces).toHaveLength(0)
|
|
24
|
+
})
|