@tscircuit/schematic-trace-solver 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +6 -0
- package/.github/workflows/bun-formatcheck.yml +26 -0
- package/.github/workflows/bun-pver-release.yml +28 -0
- package/.github/workflows/bun-test.yml +28 -0
- package/.github/workflows/bun-typecheck.yml +26 -0
- package/LICENSE +21 -0
- package/README.md +75 -0
- package/biome.json +93 -0
- package/bunfig.toml +5 -0
- package/cosmos.config.json +6 -0
- package/cosmos.decorator.tsx +21 -0
- package/dist/index.d.ts +420 -0
- package/dist/index.js +7863 -0
- package/index.html +17 -0
- package/lib/data-structures/ChipObstacleSpatialIndex.ts +70 -0
- package/lib/index.ts +2 -0
- package/lib/solvers/BaseSolver/BaseSolver.ts +83 -0
- package/lib/solvers/GuidelinesSolver/GuidelinesSolver.ts +138 -0
- package/lib/solvers/GuidelinesSolver/getGeneratorForAllChipPairs.ts +39 -0
- package/lib/solvers/GuidelinesSolver/getHorizontalGuidelineY.ts +18 -0
- package/lib/solvers/GuidelinesSolver/getInputChipBounds.ts +20 -0
- package/lib/solvers/GuidelinesSolver/getVerticalGuidelineX.ts +18 -0
- package/lib/solvers/GuidelinesSolver/visualizeGuidelines.ts +45 -0
- package/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts +156 -0
- package/lib/solvers/MspConnectionPairSolver/getConnectivityMapFromInputProblem.ts +20 -0
- package/lib/solvers/MspConnectionPairSolver/getMspConnectionPairsFromPins.ts +96 -0
- package/lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver.ts +192 -0
- package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/SingleNetLabelPlacementSolver.ts +479 -0
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver.ts +127 -0
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver.ts +191 -0
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/generateElbowVariants.ts +132 -0
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/getPinDirection.ts +42 -0
- package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +244 -0
- package/lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem.ts +89 -0
- package/lib/solvers/TraceOverlapShiftSolver/TraceOverlapIssueSolver/TraceOverlapIssueSolver.ts +180 -0
- package/lib/solvers/TraceOverlapShiftSolver/TraceOverlapShiftSolver.ts +264 -0
- package/lib/types/InputProblem.ts +40 -0
- package/lib/utils/dir.ts +16 -0
- package/lib/utils/getAllPossibleOrderingsGenerator.ts +47 -0
- package/lib/utils/getColorFromString.ts +7 -0
- package/package.json +29 -0
- package/site/components/GenericSolverDebugger.tsx +0 -0
- package/site/components/PipelineDebugger.tsx +29 -0
- package/site/components/PipelineStageTable.tsx +149 -0
- package/site/components/SolverBreadcrumbInputDownloader.tsx +62 -0
- package/site/components/SolverToolbar.tsx +128 -0
- package/site/examples/example01-basic.page.tsx +104 -0
- package/tests/functions/generateElbowVariants.test.ts +98 -0
- package/tests/functions/getOrthogonalMinimumSpanningTree.test.ts +23 -0
- package/tsconfig.json +37 -0
- package/vite.config.ts +12 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
2
|
+
import type {
|
|
3
|
+
NetLabelPlacement,
|
|
4
|
+
OverlappingSameNetTraceGroup,
|
|
5
|
+
} from "../NetLabelPlacementSolver"
|
|
6
|
+
import type { InputChip, InputProblem } from "lib/types/InputProblem"
|
|
7
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
8
|
+
import type { MspConnectionPairId } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver"
|
|
9
|
+
import type { FacingDirection } from "lib/utils/dir"
|
|
10
|
+
import type { GraphicsObject } from "graphics-debug"
|
|
11
|
+
import { ChipObstacleSpatialIndex } from "lib/data-structures/ChipObstacleSpatialIndex"
|
|
12
|
+
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
|
|
13
|
+
import { getColorFromString } from "lib/utils/getColorFromString"
|
|
14
|
+
|
|
15
|
+
export const NET_LABEL_HORIZONTAL_WIDTH = 0.4
|
|
16
|
+
export const NET_LABEL_HORIZONTAL_HEIGHT = 0.2
|
|
17
|
+
// NOTE: net labels, when in the y+/y- orientation, are rotated and therefore
|
|
18
|
+
// the width/height are swapped
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Find a location in the overlappingSameNetTraceGroup where a net label should
|
|
22
|
+
* be placed. We do this by looking for the largest chip, and starting our
|
|
23
|
+
* search from the segment directly connected to the largest chip. We then
|
|
24
|
+
* travel along the segment, moving to any connected segment. Each step, we
|
|
25
|
+
* check a specific segment
|
|
26
|
+
*
|
|
27
|
+
* When checking a segment, we check the following locations with each
|
|
28
|
+
* orientation:
|
|
29
|
+
* - The start of the segment
|
|
30
|
+
* - The start of the segment, plus the width of the net label
|
|
31
|
+
* - The end of the segment
|
|
32
|
+
* - The end of the segment, minus the width of the net label
|
|
33
|
+
*
|
|
34
|
+
* When checking a location, we check for the following:
|
|
35
|
+
* 1. Would placing a net label at this location cause a collision with a chip?
|
|
36
|
+
* 2. Would placing a net label at this location cause a collision with ANY
|
|
37
|
+
* trace? (Note: you must offset the anchor point slightly from the trace to
|
|
38
|
+
* avoid counting the point where the net label contacts the trace)
|
|
39
|
+
*
|
|
40
|
+
* The first location that satisfies the above conditions, in our traversal
|
|
41
|
+
* order from the largest chip, is the location we return in netLabelPlacement
|
|
42
|
+
*/
|
|
43
|
+
export class SingleNetLabelPlacementSolver extends BaseSolver {
|
|
44
|
+
inputProblem: InputProblem
|
|
45
|
+
inputTraceMap: Record<MspConnectionPairId, SolvedTracePath>
|
|
46
|
+
overlappingSameNetTraceGroup: OverlappingSameNetTraceGroup
|
|
47
|
+
availableOrientations: Array<FacingDirection>
|
|
48
|
+
|
|
49
|
+
chipObstacleSpatialIndex: ChipObstacleSpatialIndex
|
|
50
|
+
|
|
51
|
+
netLabelPlacement: NetLabelPlacement | null = null
|
|
52
|
+
testedCandidates: Array<{
|
|
53
|
+
center: { x: number; y: number }
|
|
54
|
+
width: number
|
|
55
|
+
height: number
|
|
56
|
+
bounds: { minX: number; minY: number; maxX: number; maxY: number }
|
|
57
|
+
anchor: { x: number; y: number }
|
|
58
|
+
orientation: FacingDirection
|
|
59
|
+
status: "ok" | "chip-collision" | "trace-collision" | "parallel-to-segment"
|
|
60
|
+
hostSegIndex: number
|
|
61
|
+
}> = []
|
|
62
|
+
|
|
63
|
+
constructor(params: {
|
|
64
|
+
inputProblem: InputProblem
|
|
65
|
+
inputTraceMap: Record<MspConnectionPairId, SolvedTracePath>
|
|
66
|
+
overlappingSameNetTraceGroup: OverlappingSameNetTraceGroup
|
|
67
|
+
availableOrientations: FacingDirection[]
|
|
68
|
+
}) {
|
|
69
|
+
super()
|
|
70
|
+
this.inputProblem = params.inputProblem
|
|
71
|
+
this.inputTraceMap = params.inputTraceMap
|
|
72
|
+
this.overlappingSameNetTraceGroup = params.overlappingSameNetTraceGroup
|
|
73
|
+
this.availableOrientations = params.availableOrientations
|
|
74
|
+
|
|
75
|
+
this.chipObstacleSpatialIndex =
|
|
76
|
+
params.inputProblem._chipObstacleSpatialIndex ??
|
|
77
|
+
new ChipObstacleSpatialIndex(params.inputProblem.chips)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private getDimsForOrientation(orientation: FacingDirection) {
|
|
81
|
+
if (orientation === "y+" || orientation === "y-") {
|
|
82
|
+
return {
|
|
83
|
+
width: NET_LABEL_HORIZONTAL_HEIGHT,
|
|
84
|
+
height: NET_LABEL_HORIZONTAL_WIDTH,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
width: NET_LABEL_HORIZONTAL_WIDTH,
|
|
89
|
+
height: NET_LABEL_HORIZONTAL_HEIGHT,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private getCenterFromAnchor(
|
|
94
|
+
anchor: { x: number; y: number },
|
|
95
|
+
orientation: FacingDirection,
|
|
96
|
+
width: number,
|
|
97
|
+
height: number,
|
|
98
|
+
) {
|
|
99
|
+
switch (orientation) {
|
|
100
|
+
case "x+":
|
|
101
|
+
return { x: anchor.x + width / 2, y: anchor.y }
|
|
102
|
+
case "x-":
|
|
103
|
+
return { x: anchor.x - width / 2, y: anchor.y }
|
|
104
|
+
case "y+":
|
|
105
|
+
return { x: anchor.x, y: anchor.y + height / 2 }
|
|
106
|
+
case "y-":
|
|
107
|
+
return { x: anchor.x, y: anchor.y - height / 2 }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private getRectBounds(
|
|
112
|
+
center: { x: number; y: number },
|
|
113
|
+
w: number,
|
|
114
|
+
h: number,
|
|
115
|
+
) {
|
|
116
|
+
return {
|
|
117
|
+
minX: center.x - w / 2,
|
|
118
|
+
minY: center.y - h / 2,
|
|
119
|
+
maxX: center.x + w / 2,
|
|
120
|
+
maxY: center.y + h / 2,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private segmentIntersectsRect(
|
|
125
|
+
p1: { x: number; y: number },
|
|
126
|
+
p2: { x: number; y: number },
|
|
127
|
+
rect: { minX: number; minY: number; maxX: number; maxY: number },
|
|
128
|
+
EPS = 1e-9,
|
|
129
|
+
): boolean {
|
|
130
|
+
const isVert = Math.abs(p1.x - p2.x) < EPS
|
|
131
|
+
const isHorz = Math.abs(p1.y - p2.y) < EPS
|
|
132
|
+
if (!isVert && !isHorz) return false
|
|
133
|
+
|
|
134
|
+
if (isVert) {
|
|
135
|
+
const x = p1.x
|
|
136
|
+
if (x < rect.minX - EPS || x > rect.maxX + EPS) return false
|
|
137
|
+
const segMinY = Math.min(p1.y, p2.y)
|
|
138
|
+
const segMaxY = Math.max(p1.y, p2.y)
|
|
139
|
+
const overlap =
|
|
140
|
+
Math.min(segMaxY, rect.maxY) - Math.max(segMinY, rect.minY)
|
|
141
|
+
return overlap > EPS
|
|
142
|
+
} else {
|
|
143
|
+
const y = p1.y
|
|
144
|
+
if (y < rect.minY - EPS || y > rect.maxY + EPS) return false
|
|
145
|
+
const segMinX = Math.min(p1.x, p2.x)
|
|
146
|
+
const segMaxX = Math.max(p1.x, p2.x)
|
|
147
|
+
const overlap =
|
|
148
|
+
Math.min(segMaxX, rect.maxX) - Math.max(segMinX, rect.minX)
|
|
149
|
+
return overlap > EPS
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private rectIntersectsAnyTrace(
|
|
154
|
+
bounds: { minX: number; minY: number; maxX: number; maxY: number },
|
|
155
|
+
hostPathId: MspConnectionPairId,
|
|
156
|
+
hostSegIndex: number,
|
|
157
|
+
): boolean {
|
|
158
|
+
for (const [pairId, solved] of Object.entries(this.inputTraceMap)) {
|
|
159
|
+
const pts = solved.tracePath
|
|
160
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
161
|
+
if (pairId === hostPathId && i === hostSegIndex) continue
|
|
162
|
+
if (this.segmentIntersectsRect(pts[i]!, pts[i + 1]!, bounds))
|
|
163
|
+
return true
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return false
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
override _step() {
|
|
170
|
+
if (this.netLabelPlacement) {
|
|
171
|
+
this.solved = true
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Prefer starting from the trace connected to the "largest" chip (most pins)
|
|
176
|
+
const groupId = this.overlappingSameNetTraceGroup.globalConnNetId
|
|
177
|
+
const chipsById: Record<string, InputChip> = Object.fromEntries(
|
|
178
|
+
this.inputProblem.chips.map((c) => [c.chipId, c]),
|
|
179
|
+
)
|
|
180
|
+
const groupTraces = Object.values(this.inputTraceMap).filter(
|
|
181
|
+
(t) => t.globalConnNetId === groupId,
|
|
182
|
+
)
|
|
183
|
+
const chipIdsInGroup = new Set<string>()
|
|
184
|
+
for (const t of groupTraces) {
|
|
185
|
+
chipIdsInGroup.add(t.pins[0].chipId)
|
|
186
|
+
chipIdsInGroup.add(t.pins[1].chipId)
|
|
187
|
+
}
|
|
188
|
+
let largestChipId: string | null = null
|
|
189
|
+
let largestPinCount = -1
|
|
190
|
+
for (const id of chipIdsInGroup) {
|
|
191
|
+
const chip = chipsById[id]
|
|
192
|
+
const count = chip?.pins?.length ?? 0
|
|
193
|
+
if (count > largestPinCount) {
|
|
194
|
+
largestPinCount = count
|
|
195
|
+
largestChipId = id
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const lengthOf = (path: SolvedTracePath) => {
|
|
199
|
+
let sum = 0
|
|
200
|
+
const pts = path.tracePath
|
|
201
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
202
|
+
sum +=
|
|
203
|
+
Math.abs(pts[i + 1]!.x - pts[i]!.x) +
|
|
204
|
+
Math.abs(pts[i + 1]!.y - pts[i]!.y)
|
|
205
|
+
}
|
|
206
|
+
return sum
|
|
207
|
+
}
|
|
208
|
+
const hostCandidates =
|
|
209
|
+
largestChipId == null
|
|
210
|
+
? []
|
|
211
|
+
: groupTraces.filter(
|
|
212
|
+
(t) =>
|
|
213
|
+
t.pins[0].chipId === largestChipId ||
|
|
214
|
+
t.pins[1].chipId === largestChipId,
|
|
215
|
+
)
|
|
216
|
+
let host =
|
|
217
|
+
hostCandidates.length > 0
|
|
218
|
+
? hostCandidates.reduce((a, b) => (lengthOf(a) >= lengthOf(b) ? a : b))
|
|
219
|
+
: this.overlappingSameNetTraceGroup.overlappingTraces
|
|
220
|
+
|
|
221
|
+
// Ensure we traverse the host path starting at the segment attached to the largest chip's pin
|
|
222
|
+
let pts = host.tracePath.slice()
|
|
223
|
+
if (largestChipId) {
|
|
224
|
+
const largePin =
|
|
225
|
+
host.pins[0].chipId === largestChipId ? host.pins[0] : host.pins[1]
|
|
226
|
+
const d0 =
|
|
227
|
+
Math.abs(pts[0].x - largePin.x) + Math.abs(pts[0].y - largePin.y)
|
|
228
|
+
const dL =
|
|
229
|
+
Math.abs(pts[pts.length - 1].x - largePin.x) +
|
|
230
|
+
Math.abs(pts[pts.length - 1].y - largePin.y)
|
|
231
|
+
if (d0 > dL) {
|
|
232
|
+
pts = pts.slice().reverse()
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const orientations =
|
|
237
|
+
this.availableOrientations.length > 0
|
|
238
|
+
? this.availableOrientations
|
|
239
|
+
: (["x+", "x-", "y+", "y-"] as FacingDirection[])
|
|
240
|
+
|
|
241
|
+
const EPS = 1e-6
|
|
242
|
+
const anchorsForSegment = (
|
|
243
|
+
a: { x: number; y: number },
|
|
244
|
+
b: { x: number; y: number },
|
|
245
|
+
) => {
|
|
246
|
+
// Start, midpoint, end
|
|
247
|
+
return [
|
|
248
|
+
{ x: a.x, y: a.y },
|
|
249
|
+
{ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 },
|
|
250
|
+
{ x: b.x, y: b.y },
|
|
251
|
+
]
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
for (let si = 0; si < pts.length - 1; si++) {
|
|
255
|
+
const a = pts[si]!
|
|
256
|
+
const b = pts[si + 1]!
|
|
257
|
+
const isH = Math.abs(a.y - b.y) < EPS
|
|
258
|
+
const isV = Math.abs(a.x - b.x) < EPS
|
|
259
|
+
if (!isH && !isV) continue
|
|
260
|
+
|
|
261
|
+
// Only consider orientations perpendicular to the segment to avoid
|
|
262
|
+
// self-overlap with the host segment.
|
|
263
|
+
const segmentAllowed: FacingDirection[] = isH
|
|
264
|
+
? (["y+", "y-"] as FacingDirection[])
|
|
265
|
+
: (["x+", "x-"] as FacingDirection[])
|
|
266
|
+
const candidateOrients = orientations.filter((o) =>
|
|
267
|
+
segmentAllowed.includes(o),
|
|
268
|
+
)
|
|
269
|
+
if (candidateOrients.length === 0) continue
|
|
270
|
+
|
|
271
|
+
const anchors = anchorsForSegment(a, b)
|
|
272
|
+
for (const anchor of anchors) {
|
|
273
|
+
for (const orientation of candidateOrients) {
|
|
274
|
+
const { width, height } = this.getDimsForOrientation(orientation)
|
|
275
|
+
const center = this.getCenterFromAnchor(
|
|
276
|
+
anchor,
|
|
277
|
+
orientation,
|
|
278
|
+
width,
|
|
279
|
+
height,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
// Small outward offset to avoid counting the touching trace as a collision
|
|
283
|
+
const outward =
|
|
284
|
+
orientation === "x+"
|
|
285
|
+
? { x: 1, y: 0 }
|
|
286
|
+
: orientation === "x-"
|
|
287
|
+
? { x: -1, y: 0 }
|
|
288
|
+
: orientation === "y+"
|
|
289
|
+
? { x: 0, y: 1 }
|
|
290
|
+
: { x: 0, y: -1 }
|
|
291
|
+
const offset = 1e-4
|
|
292
|
+
const testCenter = {
|
|
293
|
+
x: center.x + outward.x * offset,
|
|
294
|
+
y: center.y + outward.y * offset,
|
|
295
|
+
}
|
|
296
|
+
const bounds = this.getRectBounds(testCenter, width, height)
|
|
297
|
+
|
|
298
|
+
// Chip collision check
|
|
299
|
+
const chips = this.chipObstacleSpatialIndex.getChipsInBounds(bounds)
|
|
300
|
+
if (chips.length > 0) {
|
|
301
|
+
this.testedCandidates.push({
|
|
302
|
+
center: testCenter,
|
|
303
|
+
width,
|
|
304
|
+
height,
|
|
305
|
+
bounds,
|
|
306
|
+
anchor,
|
|
307
|
+
orientation,
|
|
308
|
+
status: "chip-collision",
|
|
309
|
+
hostSegIndex: si,
|
|
310
|
+
})
|
|
311
|
+
continue
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Trace collision check (ignore the host segment)
|
|
315
|
+
if (this.rectIntersectsAnyTrace(bounds, host.mspPairId, si)) {
|
|
316
|
+
this.testedCandidates.push({
|
|
317
|
+
center: testCenter,
|
|
318
|
+
width,
|
|
319
|
+
height,
|
|
320
|
+
bounds,
|
|
321
|
+
anchor,
|
|
322
|
+
orientation,
|
|
323
|
+
status: "trace-collision",
|
|
324
|
+
hostSegIndex: si,
|
|
325
|
+
})
|
|
326
|
+
continue
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Found a valid placement
|
|
330
|
+
this.testedCandidates.push({
|
|
331
|
+
center: testCenter,
|
|
332
|
+
width,
|
|
333
|
+
height,
|
|
334
|
+
bounds,
|
|
335
|
+
anchor,
|
|
336
|
+
orientation,
|
|
337
|
+
status: "ok",
|
|
338
|
+
hostSegIndex: si,
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
this.netLabelPlacement = {
|
|
342
|
+
globalConnNetId: this.overlappingSameNetTraceGroup.globalConnNetId,
|
|
343
|
+
dcConnNetId: host.dcConnNetId,
|
|
344
|
+
orientation,
|
|
345
|
+
anchorPoint: anchor,
|
|
346
|
+
width,
|
|
347
|
+
height,
|
|
348
|
+
center,
|
|
349
|
+
}
|
|
350
|
+
this.solved = true
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
this.failed = true
|
|
357
|
+
this.error = "Could not place net label without collisions"
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
override visualize(): GraphicsObject {
|
|
361
|
+
const graphics = visualizeInputProblem(this.inputProblem)
|
|
362
|
+
|
|
363
|
+
// Visualize the entire trace group for this net id
|
|
364
|
+
const groupId = this.overlappingSameNetTraceGroup.globalConnNetId
|
|
365
|
+
// Choose host as in _step: the trace that touches the largest chip in the group
|
|
366
|
+
const chipsById: Record<string, InputChip> = Object.fromEntries(
|
|
367
|
+
this.inputProblem.chips.map((c) => [c.chipId, c]),
|
|
368
|
+
)
|
|
369
|
+
const groupTraces = Object.values(this.inputTraceMap).filter(
|
|
370
|
+
(t) => t.globalConnNetId === groupId,
|
|
371
|
+
)
|
|
372
|
+
const chipIdsInGroup = new Set<string>()
|
|
373
|
+
for (const t of groupTraces) {
|
|
374
|
+
chipIdsInGroup.add(t.pins[0].chipId)
|
|
375
|
+
chipIdsInGroup.add(t.pins[1].chipId)
|
|
376
|
+
}
|
|
377
|
+
let largestChipId: string | null = null
|
|
378
|
+
let largestPinCount = -1
|
|
379
|
+
for (const id of chipIdsInGroup) {
|
|
380
|
+
const chip = chipsById[id]
|
|
381
|
+
const count = chip?.pins?.length ?? 0
|
|
382
|
+
if (count > largestPinCount) {
|
|
383
|
+
largestPinCount = count
|
|
384
|
+
largestChipId = id
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const lengthOf = (path: SolvedTracePath) => {
|
|
388
|
+
let sum = 0
|
|
389
|
+
const pts = path.tracePath
|
|
390
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
391
|
+
sum +=
|
|
392
|
+
Math.abs(pts[i + 1]!.x - pts[i]!.x) +
|
|
393
|
+
Math.abs(pts[i + 1]!.y - pts[i]!.y)
|
|
394
|
+
}
|
|
395
|
+
return sum
|
|
396
|
+
}
|
|
397
|
+
const hostCandidates =
|
|
398
|
+
largestChipId == null
|
|
399
|
+
? []
|
|
400
|
+
: groupTraces.filter(
|
|
401
|
+
(t) =>
|
|
402
|
+
t.pins[0].chipId === largestChipId ||
|
|
403
|
+
t.pins[1].chipId === largestChipId,
|
|
404
|
+
)
|
|
405
|
+
const host =
|
|
406
|
+
hostCandidates.length > 0
|
|
407
|
+
? hostCandidates.reduce((a, b) => (lengthOf(a) >= lengthOf(b) ? a : b))
|
|
408
|
+
: this.overlappingSameNetTraceGroup.overlappingTraces
|
|
409
|
+
const groupStroke = getColorFromString(groupId, 0.9)
|
|
410
|
+
const groupFill = getColorFromString(groupId, 0.5)
|
|
411
|
+
|
|
412
|
+
for (const trace of Object.values(this.inputTraceMap)) {
|
|
413
|
+
if (trace.globalConnNetId !== groupId) continue
|
|
414
|
+
const isHost = trace.mspPairId === host.mspPairId
|
|
415
|
+
graphics.lines!.push({
|
|
416
|
+
points: trace.tracePath,
|
|
417
|
+
strokeColor: isHost ? groupStroke : groupFill,
|
|
418
|
+
strokeWidth: isHost ? 0.006 : 0.003,
|
|
419
|
+
strokeDash: isHost ? undefined : "4 2",
|
|
420
|
+
} as any)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Visualize all tested candidate rectangles with reason coloring
|
|
424
|
+
for (const c of this.testedCandidates) {
|
|
425
|
+
const fill =
|
|
426
|
+
c.status === "ok"
|
|
427
|
+
? "rgba(0, 180, 0, 0.25)"
|
|
428
|
+
: c.status === "chip-collision"
|
|
429
|
+
? "rgba(220, 0, 0, 0.25)"
|
|
430
|
+
: c.status === "trace-collision"
|
|
431
|
+
? "rgba(220, 140, 0, 0.25)"
|
|
432
|
+
: "rgba(120, 120, 120, 0.15)"
|
|
433
|
+
const stroke =
|
|
434
|
+
c.status === "ok"
|
|
435
|
+
? "green"
|
|
436
|
+
: c.status === "chip-collision"
|
|
437
|
+
? "red"
|
|
438
|
+
: c.status === "trace-collision"
|
|
439
|
+
? "orange"
|
|
440
|
+
: "gray"
|
|
441
|
+
|
|
442
|
+
graphics.rects!.push({
|
|
443
|
+
center: {
|
|
444
|
+
x: (c.bounds.minX + c.bounds.maxX) / 2,
|
|
445
|
+
y: (c.bounds.minY + c.bounds.maxY) / 2,
|
|
446
|
+
},
|
|
447
|
+
width: c.width,
|
|
448
|
+
height: c.height,
|
|
449
|
+
fill,
|
|
450
|
+
strokeColor: stroke,
|
|
451
|
+
} as any)
|
|
452
|
+
|
|
453
|
+
graphics.points!.push({
|
|
454
|
+
x: c.anchor.x,
|
|
455
|
+
y: c.anchor.y,
|
|
456
|
+
color: stroke,
|
|
457
|
+
} as any)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Visualize the final accepted label (if any)
|
|
461
|
+
if (this.netLabelPlacement) {
|
|
462
|
+
const p = this.netLabelPlacement
|
|
463
|
+
graphics.rects!.push({
|
|
464
|
+
center: p.center,
|
|
465
|
+
width: p.width,
|
|
466
|
+
height: p.height,
|
|
467
|
+
fill: "rgba(0, 128, 255, 0.35)",
|
|
468
|
+
strokeColor: "blue",
|
|
469
|
+
} as any)
|
|
470
|
+
graphics.points!.push({
|
|
471
|
+
x: p.anchorPoint.x,
|
|
472
|
+
y: p.anchorPoint.y,
|
|
473
|
+
color: "blue",
|
|
474
|
+
} as any)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return graphics
|
|
478
|
+
}
|
|
479
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
2
|
+
import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualizeInputProblem"
|
|
3
|
+
import { getBounds, type GraphicsObject } from "graphics-debug"
|
|
4
|
+
import type { InputChip, InputProblem } from "lib/types/InputProblem"
|
|
5
|
+
import type {
|
|
6
|
+
MspConnectionPair,
|
|
7
|
+
MspConnectionPairId,
|
|
8
|
+
} from "../MspConnectionPairSolver/MspConnectionPairSolver"
|
|
9
|
+
import type { ConnectivityMap } from "connectivity-map"
|
|
10
|
+
import { SchematicTraceSingleLineSolver } from "./SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver"
|
|
11
|
+
import type { Guideline } from "../GuidelinesSolver/GuidelinesSolver"
|
|
12
|
+
import { visualizeGuidelines } from "../GuidelinesSolver/visualizeGuidelines"
|
|
13
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
14
|
+
|
|
15
|
+
export interface SolvedTracePath extends MspConnectionPair {
|
|
16
|
+
tracePath: Point[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class SchematicTraceLinesSolver extends BaseSolver {
|
|
20
|
+
inputProblem: InputProblem
|
|
21
|
+
guidelines: Guideline[]
|
|
22
|
+
mspConnectionPairs: MspConnectionPair[]
|
|
23
|
+
|
|
24
|
+
dcConnMap: ConnectivityMap
|
|
25
|
+
globalConnMap: ConnectivityMap
|
|
26
|
+
|
|
27
|
+
queuedConnectionPairs: MspConnectionPair[]
|
|
28
|
+
chipMap: Record<string, InputChip>
|
|
29
|
+
|
|
30
|
+
currentConnectionPair: MspConnectionPair | null = null
|
|
31
|
+
|
|
32
|
+
solvedTracePaths: Array<SolvedTracePath> = []
|
|
33
|
+
|
|
34
|
+
declare activeSubSolver: SchematicTraceSingleLineSolver | null
|
|
35
|
+
|
|
36
|
+
constructor(params: {
|
|
37
|
+
mspConnectionPairs: MspConnectionPair[]
|
|
38
|
+
chipMap: Record<string, InputChip>
|
|
39
|
+
dcConnMap: ConnectivityMap
|
|
40
|
+
globalConnMap: ConnectivityMap
|
|
41
|
+
inputProblem: InputProblem
|
|
42
|
+
guidelines: Guideline[]
|
|
43
|
+
}) {
|
|
44
|
+
super()
|
|
45
|
+
this.inputProblem = params.inputProblem
|
|
46
|
+
this.mspConnectionPairs = params.mspConnectionPairs
|
|
47
|
+
this.dcConnMap = params.dcConnMap
|
|
48
|
+
this.globalConnMap = params.globalConnMap
|
|
49
|
+
this.guidelines = params.guidelines
|
|
50
|
+
this.chipMap = params.chipMap
|
|
51
|
+
|
|
52
|
+
this.queuedConnectionPairs = [...this.mspConnectionPairs]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
override getConstructorParams(): ConstructorParameters<
|
|
56
|
+
typeof SchematicTraceLinesSolver
|
|
57
|
+
>[0] {
|
|
58
|
+
return {
|
|
59
|
+
inputProblem: this.inputProblem,
|
|
60
|
+
chipMap: this.chipMap,
|
|
61
|
+
mspConnectionPairs: this.mspConnectionPairs,
|
|
62
|
+
dcConnMap: this.dcConnMap,
|
|
63
|
+
globalConnMap: this.globalConnMap,
|
|
64
|
+
guidelines: this.guidelines,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
override _step() {
|
|
69
|
+
if (this.activeSubSolver?.solved) {
|
|
70
|
+
this.solvedTracePaths.push({
|
|
71
|
+
...this.currentConnectionPair!,
|
|
72
|
+
tracePath: this.activeSubSolver!.solvedTracePath!,
|
|
73
|
+
})
|
|
74
|
+
this.activeSubSolver = null
|
|
75
|
+
this.currentConnectionPair = null
|
|
76
|
+
}
|
|
77
|
+
if (this.activeSubSolver?.failed) {
|
|
78
|
+
this.failed = true
|
|
79
|
+
this.error = this.activeSubSolver.error
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (this.activeSubSolver) {
|
|
84
|
+
this.activeSubSolver.step()
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const connectionPair = this.queuedConnectionPairs.shift()
|
|
89
|
+
this.currentConnectionPair = connectionPair!
|
|
90
|
+
|
|
91
|
+
if (!connectionPair) {
|
|
92
|
+
this.solved = true
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const { pins } = connectionPair
|
|
97
|
+
|
|
98
|
+
this.activeSubSolver = new SchematicTraceSingleLineSolver({
|
|
99
|
+
inputProblem: this.inputProblem,
|
|
100
|
+
pins,
|
|
101
|
+
chipMap: this.chipMap,
|
|
102
|
+
guidelines: this.guidelines,
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
override visualize(): GraphicsObject {
|
|
107
|
+
if (this.activeSubSolver) {
|
|
108
|
+
return this.activeSubSolver.visualize()
|
|
109
|
+
}
|
|
110
|
+
const graphics = visualizeInputProblem(this.inputProblem, {
|
|
111
|
+
chipAlpha: 0.1,
|
|
112
|
+
connectionAlpha: 0.1,
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
visualizeGuidelines({ guidelines: this.guidelines, graphics })
|
|
116
|
+
|
|
117
|
+
for (const { mspPairId, tracePath } of this.solvedTracePaths) {
|
|
118
|
+
graphics.lines!.push({
|
|
119
|
+
points: tracePath,
|
|
120
|
+
strokeWidth: 0.005,
|
|
121
|
+
strokeColor: "green",
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return graphics
|
|
126
|
+
}
|
|
127
|
+
}
|