@tscircuit/schematic-trace-solver 0.0.182 → 0.0.183
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/dist/index.d.ts +5 -0
- package/dist/index.js +311 -112
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver.ts +45 -2
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/getPinDirection.ts +58 -10
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +166 -57
- package/lib/solvers/TraceCleanupSolver/hasCollisionsWithLabels.ts +108 -3
- package/lib/solvers/TraceCleanupSolver/minimizeTurnsWithFilteredLabels.ts +62 -15
- package/lib/solvers/TraceCleanupSolver/turnMinimization.ts +5 -0
- package/package.json +1 -1
- package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +13 -20
- package/tests/repros/__snapshots__/board1096-usb-label-overlap-iteration.snap.svg +13 -13
- package/tests/repros/__snapshots__/repro-mouse-switch-ground-unnecessary-jog.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-pmp11282-isolated-dcdc.snap.svg +8 -8
- package/tests/repros/__snapshots__/repro-rf1-unnecessary-dogleg.snap.svg +5 -5
|
@@ -11,6 +11,7 @@ import { SchematicTraceSingleLineSolver2 } from "./SchematicTraceSingleLineSolve
|
|
|
11
11
|
import type { Guideline } from "../GuidelinesSolver/GuidelinesSolver"
|
|
12
12
|
import { visualizeGuidelines } from "../GuidelinesSolver/visualizeGuidelines"
|
|
13
13
|
import type { Point } from "@tscircuit/math-utils"
|
|
14
|
+
import { getPinDirectionCandidates } from "./SchematicTraceSingleLineSolver/getPinDirection"
|
|
14
15
|
|
|
15
16
|
const shouldPreferExteriorDetours = ({
|
|
16
17
|
connectionPair,
|
|
@@ -62,6 +63,7 @@ export class SchematicTraceLinesSolver extends BaseSolver {
|
|
|
62
63
|
chipMap: Record<string, InputChip>
|
|
63
64
|
|
|
64
65
|
currentConnectionPair: MspConnectionPair | null = null
|
|
66
|
+
retryingWithoutNetLabelClearance = false
|
|
65
67
|
|
|
66
68
|
solvedTracePaths: Array<SolvedTracePath> = []
|
|
67
69
|
failedConnectionPairs: Array<MspConnectionPair & { error?: string }> = []
|
|
@@ -101,6 +103,7 @@ export class SchematicTraceLinesSolver extends BaseSolver {
|
|
|
101
103
|
if (this.activeSubSolver?.solved) {
|
|
102
104
|
this.solvedTracePaths.push({
|
|
103
105
|
...this.currentConnectionPair!,
|
|
106
|
+
pins: this.activeSubSolver.pins,
|
|
104
107
|
tracePath: this.activeSubSolver!.solvedTracePath!,
|
|
105
108
|
mspConnectionPairIds: [this.currentConnectionPair!.mspPairId],
|
|
106
109
|
pinIds: [
|
|
@@ -110,8 +113,33 @@ export class SchematicTraceLinesSolver extends BaseSolver {
|
|
|
110
113
|
})
|
|
111
114
|
this.activeSubSolver = null
|
|
112
115
|
this.currentConnectionPair = null
|
|
116
|
+
this.retryingWithoutNetLabelClearance = false
|
|
113
117
|
}
|
|
114
118
|
if (this.activeSubSolver?.failed) {
|
|
119
|
+
if (
|
|
120
|
+
this.currentConnectionPair &&
|
|
121
|
+
!this.retryingWithoutNetLabelClearance &&
|
|
122
|
+
this.activeSubSolver.hasAmbiguousPinDirections
|
|
123
|
+
) {
|
|
124
|
+
const connectionPair = this.currentConnectionPair
|
|
125
|
+
this.retryingWithoutNetLabelClearance = true
|
|
126
|
+
this.activeSubSolver = new SchematicTraceSingleLineSolver2({
|
|
127
|
+
inputProblem: this.inputProblem,
|
|
128
|
+
pins: connectionPair.pins.map((pin) => ({
|
|
129
|
+
...pin,
|
|
130
|
+
})) as MspConnectionPair["pins"],
|
|
131
|
+
connectionPair,
|
|
132
|
+
chipMap: this.chipMap,
|
|
133
|
+
preferExteriorDetours: shouldPreferExteriorDetours({
|
|
134
|
+
connectionPair,
|
|
135
|
+
allConnectionPairs: this.mspConnectionPairs,
|
|
136
|
+
inputProblem: this.inputProblem,
|
|
137
|
+
}),
|
|
138
|
+
reserveNetLabelClearance: false,
|
|
139
|
+
})
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
115
143
|
// Record the failure for this connection and continue to the next pair
|
|
116
144
|
if (this.currentConnectionPair) {
|
|
117
145
|
this.failedConnectionPairs.push({
|
|
@@ -121,6 +149,7 @@ export class SchematicTraceLinesSolver extends BaseSolver {
|
|
|
121
149
|
}
|
|
122
150
|
this.activeSubSolver = null
|
|
123
151
|
this.currentConnectionPair = null
|
|
152
|
+
this.retryingWithoutNetLabelClearance = false
|
|
124
153
|
// Do not fail the whole solver; proceed to schedule the next pair
|
|
125
154
|
}
|
|
126
155
|
|
|
@@ -137,8 +166,22 @@ export class SchematicTraceLinesSolver extends BaseSolver {
|
|
|
137
166
|
}
|
|
138
167
|
|
|
139
168
|
this.currentConnectionPair = connectionPair
|
|
140
|
-
|
|
141
|
-
|
|
169
|
+
this.retryingWithoutNetLabelClearance = false
|
|
170
|
+
|
|
171
|
+
// Corner pins can legitimately face either adjacent edge. Keep their
|
|
172
|
+
// inferred direction local to this route so solving one branch cannot
|
|
173
|
+
// constrain a later branch that shares the same corner pin. Pins with one
|
|
174
|
+
// geometric direction retain the established shared representation.
|
|
175
|
+
const hasAmbiguousCornerPin = connectionPair.pins.some((pin) => {
|
|
176
|
+
if (pin._facingDirection) return false
|
|
177
|
+
const chip = this.chipMap[pin.chipId]
|
|
178
|
+
return chip && getPinDirectionCandidates(pin, chip).length > 1
|
|
179
|
+
})
|
|
180
|
+
const pins = hasAmbiguousCornerPin
|
|
181
|
+
? (connectionPair.pins.map((pin) => ({
|
|
182
|
+
...pin,
|
|
183
|
+
})) as MspConnectionPair["pins"])
|
|
184
|
+
: connectionPair.pins
|
|
142
185
|
|
|
143
186
|
this.activeSubSolver = new SchematicTraceSingleLineSolver2({
|
|
144
187
|
inputProblem: this.inputProblem,
|
package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/getPinDirection.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { InputChip, InputPin } from "lib/types/InputProblem"
|
|
2
2
|
|
|
3
|
-
export
|
|
3
|
+
export type PinDirection = "x+" | "x-" | "y+" | "y-"
|
|
4
|
+
|
|
5
|
+
export const getPinDirectionCandidates = (
|
|
4
6
|
pin: InputPin,
|
|
5
7
|
chip: InputChip,
|
|
6
|
-
|
|
8
|
+
connectedPin?: InputPin,
|
|
9
|
+
): PinDirection[] => {
|
|
7
10
|
// Determine what edge the pin lies on
|
|
8
11
|
const { x, y } = pin
|
|
9
12
|
const { center, width, height } = chip
|
|
@@ -26,17 +29,62 @@ export const getPinDirection = (
|
|
|
26
29
|
xMinusDistance,
|
|
27
30
|
)
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
// Preserve the established single-direction result as the primary choice.
|
|
33
|
+
// Candidate routing may treat nearly equal edge distances as an ambiguous
|
|
34
|
+
// corner, but context-free callers (such as visualization) should not change
|
|
35
|
+
// direction solely because the candidate comparison uses a tolerance.
|
|
36
|
+
const primaryDirection: PinDirection =
|
|
37
|
+
minDistance === yPlusDistance
|
|
38
|
+
? "y+"
|
|
39
|
+
: minDistance === yMinusDistance
|
|
40
|
+
? "y-"
|
|
41
|
+
: minDistance === xPlusDistance
|
|
42
|
+
? "x+"
|
|
43
|
+
: "x-"
|
|
44
|
+
|
|
45
|
+
const matchingDirections = [
|
|
46
|
+
{ direction: "y+" as const, distance: yPlusDistance },
|
|
47
|
+
{ direction: "y-" as const, distance: yMinusDistance },
|
|
48
|
+
{ direction: "x+" as const, distance: xPlusDistance },
|
|
49
|
+
{ direction: "x-" as const, distance: xMinusDistance },
|
|
50
|
+
]
|
|
51
|
+
.filter(({ distance }) => Math.abs(distance - minDistance) <= 1e-9)
|
|
52
|
+
.map(({ direction }) => direction)
|
|
53
|
+
|
|
54
|
+
const closestDirections = [
|
|
55
|
+
primaryDirection,
|
|
56
|
+
...matchingDirections.filter((direction) => direction !== primaryDirection),
|
|
57
|
+
]
|
|
32
58
|
|
|
33
|
-
if (
|
|
34
|
-
return
|
|
59
|
+
if (!connectedPin || closestDirections.length <= 1) {
|
|
60
|
+
return closestDirections
|
|
35
61
|
}
|
|
36
62
|
|
|
37
|
-
|
|
38
|
-
|
|
63
|
+
const xDistance = connectedPin.x - pin.x
|
|
64
|
+
const yDistance = connectedPin.y - pin.y
|
|
65
|
+
const directionTowardConnectedPin: PinDirection =
|
|
66
|
+
Math.abs(xDistance) >= Math.abs(yDistance)
|
|
67
|
+
? xDistance >= 0
|
|
68
|
+
? "x+"
|
|
69
|
+
: "x-"
|
|
70
|
+
: yDistance >= 0
|
|
71
|
+
? "y+"
|
|
72
|
+
: "y-"
|
|
73
|
+
|
|
74
|
+
if (!closestDirections.includes(directionTowardConnectedPin)) {
|
|
75
|
+
return closestDirections
|
|
39
76
|
}
|
|
40
77
|
|
|
41
|
-
return
|
|
78
|
+
return [
|
|
79
|
+
directionTowardConnectedPin,
|
|
80
|
+
...closestDirections.filter(
|
|
81
|
+
(direction) => direction !== directionTowardConnectedPin,
|
|
82
|
+
),
|
|
83
|
+
]
|
|
42
84
|
}
|
|
85
|
+
|
|
86
|
+
export const getPinDirection = (
|
|
87
|
+
pin: InputPin,
|
|
88
|
+
chip: InputChip,
|
|
89
|
+
connectedPin?: InputPin,
|
|
90
|
+
): PinDirection => getPinDirectionCandidates(pin, chip, connectedPin)[0]!
|
|
@@ -9,8 +9,12 @@ import type { InputChip, InputProblem } from "lib/types/InputProblem"
|
|
|
9
9
|
import type { FacingDirection } from "lib/utils/dir"
|
|
10
10
|
import { getNetLabelWidthForConnection } from "lib/utils/getNetLabelWidthForConnection"
|
|
11
11
|
import { getTextBoxBounds, type RectPadding } from "lib/utils/textBoxBounds"
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
12
|
+
import { getPinDirectionCandidates } from "../SchematicTraceSingleLineSolver/getPinDirection"
|
|
13
|
+
import {
|
|
14
|
+
calculateDirectShortPath,
|
|
15
|
+
pathMatchesPinDirections,
|
|
16
|
+
segmentDirection,
|
|
17
|
+
} from "./calculateDirectShortPath"
|
|
14
18
|
import {
|
|
15
19
|
findFirstCollision,
|
|
16
20
|
isHorizontal,
|
|
@@ -78,6 +82,55 @@ const pinsFaceEachOther = ({
|
|
|
78
82
|
: pin1._facingDirection === "y-" && pin2._facingDirection === "y+"
|
|
79
83
|
}
|
|
80
84
|
|
|
85
|
+
const getPathLength = (points: Point[]) => {
|
|
86
|
+
let length = 0
|
|
87
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
88
|
+
length +=
|
|
89
|
+
Math.abs(points[i + 1]!.x - points[i]!.x) +
|
|
90
|
+
Math.abs(points[i + 1]!.y - points[i]!.y)
|
|
91
|
+
}
|
|
92
|
+
return length
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const getInitialPathForPins = ({
|
|
96
|
+
pins,
|
|
97
|
+
obstacles,
|
|
98
|
+
}: {
|
|
99
|
+
pins: MspConnectionPair["pins"]
|
|
100
|
+
obstacles: ObstacleRect[]
|
|
101
|
+
}): { path: Point[]; isDirectShortPath: boolean } => {
|
|
102
|
+
const [pin1, pin2] = pins
|
|
103
|
+
const directShortPath = calculateDirectShortPath(pin1, pin2)
|
|
104
|
+
const defaultElbow = calculateElbowForPins({
|
|
105
|
+
pin1,
|
|
106
|
+
pin2,
|
|
107
|
+
overshoot: 0.2,
|
|
108
|
+
})
|
|
109
|
+
const routingDistance = Math.abs(pin1.x - pin2.x) + Math.abs(pin1.y - pin2.y)
|
|
110
|
+
const adaptiveElbow = calculateElbowForPins({
|
|
111
|
+
pin1,
|
|
112
|
+
pin2,
|
|
113
|
+
overshoot: Math.min(0.2, Math.max(0.02, routingDistance / 4)),
|
|
114
|
+
})
|
|
115
|
+
const adaptiveElbowIsShorter =
|
|
116
|
+
getPathLength(adaptiveElbow) < getPathLength(defaultElbow)
|
|
117
|
+
const defaultElbowBacktracks =
|
|
118
|
+
getPathLength(defaultElbow) > routingDistance + 1e-9
|
|
119
|
+
const shouldUseAdaptiveElbow =
|
|
120
|
+
findFirstCollision(adaptiveElbow, obstacles) === null &&
|
|
121
|
+
((pinsFaceEachOther({ pin1, pin2 }) &&
|
|
122
|
+
defaultElbowBacktracks &&
|
|
123
|
+
adaptiveElbowIsShorter) ||
|
|
124
|
+
findFirstCollision(defaultElbow, obstacles) !== null)
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
path:
|
|
128
|
+
directShortPath ??
|
|
129
|
+
(shouldUseAdaptiveElbow ? adaptiveElbow : defaultElbow),
|
|
130
|
+
isDirectShortPath: directShortPath !== null,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
81
134
|
export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
82
135
|
pins: MspConnectionPair["pins"]
|
|
83
136
|
connectionPair?: MspConnectionPair
|
|
@@ -91,12 +144,18 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
91
144
|
|
|
92
145
|
baseElbow: Point[]
|
|
93
146
|
preferExteriorDetours: boolean
|
|
147
|
+
reserveNetLabelClearance: boolean
|
|
94
148
|
|
|
95
149
|
solvedTracePath: Point[] | null = null
|
|
96
150
|
|
|
97
|
-
private queue: Array<{
|
|
98
|
-
[]
|
|
151
|
+
private queue: Array<{
|
|
152
|
+
path: Point[]
|
|
153
|
+
collisionRects: Set<ObstacleRect>
|
|
154
|
+
directions: readonly [FacingDirection, FacingDirection]
|
|
155
|
+
}> = []
|
|
99
156
|
private visited: Set<PathKey> = new Set()
|
|
157
|
+
private inferredPinIndexes = new Set<number>()
|
|
158
|
+
hasAmbiguousPinDirections = false
|
|
100
159
|
|
|
101
160
|
constructor(params: {
|
|
102
161
|
pins: MspConnectionPair["pins"]
|
|
@@ -104,6 +163,7 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
104
163
|
inputProblem: InputProblem
|
|
105
164
|
chipMap: Record<string, InputChip>
|
|
106
165
|
preferExteriorDetours?: boolean
|
|
166
|
+
reserveNetLabelClearance?: boolean
|
|
107
167
|
}) {
|
|
108
168
|
super()
|
|
109
169
|
this.pins = params.pins
|
|
@@ -111,19 +171,14 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
111
171
|
this.inputProblem = params.inputProblem
|
|
112
172
|
this.chipMap = params.chipMap
|
|
113
173
|
this.preferExteriorDetours = params.preferExteriorDetours ?? true
|
|
114
|
-
|
|
115
|
-
// Ensure facing directions are present
|
|
116
|
-
for (const pin of this.pins) {
|
|
117
|
-
if (!pin._facingDirection) {
|
|
118
|
-
const chip = this.chipMap[pin.chipId]
|
|
119
|
-
pin._facingDirection = getPinDirection(pin, chip)
|
|
120
|
-
}
|
|
121
|
-
}
|
|
174
|
+
this.reserveNetLabelClearance = params.reserveNetLabelClearance ?? true
|
|
122
175
|
|
|
123
176
|
// Build obstacle rects from chips and schematic text boxes. Text boxes are
|
|
124
177
|
// padded by the label footprint for this net so labels have clearance too.
|
|
125
178
|
this.obstacles = getObstacleRects(this.inputProblem, {
|
|
126
|
-
textBoxPadding: this.
|
|
179
|
+
textBoxPadding: this.reserveNetLabelClearance
|
|
180
|
+
? this.getTextBoxPaddingForConnectionPair()
|
|
181
|
+
: undefined,
|
|
127
182
|
})
|
|
128
183
|
this.textObstacles = new Set(this.obstacles.filter(isTextBoxObstacle))
|
|
129
184
|
const endpointChipIds = new Set(this.pins.map((pin) => pin.chipId))
|
|
@@ -139,40 +194,71 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
139
194
|
),
|
|
140
195
|
)
|
|
141
196
|
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const adaptiveElbow = calculateElbowForPins({
|
|
152
|
-
pin1,
|
|
153
|
-
pin2,
|
|
154
|
-
overshoot: Math.min(0.2, Math.max(0.02, routingDistance / 4)),
|
|
197
|
+
const directionOptions = this.pins.map((pin, pinIndex) => {
|
|
198
|
+
if (pin._facingDirection) return [pin._facingDirection]
|
|
199
|
+
this.inferredPinIndexes.add(pinIndex)
|
|
200
|
+
const connectedPin = this.pins[pinIndex === 0 ? 1 : 0]
|
|
201
|
+
return getPinDirectionCandidates(
|
|
202
|
+
pin,
|
|
203
|
+
this.chipMap[pin.chipId],
|
|
204
|
+
connectedPin,
|
|
205
|
+
)
|
|
155
206
|
})
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
207
|
+
const directionPairs = directionOptions[0]!.flatMap(
|
|
208
|
+
(firstDirection, firstIndex) =>
|
|
209
|
+
directionOptions[1]!.map((secondDirection, secondIndex) => ({
|
|
210
|
+
directions: [firstDirection, secondDirection] as const,
|
|
211
|
+
preferenceIndex: firstIndex + secondIndex,
|
|
212
|
+
})),
|
|
213
|
+
)
|
|
214
|
+
this.hasAmbiguousPinDirections = directionOptions.some(
|
|
215
|
+
(directions) => directions.length > 1,
|
|
216
|
+
)
|
|
217
|
+
const rankedDirectionPairs = directionPairs
|
|
218
|
+
.map(({ directions, preferenceIndex }) => {
|
|
219
|
+
const candidatePins = this.pins.map((pin, index) => ({
|
|
220
|
+
...pin,
|
|
221
|
+
_facingDirection: directions[index]!,
|
|
222
|
+
})) as MspConnectionPair["pins"]
|
|
223
|
+
const initialPath = getInitialPathForPins({
|
|
224
|
+
pins: candidatePins,
|
|
225
|
+
obstacles: this.obstacles,
|
|
226
|
+
})
|
|
227
|
+
return {
|
|
228
|
+
directions,
|
|
229
|
+
baseElbow: initialPath.path,
|
|
230
|
+
isDirectShortPath: initialPath.isDirectShortPath,
|
|
231
|
+
preferenceIndex,
|
|
232
|
+
collisionCount:
|
|
233
|
+
findFirstCollision(initialPath.path, this.obstacles) === null
|
|
234
|
+
? 0
|
|
235
|
+
: 1,
|
|
236
|
+
pathLength: getPathLength(initialPath.path),
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
.sort(
|
|
240
|
+
(first, second) =>
|
|
241
|
+
first.collisionCount - second.collisionCount ||
|
|
242
|
+
first.pathLength - second.pathLength ||
|
|
243
|
+
first.preferenceIndex - second.preferenceIndex,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
const preferredCandidate = rankedDirectionPairs[0]!
|
|
247
|
+
for (const [pinIndex, pin] of this.pins.entries()) {
|
|
248
|
+
pin._facingDirection = preferredCandidate.directions[pinIndex]
|
|
171
249
|
}
|
|
172
|
-
|
|
173
|
-
|
|
250
|
+
|
|
251
|
+
const [pin1, pin2] = this.pins
|
|
252
|
+
this.baseElbow = preferredCandidate.baseElbow
|
|
253
|
+
|
|
254
|
+
// Short paths are generated by a dedicated routine that validates both
|
|
255
|
+
// endpoint directions. Preserve the established behavior of accepting
|
|
256
|
+
// that local connection directly instead of sending it through obstacle
|
|
257
|
+
// detour search, which can turn overlapping endpoint symbols into two
|
|
258
|
+
// fallback labels.
|
|
259
|
+
if (preferredCandidate.isDirectShortPath) {
|
|
260
|
+
this.solvedTracePath = preferredCandidate.baseElbow
|
|
174
261
|
}
|
|
175
|
-
this.solvedTracePath = directShortPath
|
|
176
262
|
|
|
177
263
|
// Bounds defined by PA and PB
|
|
178
264
|
this.aabb = aabbFromPoints(
|
|
@@ -180,9 +266,19 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
180
266
|
{ x: pin2.x, y: pin2.y },
|
|
181
267
|
)
|
|
182
268
|
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
269
|
+
// A corner pin has more than one geometrically valid outward direction.
|
|
270
|
+
// Seed each distinct elbow so obstacle routing, rather than component type,
|
|
271
|
+
// decides which direction is usable.
|
|
272
|
+
for (const candidate of rankedDirectionPairs) {
|
|
273
|
+
const key = pathKey(candidate.baseElbow)
|
|
274
|
+
if (this.visited.has(key)) continue
|
|
275
|
+
this.visited.add(key)
|
|
276
|
+
this.queue.push({
|
|
277
|
+
path: candidate.baseElbow,
|
|
278
|
+
collisionRects: new Set(),
|
|
279
|
+
directions: candidate.directions,
|
|
280
|
+
})
|
|
281
|
+
}
|
|
186
282
|
}
|
|
187
283
|
|
|
188
284
|
override getConstructorParams(): ConstructorParameters<
|
|
@@ -194,6 +290,7 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
194
290
|
connectionPair: this.connectionPair,
|
|
195
291
|
inputProblem: this.inputProblem,
|
|
196
292
|
preferExteriorDetours: this.preferExteriorDetours,
|
|
293
|
+
reserveNetLabelClearance: this.reserveNetLabelClearance,
|
|
197
294
|
}
|
|
198
295
|
}
|
|
199
296
|
|
|
@@ -268,13 +365,7 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
268
365
|
}
|
|
269
366
|
|
|
270
367
|
private pathLength(pts: Point[]): number {
|
|
271
|
-
|
|
272
|
-
for (let i = 0; i < pts.length - 1; i++) {
|
|
273
|
-
sum +=
|
|
274
|
-
Math.abs(pts[i + 1]!.x - pts[i]!.x) +
|
|
275
|
-
Math.abs(pts[i + 1]!.y - pts[i]!.y)
|
|
276
|
-
}
|
|
277
|
-
return sum
|
|
368
|
+
return getPathLength(pts)
|
|
278
369
|
}
|
|
279
370
|
|
|
280
371
|
private getPinBandPenalty(path: Point[]): number {
|
|
@@ -318,7 +409,7 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
318
409
|
return
|
|
319
410
|
}
|
|
320
411
|
|
|
321
|
-
const { path, collisionRects } = state
|
|
412
|
+
const { path, collisionRects, directions } = state
|
|
322
413
|
|
|
323
414
|
const [PA, PB] = this.pins
|
|
324
415
|
const collision = findFirstCollision(path, this.obstacles, {
|
|
@@ -409,8 +500,21 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
409
500
|
Math.abs(p.x - q.x) < EPS && Math.abs(p.y - q.y) < EPS
|
|
410
501
|
if (
|
|
411
502
|
samePoint(first, { x: PA.x, y: PA.y }) &&
|
|
412
|
-
samePoint(last, { x: PB.x, y: PB.y })
|
|
503
|
+
samePoint(last, { x: PB.x, y: PB.y }) &&
|
|
504
|
+
(!this.hasAmbiguousPinDirections ||
|
|
505
|
+
pathMatchesPinDirections({
|
|
506
|
+
path,
|
|
507
|
+
pin1: { ...PA, _facingDirection: directions[0] },
|
|
508
|
+
pin2: { ...PB, _facingDirection: directions[1] },
|
|
509
|
+
}))
|
|
413
510
|
) {
|
|
511
|
+
for (const pinIndex of this.inferredPinIndexes) {
|
|
512
|
+
const direction =
|
|
513
|
+
pinIndex === 0
|
|
514
|
+
? segmentDirection(path[0]!, path[1]!)
|
|
515
|
+
: segmentDirection(path.at(-1)!, path.at(-2)!)
|
|
516
|
+
if (direction) this.pins[pinIndex]!._facingDirection = direction
|
|
517
|
+
}
|
|
414
518
|
this.solvedTracePath = path
|
|
415
519
|
this.solved = true
|
|
416
520
|
}
|
|
@@ -471,6 +575,7 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
471
575
|
this.queue.push({
|
|
472
576
|
path: detour,
|
|
473
577
|
collisionRects: nextCollisionRects,
|
|
578
|
+
directions,
|
|
474
579
|
})
|
|
475
580
|
}
|
|
476
581
|
return
|
|
@@ -630,7 +735,11 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
630
735
|
(a, b) => a.length - b.length || a.pinBandPenalty - b.pinBandPenalty,
|
|
631
736
|
)
|
|
632
737
|
for (const st of newStates) {
|
|
633
|
-
this.queue.push({
|
|
738
|
+
this.queue.push({
|
|
739
|
+
path: st.path,
|
|
740
|
+
collisionRects: st.collisionRects,
|
|
741
|
+
directions,
|
|
742
|
+
})
|
|
634
743
|
}
|
|
635
744
|
}
|
|
636
745
|
|
|
@@ -1,16 +1,121 @@
|
|
|
1
1
|
import type { Point } from "@tscircuit/math-utils"
|
|
2
2
|
import { segmentIntersectsRect } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
|
|
3
3
|
|
|
4
|
+
type LabelBounds = {
|
|
5
|
+
minX: number
|
|
6
|
+
maxX: number
|
|
7
|
+
minY: number
|
|
8
|
+
maxY: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const EPSILON = 1e-9
|
|
12
|
+
|
|
13
|
+
const rangesMeetWithoutOverlap = (
|
|
14
|
+
firstMin: number,
|
|
15
|
+
firstMax: number,
|
|
16
|
+
secondMin: number,
|
|
17
|
+
secondMax: number,
|
|
18
|
+
) =>
|
|
19
|
+
firstMax >= secondMin - EPSILON &&
|
|
20
|
+
secondMax >= firstMin - EPSILON &&
|
|
21
|
+
Math.min(firstMax, secondMax) - Math.max(firstMin, secondMin) <= EPSILON
|
|
22
|
+
|
|
23
|
+
const pointsEqual = (first: Point, second: Point) =>
|
|
24
|
+
Math.abs(first.x - second.x) <= EPSILON &&
|
|
25
|
+
Math.abs(first.y - second.y) <= EPSILON
|
|
26
|
+
|
|
27
|
+
const continuesExistingBoundarySegment = ({
|
|
28
|
+
start,
|
|
29
|
+
end,
|
|
30
|
+
label,
|
|
31
|
+
originalPath,
|
|
32
|
+
}: {
|
|
33
|
+
start: Point
|
|
34
|
+
end: Point
|
|
35
|
+
label: LabelBounds
|
|
36
|
+
originalPath: Point[]
|
|
37
|
+
}) => {
|
|
38
|
+
const isVertical = Math.abs(start.x - end.x) <= EPSILON
|
|
39
|
+
const isHorizontal = Math.abs(start.y - end.y) <= EPSILON
|
|
40
|
+
const isOnBoundary = isVertical
|
|
41
|
+
? Math.abs(start.x - label.minX) <= EPSILON ||
|
|
42
|
+
Math.abs(start.x - label.maxX) <= EPSILON
|
|
43
|
+
: isHorizontal
|
|
44
|
+
? Math.abs(start.y - label.minY) <= EPSILON ||
|
|
45
|
+
Math.abs(start.y - label.maxY) <= EPSILON
|
|
46
|
+
: false
|
|
47
|
+
|
|
48
|
+
if (!isOnBoundary) return false
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < originalPath.length - 1; i++) {
|
|
51
|
+
const isEndpointSegment = i === 0 || i === originalPath.length - 2
|
|
52
|
+
if (!isEndpointSegment) continue
|
|
53
|
+
|
|
54
|
+
const originalStart = originalPath[i]!
|
|
55
|
+
const originalEnd = originalPath[i + 1]!
|
|
56
|
+
const innerEndpoint = i === 0 ? originalEnd : originalStart
|
|
57
|
+
if (
|
|
58
|
+
!pointsEqual(start, innerEndpoint) &&
|
|
59
|
+
!pointsEqual(end, innerEndpoint)
|
|
60
|
+
) {
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
if (!segmentIntersectsRect(originalStart, originalEnd, label)) continue
|
|
64
|
+
|
|
65
|
+
if (
|
|
66
|
+
isVertical &&
|
|
67
|
+
Math.abs(originalStart.x - originalEnd.x) <= EPSILON &&
|
|
68
|
+
Math.abs(originalStart.x - start.x) <= EPSILON &&
|
|
69
|
+
rangesMeetWithoutOverlap(
|
|
70
|
+
Math.min(start.y, end.y),
|
|
71
|
+
Math.max(start.y, end.y),
|
|
72
|
+
Math.min(originalStart.y, originalEnd.y),
|
|
73
|
+
Math.max(originalStart.y, originalEnd.y),
|
|
74
|
+
)
|
|
75
|
+
) {
|
|
76
|
+
return true
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (
|
|
80
|
+
isHorizontal &&
|
|
81
|
+
Math.abs(originalStart.y - originalEnd.y) <= EPSILON &&
|
|
82
|
+
Math.abs(originalStart.y - start.y) <= EPSILON &&
|
|
83
|
+
rangesMeetWithoutOverlap(
|
|
84
|
+
Math.min(start.x, end.x),
|
|
85
|
+
Math.max(start.x, end.x),
|
|
86
|
+
Math.min(originalStart.x, originalEnd.x),
|
|
87
|
+
Math.max(originalStart.x, originalEnd.x),
|
|
88
|
+
)
|
|
89
|
+
) {
|
|
90
|
+
return true
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return false
|
|
95
|
+
}
|
|
96
|
+
|
|
4
97
|
export const hasCollisionsWithLabels = (
|
|
5
98
|
pathSegments: Point[],
|
|
6
|
-
labels:
|
|
99
|
+
labels: LabelBounds[],
|
|
100
|
+
options: { originalPath?: Point[] } = {},
|
|
7
101
|
): boolean => {
|
|
8
102
|
for (let i = 0; i < pathSegments.length - 1; i++) {
|
|
9
|
-
const p1 = pathSegments[i]
|
|
10
|
-
const p2 = pathSegments[i + 1]
|
|
103
|
+
const p1 = pathSegments[i]!
|
|
104
|
+
const p2 = pathSegments[i + 1]!
|
|
11
105
|
|
|
12
106
|
for (const label of labels) {
|
|
13
107
|
if (segmentIntersectsRect(p1, p2, label)) {
|
|
108
|
+
if (
|
|
109
|
+
options.originalPath &&
|
|
110
|
+
continuesExistingBoundarySegment({
|
|
111
|
+
start: p1,
|
|
112
|
+
end: p2,
|
|
113
|
+
label,
|
|
114
|
+
originalPath: options.originalPath,
|
|
115
|
+
})
|
|
116
|
+
) {
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
14
119
|
return true
|
|
15
120
|
}
|
|
16
121
|
}
|