@tscircuit/schematic-trace-solver 0.0.182 → 0.0.184
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 +542 -132
- package/lib/solvers/SameNetJunctionAlignmentSolver/alignSameNetJunctions.ts +30 -12
- package/lib/solvers/SameNetJunctionAlignmentSolver/collapseRedundantSharedEndpointStubs.ts +267 -0
- package/lib/solvers/SameNetJunctionAlignmentSolver/collapseSameNetCycles.ts +1 -1
- package/lib/solvers/SameNetJunctionAlignmentSolver/getSharedPin.ts +13 -0
- 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-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +13 -20
- package/tests/bug-reports/bug-report-20260707T020342Z/__snapshots__/bug-report-20260707T020342Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260707T020342Z/bug-report-20260707T020342Z.test.ts +10 -0
- package/tests/bug-reports/bug-report-20260707T092615Z/__snapshots__/bug-report-20260707T092615Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260707T134549Z/__snapshots__/bug-report-20260707T134549Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260707T140410Z/__snapshots__/bug-report-20260707T140410Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260707T141025Z/__snapshots__/bug-report-20260707T141025Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260707T141421Z/__snapshots__/bug-report-20260707T141421Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260707T230831Z/__snapshots__/bug-report-20260707T230831Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260708T095725Z/__snapshots__/bug-report-20260708T095725Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260716T144856Z/__snapshots__/bug-report-20260716T144856Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260717T022934Z/__snapshots__/bug-report-20260717T022934Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260717T042845Z/__snapshots__/bug-report-20260717T042845Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260721T221026Z/__snapshots__/bug-report-20260721T221026Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260724T175257Z/__snapshots__/bug-report-20260724T175257Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260728T144234Z/__snapshots__/bug-report-20260728T144234Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260728T144234Z/bug-report-20260728T144234Z.test.ts +2 -0
- package/tests/bug-reports/bug-report-20260730T061837Z/__snapshots__/bug-report-20260730T061837Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260730T061837Z/bug-report-20260730T061837Z.test.ts +1 -1
- package/tests/bug-reports/bug-report-20260731T052229Z/__snapshots__/bug-report-20260731T052229Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260804T171919Z/__snapshots__/bug-report-20260804T171919Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260804T225912Z/__snapshots__/bug-report-20260804T225912Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260805T061316Z/__snapshots__/bug-report-20260805T061316Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260806T061548Z/__snapshots__/bug-report-20260806T061548Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260815T073240Z/__snapshots__/bug-report-20260815T073240Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260819T091818Z/__snapshots__/bug-report-20260819T091818Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260825T045913Z/__snapshots__/bug-report-20260825T045913Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260825T103621Z/__snapshots__/bug-report-20260825T103621Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260826T072956Z/__snapshots__/bug-report-20260826T072956Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260901T055358Z/__snapshots__/bug-report-20260901T055358Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260901T064117Z/__snapshots__/bug-report-20260901T064117Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260901T134241Z/__snapshots__/bug-report-20260901T134241Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260902T092425Z/__snapshots__/bug-report-20260902T092425Z.snap.svg +2 -2
- package/tests/examples/__snapshots__/example02.snap.svg +2 -2
- package/tests/examples/__snapshots__/example13.snap.svg +2 -2
- package/tests/examples/__snapshots__/example14.snap.svg +2 -2
- package/tests/examples/__snapshots__/example15.snap.svg +2 -2
- package/tests/examples/__snapshots__/example18.snap.svg +2 -2
- package/tests/examples/__snapshots__/example19.snap.svg +2 -2
- package/tests/examples/__snapshots__/example21.snap.svg +2 -2
- package/tests/examples/__snapshots__/example27.snap.svg +2 -2
- package/tests/examples/__snapshots__/example29.snap.svg +2 -2
- package/tests/examples/__snapshots__/example42.snap.svg +2 -2
- package/tests/examples/__snapshots__/example45.snap.svg +2 -2
- package/tests/examples/__snapshots__/example46.snap.svg +2 -2
- package/tests/examples/__snapshots__/example47.snap.svg +2 -2
- package/tests/examples/__snapshots__/example51.snap.svg +2 -2
- package/tests/repros/__snapshots__/board-1273-trace-overlap-cycle.snap.svg +2 -2
- package/tests/repros/__snapshots__/board-15984-same-net-junction.snap.svg +2 -2
- package/tests/repros/__snapshots__/board1096-usb-label-overlap-iteration.snap.svg +13 -13
- package/tests/repros/__snapshots__/bugreport-001-gnd-overlap.snap.svg +2 -2
- package/tests/repros/__snapshots__/netlabel-connector-through-rail-label.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-battery-management-bq24073.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-bc107a-common-emitter-routing.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-bluetooth-controller-ground-decoupling-groups.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-board-589-regulator-section.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-board-648-esp12f-section.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-board-8090-power-section.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-bq24074-battery-charger.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-bq25895-cross-net-trace-overlap.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-bq40z60-current-sense.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-cc2340r5.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-core-ground-inline-label-fallback.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-esp12f-section.snap.svg +508 -0
- package/tests/repros/__snapshots__/repro-example35-minimize-trace-crossing.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-hdc2080-ground-label-alignment.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-hdc3020-ground-label-alignment.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-hub-usb-sheet.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-ina237-current-monitor.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-isolated-rs485-isow7841.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-mouse-switch-ground-unnecessary-jog.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-mspm0l1306-capacitor-symmetry.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-pga300-c6-trace-alignment.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-pga300-redundant-ground-traces.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-pmp11282-isolated-dcdc.snap.svg +26 -26
- package/tests/repros/__snapshots__/repro-power-group-rail-labels.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-powerbank-3v-system-power.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-rectifier-trace-overlap.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-rf1-unnecessary-dogleg.snap.svg +5 -5
- package/tests/repros/__snapshots__/repro-rp2040-gamepad-trace-alignment.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-rp2040-usb-cc2-ground-overlap.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-smartwatch-power-sheet.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-ti-power-output-section.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-tida-01389-hbridge.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-tida00553-j4-cell-divider.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-tida010076-j4-ground-bus.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-tida010076-page02.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-tmp1075-ground-label-overlap.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-tps61222-trace-intersection.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-trellis-core-gnd-net-label-overlap.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-type-c-vbus-led-ground-label.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-usb-power-vbus-label-detour.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro130-bq27441-fuel-gauge-trace-through-c1.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro161-power-section-autolayout.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro48-555-timer-vcc-rail-label.snap.svg +2 -2
- package/tests/repros/assets/repro-esp12f-status-section.input.json +1394 -0
- package/tests/repros/repro-esp12f-section.test.ts +12 -0
- package/tests/solvers/SameNetJunctionAlignmentSolver/collapse-redundant-shared-endpoint-stubs.test.ts +101 -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
|
}
|
|
@@ -11,6 +11,39 @@ import {
|
|
|
11
11
|
} from "./sameNetRailAlignment/geometry"
|
|
12
12
|
import { shortenExcessiveLabelPaddingDetour } from "./shortenExcessiveLabelPaddingDetour"
|
|
13
13
|
|
|
14
|
+
const PATH_LENGTH_EPSILON = 1e-9
|
|
15
|
+
type ObstacleBounds = {
|
|
16
|
+
minX: number
|
|
17
|
+
minY: number
|
|
18
|
+
maxX: number
|
|
19
|
+
maxY: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const getPathLength = (path: SolvedTracePath["tracePath"]): number =>
|
|
23
|
+
path.slice(1).reduce((length, point, pointIndex) => {
|
|
24
|
+
const previousPoint = path[pointIndex]!
|
|
25
|
+
return (
|
|
26
|
+
length +
|
|
27
|
+
Math.abs(point.x - previousPoint.x) +
|
|
28
|
+
Math.abs(point.y - previousPoint.y)
|
|
29
|
+
)
|
|
30
|
+
}, 0)
|
|
31
|
+
|
|
32
|
+
const chooseLengthPreservingBoundaryRoute = ({
|
|
33
|
+
strictPath,
|
|
34
|
+
boundaryPath,
|
|
35
|
+
}: {
|
|
36
|
+
strictPath: SolvedTracePath["tracePath"]
|
|
37
|
+
boundaryPath: SolvedTracePath["tracePath"]
|
|
38
|
+
}) => {
|
|
39
|
+
const doesNotAddTurns = countTurns(boundaryPath) <= countTurns(strictPath)
|
|
40
|
+
const preservesLength =
|
|
41
|
+
Math.abs(getPathLength(boundaryPath) - getPathLength(strictPath)) <=
|
|
42
|
+
PATH_LENGTH_EPSILON
|
|
43
|
+
|
|
44
|
+
return doesNotAddTurns && preservesLength ? boundaryPath : strictPath
|
|
45
|
+
}
|
|
46
|
+
|
|
14
47
|
/**
|
|
15
48
|
* Minimizes turns with a strict pass that treats every other trace as an
|
|
16
49
|
* obstacle and a relaxed pass that permits joining endpoint-sharing same-net
|
|
@@ -95,22 +128,36 @@ export const minimizeTurnsWithFilteredLabels = ({
|
|
|
95
128
|
maxY: nl.center.y + nl.height / 2 + paddingBuffer,
|
|
96
129
|
}))
|
|
97
130
|
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
131
|
+
const minimizeForObstacles = (obstacles: ObstacleBounds[]) => {
|
|
132
|
+
const strictPath = minimizeTurns({
|
|
133
|
+
path: originalPath,
|
|
134
|
+
obstacles,
|
|
135
|
+
labelBounds,
|
|
136
|
+
originalPath,
|
|
137
|
+
})
|
|
138
|
+
const boundaryPath = minimizeTurns({
|
|
139
|
+
path: originalPath,
|
|
140
|
+
obstacles,
|
|
141
|
+
labelBounds,
|
|
142
|
+
originalPath,
|
|
143
|
+
allowLabelBoundaryExtension: true,
|
|
144
|
+
})
|
|
104
145
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
146
|
+
return chooseLengthPreservingBoundaryRoute({
|
|
147
|
+
strictPath,
|
|
148
|
+
boundaryPath,
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const strictPath = minimizeForObstacles([
|
|
153
|
+
...staticObstacles,
|
|
154
|
+
...getTraceObstacles(otherTraces),
|
|
155
|
+
])
|
|
156
|
+
|
|
157
|
+
const relaxedPath = minimizeForObstacles([
|
|
158
|
+
...staticObstacles,
|
|
159
|
+
...getTraceObstacles(relaxedObstacleTraces),
|
|
160
|
+
])
|
|
114
161
|
|
|
115
162
|
const sameNetTraces = otherTraces.filter(
|
|
116
163
|
(trace) => trace.globalConnNetId === targetTrace.globalConnNetId,
|
|
@@ -20,11 +20,13 @@ export const minimizeTurns = ({
|
|
|
20
20
|
obstacles,
|
|
21
21
|
labelBounds,
|
|
22
22
|
originalPath,
|
|
23
|
+
allowLabelBoundaryExtension = false,
|
|
23
24
|
}: {
|
|
24
25
|
path: Point[]
|
|
25
26
|
obstacles: any[]
|
|
26
27
|
labelBounds: any[]
|
|
27
28
|
originalPath: Point[]
|
|
29
|
+
allowLabelBoundaryExtension?: boolean
|
|
28
30
|
}): Point[] => {
|
|
29
31
|
if (path.length <= 2) {
|
|
30
32
|
return path
|
|
@@ -73,6 +75,7 @@ export const minimizeTurns = ({
|
|
|
73
75
|
const collidesWithLabels = hasCollisionsWithLabels(
|
|
74
76
|
connection,
|
|
75
77
|
labelBounds,
|
|
78
|
+
allowLabelBoundaryExtension ? { originalPath } : {},
|
|
76
79
|
)
|
|
77
80
|
|
|
78
81
|
if (!collidesWithObstacles && !collidesWithLabels) {
|
|
@@ -136,6 +139,7 @@ export const minimizeTurns = ({
|
|
|
136
139
|
const collidesWithLabels = hasCollisionsWithLabels(
|
|
137
140
|
connectionSegments,
|
|
138
141
|
labelBounds,
|
|
142
|
+
allowLabelBoundaryExtension ? { originalPath } : {},
|
|
139
143
|
)
|
|
140
144
|
|
|
141
145
|
if (!collidesWithObstacles && !collidesWithLabels) {
|
|
@@ -186,6 +190,7 @@ export const minimizeTurns = ({
|
|
|
186
190
|
const collidesWithLabels = hasCollisionsWithLabels(
|
|
187
191
|
[p1, p3],
|
|
188
192
|
labelBounds,
|
|
193
|
+
allowLabelBoundaryExtension ? { originalPath } : {},
|
|
189
194
|
)
|
|
190
195
|
|
|
191
196
|
if (!collidesWithObstacles && !collidesWithLabels) {
|