@tscircuit/schematic-trace-solver 0.0.31 → 0.0.33
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.md +8 -0
- package/dist/index.d.ts +24 -53
- package/dist/index.js +494 -807
- package/lib/index.ts +1 -0
- package/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts +54 -3
- package/lib/solvers/MspConnectionPairSolver/doesPairCrossRestrictedCenterLines.ts +62 -0
- package/lib/solvers/MspConnectionPairSolver/getMspConnectionPairsFromPins.ts +7 -2
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver.ts +5 -11
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver.ts +37 -7
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/generateElbowVariants.ts +7 -0
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +239 -0
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions.ts +57 -0
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/mid.ts +97 -0
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/pathOps.ts +65 -0
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect.ts +19 -0
- package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +14 -14
- package/package.json +1 -1
- package/site/SchematicTracePipelineSolver/SchematicTracePipelineSolver03.page.tsx +486 -0
- package/site/examples/example09.page.tsx +1 -1
- package/tests/examples/__snapshots__/example01.snap.svg +29 -29
- package/tests/examples/__snapshots__/example02.snap.svg +13 -10
- package/tests/examples/__snapshots__/example04.snap.svg +12 -12
- package/tests/examples/__snapshots__/example05.snap.svg +38 -38
- package/tests/examples/__snapshots__/example06.snap.svg +14 -14
- package/tests/examples/__snapshots__/example08.snap.svg +29 -23
- package/tests/examples/__snapshots__/example09.snap.svg +119 -149
- package/tests/examples/__snapshots__/example11.snap.svg +39 -33
- package/tests/examples/__snapshots__/example12.snap.svg +32 -29
- package/tests/examples/__snapshots__/example13.snap.svg +87 -84
- package/tests/examples/__snapshots__/example15.snap.svg +57 -57
- package/tests/examples/__snapshots__/example16.snap.svg +3 -3
- package/tests/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver_repro03.test.ts +491 -0
package/lib/index.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
InputChip,
|
|
4
|
+
InputPin,
|
|
5
|
+
InputProblem,
|
|
6
|
+
PinId,
|
|
7
|
+
} from "lib/types/InputProblem"
|
|
3
8
|
import { ConnectivityMap } from "connectivity-map"
|
|
4
9
|
import { getConnectivityMapsFromInputProblem } from "./getConnectivityMapFromInputProblem"
|
|
5
10
|
import { getOrthogonalMinimumSpanningTree } from "./getMspConnectionPairsFromPins"
|
|
11
|
+
import { doesPairCrossRestrictedCenterLines } from "./doesPairCrossRestrictedCenterLines"
|
|
6
12
|
import type { GraphicsObject } from "graphics-debug"
|
|
7
13
|
import { getColorFromString } from "lib/utils/getColorFromString"
|
|
8
14
|
import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualizeInputProblem"
|
|
@@ -104,6 +110,22 @@ export class MspConnectionPairSolver extends BaseSolver {
|
|
|
104
110
|
// Too far apart; skip creating an MSP pair for this net
|
|
105
111
|
return
|
|
106
112
|
}
|
|
113
|
+
// Avoid pairs that would cross restricted center lines
|
|
114
|
+
const pinIdMap = new Map(Object.entries(this.pinMap)) as Map<
|
|
115
|
+
PinId,
|
|
116
|
+
InputPin & { chipId: string }
|
|
117
|
+
>
|
|
118
|
+
if (
|
|
119
|
+
doesPairCrossRestrictedCenterLines({
|
|
120
|
+
inputProblem: this.inputProblem,
|
|
121
|
+
chipMap: this.chipMap,
|
|
122
|
+
pinIdMap,
|
|
123
|
+
p1,
|
|
124
|
+
p2,
|
|
125
|
+
})
|
|
126
|
+
) {
|
|
127
|
+
return
|
|
128
|
+
}
|
|
107
129
|
|
|
108
130
|
const globalConnNetId = this.globalConnMap.getNetConnectedToId(pin1!)!
|
|
109
131
|
const userNetId =
|
|
@@ -122,12 +144,41 @@ export class MspConnectionPairSolver extends BaseSolver {
|
|
|
122
144
|
|
|
123
145
|
// There are more than 3 pins, so we need to run MSP to find the best pairs
|
|
124
146
|
|
|
147
|
+
const pinIdMap = new Map(Object.entries(this.pinMap)) as Map<
|
|
148
|
+
PinId,
|
|
149
|
+
InputPin & { chipId: string }
|
|
150
|
+
>
|
|
125
151
|
const msp = getOrthogonalMinimumSpanningTree(
|
|
126
152
|
directlyConnectedPins.map((p) => this.pinMap[p]!).filter(Boolean),
|
|
127
|
-
{
|
|
153
|
+
{
|
|
154
|
+
maxDistance: this.maxMspPairDistance,
|
|
155
|
+
forbidEdge: (a, b) =>
|
|
156
|
+
doesPairCrossRestrictedCenterLines({
|
|
157
|
+
inputProblem: this.inputProblem,
|
|
158
|
+
chipMap: this.chipMap,
|
|
159
|
+
pinIdMap,
|
|
160
|
+
p1: a as InputPin & { chipId: string },
|
|
161
|
+
p2: b as InputPin & { chipId: string },
|
|
162
|
+
}),
|
|
163
|
+
},
|
|
128
164
|
)
|
|
129
165
|
|
|
130
166
|
for (const [pin1, pin2] of msp) {
|
|
167
|
+
const p1Obj = this.pinMap[pin1!]!
|
|
168
|
+
const p2Obj = this.pinMap[pin2!]!
|
|
169
|
+
// Skip any edge that would cross a restricted center line (safety filter)
|
|
170
|
+
if (
|
|
171
|
+
doesPairCrossRestrictedCenterLines({
|
|
172
|
+
inputProblem: this.inputProblem,
|
|
173
|
+
chipMap: this.chipMap,
|
|
174
|
+
pinIdMap,
|
|
175
|
+
p1: p1Obj,
|
|
176
|
+
p2: p2Obj,
|
|
177
|
+
})
|
|
178
|
+
) {
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
|
|
131
182
|
const globalConnNetId = this.globalConnMap.getNetConnectedToId(pin1!)!
|
|
132
183
|
const userNetId =
|
|
133
184
|
this.userNetIdByPinId[pin1!] ?? this.userNetIdByPinId[pin2!]
|
|
@@ -136,7 +187,7 @@ export class MspConnectionPairSolver extends BaseSolver {
|
|
|
136
187
|
dcConnNetId: dcNetId,
|
|
137
188
|
globalConnNetId,
|
|
138
189
|
userNetId,
|
|
139
|
-
pins: [
|
|
190
|
+
pins: [p1Obj, p2Obj],
|
|
140
191
|
})
|
|
141
192
|
}
|
|
142
193
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
InputChip,
|
|
3
|
+
InputPin,
|
|
4
|
+
InputProblem,
|
|
5
|
+
PinId,
|
|
6
|
+
} from "lib/types/InputProblem"
|
|
7
|
+
import { getRestrictedCenterLines } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/getRestrictedCenterLines"
|
|
8
|
+
|
|
9
|
+
export const doesPairCrossRestrictedCenterLines = (params: {
|
|
10
|
+
inputProblem: InputProblem
|
|
11
|
+
chipMap: Record<string, InputChip>
|
|
12
|
+
pinIdMap: Map<PinId, InputPin & { chipId: string }>
|
|
13
|
+
p1: InputPin & { chipId: string }
|
|
14
|
+
p2: InputPin & { chipId: string }
|
|
15
|
+
}): boolean => {
|
|
16
|
+
const { inputProblem, chipMap, pinIdMap, p1, p2 } = params
|
|
17
|
+
|
|
18
|
+
const restrictedCenterLines = getRestrictedCenterLines({
|
|
19
|
+
pins: [p1, p2],
|
|
20
|
+
inputProblem,
|
|
21
|
+
pinIdMap,
|
|
22
|
+
chipMap,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
if (restrictedCenterLines.size === 0) return false
|
|
26
|
+
|
|
27
|
+
const EPS = 1e-9
|
|
28
|
+
|
|
29
|
+
const crossesSegment = (
|
|
30
|
+
a: { x: number; y: number },
|
|
31
|
+
b: { x: number; y: number },
|
|
32
|
+
): boolean => {
|
|
33
|
+
for (const [, rcl] of restrictedCenterLines) {
|
|
34
|
+
if (rcl.axes.has("x") && typeof rcl.x === "number") {
|
|
35
|
+
if ((a.x - rcl.x) * (b.x - rcl.x) < -EPS) return true
|
|
36
|
+
}
|
|
37
|
+
if (rcl.axes.has("y") && typeof rcl.y === "number") {
|
|
38
|
+
if ((a.y - rcl.y) * (b.y - rcl.y) < -EPS) return true
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// If already aligned on one axis, just check that single segment
|
|
45
|
+
if (Math.abs(p1.x - p2.x) < EPS || Math.abs(p1.y - p2.y) < EPS) {
|
|
46
|
+
return crossesSegment({ x: p1.x, y: p1.y }, { x: p2.x, y: p2.y })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Two L-shape possibilities: horizontal-then-vertical or vertical-then-horizontal
|
|
50
|
+
const elbowHV = { x: p2.x, y: p1.y }
|
|
51
|
+
const elbowVH = { x: p1.x, y: p2.y }
|
|
52
|
+
|
|
53
|
+
const hvCrosses =
|
|
54
|
+
crossesSegment({ x: p1.x, y: p1.y }, elbowHV) ||
|
|
55
|
+
crossesSegment(elbowHV, { x: p2.x, y: p2.y })
|
|
56
|
+
const vhCrosses =
|
|
57
|
+
crossesSegment({ x: p1.x, y: p1.y }, elbowVH) ||
|
|
58
|
+
crossesSegment(elbowVH, { x: p2.x, y: p2.y })
|
|
59
|
+
|
|
60
|
+
// Forbid the pair only if both L-shape routes would cross a restricted center line
|
|
61
|
+
return hvCrosses && vhCrosses
|
|
62
|
+
}
|
|
@@ -18,7 +18,10 @@ import type { InputPin, PinId } from "lib/types/InputProblem"
|
|
|
18
18
|
*/
|
|
19
19
|
export function getOrthogonalMinimumSpanningTree(
|
|
20
20
|
pins: InputPin[],
|
|
21
|
-
opts: {
|
|
21
|
+
opts: {
|
|
22
|
+
maxDistance?: number
|
|
23
|
+
forbidEdge?: (a: InputPin, b: InputPin) => boolean
|
|
24
|
+
} = {},
|
|
22
25
|
): Array<[PinId, PinId]> {
|
|
23
26
|
const n = pins.length
|
|
24
27
|
const maxDistance = opts?.maxDistance ?? Number.POSITIVE_INFINITY
|
|
@@ -82,7 +85,9 @@ export function getOrthogonalMinimumSpanningTree(
|
|
|
82
85
|
for (let v = 0; v < n; v++) {
|
|
83
86
|
if (!inTree[v]) {
|
|
84
87
|
const d0 = manhattan(pins[u], pins[v])
|
|
85
|
-
const
|
|
88
|
+
const isForbidden = opts?.forbidEdge?.(pins[u], pins[v]) ?? false
|
|
89
|
+
const d =
|
|
90
|
+
d0 > maxDistance || isForbidden ? Number.POSITIVE_INFINITY : d0
|
|
86
91
|
if (
|
|
87
92
|
d < bestDist[v] ||
|
|
88
93
|
(d === bestDist[v] && pins[u].pinId < pins[parent[v]]?.pinId)
|
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
MspConnectionPairId,
|
|
8
8
|
} from "../MspConnectionPairSolver/MspConnectionPairSolver"
|
|
9
9
|
import type { ConnectivityMap } from "connectivity-map"
|
|
10
|
-
import {
|
|
10
|
+
import { SchematicTraceSingleLineSolver2 } from "./SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2"
|
|
11
11
|
import type { Guideline } from "../GuidelinesSolver/GuidelinesSolver"
|
|
12
12
|
import { visualizeGuidelines } from "../GuidelinesSolver/visualizeGuidelines"
|
|
13
13
|
import type { Point } from "@tscircuit/math-utils"
|
|
@@ -20,7 +20,6 @@ export interface SolvedTracePath extends MspConnectionPair {
|
|
|
20
20
|
|
|
21
21
|
export class SchematicTraceLinesSolver extends BaseSolver {
|
|
22
22
|
inputProblem: InputProblem
|
|
23
|
-
guidelines: Guideline[]
|
|
24
23
|
mspConnectionPairs: MspConnectionPair[]
|
|
25
24
|
|
|
26
25
|
dcConnMap: ConnectivityMap
|
|
@@ -34,7 +33,7 @@ export class SchematicTraceLinesSolver extends BaseSolver {
|
|
|
34
33
|
solvedTracePaths: Array<SolvedTracePath> = []
|
|
35
34
|
failedConnectionPairs: Array<MspConnectionPair & { error?: string }> = []
|
|
36
35
|
|
|
37
|
-
declare activeSubSolver:
|
|
36
|
+
declare activeSubSolver: SchematicTraceSingleLineSolver2 | null
|
|
38
37
|
|
|
39
38
|
constructor(params: {
|
|
40
39
|
mspConnectionPairs: MspConnectionPair[]
|
|
@@ -42,14 +41,12 @@ export class SchematicTraceLinesSolver extends BaseSolver {
|
|
|
42
41
|
dcConnMap: ConnectivityMap
|
|
43
42
|
globalConnMap: ConnectivityMap
|
|
44
43
|
inputProblem: InputProblem
|
|
45
|
-
guidelines: Guideline[]
|
|
46
44
|
}) {
|
|
47
45
|
super()
|
|
48
46
|
this.inputProblem = params.inputProblem
|
|
49
47
|
this.mspConnectionPairs = params.mspConnectionPairs
|
|
50
48
|
this.dcConnMap = params.dcConnMap
|
|
51
49
|
this.globalConnMap = params.globalConnMap
|
|
52
|
-
this.guidelines = params.guidelines
|
|
53
50
|
this.chipMap = params.chipMap
|
|
54
51
|
|
|
55
52
|
this.queuedConnectionPairs = [...this.mspConnectionPairs]
|
|
@@ -64,7 +61,6 @@ export class SchematicTraceLinesSolver extends BaseSolver {
|
|
|
64
61
|
mspConnectionPairs: this.mspConnectionPairs,
|
|
65
62
|
dcConnMap: this.dcConnMap,
|
|
66
63
|
globalConnMap: this.globalConnMap,
|
|
67
|
-
guidelines: this.guidelines,
|
|
68
64
|
}
|
|
69
65
|
}
|
|
70
66
|
|
|
@@ -101,20 +97,20 @@ export class SchematicTraceLinesSolver extends BaseSolver {
|
|
|
101
97
|
}
|
|
102
98
|
|
|
103
99
|
const connectionPair = this.queuedConnectionPairs.shift()
|
|
104
|
-
this.currentConnectionPair = connectionPair!
|
|
105
100
|
|
|
106
101
|
if (!connectionPair) {
|
|
107
102
|
this.solved = true
|
|
108
103
|
return
|
|
109
104
|
}
|
|
110
105
|
|
|
106
|
+
this.currentConnectionPair = connectionPair
|
|
107
|
+
|
|
111
108
|
const { pins } = connectionPair
|
|
112
109
|
|
|
113
|
-
this.activeSubSolver = new
|
|
110
|
+
this.activeSubSolver = new SchematicTraceSingleLineSolver2({
|
|
114
111
|
inputProblem: this.inputProblem,
|
|
115
112
|
pins,
|
|
116
113
|
chipMap: this.chipMap,
|
|
117
|
-
guidelines: this.guidelines,
|
|
118
114
|
})
|
|
119
115
|
}
|
|
120
116
|
|
|
@@ -127,8 +123,6 @@ export class SchematicTraceLinesSolver extends BaseSolver {
|
|
|
127
123
|
connectionAlpha: 0.1,
|
|
128
124
|
})
|
|
129
125
|
|
|
130
|
-
visualizeGuidelines({ guidelines: this.guidelines, graphics })
|
|
131
|
-
|
|
132
126
|
for (const { mspPairId, tracePath } of this.solvedTracePaths) {
|
|
133
127
|
graphics.lines!.push({
|
|
134
128
|
points: tracePath,
|
|
@@ -97,6 +97,7 @@ export class SchematicTraceSingleLineSolver extends BaseSolver {
|
|
|
97
97
|
const { elbowVariants, movableSegments } = generateElbowVariants({
|
|
98
98
|
baseElbow: this.baseElbow,
|
|
99
99
|
guidelines: this.guidelines,
|
|
100
|
+
maxVariants: 1000,
|
|
100
101
|
})
|
|
101
102
|
|
|
102
103
|
this.movableSegments = movableSegments
|
|
@@ -144,6 +145,9 @@ export class SchematicTraceSingleLineSolver extends BaseSolver {
|
|
|
144
145
|
chipMap: this.chipMap,
|
|
145
146
|
})
|
|
146
147
|
|
|
148
|
+
// Check if this candidate path is valid
|
|
149
|
+
let pathIsValid = true
|
|
150
|
+
|
|
147
151
|
for (let i = 0; i < nextCandidatePath.length - 1; i++) {
|
|
148
152
|
const start = nextCandidatePath[i]
|
|
149
153
|
const end = nextCandidatePath[i + 1]
|
|
@@ -156,14 +160,22 @@ export class SchematicTraceSingleLineSolver extends BaseSolver {
|
|
|
156
160
|
for (const [, rcl] of restrictedCenterLines) {
|
|
157
161
|
if (rcl.axes.has("x") && typeof rcl.x === "number") {
|
|
158
162
|
// segment strictly crosses vertical center line
|
|
159
|
-
if ((start.x - rcl.x) * (end.x - rcl.x) < -EPS)
|
|
163
|
+
if ((start.x - rcl.x) * (end.x - rcl.x) < -EPS) {
|
|
164
|
+
pathIsValid = false
|
|
165
|
+
break
|
|
166
|
+
}
|
|
160
167
|
}
|
|
161
168
|
if (rcl.axes.has("y") && typeof rcl.y === "number") {
|
|
162
169
|
// segment strictly crosses horizontal center line
|
|
163
|
-
if ((start.y - rcl.y) * (end.y - rcl.y) < -EPS)
|
|
170
|
+
if ((start.y - rcl.y) * (end.y - rcl.y) < -EPS) {
|
|
171
|
+
pathIsValid = false
|
|
172
|
+
break
|
|
173
|
+
}
|
|
164
174
|
}
|
|
165
175
|
}
|
|
166
176
|
|
|
177
|
+
if (!pathIsValid) break
|
|
178
|
+
|
|
167
179
|
// Always exclude chips that contain the start or end points of this segment
|
|
168
180
|
// if those points are actually pin locations
|
|
169
181
|
const isStartPin = this.pins.some(
|
|
@@ -197,13 +209,18 @@ export class SchematicTraceSingleLineSolver extends BaseSolver {
|
|
|
197
209
|
(onRight && dx < -EPS) ||
|
|
198
210
|
(onBottom && dy > EPS) ||
|
|
199
211
|
(onTop && dy < -EPS)
|
|
200
|
-
if (entersInterior)
|
|
212
|
+
if (entersInterior) {
|
|
213
|
+
pathIsValid = false
|
|
214
|
+
break
|
|
215
|
+
}
|
|
201
216
|
if (!excludeChipIds.includes(startPin.chipId)) {
|
|
202
217
|
excludeChipIds.push(startPin.chipId)
|
|
203
218
|
}
|
|
204
219
|
}
|
|
205
220
|
}
|
|
206
221
|
|
|
222
|
+
if (!pathIsValid) break
|
|
223
|
+
|
|
207
224
|
if (isEndPin) {
|
|
208
225
|
const endPin = this.pins.find(
|
|
209
226
|
(pin) =>
|
|
@@ -224,24 +241,37 @@ export class SchematicTraceSingleLineSolver extends BaseSolver {
|
|
|
224
241
|
(onRight && dx < -EPS) ||
|
|
225
242
|
(onBottom && dy > EPS) ||
|
|
226
243
|
(onTop && dy < -EPS)
|
|
227
|
-
if (entersInterior)
|
|
244
|
+
if (entersInterior) {
|
|
245
|
+
pathIsValid = false
|
|
246
|
+
break
|
|
247
|
+
}
|
|
228
248
|
if (!excludeChipIds.includes(endPin.chipId)) {
|
|
229
249
|
excludeChipIds.push(endPin.chipId)
|
|
230
250
|
}
|
|
231
251
|
}
|
|
232
252
|
}
|
|
233
253
|
|
|
254
|
+
if (!pathIsValid) break
|
|
255
|
+
|
|
234
256
|
const obstacleOps = { excludeChipIds }
|
|
235
257
|
const intersects =
|
|
236
258
|
this.chipObstacleSpatialIndex.doesOrthogonalLineIntersectChip(
|
|
237
259
|
[start, end],
|
|
238
260
|
obstacleOps,
|
|
239
261
|
)
|
|
240
|
-
if (intersects)
|
|
262
|
+
if (intersects) {
|
|
263
|
+
pathIsValid = false
|
|
264
|
+
break
|
|
265
|
+
}
|
|
241
266
|
}
|
|
242
267
|
|
|
243
|
-
this
|
|
244
|
-
|
|
268
|
+
// If this path is valid, use it as the solution
|
|
269
|
+
if (pathIsValid) {
|
|
270
|
+
this.solvedTracePath = nextCandidatePath
|
|
271
|
+
this.solved = true
|
|
272
|
+
}
|
|
273
|
+
// If this path is invalid, continue to next step to try the next candidate
|
|
274
|
+
// The next _step() call will try the next candidate path
|
|
245
275
|
}
|
|
246
276
|
|
|
247
277
|
override visualize(): GraphicsObject {
|
|
@@ -260,9 +260,11 @@ const cartesian = <T>(arrays: T[][]): T[][] =>
|
|
|
260
260
|
export const generateElbowVariants = ({
|
|
261
261
|
baseElbow,
|
|
262
262
|
guidelines,
|
|
263
|
+
maxVariants = 10000,
|
|
263
264
|
}: {
|
|
264
265
|
baseElbow: Point[]
|
|
265
266
|
guidelines: Guideline[]
|
|
267
|
+
maxVariants?: number
|
|
266
268
|
}): {
|
|
267
269
|
elbowVariants: Array<Point[]>
|
|
268
270
|
movableSegments: Array<MovableSegment>
|
|
@@ -395,6 +397,11 @@ export const generateElbowVariants = ({
|
|
|
395
397
|
if (!seen.has(key)) {
|
|
396
398
|
seen.add(key)
|
|
397
399
|
elbowVariants.push(variant)
|
|
400
|
+
|
|
401
|
+
// Stop if we've hit the maximum number of variants
|
|
402
|
+
if (elbowVariants.length >= maxVariants) {
|
|
403
|
+
break
|
|
404
|
+
}
|
|
398
405
|
}
|
|
399
406
|
}
|
|
400
407
|
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import type { GraphicsObject } from "graphics-debug"
|
|
2
|
+
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
|
|
3
|
+
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
4
|
+
import type { MspConnectionPair } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver"
|
|
5
|
+
import type {
|
|
6
|
+
ChipId,
|
|
7
|
+
InputChip,
|
|
8
|
+
InputProblem,
|
|
9
|
+
PinId,
|
|
10
|
+
} from "lib/types/InputProblem"
|
|
11
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
12
|
+
import { calculateElbow } from "calculate-elbow"
|
|
13
|
+
import { getPinDirection } from "../SchematicTraceSingleLineSolver/getPinDirection"
|
|
14
|
+
import { getObstacleRects, type ChipWithBounds } from "./rect"
|
|
15
|
+
import { findFirstCollision, isHorizontal, isVertical } from "./collisions"
|
|
16
|
+
import {
|
|
17
|
+
aabbFromPoints,
|
|
18
|
+
candidateMidsFromSet,
|
|
19
|
+
midBetweenPointAndRect,
|
|
20
|
+
type Axis,
|
|
21
|
+
} from "./mid"
|
|
22
|
+
import { pathKey, shiftSegmentOrth } from "./pathOps"
|
|
23
|
+
|
|
24
|
+
type PathKey = string
|
|
25
|
+
|
|
26
|
+
export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
27
|
+
pins: MspConnectionPair["pins"]
|
|
28
|
+
inputProblem: InputProblem
|
|
29
|
+
chipMap: Record<string, InputChip>
|
|
30
|
+
|
|
31
|
+
obstacles: ChipWithBounds[]
|
|
32
|
+
rectById: Map<string, ChipWithBounds>
|
|
33
|
+
aabb: { minX: number; maxX: number; minY: number; maxY: number }
|
|
34
|
+
|
|
35
|
+
baseElbow: Point[]
|
|
36
|
+
|
|
37
|
+
solvedTracePath: Point[] | null = null
|
|
38
|
+
|
|
39
|
+
private queue: Array<{ path: Point[]; collisionChipIds: Set<ChipId> }> = []
|
|
40
|
+
private visited: Set<PathKey> = new Set()
|
|
41
|
+
|
|
42
|
+
constructor(params: {
|
|
43
|
+
pins: MspConnectionPair["pins"]
|
|
44
|
+
inputProblem: InputProblem
|
|
45
|
+
chipMap: Record<string, InputChip>
|
|
46
|
+
}) {
|
|
47
|
+
super()
|
|
48
|
+
this.pins = params.pins
|
|
49
|
+
this.inputProblem = params.inputProblem
|
|
50
|
+
this.chipMap = params.chipMap
|
|
51
|
+
|
|
52
|
+
// Ensure facing directions are present
|
|
53
|
+
for (const pin of this.pins) {
|
|
54
|
+
if (!pin._facingDirection) {
|
|
55
|
+
const chip = this.chipMap[pin.chipId]
|
|
56
|
+
pin._facingDirection = getPinDirection(pin, chip)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Build obstacle rects from chips
|
|
61
|
+
this.obstacles = getObstacleRects(this.inputProblem)
|
|
62
|
+
this.rectById = new Map(this.obstacles.map((r) => [r.chipId, r]))
|
|
63
|
+
|
|
64
|
+
// Build initial elbow path
|
|
65
|
+
const [pin1, pin2] = this.pins
|
|
66
|
+
this.baseElbow = calculateElbow(
|
|
67
|
+
{
|
|
68
|
+
x: pin1.x,
|
|
69
|
+
y: pin1.y,
|
|
70
|
+
facingDirection: pin1._facingDirection!,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
x: pin2.x,
|
|
74
|
+
y: pin2.y,
|
|
75
|
+
facingDirection: pin2._facingDirection!,
|
|
76
|
+
},
|
|
77
|
+
{ overshoot: 0.2 },
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
// Bounds defined by PA and PB
|
|
81
|
+
this.aabb = aabbFromPoints(
|
|
82
|
+
{ x: pin1.x, y: pin1.y },
|
|
83
|
+
{ x: pin2.x, y: pin2.y },
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
// Seed search
|
|
87
|
+
this.queue.push({ path: this.baseElbow, collisionChipIds: new Set() })
|
|
88
|
+
this.visited.add(pathKey(this.baseElbow))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
override getConstructorParams(): ConstructorParameters<
|
|
92
|
+
typeof SchematicTraceSingleLineSolver2
|
|
93
|
+
>[0] {
|
|
94
|
+
return {
|
|
95
|
+
chipMap: this.chipMap,
|
|
96
|
+
pins: this.pins,
|
|
97
|
+
inputProblem: this.inputProblem,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private axisOfSegment(a: Point, b: Point): Axis | null {
|
|
102
|
+
if (isVertical(a, b)) return "x"
|
|
103
|
+
if (isHorizontal(a, b)) return "y"
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private pathLength(pts: Point[]): number {
|
|
108
|
+
let sum = 0
|
|
109
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
110
|
+
sum +=
|
|
111
|
+
Math.abs(pts[i + 1]!.x - pts[i]!.x) +
|
|
112
|
+
Math.abs(pts[i + 1]!.y - pts[i]!.y)
|
|
113
|
+
}
|
|
114
|
+
return sum
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
override _step() {
|
|
118
|
+
if (this.solvedTracePath) {
|
|
119
|
+
this.solved = true
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const state = this.queue.shift()
|
|
124
|
+
if (!state) {
|
|
125
|
+
this.failed = true
|
|
126
|
+
this.error = "No collision-free path found"
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const { path, collisionChipIds } = state
|
|
131
|
+
|
|
132
|
+
const collision = findFirstCollision(path, this.obstacles)
|
|
133
|
+
|
|
134
|
+
if (!collision) {
|
|
135
|
+
this.solvedTracePath = path
|
|
136
|
+
this.solved = true
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let { segIndex, rect } = collision
|
|
141
|
+
|
|
142
|
+
// Never move the first or last segments - move adjacent segment instead
|
|
143
|
+
const isFirstSegment = segIndex === 0
|
|
144
|
+
const isLastSegment = segIndex === path.length - 2
|
|
145
|
+
|
|
146
|
+
if (isFirstSegment) {
|
|
147
|
+
// If first segment collides, move the second segment instead
|
|
148
|
+
if (path.length < 3) {
|
|
149
|
+
// Path too short to have an adjacent segment to move
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
segIndex = 1
|
|
153
|
+
} else if (isLastSegment) {
|
|
154
|
+
// If last segment collides, move the second-to-last segment instead
|
|
155
|
+
if (path.length < 3) {
|
|
156
|
+
// Path too short to have an adjacent segment to move
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
segIndex = path.length - 3
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const a = path[segIndex]!
|
|
163
|
+
const b = path[segIndex + 1]!
|
|
164
|
+
const axis = this.axisOfSegment(a, b)
|
|
165
|
+
if (!axis) {
|
|
166
|
+
// Should not happen for elbow paths
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const [PA, PB] = this.pins
|
|
171
|
+
const candidates: number[] = []
|
|
172
|
+
|
|
173
|
+
if (collisionChipIds.size === 0) {
|
|
174
|
+
// First collision on this search branch: use mid(PA, C) and mid(PB, C)
|
|
175
|
+
const m1 = midBetweenPointAndRect(axis, { x: PA.x, y: PA.y }, rect)
|
|
176
|
+
const m2 = midBetweenPointAndRect(axis, { x: PB.x, y: PB.y }, rect)
|
|
177
|
+
|
|
178
|
+
// Combine and deduplicate candidates
|
|
179
|
+
const allCandidates = [...m1, ...m2]
|
|
180
|
+
const uniqueCandidates = [...new Set(allCandidates)]
|
|
181
|
+
candidates.push(...uniqueCandidates)
|
|
182
|
+
} else {
|
|
183
|
+
// Subsequent collisions: mid between C and nearest rect/bounds from the set
|
|
184
|
+
const mids = candidateMidsFromSet(
|
|
185
|
+
axis,
|
|
186
|
+
rect,
|
|
187
|
+
this.rectById,
|
|
188
|
+
collisionChipIds,
|
|
189
|
+
this.aabb,
|
|
190
|
+
)
|
|
191
|
+
candidates.push(...mids)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Generate new shifted paths, order by total path length (shorter first)
|
|
195
|
+
const newStates: Array<{
|
|
196
|
+
path: Point[]
|
|
197
|
+
collisionRectIds: Set<string>
|
|
198
|
+
len: number
|
|
199
|
+
}> = []
|
|
200
|
+
|
|
201
|
+
for (const coord of candidates) {
|
|
202
|
+
const newPath = shiftSegmentOrth(path, segIndex, axis, coord)
|
|
203
|
+
if (!newPath) continue
|
|
204
|
+
const key = pathKey(newPath)
|
|
205
|
+
if (this.visited.has(key)) continue
|
|
206
|
+
this.visited.add(key)
|
|
207
|
+
const nextSet = new Set(collisionChipIds)
|
|
208
|
+
nextSet.add(rect.chipId)
|
|
209
|
+
const len = this.pathLength(newPath)
|
|
210
|
+
newStates.push({ path: newPath, collisionRectIds: nextSet, len })
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
newStates.sort((a, b) => a.len - b.len)
|
|
214
|
+
for (const st of newStates) {
|
|
215
|
+
this.queue.push({ path: st.path, collisionChipIds: st.collisionRectIds })
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
override visualize(): GraphicsObject {
|
|
220
|
+
const g = visualizeInputProblem(this.inputProblem, {
|
|
221
|
+
chipAlpha: 0.1,
|
|
222
|
+
connectionAlpha: 0.1,
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
// Draw all the new candidates
|
|
226
|
+
for (const { path, collisionChipIds: collisionRectIds } of this.queue) {
|
|
227
|
+
g.lines!.push({ points: path, strokeColor: "teal", strokeDash: "2 2" })
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (this.solvedTracePath) {
|
|
231
|
+
g.lines!.push({ points: this.solvedTracePath, strokeColor: "green" })
|
|
232
|
+
} else if (this.queue.length > 0) {
|
|
233
|
+
// Show next candidate to be explored
|
|
234
|
+
g.lines!.push({ points: this.queue[0]!.path, strokeColor: "orange" })
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return g
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
import type { ChipWithBounds } from "./rect"
|
|
3
|
+
|
|
4
|
+
const EPS = 1e-9
|
|
5
|
+
|
|
6
|
+
export const isVertical = (a: Point, b: Point, eps = EPS) =>
|
|
7
|
+
Math.abs(a.x - b.x) < eps
|
|
8
|
+
export const isHorizontal = (a: Point, b: Point, eps = EPS) =>
|
|
9
|
+
Math.abs(a.y - b.y) < eps
|
|
10
|
+
|
|
11
|
+
export const segmentIntersectsRect = (
|
|
12
|
+
a: Point,
|
|
13
|
+
b: Point,
|
|
14
|
+
r: ChipWithBounds,
|
|
15
|
+
eps = EPS,
|
|
16
|
+
): boolean => {
|
|
17
|
+
const vert = isVertical(a, b, eps)
|
|
18
|
+
const horz = isHorizontal(a, b, eps)
|
|
19
|
+
if (!vert && !horz) return false
|
|
20
|
+
|
|
21
|
+
if (vert) {
|
|
22
|
+
const x = a.x
|
|
23
|
+
if (x < r.minX - eps || x > r.maxX + eps) return false
|
|
24
|
+
const segMinY = Math.min(a.y, b.y)
|
|
25
|
+
const segMaxY = Math.max(a.y, b.y)
|
|
26
|
+
const overlap = Math.min(segMaxY, r.maxY) - Math.max(segMinY, r.minY)
|
|
27
|
+
return overlap > eps
|
|
28
|
+
} else {
|
|
29
|
+
const y = a.y
|
|
30
|
+
if (y < r.minY - eps || y > r.maxY + eps) return false
|
|
31
|
+
const segMinX = Math.min(a.x, b.x)
|
|
32
|
+
const segMaxX = Math.max(a.x, b.x)
|
|
33
|
+
const overlap = Math.min(segMaxX, r.maxX) - Math.max(segMinX, r.minX)
|
|
34
|
+
return overlap > eps
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const findFirstCollision = (
|
|
39
|
+
pts: Point[],
|
|
40
|
+
rects: ChipWithBounds[],
|
|
41
|
+
opts: {
|
|
42
|
+
excludeRectIdsForSegment?: (segIndex: number) => Set<string>
|
|
43
|
+
} = {},
|
|
44
|
+
): { segIndex: number; rect: ChipWithBounds } | null => {
|
|
45
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
46
|
+
const a = pts[i]!
|
|
47
|
+
const b = pts[i + 1]!
|
|
48
|
+
const excluded = opts.excludeRectIdsForSegment?.(i) ?? new Set<string>()
|
|
49
|
+
for (const r of rects) {
|
|
50
|
+
if (excluded.has(r.chipId)) continue
|
|
51
|
+
if (segmentIntersectsRect(a, b, r)) {
|
|
52
|
+
return { segIndex: i, rect: r }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null
|
|
57
|
+
}
|