@tscircuit/schematic-trace-solver 0.0.41 → 0.0.43

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 (58) hide show
  1. package/dist/index.d.ts +118 -8
  2. package/dist/index.js +1067 -748
  3. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +48 -16
  4. package/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts +133 -0
  5. package/lib/solvers/TraceCleanupSolver/balanceZShapes.ts +200 -0
  6. package/lib/solvers/{TraceLabelOverlapAvoidanceSolver → TraceCleanupSolver}/hasCollisions.ts +1 -1
  7. package/lib/solvers/{TraceLabelOverlapAvoidanceSolver → TraceCleanupSolver}/hasCollisionsWithLabels.ts +1 -1
  8. package/lib/solvers/TraceCleanupSolver/minimizeTurnsWithFilteredLabels.ts +76 -0
  9. package/lib/solvers/{TraceLabelOverlapAvoidanceSolver → TraceCleanupSolver}/simplifyPath.ts +1 -1
  10. package/lib/solvers/{TraceLabelOverlapAvoidanceSolver → TraceCleanupSolver}/turnMinimization.ts +0 -2
  11. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver.ts +74 -192
  12. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts +13 -24
  13. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/LabelMergingSolver.ts +205 -0
  14. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/OverlapAvoidanceStepSolver.ts +144 -0
  15. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts +117 -0
  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/site/examples/example26.page.tsx +4 -0
  20. package/tests/assets/1.input.json +379 -0
  21. package/tests/assets/2.input.json +330 -0
  22. package/tests/assets/3.input.json +388 -0
  23. package/tests/assets/MergedNetLabelObstacles.test.input.json +154 -0
  24. package/tests/assets/OverlapAvoidanceStepSolver.test.input.json +682 -0
  25. package/tests/assets/SingleOverlapSolver.test.input.json +170 -0
  26. package/tests/assets/TraceCleanupSolver.test.input.json +216 -0
  27. package/tests/assets/TraceLabelOverlapAvoidanceSolver.test.input.json +424 -0
  28. package/tests/assets/example26.json +1206 -0
  29. package/tests/examples/__snapshots__/example02.snap.svg +1 -1
  30. package/tests/examples/__snapshots__/example09.snap.svg +34 -34
  31. package/tests/examples/__snapshots__/example11.snap.svg +13 -13
  32. package/tests/examples/__snapshots__/example12.snap.svg +3 -3
  33. package/tests/examples/__snapshots__/example13.snap.svg +12 -12
  34. package/tests/examples/__snapshots__/example16.snap.svg +4 -4
  35. package/tests/examples/__snapshots__/example19.snap.svg +9 -9
  36. package/tests/examples/__snapshots__/example28.snap.svg +3 -3
  37. package/tests/examples/__snapshots__/example29.snap.svg +1208 -0
  38. package/tests/examples/example29.test.ts +13 -0
  39. package/tests/solvers/TraceCleanupSolver/TraceCleanupSolver.test.ts +19 -0
  40. package/tests/solvers/TraceCleanupSolver/__snapshots__/TraceCleanupSolver.snap.svg +119 -0
  41. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver.test.ts +18 -0
  42. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/__snapshots__/TraceLabelOverlapAvoidanceSolver.snap.svg +243 -0
  43. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/__snapshots__/renderComparisonView01.snap.svg +223 -0
  44. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/__snapshots__/renderComparisonView02.snap.svg +205 -0
  45. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/__snapshots__/renderComparisonView03.snap.svg +184 -0
  46. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/renderComparisonView01.test.ts +29 -0
  47. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/renderComparisonView02.test.ts +27 -0
  48. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/renderComparisonView03.test.ts +39 -0
  49. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/MergedNetLabelObstacles.test.ts +14 -0
  50. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/OverlapAvoidanceStepSolver.test.ts +20 -0
  51. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/SingleOverlapSolver.test.ts +10 -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/lib/solvers/TraceLabelOverlapAvoidanceSolver/balanceLShapes.ts +0 -207
  56. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/minimizeTurnsWithFilteredLabels.ts +0 -66
  57. /package/lib/solvers/{TraceLabelOverlapAvoidanceSolver → TraceCleanupSolver}/countTurns.ts +0 -0
  58. /package/lib/solvers/{TraceLabelOverlapAvoidanceSolver → TraceCleanupSolver}/tryConnectPoints.ts +0 -0
@@ -1,16 +1,12 @@
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"
9
8
  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"
9
+ import { OverlapAvoidanceStepSolver } from "./sub-solvers/OverlapAvoidanceStepSolver/OverlapAvoidanceStepSolver"
14
10
 
