@tscircuit/schematic-trace-solver 0.0.40 → 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 (57) hide show
  1. package/dist/index.d.ts +155 -9
  2. package/dist/index.js +998 -479
  3. package/lib/solvers/LongDistancePairSolver/LongDistancePairSolver.ts +238 -0
  4. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +61 -26
  5. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver.ts +94 -192
  6. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts +13 -24
  7. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/LabelMergingSolver.ts +205 -0
  8. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/OverlapAvoidanceStepSolver.ts +144 -0
  9. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts +117 -0
  10. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/TraceCleanupSolver/TraceCleanupSolver.ts +132 -0
  11. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/{balanceLShapes.ts → sub-solvers/TraceCleanupSolver/balanceLShapes.ts} +12 -10
  12. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/{hasCollisions.ts → sub-solvers/TraceCleanupSolver/hasCollisions.ts} +1 -1
  13. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/{hasCollisionsWithLabels.ts → sub-solvers/TraceCleanupSolver/hasCollisionsWithLabels.ts} +1 -1
  14. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/{minimizeTurnsWithFilteredLabels.ts → sub-solvers/TraceCleanupSolver/minimizeTurnsWithFilteredLabels.ts} +6 -6
  15. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/{simplifyPath.ts → sub-solvers/TraceCleanupSolver/simplifyPath.ts} +1 -1
  16. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/{turnMinimization.ts → sub-solvers/TraceCleanupSolver/turnMinimization.ts} +0 -2
  17. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/tryFourPointDetour.ts +104 -0
  18. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/trySnipAndReconnect.ts +24 -154
  19. package/lib/utils/does-trace-overlap-with-existing-traces.ts +33 -0
  20. package/package.json +1 -1
  21. package/site/examples/example25.page.tsx +4 -0
  22. package/tests/assets/1.input.json +379 -0
  23. package/tests/assets/2.input.json +330 -0
  24. package/tests/assets/3.input.json +388 -0
  25. package/tests/assets/MergedNetLabelObstacles.test.input.json +154 -0
  26. package/tests/assets/OverlapAvoidanceStepSolver.test.input.json +682 -0
  27. package/tests/assets/SingleOverlapSolver.test.input.json +170 -0
  28. package/tests/assets/TraceCleanupSolver.test.input.json +216 -0
  29. package/tests/assets/TraceLabelOverlapAvoidanceSolver.test.input.json +424 -0
  30. package/tests/assets/example25.json +96 -0
  31. package/tests/examples/__snapshots__/example03.snap.svg +67 -76
  32. package/tests/examples/__snapshots__/example10.snap.svg +24 -30
  33. package/tests/examples/__snapshots__/example13.snap.svg +81 -84
  34. package/tests/examples/__snapshots__/example15.snap.svg +5 -8
  35. package/tests/examples/__snapshots__/example21.snap.svg +68 -74
  36. package/tests/examples/__snapshots__/example25.snap.svg +3 -3
  37. package/tests/examples/__snapshots__/example26.snap.svg +3 -3
  38. package/tests/examples/__snapshots__/example28.snap.svg +273 -0
  39. package/tests/examples/example28.test.ts +13 -0
  40. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver.test.ts +18 -0
  41. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/__snapshots__/TraceLabelOverlapAvoidanceSolver.snap.svg +243 -0
  42. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/__snapshots__/renderComparisonView01.snap.svg +223 -0
  43. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/__snapshots__/renderComparisonView02.snap.svg +205 -0
  44. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/__snapshots__/renderComparisonView03.snap.svg +184 -0
  45. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/renderComparisonView01.test.ts +29 -0
  46. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/renderComparisonView02.test.ts +27 -0
  47. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/renderComparisonView03.test.ts +39 -0
  48. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/MergedNetLabelObstacles.test.ts +14 -0
  49. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/OverlapAvoidanceStepSolver.test.ts +20 -0
  50. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/SingleOverlapSolver.test.ts +10 -0
  51. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/TraceCleanupSolver.test.ts +19 -0
  52. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/MergedNetLabelObstacles.snap.svg +140 -0
  53. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/OverlapAvoidanceStepSolver.snap.svg +216 -0
  54. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/SingleOverlapSolver.snap.svg +125 -0
  55. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/TraceCleanupSolver.snap.svg +119 -0
  56. /package/lib/solvers/TraceLabelOverlapAvoidanceSolver/{countTurns.ts → sub-solvers/TraceCleanupSolver/countTurns.ts} +0 -0
  57. /package/lib/solvers/TraceLabelOverlapAvoidanceSolver/{tryConnectPoints.ts → sub-solvers/TraceCleanupSolver/tryConnectPoints.ts} +0 -0
