@tscircuit/schematic-trace-solver 0.0.41 → 0.0.42

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 (46) hide show
  1. package/dist/index.d.ts +118 -7
  2. package/dist/index.js +760 -432
  3. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +30 -17
  4. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver.ts +94 -192
  5. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts +13 -24
  6. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/LabelMergingSolver.ts +205 -0
  7. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/OverlapAvoidanceStepSolver.ts +144 -0
  8. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts +117 -0
  9. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/TraceCleanupSolver/TraceCleanupSolver.ts +132 -0
  10. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/{balanceLShapes.ts → sub-solvers/TraceCleanupSolver/balanceLShapes.ts} +12 -10
  11. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/{hasCollisions.ts → sub-solvers/TraceCleanupSolver/hasCollisions.ts} +1 -1
  12. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/{hasCollisionsWithLabels.ts → sub-solvers/TraceCleanupSolver/hasCollisionsWithLabels.ts} +1 -1
  13. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/{minimizeTurnsWithFilteredLabels.ts → sub-solvers/TraceCleanupSolver/minimizeTurnsWithFilteredLabels.ts} +6 -6
  14. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/{simplifyPath.ts → sub-solvers/TraceCleanupSolver/simplifyPath.ts} +1 -1
  15. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/{turnMinimization.ts → sub-solvers/TraceCleanupSolver/turnMinimization.ts} +0 -2
  16. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/tryFourPointDetour.ts +104 -0
  17. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/trySnipAndReconnect.ts +24 -154
  18. package/package.json +1 -1
  19. package/tests/assets/1.input.json +379 -0
  20. package/tests/assets/2.input.json +330 -0
  21. package/tests/assets/3.input.json +388 -0
  22. package/tests/assets/MergedNetLabelObstacles.test.input.json +154 -0
  23. package/tests/assets/OverlapAvoidanceStepSolver.test.input.json +682 -0
  24. package/tests/assets/SingleOverlapSolver.test.input.json +170 -0
  25. package/tests/assets/TraceCleanupSolver.test.input.json +216 -0
  26. package/tests/assets/TraceLabelOverlapAvoidanceSolver.test.input.json +424 -0
  27. package/tests/examples/__snapshots__/example25.snap.svg +3 -3
  28. package/tests/examples/__snapshots__/example26.snap.svg +3 -3
  29. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver.test.ts +18 -0
  30. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/__snapshots__/TraceLabelOverlapAvoidanceSolver.snap.svg +243 -0
  31. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/__snapshots__/renderComparisonView01.snap.svg +223 -0
  32. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/__snapshots__/renderComparisonView02.snap.svg +205 -0
  33. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/__snapshots__/renderComparisonView03.snap.svg +184 -0
  34. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/renderComparisonView01.test.ts +29 -0
  35. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/renderComparisonView02.test.ts +27 -0
  36. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/renderComparisonView03.test.ts +39 -0
  37. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/MergedNetLabelObstacles.test.ts +14 -0
  38. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/OverlapAvoidanceStepSolver.test.ts +20 -0
  39. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/SingleOverlapSolver.test.ts +10 -0
  40. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/TraceCleanupSolver.test.ts +19 -0
  41. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/MergedNetLabelObstacles.snap.svg +140 -0
  42. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/OverlapAvoidanceStepSolver.snap.svg +216 -0
  43. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/SingleOverlapSolver.snap.svg +125 -0
  44. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/TraceCleanupSolver.snap.svg +119 -0
  45. /package/lib/solvers/TraceLabelOverlapAvoidanceSolver/{countTurns.ts → sub-solvers/TraceCleanupSolver/countTurns.ts} +0 -0
  46. /package/lib/solvers/TraceLabelOverlapAvoidanceSolver/{tryConnectPoints.ts → sub-solvers/TraceCleanupSolver/tryConnectPoints.ts} +0 -0