15
11
  interface TraceLabelOverlapAvoidanceSolverInput {
16
12
  inputProblem: InputProblem
@@ -18,222 +14,108 @@ interface TraceLabelOverlapAvoidanceSolverInput {
18
14
  netLabelPlacements: NetLabelPlacement[]
19
15
  }
20
16
 
17
+ /**
18
+ * This solver is a pipeline that runs a series of sub-solvers to resolve
19
+ * trace-label overlaps and clean up the resulting traces.
20
+ */
21
21
  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
22
+ inputProblem: InputProblem
23
+ traces: SolvedTracePath[]
24
+ netLabelPlacements: NetLabelPlacement[]
25
+
26
+ // sub-solver instances
27
+ labelMergingSolver?: MergedNetLabelObstacleSolver
28
+ overlapAvoidanceSolver?: OverlapAvoidanceStepSolver
29
+ pipelineStepIndex = 0
30
30
 
31
31
  constructor(solverInput: TraceLabelOverlapAvoidanceSolverInput) {
32
32
  super()
33
- this.problem = solverInput.inputProblem
33
+ this.inputProblem = solverInput.inputProblem
34
34
  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
35
+ this.netLabelPlacements = solverInput.netLabelPlacements
99
36
  }
100
37
 
101
38
  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)
39
+ // If a sub-solver is active, step it and check for completion.
40
+ if (this.activeSubSolver) {
41
+ this.activeSubSolver.step()
42
+
43
+ if (this.activeSubSolver.solved) {
44
+ this.activeSubSolver = null
45
+ this.pipelineStepIndex++
46
+ } else if (this.activeSubSolver.failed) {
47
+ this.failed = true // If any sub-solver fails, the whole thing fails
48
+ this.activeSubSolver = null
128
49
  }
129
- return o.trace.globalConnNetId !== o.label.globalConnNetId
130
- })
131
-
132
- if (unfriendlyOverlaps.length === 0) {
133
- this.solved = true
134
- return
50
+ return // Return to allow the sub-solver to run
135
51
  }
136
52
 
137
- const updatedTracesMap: Record<string, SolvedTracePath> = {}
138
- for (const trace of this.traces) {
139
- updatedTracesMap[trace.mspPairId] = trace
53
+ // If no sub-solver is active, create the next one in the pipeline.
54
+ switch (this.pipelineStepIndex) {
55
+ case 0:
56
+ this.labelMergingSolver = new MergedNetLabelObstacleSolver({
57
+ netLabelPlacements: this.netLabelPlacements,
58
+ inputProblem: this.inputProblem,
59
+ traces: this.traces,
60
+ })
61
+ this.activeSubSolver = this.labelMergingSolver
62
+ break
63
+
64
+ case 1:
65
+ this.overlapAvoidanceSolver = new OverlapAvoidanceStepSolver({
66
+ inputProblem: this.inputProblem,
67
+ traces: this.traces,
68
+ netLabelPlacements:
69
+ this.labelMergingSolver!.getOutput().netLabelPlacements,
70
+ mergedLabelNetIdMap:
71
+ this.labelMergingSolver!.getOutput().mergedLabelNetIdMap,
72
+ })
73
+ this.activeSubSolver = this.overlapAvoidanceSolver
74
+ break
75
+
76
+ default:
77
+ this.solved = true
78
+ break
140
79
  }
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
80
  }
203
81
 
204
82
  getOutput() {
205
83
  return {
206
- traces: this.updatedTraces,
207
- netLabelPlacements: this.netLabelPlacements,
84
+ traces: this.overlapAvoidanceSolver?.getOutput().allTraces ?? this.traces,
85
+ netLabelPlacements:
86
+ this.labelMergingSolver?.getOutput().netLabelPlacements ??
87
+ this.netLabelPlacements,
208
88
  }
209
89
  }
210
90
 
211
91
  override visualize(): GraphicsObject {
212
- const graphics = visualizeInputProblem(this.problem)
92
+ if (this.activeSubSolver) {
93
+ return this.activeSubSolver.visualize()
94
+ }
213
95
 
96
+ // When no sub-solver is active, show the current state of the pipeline
97
+ const graphics = visualizeInputProblem(this.inputProblem)
214
98
  if (!graphics.lines) graphics.lines = []
215
- if (!graphics.circles) graphics.circles = []
216
- if (!graphics.texts) graphics.texts = []
217
99
  if (!graphics.rects) graphics.rects = []
218
100
 
219
- for (const trace of this.updatedTraces) {
101
+ const output = this.getOutput()
102
+
103
+ for (const trace of output.traces) {
220
104
  graphics.lines!.push({
221
105
  points: trace.tracePath,
222
106
  strokeColor: "purple",
223
107
  })
224
108
  }
225
109
 
226
- for (const p of this.netLabelPlacements) {
110
+ for (const label of output.netLabelPlacements) {
111
+ const color = getColorFromString(label.globalConnNetId, 0.3)
227
112
  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),
113
+ center: label.center,
114
+ width: label.width,
115
+ height: label.height,
116
+ fill: color,
117
+ stroke: color.replace("0.3", "1"),
118
+ label: label.globalConnNetId,
237
119
  })
238
120
  }
239
121
 
@@ -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 { generateFourPointDetourCandidates } from "./tryFourPointDetour"
9
+ import { simplifyPath } from "../TraceCleanupSolver/simplifyPath"
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
+ }