@@ -1,16 +1,13 @@
1
1
  import { BaseSolver } from "../BaseSolver/BaseSolver"
2
- import { detectTraceLabelOverlap } from "./detectTraceLabelOverlap"
3
- import { rerouteCollidingTrace } from "./rerouteCollidingTrace"
4
2
  import type { SolvedTracePath } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver"
5
3
  import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver"
6
4
  import type { GraphicsObject } from "graphics-debug"
7
5
  import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualizeInputProblem"
8
- import { getRectBounds } from "../NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
6
+ import type { InputProblem } from "../../types/InputProblem"
7
+ import { MergedNetLabelObstacleSolver } from "./sub-solvers/LabelMergingSolver/LabelMergingSolver"
8
+ import { TraceCleanupSolver } from "./sub-solvers/TraceCleanupSolver/TraceCleanupSolver"
9
9
  import { getColorFromString } from "lib/utils/getColorFromString"
10
- import type { InputProblem } from "lib/types/InputProblem"
11
- import { minimizeTurnsWithFilteredLabels } from "./minimizeTurnsWithFilteredLabels"
12
- import { balanceLShapes } from "./balanceLShapes"
13
- import { NetLabelPlacementSolver } from "../NetLabelPlacementSolver/NetLabelPlacementSolver"
10
+ import { OverlapAvoidanceStepSolver } from "./sub-solvers/OverlapAvoidanceStepSolver/OverlapAvoidanceStepSolver"
14
11
 
