@tscircuit/schematic-trace-solver 0.0.97 → 0.0.99

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.
Files changed (50) hide show
  1. package/dist/index.d.ts +10 -0
  2. package/dist/index.js +1077 -519
  3. package/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts +19 -9
  4. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +30 -0
  5. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/generateEndpointCollisionDetours.ts +84 -0
  6. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +45 -1
  7. package/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts +55 -13
  8. package/lib/solvers/TraceCleanupSolver/alignSameNetRails.ts +66 -0
  9. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/evaluateRailGroup.ts +124 -0
  10. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/geometry.ts +108 -0
  11. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getComponentSideRailSegments.ts +121 -0
  12. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getRailGroups.ts +114 -0
  13. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/moveRailSegments.ts +27 -0
  14. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/preservesLabelAnchors.ts +30 -0
  15. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/scoreRailAlignment.ts +67 -0
  16. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/types.ts +34 -0
  17. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts +1 -60
  18. package/lib/utils/doesPathCoincideWithTraces.ts +61 -0
  19. package/package.json +1 -1
  20. package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +18 -18
  21. package/tests/bug-reports/bug-report-20260707T092615Z/__snapshots__/bug-report-20260707T092615Z.snap.svg +7 -7
  22. package/tests/bug-reports/bug-report-20260707T134549Z/__snapshots__/bug-report-20260707T134549Z.snap.svg +5 -7
  23. package/tests/bug-reports/bug-report-20260707T140410Z/__snapshots__/bug-report-20260707T140410Z.snap.svg +6 -12
  24. package/tests/bug-reports/bug-report-20260707T141025Z/__snapshots__/bug-report-20260707T141025Z.snap.svg +1 -1
  25. package/tests/examples/__snapshots__/example02.snap.svg +1 -1
  26. package/tests/examples/__snapshots__/example09.snap.svg +9 -13
  27. package/tests/examples/__snapshots__/example14.snap.svg +1 -1
  28. package/tests/examples/__snapshots__/example18.snap.svg +1 -1
  29. package/tests/examples/__snapshots__/example21.snap.svg +30 -32
  30. package/tests/examples/__snapshots__/example29.snap.svg +13 -19
  31. package/tests/examples/__snapshots__/example46.snap.svg +1 -1
  32. package/tests/repros/__snapshots__/repro-cc2340r5.snap.svg +1 -1
  33. package/tests/repros/__snapshots__/repro-rp2040-gamepad-trace-alignment.snap.svg +1 -1
  34. package/tests/repros/__snapshots__/repro129-host-custom-symbol-passives.snap.svg +60 -0
  35. package/tests/repros/__snapshots__/repro130-bq27441-fuel-gauge-trace-through-c1.snap.svg +1 -1
  36. package/tests/repros/assets/repro129-host-custom-symbol-passives.input.json +94 -0
  37. package/tests/repros/repro-rp2040-gamepad-trace-alignment.test.ts +24 -0
  38. package/tests/repros/repro129-host-custom-symbol-passives.test.ts +16 -0
  39. package/tests/solvers/MspConnectionPairSolver/msp-connection-pair-solver-direct-connection-distance.test.ts +46 -0
  40. package/tests/solvers/SchematicTraceSingleLineSolver2/generate-endpoint-collision-detours.test.ts +38 -0
  41. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-component-scope.test.ts +38 -0
  42. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-eligible-traces.test.ts +28 -0
  43. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-horizontal.test.ts +62 -0
  44. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-label-anchor.test.ts +36 -0
  45. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-label-junction.test.ts +42 -0
  46. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-obstacle.test.ts +28 -0
  47. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-pipeline-label-connector.test.ts +52 -0
  48. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-vertical.test.ts +25 -0
  49. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-visible-length.test.ts +40 -0
  50. package/tests/solvers/TraceCleanupSolver/fixtures/alignSameNetRails.ts +89 -0