@@ -0,0 +1,205 @@
1
+ import type { SolvedTracePath } from "../../../SchematicTraceLinesSolver/SchematicTraceLinesSolver"
2
+ import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
3
+ import type { NetLabelPlacement } from "../../../NetLabelPlacementSolver/NetLabelPlacementSolver"
4
+ import { getRectBounds } from "../../../NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
5
+ import type { GraphicsObject, Line } from "graphics-debug"
6
+ import { getColorFromString } from "lib/utils/getColorFromString"
7
+ import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
8
+ import type { InputProblem } from "lib/types/InputProblem"
9
+
10
+ interface LabelMergingSolverInput {
11
+ netLabelPlacements: NetLabelPlacement[]
12
+ inputProblem: InputProblem
13
+ traces: SolvedTracePath[]
14
+ }
15
+
16
+ interface LabelMergingSolverOutput {
17
+ netLabelPlacements: NetLabelPlacement[]
18
+ mergedLabelNetIdMap: Record<string, Set<string>>
19
+ }
20
+
21
+ /**
22
+ * Merges multiple net labels into a single, larger label if they are on the
23
+ * same side of the same chip, reducing schematic clutter.
24
+ */
25
+ export class MergedNetLabelObstacleSolver extends BaseSolver {
26
+ private input: LabelMergingSolverInput
27
+ private output!: LabelMergingSolverOutput
28
+ private inputProblem: InputProblem
29
+ private traces: SolvedTracePath[]
30
+
31
+ constructor(solverInput: LabelMergingSolverInput) {
32
+ // console.log(JSON.stringify(solverInput));
33
+
34
+ super()
35
+ this.input = solverInput
36
+ this.inputProblem = solverInput.inputProblem
37
+ this.traces = solverInput.traces
38
+
39
+ // Initialize output to a default state to allow visualization before the first step
40
+ this.output = {
41
+ netLabelPlacements: solverInput.netLabelPlacements,
42
+ mergedLabelNetIdMap: {},
43
+ }
44
+ }
45
+
46
+ override _step() {
47
+ const originalLabels = this.input.netLabelPlacements
48
+ const mergedLabelNetIdMap: Record<string, Set<string>> = {}
49
+
50
+ if (!originalLabels || originalLabels.length === 0) {
51
+ this.output = {
52
+ netLabelPlacements: [],
53
+ mergedLabelNetIdMap: {},
54
+ }
55
+ this.solved = true
56
+ return
57
+ }
58
+
59
+ const labelGroups: Record<string, NetLabelPlacement[]> = {}
60
+
61
+ for (const p of originalLabels) {
62
+ if (p.pinIds.length === 0) continue
63
+ const chipId = p.pinIds[0].split(".")[0]
64
+ if (!chipId) continue
65
+ const key = `${chipId}-${p.orientation}`
66
+ if (!(key in labelGroups)) {
67
+ labelGroups[key] = []
68
+ }
69
+ labelGroups[key]!.push(p)
70
+ }
71
+
72
+ const finalPlacements: NetLabelPlacement[] = []
73
+ for (const [key, group] of Object.entries(labelGroups)) {
74
+ if (group.length <= 1) {
75
+ finalPlacements.push(...group)
76
+ continue
77
+ }
78
+
79
+ let minX = Infinity
80
+ let minY = Infinity
81
+ let maxX = -Infinity
82
+ let maxY = -Infinity
83
+ for (const p of group) {
84
+ const bounds = getRectBounds(p.center, p.width, p.height)
85
+ minX = Math.min(minX, bounds.minX)
86
+ minY = Math.min(minY, bounds.minY)
87
+ maxX = Math.max(maxX, bounds.maxX)
88
+ maxY = Math.max(maxY, bounds.maxY)
89
+ }
90
+
91
+ const newWidth = maxX - minX
92
+ const newHeight = maxY - minY
93
+ const template = group[0]!
94
+ const syntheticId = `merged-group-${key}`
95
+ const originalNetIds = new Set(group.map((p) => p.globalConnNetId))
96
+ mergedLabelNetIdMap[syntheticId] = originalNetIds
97
+
98
+ finalPlacements.push({
99
+ ...template,
100
+ globalConnNetId: syntheticId,
101
+ width: newWidth,
102
+ height: newHeight,
103
+ center: { x: minX + newWidth / 2, y: minY + newHeight / 2 },
104
+ pinIds: [...new Set(group.flatMap((p) => p.pinIds))],
105
+ mspConnectionPairIds: [
106
+ ...new Set(group.flatMap((p) => p.mspConnectionPairIds)),
107
+ ],
108
+ })
109
+ }
110
+
111
+ this.output = {
112
+ netLabelPlacements: finalPlacements,
113
+ mergedLabelNetIdMap,
114
+ }
115
+ this.solved = true
116
+ }
117
+
118
+ getOutput(): LabelMergingSolverOutput {
119
+ return this.output
120
+ }
121
+
122
+ override visualize(): GraphicsObject {
123
+ const graphics = visualizeInputProblem(this.inputProblem, {
124
+ chipAlpha: 0.1,
125
+ connectionAlpha: 0.1,
126
+ })
127
+
128
+ if (!graphics.rects) graphics.rects = []
129
+ if (!graphics.lines) graphics.lines = []
130
+ if (!graphics.points) graphics.points = []
131
+ if (!graphics.texts) graphics.texts = []
132
+
133
+ const originalLabelsById = new Map<string, NetLabelPlacement>()
134
+ for (const label of this.input.netLabelPlacements) {
135
+ originalLabelsById.set(label.globalConnNetId, label)
136
+ }
137
+
138
+ for (const trace of this.traces) {
139
+ const line: Line = {
140
+ points: trace.tracePath.map((p) => ({ x: p.x, y: p.y })),
141
+ strokeColor: "blue",
142
+ }
143
+ graphics.lines!.push(line)
144
+ }
145
+
146
+ for (const finalLabel of this.output.netLabelPlacements) {
147
+ const isMerged = finalLabel.globalConnNetId.startsWith("merged-group-")
148
+ const color = getColorFromString(finalLabel.globalConnNetId)
149
+
150
+ if (isMerged) {
151
+ // Draw the new merged label
152
+ graphics.rects.push({
153
+ center: finalLabel.center,
154
+ width: finalLabel.width,
155
+ height: finalLabel.height,
156
+ fill: color.replace(/, 1\)/, ", 0.2)"), // semi-transparent
157
+ stroke: color,
158
+ label: finalLabel.globalConnNetId,
159
+ })
160
+
161
+ const originalNetIds =
162
+ this.output.mergedLabelNetIdMap[finalLabel.globalConnNetId]
163
+ if (originalNetIds) {
164
+ for (const originalNetId of originalNetIds) {
165
+ const originalLabel = originalLabelsById.get(originalNetId)
166
+ if (originalLabel) {
167
+ // Draw the original label as a dashed box
168
+ const bounds = getRectBounds(
169
+ originalLabel.center,
170
+ originalLabel.width,
171
+ originalLabel.height,
172
+ )
173
+ const p1 = { x: bounds.minX, y: bounds.minY }
174
+ const p2 = { x: bounds.maxX, y: bounds.minY }
175
+ const p3 = { x: bounds.maxX, y: bounds.maxY }
176
+ const p4 = { x: bounds.minX, y: bounds.maxY }
177
+ graphics.lines.push({
178
+ points: [p1, p2, p3, p4, p1],
179
+ strokeColor: color,
180
+ strokeDash: "4 4",
181
+ })
182
+ // Draw line from original to new center
183
+ graphics.lines.push({
184
+ points: [originalLabel.center, finalLabel.center],
185
+ strokeColor: color,
186
+ strokeDash: "2 2",
187
+ })
188
+ }
189
+ }
190
+ }
191
+ } else {
192
+ // Draw un-merged labels
193
+ graphics.rects.push({
194
+ center: finalLabel.center,
195
+ width: finalLabel.width,
196
+ height: finalLabel.height,
197
+ stroke: color,
198
+ label: finalLabel.globalConnNetId,
199
+ })
200
+ }
201
+ }
202
+
203
+ return graphics
204
+ }
205
+ }
@@ -0,0 +1,144 @@
1
+ import type { GraphicsObject } from "graphics-debug"
2
+ import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
3
+ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
4
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
5
+ import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
6
+ import type { InputProblem } from "lib/types/InputProblem"
7
+ import { detectTraceLabelOverlap } from "../../detectTraceLabelOverlap"
8
+ import { SingleOverlapSolver } from "../SingleOverlapSolver/SingleOverlapSolver"
9
+
10
+ type Overlap = ReturnType<typeof detectTraceLabelOverlap>[0]
11
+
12
+ // Define a type for the input of the internal overlap solver to avoid conflicts
13
+ interface OverlapCollectionSolverInput {
14
+ inputProblem: InputProblem
15
+ traces: SolvedTracePath[]
16
+ netLabelPlacements: NetLabelPlacement[]
17
+ mergedLabelNetIdMap: Record<string, Set<string>>
18
+ }
19
+
20
+ /**
21
+ * This is an internal solver that manages the step-by-step process of avoiding
22
+ * multiple overlaps. It follows the pattern of SchematicTraceLinesSolver.
23
+ */
24
+ export class OverlapAvoidanceStepSolver extends BaseSolver {
25
+ inputProblem: InputProblem
26
+ netLabelPlacements: NetLabelPlacement[]
27
+ mergedLabelNetIdMap: Record<string, Set<string>>
28
+
29
+ allTraces: SolvedTracePath[]
30
+ modifiedTraces: SolvedTracePath[] = []
31
+
32
+ private detourCountByLabel: Record<string, number> = {}
33
+ private readonly PADDING_BUFFER = 0.1
34
+
35
+ public override activeSubSolver: SingleOverlapSolver | null = null
36
+ private overlapQueue: Overlap[] = []
37
+ private recentlyFailed: Set<string> = new Set()
38
+
39
+ constructor(solverInput: OverlapCollectionSolverInput) {
40
+ super()
41
+ this.inputProblem = solverInput.inputProblem
42
+ this.netLabelPlacements = solverInput.netLabelPlacements
43
+ this.mergedLabelNetIdMap = solverInput.mergedLabelNetIdMap
44
+ this.allTraces = [...solverInput.traces]
45
+ }
46
+
47
+ override _step() {
48
+ if (this.activeSubSolver) {
49
+ this.activeSubSolver.step()
50
+
51
+ if (this.activeSubSolver.solved) {
52
+ const solvedPath = this.activeSubSolver.solvedTracePath
53
+ if (solvedPath) {
54
+ const traceIndex = this.allTraces.findIndex(
55
+ (t) => t.mspPairId === this.activeSubSolver!.initialTrace.mspPairId,
56
+ )
57
+ if (traceIndex !== -1) {
58
+ this.allTraces[traceIndex].tracePath = solvedPath
59
+ this.modifiedTraces.push(this.allTraces[traceIndex])
60
+ }
61
+ }
62
+ this.activeSubSolver = null
63
+ this.recentlyFailed.clear()
64
+ } else if (this.activeSubSolver.failed) {
65
+ const overlapId = `${this.activeSubSolver.initialTrace.mspPairId}-${this.activeSubSolver.label.globalConnNetId}`
66
+ this.recentlyFailed.add(overlapId)
67
+ this.activeSubSolver = null
68
+ }
69
+ return
70
+ }
71
+
72
+ const overlaps = detectTraceLabelOverlap(
73
+ this.allTraces,
74
+ this.netLabelPlacements,
75
+ ).filter((o) => {
76
+ const originalNetIds = this.mergedLabelNetIdMap[o.label.globalConnNetId]
77
+ if (originalNetIds) {
78
+ return !originalNetIds.has(o.trace.globalConnNetId)
79
+ }
80
+ return o.trace.globalConnNetId !== o.label.globalConnNetId
81
+ })
82
+
83
+ if (overlaps.length === 0) {
84
+ this.solved = true
85
+ return
86
+ }
87
+
88
+ const nonFailedOverlaps = overlaps.filter((o) => {
89
+ const overlapId = `${o.trace.mspPairId}-${o.label.globalConnNetId}`
90
+ return !this.recentlyFailed.has(overlapId)
91
+ })
92
+
93
+ if (nonFailedOverlaps.length === 0) {
94
+ this.solved = true // No more progress can be made
95
+ return
96
+ }
97
+
98
+ this.overlapQueue = nonFailedOverlaps
99
+
100
+ const nextOverlap = this.overlapQueue.shift()
101
+
102
+ if (nextOverlap) {
103
+ const traceToFix = this.allTraces.find(
104
+ (t) => t.mspPairId === nextOverlap.trace.mspPairId,
105
+ )
106
+ if (traceToFix) {
107
+ const labelId = nextOverlap.label.globalConnNetId
108
+ const detourCount = this.detourCountByLabel[labelId] || 0
109
+ this.detourCountByLabel[labelId] = detourCount + 1
110
+
111
+ this.activeSubSolver = new SingleOverlapSolver({
112
+ trace: traceToFix,
113
+ label: nextOverlap.label,
114
+ problem: this.inputProblem,
115
+ paddingBuffer: this.PADDING_BUFFER,
116
+ detourCount,
117
+ })
118
+ }
119
+ }
120
+ }
121
+
122
+ getOutput() {
123
+ return {
124
+ allTraces: this.allTraces,
125
+ modifiedTraces: this.modifiedTraces,
126
+ }
127
+ }
128
+
129
+ override visualize(): GraphicsObject {
130
+ if (this.activeSubSolver) {
131
+ return this.activeSubSolver.visualize()
132
+ }
133
+ // When idle, show all the traces
134
+ const graphics = visualizeInputProblem(this.inputProblem)
135
+ if (!graphics.lines) graphics.lines = []
136
+ for (const trace of this.allTraces) {
137
+ graphics.lines!.push({
138
+ points: trace.tracePath,
139
+ strokeColor: "purple",
140
+ })
141
+ }
142
+ return graphics
143
+ }
144
+ }
@@ -0,0 +1,117 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+ import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
3
+ import type { NetLabelPlacement } from "../../../NetLabelPlacementSolver/NetLabelPlacementSolver"
4
+ import type { InputProblem } from "lib/types/InputProblem"
5
+ import type { GraphicsObject } from "graphics-debug"
6
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
7
+ import { isPathCollidingWithObstacles } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
8
+ import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
9
+ import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
10
+ import { generateRerouteCandidates } from "../../rerouteCollidingTrace"
11
+ import { simplifyPath } from "../TraceCleanupSolver/simplifyPath"
12
+
13
+ interface SingleOverlapSolverInput {
14
+ trace: SolvedTracePath
15
+ label: NetLabelPlacement
16
+ problem: InputProblem
17
+ paddingBuffer: number
18
+ detourCount: number
19
+ }
20
+
21
+ /**
22
+ * This solver attempts to find a valid rerouting for a single trace that is
23
+ * overlapping with a net label. It tries various candidate paths until it
24
+ * finds one that does not introduce new collisions.
25
+ */
26
+ export class SingleOverlapSolver extends BaseSolver {
27
+ queuedCandidatePaths: Point[][]
28
+ solvedTracePath: Point[] | null = null
29
+ initialTrace: SolvedTracePath
30
+ problem: InputProblem
31
+ obstacles: ReturnType<typeof getObstacleRects>
32
+ label: NetLabelPlacement
33
+
34
+ constructor(solverInput: SingleOverlapSolverInput) {
35
+ super()
36
+ this.initialTrace = solverInput.trace
37
+ this.problem = solverInput.problem
38
+ this.label = solverInput.label
39
+
40
+ const candidates = generateRerouteCandidates({
41
+ ...solverInput,
42
+ })
43
+
44
+ const getPathLength = (pts: Point[]) => {
45
+ let len = 0
46
+ for (let i = 0; i < pts.length - 1; i++) {
47
+ const dx = pts[i + 1].x - pts[i].x
48
+ const dy = pts[i + 1].y - pts[i].y
49
+ len += Math.sqrt(dx * dx + dy * dy)
50
+ }
51
+ return len
52
+ }
53
+
54
+ this.queuedCandidatePaths = candidates.sort(
55
+ (a, b) => getPathLength(a) - getPathLength(b),
56
+ )
57
+ this.obstacles = getObstacleRects(this.problem)
58
+ }
59
+
60
+ override _step() {
61
+ if (this.queuedCandidatePaths.length === 0) {
62
+ this.failed = true
63
+ return
64
+ }
65
+
66
+ const nextCandidatePath = this.queuedCandidatePaths.shift()!
67
+ const simplifiedPath = simplifyPath(nextCandidatePath)
68
+
69
+ if (!isPathCollidingWithObstacles(simplifiedPath, this.obstacles)) {
70
+ this.solvedTracePath = simplifiedPath
71
+ this.solved = true
72
+ }
73
+ }
74
+
75
+ override visualize(): GraphicsObject {
76
+ const graphics = visualizeInputProblem(this.problem, {
77
+ chipAlpha: 0.1,
78
+ connectionAlpha: 0.1,
79
+ })
80
+
81
+ if (!graphics.lines) graphics.lines = []
82
+ if (!graphics.rects) graphics.rects = []
83
+
84
+ // Draw initial trace
85
+ graphics.lines.push({
86
+ points: this.initialTrace.tracePath,
87
+ strokeColor: "red",
88
+ strokeDash: "4 4",
89
+ })
90
+
91
+ // Draw label
92
+ graphics.rects.push({
93
+ center: this.label.center,
94
+ width: this.label.width,
95
+ height: this.label.height,
96
+ fill: "rgba(255, 0, 0, 0.2)",
97
+ })
98
+
99
+ // Draw next candidate
100
+ if (this.queuedCandidatePaths.length > 0) {
101
+ graphics.lines.push({
102
+ points: this.queuedCandidatePaths[0],
103
+ strokeColor: "orange",
104
+ })
105
+ }
106
+
107
+ // Draw solved path
108
+ if (this.solvedTracePath) {
109
+ graphics.lines.push({
110
+ points: this.solvedTracePath,
111
+ strokeColor: "green",
112
+ })
113
+ }
114
+
115
+ return graphics
116
+ }
117
+ }
@@ -0,0 +1,132 @@
1
+ import type { InputProblem } from "lib/types/InputProblem"
2
+ import type { GraphicsObject, Line } from "graphics-debug"
3
+ import type { NetLabelPlacement } from "../../../NetLabelPlacementSolver/NetLabelPlacementSolver"
4
+ import { minimizeTurnsWithFilteredLabels } from "./minimizeTurnsWithFilteredLabels"
5
+ import { balanceLShapes } from "./balanceLShapes"
6
+ import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
7
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
8
+ import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
9
+
10
+ interface TraceCleanupSolverInput {
11
+ inputProblem: InputProblem
12
+ allTraces: SolvedTracePath[]
13
+ targetTraceIds: Set<string>
14
+ allLabelPlacements: NetLabelPlacement[]
15
+ mergedLabelNetIdMap: Record<string, Set<string>>
16
+ paddingBuffer: number
17
+ }
18
+
19
+ /**
20
+ * Cleans up traces by minimizing turns and balancing L-shapes to improve
21
+ * the overall aesthetics and readability of the schematic.
22
+ */
23
+ export class TraceCleanupSolver extends BaseSolver {
24
+ private input: TraceCleanupSolverInput
25
+ private outputTraces: SolvedTracePath[]
26
+ private pipelineStepIndex = 0
27
+ private tracesToProcess: SolvedTracePath[]
28
+ private minimizedTraces: SolvedTracePath[] | null = null
29
+ private balancedTraces: SolvedTracePath[] | null = null
30
+
31
+ constructor(solverInput: TraceCleanupSolverInput) {
32
+ super()
33
+ this.input = solverInput
34
+ this.outputTraces = [...solverInput.allTraces]
35
+ this.tracesToProcess = this.outputTraces.filter((t) =>
36
+ this.input.targetTraceIds.has(t.mspPairId),
37
+ )
38
+ }
39
+
40
+ override _step() {
41
+ const {
42
+ targetTraceIds,
43
+ inputProblem,
44
+ allLabelPlacements,
45
+ mergedLabelNetIdMap,
46
+ paddingBuffer,
47
+ } = this.input
48
+
49
+ if (targetTraceIds.size === 0) {
50
+ this.solved = true
51
+ return
52
+ }
53
+
54
+ switch (this.pipelineStepIndex) {
55
+ case 0: {
56
+ // Step 0: Minimize turns
57
+ const minimizedTracesResult = minimizeTurnsWithFilteredLabels({
58
+ traces: this.tracesToProcess,
59
+ inputProblem,
60
+ allLabelPlacements,
61
+ mergedLabelNetIdMap,
62
+ paddingBuffer,
63
+ })
64
+
65
+ this.minimizedTraces = minimizedTracesResult ?? this.tracesToProcess
66
+
67
+ const tracesMap = new Map(
68
+ this.input.allTraces.map((t) => [t.mspPairId, t]),
69
+ )
70
+ for (const trace of this.minimizedTraces) {
71
+ tracesMap.set(trace.mspPairId, trace)
72
+ }
73
+ this.outputTraces = Array.from(tracesMap.values())
74
+ this.pipelineStepIndex++
75
+ break
76
+ }
77
+ case 1: {
78
+ // Step 1: Balance L-shapes
79
+ const balancedTracesResult = balanceLShapes({
80
+ traces: this.minimizedTraces!,
81
+ inputProblem,
82
+ allLabelPlacements,
83
+ })
84
+
85
+ this.balancedTraces = balancedTracesResult ?? this.minimizedTraces
86
+
87
+ const tracesMap = new Map(
88
+ this.input.allTraces.map((t) => [t.mspPairId, t]),
89
+ )
90
+ for (const trace of this.balancedTraces!) {
91
+ tracesMap.set(trace.mspPairId, trace)
92
+ }
93
+ this.outputTraces = Array.from(tracesMap.values())
94
+ this.pipelineStepIndex++
95
+ break
96
+ }
97
+ case 2: {
98
+ // Step 2: Done
99
+ this.solved = true
100
+ break
101
+ }
102
+ }
103
+ }
104
+
105
+ getOutput() {
106
+ return {
107
+ traces: this.outputTraces,
108
+ }
109
+ }
110
+
111
+ override visualize(): GraphicsObject {
112
+ const graphics = visualizeInputProblem(this.input.inputProblem, {
113
+ chipAlpha: 0.1,
114
+ connectionAlpha: 0.1,
115
+ })
116
+
117
+ if (!graphics.lines) graphics.lines = []
118
+ if (!graphics.points) graphics.points = []
119
+ if (!graphics.rects) graphics.rects = []
120
+ if (!graphics.circles) graphics.circles = []
121
+ if (!graphics.texts) graphics.texts = []
122
+
123
+ for (const trace of this.outputTraces) {
124
+ const line: Line = {
125
+ points: trace.tracePath.map((p) => ({ x: p.x, y: p.y })),
126
+ strokeColor: "blue",
127
+ }
128
+ graphics.lines!.push(line)
129
+ }
130
+ return graphics
131
+ }
132
+ }
@@ -1,18 +1,18 @@
1
1
  import type { Point } from "graphics-debug"