15
12
  interface TraceLabelOverlapAvoidanceSolverInput {
16
13
  inputProblem: InputProblem
@@ -18,222 +15,127 @@ interface TraceLabelOverlapAvoidanceSolverInput {
18
15
  netLabelPlacements: NetLabelPlacement[]
19
16
  }
20
17
 
18
+ /**
19
+ * This solver is a pipeline that runs a series of sub-solvers to resolve
20
+ * trace-label overlaps and clean up the resulting traces.
21
+ */
21
22
  export class TraceLabelOverlapAvoidanceSolver extends BaseSolver {
22
- private problem: InputProblem
23
- private traces: SolvedTracePath[]
24
- private netTempLabelPlacements: NetLabelPlacement[]
25
- private netLabelPlacements: NetLabelPlacement[]
26
- private updatedTraces: SolvedTracePath[]
27
- private mergedLabelNetIdMap: Record<string, Set<string>>
28
- private detourCountByLabel: Record<string, number>
29
- private readonly PADDING_BUFFER = 0.1
23
+ inputProblem: InputProblem
24
+ traces: SolvedTracePath[]
25
+ netLabelPlacements: NetLabelPlacement[]
26
+
27
+ // sub-solver instances
28
+ labelMergingSolver?: MergedNetLabelObstacleSolver
29
+ overlapAvoidanceSolver?: OverlapAvoidanceStepSolver
30
+ traceCleanupSolver?: TraceCleanupSolver
31
+ pipelineStepIndex = 0
30
32
 
31
33
  constructor(solverInput: TraceLabelOverlapAvoidanceSolverInput) {
32
34
  super()
33
- this.problem = solverInput.inputProblem
35
+ this.inputProblem = solverInput.inputProblem
34
36
  this.traces = solverInput.traces
35
- this.updatedTraces = [...solverInput.traces]
36
- this.mergedLabelNetIdMap = {}
37
- this.detourCountByLabel = {}
38
-
39
- const originalLabels = solverInput.netLabelPlacements
40
- this.netLabelPlacements = originalLabels
41
- if (!originalLabels || originalLabels.length === 0) {
42
- this.netTempLabelPlacements = []
43
- return
44
- }
45
-
46
- const labelGroups: Record<string, NetLabelPlacement[]> = {}
47
-
48
- for (const p of originalLabels) {
49
- if (p.pinIds.length === 0) continue
50
- const chipId = p.pinIds[0].split(".")[0]
51
- if (!chipId) continue
52
- const key = `${chipId}-${p.orientation}`
53
- if (!(key in labelGroups)) {
54
- labelGroups[key] = []
55
- }
56
- labelGroups[key]!.push(p)
57
- }
58
-
59
- const finalPlacements: NetLabelPlacement[] = []
60
- for (const [key, group] of Object.entries(labelGroups)) {
61
- if (group.length <= 1) {
62
- finalPlacements.push(...group)
63
- continue
64
- }
65
-
66
- let minX = Infinity,
67
- minY = Infinity,
68
- maxX = -Infinity,
69
- maxY = -Infinity
70
- for (const p of group) {
71
- const bounds = getRectBounds(p.center, p.width, p.height)
72
- minX = Math.min(minX, bounds.minX)
73
- minY = Math.min(minY, bounds.minY)
74
- maxX = Math.max(maxX, bounds.maxX)
75
- maxY = Math.max(maxY, bounds.maxY)
76
- }
77
-
78
- const newWidth = maxX - minX
79
- const newHeight = maxY - minY
80
- const template = group[0]!
81
- const syntheticId = `merged-group-${key}`
82
- const originalNetIds = new Set(group.map((p) => p.globalConnNetId))
83
- this.mergedLabelNetIdMap[syntheticId] = originalNetIds
84
-
85
- finalPlacements.push({
86
- ...template,
87
- globalConnNetId: syntheticId,
88
- width: newWidth,
89
- height: newHeight,
90
- center: { x: minX + newWidth / 2, y: minY + newHeight / 2 },
91
- pinIds: [...new Set(group.flatMap((p) => p.pinIds))],
92
- mspConnectionPairIds: [
93
- ...new Set(group.flatMap((p) => p.mspConnectionPairIds)),
94
- ],
95
- })
96
- }
97
-
98
- this.netTempLabelPlacements = finalPlacements
37
+ this.netLabelPlacements = solverInput.netLabelPlacements
99
38
  }
100
39
 
101
40
  override _step() {
102
- if (
103
- !this.traces ||
104
- this.traces.length === 0 ||
105
- !this.netTempLabelPlacements ||
106
- this.netTempLabelPlacements.length === 0
107
- ) {
108
- this.solved = true
109
- return
110
- }
111
-
112
- this.detourCountByLabel = {}
113
-
114
- const overlaps = detectTraceLabelOverlap(
115
- this.traces,
116
- this.netTempLabelPlacements,
117
- )
118
-
119
- if (overlaps.length === 0) {
120
- this.solved = true
121
- return
122
- }
123
-
124
- const unfriendlyOverlaps = overlaps.filter((o) => {
125
- const originalNetIds = this.mergedLabelNetIdMap[o.label.globalConnNetId]
126
- if (originalNetIds) {
127
- return !originalNetIds.has(o.trace.globalConnNetId)
41
+ // If a sub-solver is active, step it and check for completion.
42
+ if (this.activeSubSolver) {
43
+ this.activeSubSolver.step()
44
+
45
+ if (this.activeSubSolver.solved) {
46
+ this.activeSubSolver = null
47
+ this.pipelineStepIndex++
48
+ } else if (this.activeSubSolver.failed) {
49
+ this.failed = true // If any sub-solver fails, the whole thing fails
50
+ this.activeSubSolver = null
128
51
  }
129
- return o.trace.globalConnNetId !== o.label.globalConnNetId
130
- })
131
-
132
- if (unfriendlyOverlaps.length === 0) {
133
- this.solved = true
134
- return
52
+ return // Return to allow the sub-solver to run
135
53
  }
136
54
 
137
- const updatedTracesMap: Record<string, SolvedTracePath> = {}
138
- for (const trace of this.traces) {
139
- updatedTracesMap[trace.mspPairId] = trace
55
+ // If no sub-solver is active, create the next one in the pipeline.
56
+ switch (this.pipelineStepIndex) {
57
+ case 0:
58
+ this.labelMergingSolver = new MergedNetLabelObstacleSolver({
59
+ netLabelPlacements: this.netLabelPlacements,
60
+ inputProblem: this.inputProblem,
61
+ traces: this.traces,
62
+ })
63
+ this.activeSubSolver = this.labelMergingSolver
64
+ break
65
+
66
+ case 1:
67
+ this.overlapAvoidanceSolver = new OverlapAvoidanceStepSolver({
68
+ inputProblem: this.inputProblem,
69
+ traces: this.traces,
70
+ netLabelPlacements:
71
+ this.labelMergingSolver!.getOutput().netLabelPlacements,
72
+ mergedLabelNetIdMap:
73
+ this.labelMergingSolver!.getOutput().mergedLabelNetIdMap,
74
+ })
75
+ this.activeSubSolver = this.overlapAvoidanceSolver
76
+ break
77
+
78
+ case 2:
79
+ this.traceCleanupSolver = new TraceCleanupSolver({
80
+ inputProblem: this.inputProblem,
81
+ allTraces: this.overlapAvoidanceSolver!.getOutput().allTraces,
82
+ targetTraceIds: new Set(
83
+ this.overlapAvoidanceSolver!.getOutput().modifiedTraces.map(
84
+ (t) => t.mspPairId,
85
+ ),
86
+ ),
87
+ allLabelPlacements:
88
+ this.labelMergingSolver!.getOutput().netLabelPlacements,
89
+ mergedLabelNetIdMap:
90
+ this.labelMergingSolver!.getOutput().mergedLabelNetIdMap,
91
+ paddingBuffer: 0.01,
92
+ })
93
+ this.activeSubSolver = this.traceCleanupSolver
94
+ break
95
+
96
+ default:
97
+ this.solved = true
98
+ break
140
99
  }
141
-
142
- const processedTraceIds = new Set<string>()
143
-
144
- for (const overlap of unfriendlyOverlaps) {
145
- if (processedTraceIds.has(overlap.trace.mspPairId)) {
146
- continue
147
- }
148
-
149
- const currentTraceState = updatedTracesMap[overlap.trace.mspPairId]!
150
- const labelId = overlap.label.globalConnNetId
151
- const detourCount = this.detourCountByLabel[labelId] || 0
152
-
153
- const newTrace = rerouteCollidingTrace({
154
- trace: currentTraceState,
155
- label: overlap.label,
156
- problem: this.problem,
157
- paddingBuffer: this.PADDING_BUFFER,
158
- detourCount,
159
- })
160
-
161
- if (newTrace.tracePath !== currentTraceState.tracePath) {
162
- this.detourCountByLabel[labelId] = detourCount + 1
163
- }
164
-
165
- updatedTracesMap[currentTraceState.mspPairId] = newTrace
166
- processedTraceIds.add(currentTraceState.mspPairId)
167
- }
168
-
169
- this.updatedTraces = Object.values(updatedTracesMap)
170
-
171
- const minimizedTraces = minimizeTurnsWithFilteredLabels({
172
- traces: this.updatedTraces,
173
- problem: this.problem,
174
- allLabelPlacements: this.netTempLabelPlacements, // Use temp labels which include merged ones
175
- mergedLabelNetIdMap: this.mergedLabelNetIdMap,
176
- paddingBuffer: this.PADDING_BUFFER,
177
- })
178
- if (minimizedTraces) {
179
- this.updatedTraces = minimizedTraces
180
- }
181
- const balancedTraces = balanceLShapes({
182
- traces: this.updatedTraces,
183
- problem: this.problem,
184
- allLabelPlacements: this.netLabelPlacements,
185
- })
186
- if (balancedTraces) {
187
- this.updatedTraces = balancedTraces
188
- }
189
-
190
- const finalLabelPlacementSolver = new NetLabelPlacementSolver({
191
- inputProblem: this.problem,
192
- inputTraceMap: Object.fromEntries(
193
- this.updatedTraces.map((trace) => [trace.mspPairId, trace]),
194
- ),
195
- })
196
- finalLabelPlacementSolver.solve()
197
- if (!finalLabelPlacementSolver.failed) {
198
- this.netLabelPlacements = finalLabelPlacementSolver.netLabelPlacements
199
- }
200
-
201
- this.solved = true
202
100
  }
203
101
 
204
102
  getOutput() {
205
103
  return {
206
- traces: this.updatedTraces,
207
- netLabelPlacements: this.netLabelPlacements,
104
+ traces: this.traceCleanupSolver?.getOutput().traces ?? this.traces,
105
+ netLabelPlacements:
106
+ this.labelMergingSolver?.getOutput().netLabelPlacements ??
107
+ this.netLabelPlacements,
208
108
  }
209
109
  }
210
110
 
211
111
  override visualize(): GraphicsObject {
212
- const graphics = visualizeInputProblem(this.problem)
112
+ if (this.activeSubSolver) {
113
+ return this.activeSubSolver.visualize()
114
+ }
213
115
 
116
+ // When no sub-solver is active, show the current state of the pipeline
117
+ const graphics = visualizeInputProblem(this.inputProblem)
214
118
  if (!graphics.lines) graphics.lines = []
215
- if (!graphics.circles) graphics.circles = []
216
- if (!graphics.texts) graphics.texts = []
217
119
  if (!graphics.rects) graphics.rects = []
218
120
 
219
- for (const trace of this.updatedTraces) {
121
+ const output = this.getOutput()
122
+
123
+ for (const trace of output.traces) {
220
124
  graphics.lines!.push({
221
125
  points: trace.tracePath,
222
126
  strokeColor: "purple",
223
127
  })
224
128
  }
225
129
 
226
- for (const p of this.netLabelPlacements) {
130
+ for (const label of output.netLabelPlacements) {
131
+ const color = getColorFromString(label.globalConnNetId, 0.3)
227
132
  graphics.rects!.push({
228
- center: p.center,
229
- width: p.width,
230
- height: p.height,
231
- fill: getColorFromString(p.globalConnNetId, 0.35),
232
- })
233
- graphics.points!.push({
234
- x: p.anchorPoint.x,
235
- y: p.anchorPoint.y,
236
- color: getColorFromString(p.globalConnNetId, 0.9),
133
+ center: label.center,
134
+ width: label.width,
135
+ height: label.height,
136
+ fill: color,
137
+ stroke: color.replace("0.3", "1"),
138
+ label: label.globalConnNetId,
237
139
  })
238
140
  }
239
141
 
@@ -1,16 +1,16 @@
1
+ import type { Point } from "@tscircuit/math-utils"
1
2
  import type { SolvedTracePath } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver"
2
3
  import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver"
3
4
  import { getRectBounds } from "../NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
4
- import { getObstacleRects } from "../SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
5
5
  import type { InputProblem } from "lib/types/InputProblem"
6
6
  import { findTraceViolationZone } from "./violation"
7
- import { tryFourPointDetour, trySnipAndReconnect } from "./trySnipAndReconnect"
8
- import { simplifyPath } from "./simplifyPath"
7
+ import { generateSnipAndReconnectCandidates } from "./trySnipAndReconnect"
8
+ import { simplifyPath } from "./sub-solvers/TraceCleanupSolver/simplifyPath"
9
+ import { generateFourPointDetourCandidates } from "./tryFourPointDetour"
9
10
 
10
- export const rerouteCollidingTrace = ({
11
+ export const generateRerouteCandidates = ({
11
12
  trace,
12
13
  label,
13
- problem,
14
14
  paddingBuffer,
15
15
  detourCount,
16
16
  }: {
@@ -19,14 +19,13 @@ export const rerouteCollidingTrace = ({
19
19
  problem: InputProblem
20
20
  paddingBuffer: number
21
21
  detourCount: number
22
- }): SolvedTracePath => {
22
+ }): Point[][] => {
23
23
  const initialTrace = { ...trace, tracePath: simplifyPath(trace.tracePath) }
24
24
 
25
25
  if (trace.globalConnNetId === label.globalConnNetId) {
26
- return initialTrace
26
+ return [initialTrace.tracePath]
27
27
  }
28
28
 
29
- const obstacles = getObstacleRects(problem)
30
29
  const labelPadding = paddingBuffer
31
30
  const labelBoundsRaw = getRectBounds(label.center, label.width, label.height)
32
31
  const labelBounds = {
@@ -37,37 +36,27 @@ export const rerouteCollidingTrace = ({
37
36
  chipId: `netlabel-${label.netId}`,
38
37
  }
39
38
 
40
- const fourPointResult = tryFourPointDetour({
39
+ const fourPointCandidates = generateFourPointDetourCandidates({
41
40
  initialTrace,
42
41
  label,
43
42
  labelBounds,
44
- obstacles,
45
43
  paddingBuffer,
46
44
  detourCount,
47
45
  })
48
- if (fourPointResult) {
49
- initialTrace.tracePath = fourPointResult.tracePath
50
- }
46
+
51
47
  const { firstInsideIndex, lastInsideIndex } = findTraceViolationZone(
52
48
  initialTrace.tracePath,
53
49
  labelBounds,
54
50
  )
55
51
 
56
- const snipReconnectResult = trySnipAndReconnect({
52
+ const snipReconnectCandidates = generateSnipAndReconnectCandidates({
57
53
  initialTrace,
58
54
  firstInsideIndex,
59
55
  lastInsideIndex,
60
56
  labelBounds,
61
- obstacles,
57
+ paddingBuffer,
58
+ detourCount,
62
59
  })
63
60
 
64
- if (snipReconnectResult) {
65
- return snipReconnectResult
66
- }
67
-
68
- if (fourPointResult) {
69
- return fourPointResult
70
- }
71
-
72
- return initialTrace
61
+ return [...fourPointCandidates, ...snipReconnectCandidates]
73
62
  }
@@ -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
+ }