@@ -0,0 +1,38 @@
1
+ import { expect, test } from "bun:test"
2
+ import { generateEndpointCollisionDetours } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/generateEndpointCollisionDetours"
3
+
4
+ test("endpoint collision detours preserve both endpoint anchors", () => {
5
+ const path = [
6
+ { x: 0, y: 1 },
7
+ { x: 2, y: 1 },
8
+ { x: 2, y: -1 },
9
+ ]
10
+
11
+ const obstacle = {
12
+ minX: 1,
13
+ maxX: 3,
14
+ minY: 0,
15
+ maxY: 2,
16
+ kind: "chip" as const,
17
+ chipId: "U1",
18
+ }
19
+
20
+ const detours = [0, 1].flatMap((collidingSegmentIndex) =>
21
+ generateEndpointCollisionDetours({
22
+ path,
23
+ collidingSegmentIndex,
24
+ obstacle,
25
+ }),
26
+ )
27
+
28
+ expect(detours.length).toBeGreaterThan(0)
29
+ expect(
30
+ detours.every(
31
+ (detour) =>
32
+ detour[0]?.x === path[0]?.x &&
33
+ detour[0]?.y === path[0]?.y &&
34
+ detour.at(-1)?.x === path.at(-1)?.x &&
35
+ detour.at(-1)?.y === path.at(-1)?.y,
36
+ ),
37
+ ).toBe(true)
38
+ })
@@ -0,0 +1,38 @@
1
+ import { expect, test } from "bun:test"
2
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
3
+ import type { InputProblem } from "lib/types/InputProblem"
4
+ import {
5
+ align,
6
+ getVerticalRailTraces,
7
+ verticalProblem,
8
+ } from "./fixtures/alignSameNetRails"
9
+
10
+ test("does not combine rails from different component sides", () => {
11
+ const traces = getVerticalRailTraces()
12
+ traces[1] = {
13
+ ...traces[1]!,
14
+ pins: traces[1]!.pins.map((item) => ({ ...item, chipId: "U2" })) as [
15
+ SolvedTracePath["pins"][0],
16
+ SolvedTracePath["pins"][1],
17
+ ],
18
+ }
19
+ const inputProblem: InputProblem = {
20
+ ...verticalProblem,
21
+ chips: [
22
+ ...verticalProblem.chips,
23
+ {
24
+ ...verticalProblem.chips[0]!,
25
+ chipId: "U2",
26
+ pins: verticalProblem.chips[0]!.pins.map((item) => ({
27
+ ...item,
28
+ pinId: item.pinId.replace("U1", "U2"),
29
+ })),
30
+ },
31
+ ],
32
+ }
33
+
34
+ const result = align(traces, { inputProblem })
35
+
36
+ expect(result.alignedRailGroupCount).toBe(0)
37
+ expect(result.traces).toEqual(traces)
38
+ })
@@ -0,0 +1,28 @@
1
+ import { expect, test } from "bun:test"
2
+ import {
3
+ align,
4
+ createTrace,
5
+ getVerticalPin,
6
+ getVerticalRailTraces,
7
+ } from "./fixtures/alignSameNetRails"
8
+
9
+ test("uses routed-trace provenance to exclude generated label connectors", () => {
10
+ const realTrace = getVerticalRailTraces()[0]!
11
+ const connector = createTrace(
12
+ "label-connector",
13
+ [
14
+ { x: -1, y: 0 },
15
+ { x: -5, y: 0 },
16
+ { x: -5, y: -2 },
17
+ { x: -1, y: -2 },
18
+ ],
19
+ [getVerticalPin("U1.2"), getVerticalPin("U1.3")],
20
+ )
21
+
22
+ const result = align([realTrace, connector], {
23
+ eligibleTraceIds: new Set([realTrace.mspPairId]),
24
+ })
25
+
26
+ expect(result.alignedRailGroupCount).toBe(0)
27
+ expect(result.traces).toEqual([realTrace, connector])
28
+ })
@@ -0,0 +1,62 @@
1
+ import { expect, test } from "bun:test"
2
+ import type { InputProblem } from "lib/types/InputProblem"
3
+ import {
4
+ align,
5
+ createTrace,
6
+ verticalProblem,
7
+ } from "./fixtures/alignSameNetRails"
8
+
9
+ test("applies the same component-side rule to horizontal rails", () => {
10
+ const inputProblem: InputProblem = {
11
+ ...verticalProblem,
12
+ chips: [
13
+ {
14
+ chipId: "U2",
15
+ center: { x: 0, y: 0 },
16
+ width: 6,
17
+ height: 2,
18
+ pins: [
19
+ { pinId: "U2.1", x: -2, y: 1, _facingDirection: "y+" },
20
+ { pinId: "U2.2", x: 0, y: 1, _facingDirection: "y+" },
21
+ { pinId: "U2.3", x: 2, y: 1, _facingDirection: "y+" },
22
+ ],
23
+ },
24
+ ],
25
+ }
26
+ const inputPins = inputProblem.chips[0]!.pins.map((inputPin) => ({
27
+ ...inputPin,
28
+ chipId: "U2",
29
+ }))
30
+ const traces = [
31
+ createTrace(
32
+ "left",
33
+ [
34
+ { x: -2, y: 1 },
35
+ { x: -2, y: 2 },
36
+ { x: 0, y: 2 },
37
+ { x: 0, y: 1 },
38
+ ],
39
+ [inputPins[0]!, inputPins[1]!],
40
+ ),
41
+ createTrace(
42
+ "right",
43
+ [
44
+ { x: 0, y: 1 },
45
+ { x: 0, y: 3 },
46
+ { x: 2, y: 3 },
47
+ { x: 2, y: 1 },
48
+ ],
49
+ [inputPins[1]!, inputPins[2]!],
50
+ ),
51
+ ]
52
+
53
+ const result = align(traces, { inputProblem })
54
+
55
+ expect(result.alignedRailGroupCount).toBe(1)
56
+ expect(result.traces[1]!.tracePath).toEqual([
57
+ { x: 0, y: 1 },
58
+ { x: 0, y: 2 },
59
+ { x: 2, y: 2 },
60
+ { x: 2, y: 1 },
61
+ ])
62
+ })
@@ -0,0 +1,36 @@
1
+ import { expect, test } from "bun:test"
2
+ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
3
+ import { align, getVerticalRailTraces } from "./fixtures/alignSameNetRails"
4
+
5
+ test("preserves existing label anchors", () => {
6
+ const traces = getVerticalRailTraces()
7
+ const labels: NetLabelPlacement[] = [
8
+ {
9
+ globalConnNetId: "power-net",
10
+ netId: "POWER",
11
+ mspConnectionPairIds: ["upper"],
12
+ pinIds: ["U1.1", "U1.2"],
13
+ orientation: "x+",
14
+ anchorPoint: { x: -2, y: 1 },
15
+ center: { x: -1.8, y: 1 },
16
+ width: 0.4,
17
+ height: 0.2,
18
+ },
19
+ {
20
+ globalConnNetId: "power-net",
21
+ netId: "POWER",
22
+ mspConnectionPairIds: ["lower"],
23
+ pinIds: ["U1.2", "U1.3"],
24
+ orientation: "x+",
25
+ anchorPoint: { x: -3, y: -1 },
26
+ center: { x: -2.8, y: -1 },
27
+ width: 0.4,
28
+ height: 0.2,
29
+ },
30
+ ]
31
+
32
+ const result = align(traces, { netLabelPlacements: labels })
33
+
34
+ expect(result.alignedRailGroupCount).toBe(0)
35
+ expect(result.traces).toEqual(traces)
36
+ })
@@ -0,0 +1,42 @@
1
+ import { expect, test } from "bun:test"
2
+ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
3
+ import {
4
+ align,
5
+ createTrace,
6
+ getVerticalRailTraces,
7
+ } from "./fixtures/alignSameNetRails"
8
+
9
+ test("preserves every trace joined at a net-label anchor", () => {
10
+ const rails = getVerticalRailTraces()
11
+ const anchorKeeper = createTrace(
12
+ "anchor-keeper",
13
+ [
14
+ { x: -3, y: -1 },
15
+ { x: -4, y: -1 },
16
+ ],
17
+ [
18
+ { pinId: "X1.1", chipId: "X1", x: -3, y: -1 },
19
+ { pinId: "X2.1", chipId: "X2", x: -4, y: -1 },
20
+ ],
21
+ )
22
+ const traces = [...rails, anchorKeeper]
23
+ const label: NetLabelPlacement = {
24
+ globalConnNetId: "power-net",
25
+ netId: "POWER",
26
+ mspConnectionPairIds: [anchorKeeper.mspPairId],
27
+ pinIds: anchorKeeper.pinIds,
28
+ orientation: "x-",
29
+ anchorPoint: { x: -3, y: -1 },
30
+ center: { x: -3.2, y: -1 },
31
+ width: 0.4,
32
+ height: 0.2,
33
+ }
34
+
35
+ const result = align(traces, {
36
+ netLabelPlacements: [label],
37
+ eligibleTraceIds: new Set(rails.map((trace) => trace.mspPairId)),
38
+ })
39
+
40
+ expect(result.alignedRailGroupCount).toBe(0)
41
+ expect(result.traces).toEqual(traces)
42
+ })
@@ -0,0 +1,28 @@
1
+ import { expect, test } from "bun:test"
2
+ import {
3
+ align,
4
+ getVerticalRailTraces,
5
+ verticalProblem,
6
+ } from "./fixtures/alignSameNetRails"
7
+
8
+ test("does not align through an obstacle", () => {
9
+ const traces = getVerticalRailTraces()
10
+ const result = align(traces, {
11
+ inputProblem: {
12
+ ...verticalProblem,
13
+ chips: [
14
+ ...verticalProblem.chips,
15
+ {
16
+ chipId: "barrier",
17
+ center: { x: -2.5, y: 0 },
18
+ width: 0.4,
19
+ height: 0.4,
20
+ pins: [],
21
+ },
22
+ ],
23
+ },
24
+ })
25
+
26
+ expect(result.alignedRailGroupCount).toBe(0)
27
+ expect(result.traces).toEqual(traces)
28
+ })
@@ -0,0 +1,52 @@
1
+ import { expect, test } from "bun:test"
2
+ import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
3
+ import type { InputProblem } from "lib/types/InputProblem"
4
+
5
+ test("pipeline does not stretch a real loop toward a generated label connector", () => {
6
+ const inputProblem: InputProblem = {
7
+ chips: [
8
+ {
9
+ chipId: "U3",
10
+ center: { x: 0, y: 0 },
11
+ width: 2.8,
12
+ height: 1.4,
13
+ pins: [
14
+ { pinId: "U3.3", x: 1.4, y: -0.3 },
15
+ { pinId: "U3.7", x: 1.4, y: -0.5 },
16
+ ],
17
+ },
18
+ ],
19
+ directConnections: [],
20
+ netConnections: [
21
+ {
22
+ netId: "V3_3",
23
+ pinIds: ["U3.3", "U3.7"],
24
+ netLabelWidth: 0.42,
25
+ netLabelHeight: 0.6,
26
+ },
27
+ ],
28
+ textBoxes: [],
29
+ availableNetLabelOrientations: { V3_3: ["y+"] },
30
+ maxMspPairDistance: 2.4,
31
+ }
32
+ const solver = new SchematicTracePipelineSolver(inputProblem)
33
+
34
+ solver.solve()
35
+
36
+ const traces = solver.traceCleanupSolver2!.getOutput().traces
37
+ const realTrace = traces.find((trace) => trace.mspPairId === "U3.3-U3.7")!
38
+ const labelConnector = traces.find((trace) =>
39
+ trace.mspPairId.startsWith("available-net-orientation-"),
40
+ )!
41
+ expect(realTrace.tracePath).toEqual([
42
+ { x: 1.4, y: -0.3 },
43
+ { x: 1.5999999999999999, y: -0.3 },
44
+ { x: 1.5999999999999999, y: -0.5 },
45
+ { x: 1.4, y: -0.5 },
46
+ ])
47
+ expect(labelConnector.mspPairId).toBe("available-net-orientation-0-V3_3")
48
+ expect(solver.traceCleanupSolver2!.stats).toMatchObject({
49
+ alignedRailGroupCount: 0,
50
+ alignedTraceCount: 0,
51
+ })
52
+ })
@@ -0,0 +1,25 @@
1
+ import { expect, test } from "bun:test"
2
+ import { align, getVerticalRailTraces } from "./fixtures/alignSameNetRails"
3
+
4
+ test("aligns same-net rails on one component side", () => {
5
+ const result = align(getVerticalRailTraces())
6
+
7
+ expect(result).toMatchObject({
8
+ alignedRailGroupCount: 1,
9
+ alignedTraceCount: 1,
10
+ })
11
+ expect(result.traces.map((trace) => trace.tracePath)).toEqual([
12
+ [
13
+ { x: -1, y: 2 },
14
+ { x: -2, y: 2 },
15
+ { x: -2, y: 0 },
16
+ { x: -1, y: 0 },
17
+ ],
18
+ [
19
+ { x: -1, y: 0 },
20
+ { x: -2, y: 0 },
21
+ { x: -2, y: -2 },
22
+ { x: -1, y: -2 },
23
+ ],
24
+ ])
25
+ })
@@ -0,0 +1,40 @@
1
+ import { expect, test } from "bun:test"
2
+ import {
3
+ align,
4
+ createTrace,
5
+ getVerticalPin,
6
+ getVerticalRailTraces,
7
+ verticalProblem,
8
+ } from "./fixtures/alignSameNetRails"
9
+
10
+ test("keeps the baseline when alignment would only lengthen visible geometry", () => {
11
+ const traces = getVerticalRailTraces()
12
+ traces[1] = createTrace(
13
+ "lower",
14
+ [
15
+ { x: -1, y: 0 },
16
+ { x: -10, y: 0 },
17
+ { x: -10, y: -2 },
18
+ { x: -1, y: -2 },
19
+ ],
20
+ [getVerticalPin("U1.2"), getVerticalPin("U1.3")],
21
+ )
22
+ const result = align(traces, {
23
+ inputProblem: {
24
+ ...verticalProblem,
25
+ chips: [
26
+ ...verticalProblem.chips,
27
+ {
28
+ chipId: "blocks-short-candidate",
29
+ center: { x: -2, y: -1 },
30
+ width: 0.2,
31
+ height: 0.4,
32
+ pins: [],
33
+ },
34
+ ],
35
+ },
36
+ })
37
+
38
+ expect(result.alignedRailGroupCount).toBe(0)
39
+ expect(result.traces).toEqual(traces)
40
+ })
@@ -0,0 +1,89 @@
1
+ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
2
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
3
+ import { alignSameNetRails } from "lib/solvers/TraceCleanupSolver/alignSameNetRails"
4
+ import type { InputPin, InputProblem } from "lib/types/InputProblem"
5
+
6
+ export const verticalProblem: InputProblem = {
7
+ chips: [
8
+ {
9
+ chipId: "U1",
10
+ center: { x: 0, y: 0 },
11
+ width: 2,
12
+ height: 6,
13
+ pins: [
14
+ { pinId: "U1.1", x: -1, y: 2, _facingDirection: "x-" },
15
+ { pinId: "U1.2", x: -1, y: 0, _facingDirection: "x-" },
16
+ { pinId: "U1.3", x: -1, y: -2, _facingDirection: "x-" },
17
+ ],
18
+ },
19
+ ],
20
+ directConnections: [],
21
+ netConnections: [],
22
+ textBoxes: [],
23
+ availableNetLabelOrientations: {},
24
+ }
25
+
26
+ export const getVerticalPin = (
27
+ pinId: string,
28
+ ): InputPin & { chipId: string } => {
29
+ const inputPin = verticalProblem.chips[0]!.pins.find(
30
+ (candidate) => candidate.pinId === pinId,
31
+ )!
32
+ return { ...inputPin, chipId: "U1" }
33
+ }
34
+
35
+ export const createTrace = (
36
+ mspPairId: string,
37
+ tracePath: Array<{ x: number; y: number }>,
38
+ pins: SolvedTracePath["pins"],
39
+ globalConnNetId = "power-net",
40
+ ): SolvedTracePath => ({
41
+ mspPairId,
42
+ globalConnNetId,
43
+ dcConnNetId: mspPairId,
44
+ userNetId: "POWER",
45
+ pins,
46
+ tracePath,
47
+ mspConnectionPairIds: [mspPairId],
48
+ pinIds: pins.map((item) => item.pinId),
49
+ })
50
+
51
+ export const getVerticalRailTraces = () => [
52
+ createTrace(
53
+ "upper",
54
+ [
55
+ { x: -1, y: 2 },
56
+ { x: -2, y: 2 },
57
+ { x: -2, y: 0 },
58
+ { x: -1, y: 0 },
59
+ ],
60
+ [getVerticalPin("U1.1"), getVerticalPin("U1.2")],
61
+ ),
62
+ createTrace(
63
+ "lower",
64
+ [
65
+ { x: -1, y: 0 },
66
+ { x: -3, y: 0 },
67
+ { x: -3, y: -2 },
68
+ { x: -1, y: -2 },
69
+ ],
70
+ [getVerticalPin("U1.2"), getVerticalPin("U1.3")],
71
+ ),
72
+ ]
73
+
74
+ export const align = (
75
+ traces: SolvedTracePath[],
76
+ options: {
77
+ inputProblem?: InputProblem
78
+ netLabelPlacements?: NetLabelPlacement[]
79
+ eligibleTraceIds?: ReadonlySet<string>
80
+ } = {},
81
+ ) =>
82
+ alignSameNetRails({
83
+ inputProblem: options.inputProblem ?? verticalProblem,
84
+ traces,
85
+ netLabelPlacements: options.netLabelPlacements ?? [],
86
+ eligibleTraceIds:
87
+ options.eligibleTraceIds ??
88
+ new Set(traces.map((trace) => trace.mspPairId)),
89
+ })