2
2
  import type { InputProblem } from "lib/types/InputProblem"
3
- import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver"
4
- import { getObstacleRects } from "../SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
5
- import type { SolvedTracePath } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver"
6
- import { segmentIntersectsRect } from "../SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
3
+ import type { NetLabelPlacement } from "../../../NetLabelPlacementSolver/NetLabelPlacementSolver"
7
4
  import { simplifyPath } from "./simplifyPath"
5
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
6
+ import { segmentIntersectsRect } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
7
+ import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
8
8
 
9
9
  export const balanceLShapes = ({
10
10
  traces,
11
- problem,
11
+ inputProblem,
12
12
  allLabelPlacements,
13
13
  }: {
14
14
  traces: SolvedTracePath[]
15
- problem: InputProblem
15
+ inputProblem: InputProblem
16
16
  allLabelPlacements: NetLabelPlacement[]
17
17
  }): SolvedTracePath[] | null => {
18
18
  const TOLERANCE = 1e-5
@@ -46,7 +46,7 @@ export const balanceLShapes = ({
46
46
  }
47
47
  }
48
48
 
49
- const obstacles = getObstacleRects(problem).map((obs) => ({
49
+ const obstacles = getObstacleRects(inputProblem).map((obs) => ({
50
50
  ...obs,
51
51
  minX: obs.minX + TOLERANCE,
52
52
  maxX: obs.maxX - TOLERANCE,
@@ -81,7 +81,7 @@ export const balanceLShapes = ({
81
81
  }
82
82
 
83
83
  const newTraces = traces.map((trace) => {
84
- let newPath = [...trace.tracePath]
84
+ const newPath = [...trace.tracePath]
85
85
 
86
86
  if (newPath.length < 4) {
87
87
  return { ...trace }
@@ -94,7 +94,8 @@ export const balanceLShapes = ({
94
94
 
95
95
  if (newPath.length === 4) {
96
96
  const [p0, p1, p2, p3] = newPath
97
- let p1New: Point, p2New: Point
97
+ let p1New: Point
98
+ let p2New: Point
98
99
 
99
100
  const isHVHShape = p0.y === p1.y && p1.x === p2.x && p2.y === p3.y
100
101
 
@@ -154,7 +155,8 @@ export const balanceLShapes = ({
154
155
  continue
155
156
  }
156
157
 
157
- let p2New: Point, p3New: Point
158
+ let p2New: Point
159
+ let p3New: Point
158
160
  const len1Original = isHVHZShape
159
161
  ? Math.abs(p1.x - p2.x)
160
162
  : Math.abs(p1.y - p2.y)
@@ -1,5 +1,5 @@
1
1
  import type { Point } from "graphics-debug"
2
- import { segmentIntersectsRect } from "../SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
2
+ import { segmentIntersectsRect } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
3
3
 
4
4
  export const hasCollisions = (
5
5
  pathSegments: Point[],
@@ -1,5 +1,5 @@
1
1
  import type { Point } from "@tscircuit/math-utils"
2
- import { segmentIntersectsRect } from "../SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
2
+ import { segmentIntersectsRect } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
3
3
 
4
4
  export const hasCollisionsWithLabels = (
5
5
  pathSegments: Point[],