@tscircuit/schematic-trace-solver 0.0.49 → 0.0.51
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 +133 -1
- package/dist/index.js +1084 -41
- package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +682 -0
- package/lib/solvers/AvailableNetOrientationSolver/constants.ts +6 -0
- package/lib/solvers/AvailableNetOrientationSolver/geometry.ts +192 -0
- package/lib/solvers/AvailableNetOrientationSolver/traces.ts +43 -0
- package/lib/solvers/AvailableNetOrientationSolver/types.ts +44 -0
- package/lib/solvers/AvailableNetOrientationSolver/visualize.ts +123 -0
- package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +30 -0
- package/lib/solvers/VccNetLabelCornerPlacementSolver/VccNetLabelCornerPlacementSolver.ts +268 -0
- package/lib/solvers/VccNetLabelCornerPlacementSolver/geometry.ts +59 -0
- package/lib/solvers/VccNetLabelCornerPlacementSolver/types.ts +37 -0
- package/lib/solvers/VccNetLabelCornerPlacementSolver/visualize.ts +114 -0
- package/package.json +1 -1
- package/tests/assets/example22.json +1 -1
- package/tests/examples/__snapshots__/example12.snap.svg +33 -30
- package/tests/examples/__snapshots__/example13.snap.svg +10 -4
- package/tests/examples/__snapshots__/example14.snap.svg +72 -66
- package/tests/examples/__snapshots__/example16.snap.svg +33 -27
- package/tests/examples/__snapshots__/example20.snap.svg +29 -23
- package/tests/examples/__snapshots__/example21.snap.svg +77 -65
- package/tests/examples/__snapshots__/example22.snap.svg +31 -25
- package/tests/examples/__snapshots__/example31.snap.svg +12 -12
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
import type { GraphicsObject } from "graphics-debug"
|
|
3
|
+
import { ChipObstacleSpatialIndex } from "lib/data-structures/ChipObstacleSpatialIndex"
|
|
4
|
+
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
5
|
+
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
6
|
+
import {
|
|
7
|
+
getCenterFromAnchor,
|
|
8
|
+
getDimsForOrientation,
|
|
9
|
+
getRectBounds,
|
|
10
|
+
} from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
|
|
11
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
12
|
+
import type { InputPin, InputProblem } from "lib/types/InputProblem"
|
|
13
|
+
import { dir, type FacingDirection } from "lib/utils/dir"
|
|
14
|
+
import { EPS, LABEL_SEARCH_STEP, WICK_CLEARANCE } from "./constants"
|
|
15
|
+
import {
|
|
16
|
+
getConnectorTracePath,
|
|
17
|
+
getMaxSearchDistance,
|
|
18
|
+
getSideDistances,
|
|
19
|
+
isXOrientation,
|
|
20
|
+
isYOrientation,
|
|
21
|
+
rangesOverlap,
|
|
22
|
+
rectsOverlap,
|
|
23
|
+
traceCrossesBoundsInterior,
|
|
24
|
+
tracePathCrossesAnyTrace,
|
|
25
|
+
} from "./geometry"
|
|
26
|
+
import { getPinMap, getTracePins, toNetLabelPlacementPatch } from "./traces"
|
|
27
|
+
import type {
|
|
28
|
+
AvailableNetOrientationSolverParams,
|
|
29
|
+
Bounds,
|
|
30
|
+
CandidateLabel,
|
|
31
|
+
CandidatePhase,
|
|
32
|
+
CandidateStatus,
|
|
33
|
+
ChipSide,
|
|
34
|
+
EvaluatedCandidate,
|
|
35
|
+
} from "./types"
|
|
36
|
+
import { visualizeAvailableNetOrientationSolver } from "./visualize"
|
|
37
|
+
|
|
38
|
+
export class AvailableNetOrientationSolver extends BaseSolver {
|
|
39
|
+
inputProblem: InputProblem
|
|
40
|
+
traces: SolvedTracePath[]
|
|
41
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
42
|
+
|
|
43
|
+
outputNetLabelPlacements: NetLabelPlacement[]
|
|
44
|
+
queuedLabelIndices: number[] = []
|
|
45
|
+
currentLabelIndex: number | null = null
|
|
46
|
+
currentLabel: NetLabelPlacement | null = null
|
|
47
|
+
currentCandidateResults: EvaluatedCandidate[] = []
|
|
48
|
+
|
|
49
|
+
private traceMap: Record<string, SolvedTracePath>
|
|
50
|
+
private chipObstacleSpatialIndex: ChipObstacleSpatialIndex
|
|
51
|
+
private maxSearchDistance: number
|
|
52
|
+
private pinMap: Record<string, InputPin & { chipId: string }>
|
|
53
|
+
|
|
54
|
+
constructor(params: AvailableNetOrientationSolverParams) {
|
|
55
|
+
super()
|
|
56
|
+
this.inputProblem = params.inputProblem
|
|
57
|
+
this.traces = [...params.traces]
|
|
58
|
+
this.netLabelPlacements = params.netLabelPlacements
|
|
59
|
+
this.outputNetLabelPlacements = [...params.netLabelPlacements]
|
|
60
|
+
this.traceMap = Object.fromEntries(
|
|
61
|
+
this.traces.map((trace) => [trace.mspPairId, trace]),
|
|
62
|
+
)
|
|
63
|
+
this.pinMap = getPinMap(params.inputProblem)
|
|
64
|
+
this.chipObstacleSpatialIndex =
|
|
65
|
+
params.inputProblem._chipObstacleSpatialIndex ??
|
|
66
|
+
new ChipObstacleSpatialIndex(params.inputProblem.chips)
|
|
67
|
+
this.maxSearchDistance = getMaxSearchDistance(params.inputProblem)
|
|
68
|
+
this.queuedLabelIndices = this.getProcessableLabelIndices()
|
|
69
|
+
this.setCurrentLabel(this.queuedLabelIndices[0] ?? null)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
override getConstructorParams(): ConstructorParameters<
|
|
73
|
+
typeof AvailableNetOrientationSolver
|
|
74
|
+
>[0] {
|
|
75
|
+
return {
|
|
76
|
+
inputProblem: this.inputProblem,
|
|
77
|
+
traces: this.traces,
|
|
78
|
+
netLabelPlacements: this.netLabelPlacements,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
override _step() {
|
|
83
|
+
const labelIndex = this.queuedLabelIndices.shift()
|
|
84
|
+
if (labelIndex === undefined) {
|
|
85
|
+
this.finish()
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.processLabel(labelIndex)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getOutput() {
|
|
93
|
+
return {
|
|
94
|
+
traces: this.traces,
|
|
95
|
+
netLabelPlacements: this.outputNetLabelPlacements,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private getProcessableLabelIndices() {
|
|
100
|
+
const indices: number[] = []
|
|
101
|
+
|
|
102
|
+
for (let i = 0; i < this.outputNetLabelPlacements.length; i++) {
|
|
103
|
+
if (this.shouldProcessLabel(this.outputNetLabelPlacements[i]!, i)) {
|
|
104
|
+
indices.push(i)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return indices
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private shouldProcessLabel(label: NetLabelPlacement, labelIndex: number) {
|
|
112
|
+
const orientations = this.getAvailableOrientations(label)
|
|
113
|
+
if (orientations.length === 0) return false
|
|
114
|
+
|
|
115
|
+
return !(
|
|
116
|
+
orientations.includes(label.orientation) &&
|
|
117
|
+
this.isLabelPlacementValid(label, labelIndex)
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private processLabel(labelIndex: number) {
|
|
122
|
+
const label = this.outputNetLabelPlacements[labelIndex]!
|
|
123
|
+
this.setCurrentLabel(labelIndex)
|
|
124
|
+
this.currentCandidateResults = []
|
|
125
|
+
|
|
126
|
+
const candidate = this.findCorrectedCandidate(label, labelIndex)
|
|
127
|
+
if (!candidate) return
|
|
128
|
+
|
|
129
|
+
this.applyCandidate(label, candidate, labelIndex)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private applyCandidate(
|
|
133
|
+
label: NetLabelPlacement,
|
|
134
|
+
candidate: CandidateLabel,
|
|
135
|
+
labelIndex: number,
|
|
136
|
+
) {
|
|
137
|
+
this.outputNetLabelPlacements[labelIndex] = {
|
|
138
|
+
...label,
|
|
139
|
+
...toNetLabelPlacementPatch(candidate),
|
|
140
|
+
}
|
|
141
|
+
this.addConnectorTrace(label, candidate, labelIndex)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private addConnectorTrace(
|
|
145
|
+
label: NetLabelPlacement,
|
|
146
|
+
candidate: CandidateLabel,
|
|
147
|
+
labelIndex: number,
|
|
148
|
+
) {
|
|
149
|
+
const tracePath = getConnectorTracePath(
|
|
150
|
+
label.anchorPoint,
|
|
151
|
+
candidate.anchorPoint,
|
|
152
|
+
candidate.orientation,
|
|
153
|
+
)
|
|
154
|
+
if (tracePath.length < 2) return
|
|
155
|
+
|
|
156
|
+
const mspPairId = `available-net-orientation-${labelIndex}-${label.netId ?? label.globalConnNetId}`
|
|
157
|
+
const connectorTrace: SolvedTracePath = {
|
|
158
|
+
mspPairId,
|
|
159
|
+
dcConnNetId: label.dcConnNetId ?? label.globalConnNetId,
|
|
160
|
+
globalConnNetId: label.globalConnNetId,
|
|
161
|
+
userNetId: label.netId,
|
|
162
|
+
pins: getTracePins(label, this.pinMap),
|
|
163
|
+
tracePath,
|
|
164
|
+
mspConnectionPairIds: [mspPairId],
|
|
165
|
+
pinIds: label.pinIds,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
this.traces.push(connectorTrace)
|
|
169
|
+
this.traceMap[mspPairId] = connectorTrace
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private finish() {
|
|
173
|
+
this.currentLabelIndex = null
|
|
174
|
+
this.currentLabel = null
|
|
175
|
+
this.currentCandidateResults = []
|
|
176
|
+
this.solved = true
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private setCurrentLabel(labelIndex: number | null) {
|
|
180
|
+
this.currentLabelIndex = labelIndex
|
|
181
|
+
this.currentLabel =
|
|
182
|
+
labelIndex === null ? null : this.outputNetLabelPlacements[labelIndex]!
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private findCorrectedCandidate(label: NetLabelPlacement, labelIndex: number) {
|
|
186
|
+
const orientations = this.getAvailableOrientations(label)
|
|
187
|
+
const rotatedCandidate = this.findValidRotatedCandidate(
|
|
188
|
+
label,
|
|
189
|
+
labelIndex,
|
|
190
|
+
orientations,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
rotatedCandidate ??
|
|
195
|
+
this.findValidShiftedCandidate(label, orientations[0]!, labelIndex)
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private findValidRotatedCandidate(
|
|
200
|
+
label: NetLabelPlacement,
|
|
201
|
+
labelIndex: number,
|
|
202
|
+
orientations: FacingDirection[],
|
|
203
|
+
) {
|
|
204
|
+
for (const orientation of orientations) {
|
|
205
|
+
const candidate = this.createCandidate(
|
|
206
|
+
label,
|
|
207
|
+
this.getSearchStartAnchor(label, orientation),
|
|
208
|
+
orientation,
|
|
209
|
+
)
|
|
210
|
+
const result = this.evaluateCandidate(
|
|
211
|
+
candidate,
|
|
212
|
+
label,
|
|
213
|
+
labelIndex,
|
|
214
|
+
"rotate",
|
|
215
|
+
)
|
|
216
|
+
this.currentCandidateResults.push(result)
|
|
217
|
+
if (result.status === "valid") {
|
|
218
|
+
result.selected = true
|
|
219
|
+
return result
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return null
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private getAvailableOrientations(label: NetLabelPlacement) {
|
|
227
|
+
const effectiveNetId = label.netId ?? label.globalConnNetId
|
|
228
|
+
return this.inputProblem.availableNetLabelOrientations[effectiveNetId] ?? []
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private findValidShiftedCandidate(
|
|
232
|
+
label: NetLabelPlacement,
|
|
233
|
+
orientation: FacingDirection,
|
|
234
|
+
labelIndex: number,
|
|
235
|
+
) {
|
|
236
|
+
const direction = dir(orientation)
|
|
237
|
+
const initialBaseAnchor = this.getSearchStartAnchor(label, orientation)
|
|
238
|
+
const outwardDirection = this.getPerpendicularOutwardDirection(
|
|
239
|
+
label.anchorPoint,
|
|
240
|
+
orientation,
|
|
241
|
+
)
|
|
242
|
+
const maxSearchDistance = this.getSearchDistanceLimit(label, orientation)
|
|
243
|
+
const maxOutwardDistance =
|
|
244
|
+
outwardDirection.x === 0 && outwardDirection.y === 0
|
|
245
|
+
? 0
|
|
246
|
+
: this.maxSearchDistance
|
|
247
|
+
|
|
248
|
+
for (
|
|
249
|
+
let outwardDistance = 0;
|
|
250
|
+
outwardDistance <= maxOutwardDistance + EPS;
|
|
251
|
+
outwardDistance += LABEL_SEARCH_STEP
|
|
252
|
+
) {
|
|
253
|
+
const baseAnchor = {
|
|
254
|
+
x: initialBaseAnchor.x + outwardDirection.x * outwardDistance,
|
|
255
|
+
y: initialBaseAnchor.y + outwardDirection.y * outwardDistance,
|
|
256
|
+
}
|
|
257
|
+
const candidate = this.findValidCandidateInShiftColumn({
|
|
258
|
+
label,
|
|
259
|
+
labelIndex,
|
|
260
|
+
orientation,
|
|
261
|
+
direction,
|
|
262
|
+
baseAnchor,
|
|
263
|
+
maxSearchDistance,
|
|
264
|
+
outwardDistance,
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
if (candidate) return candidate
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return null
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private findValidCandidateInShiftColumn(params: {
|
|
274
|
+
label: NetLabelPlacement
|
|
275
|
+
labelIndex: number
|
|
276
|
+
orientation: FacingDirection
|
|
277
|
+
direction: Point
|
|
278
|
+
baseAnchor: Point
|
|
279
|
+
maxSearchDistance: number
|
|
280
|
+
outwardDistance: number
|
|
281
|
+
}) {
|
|
282
|
+
const {
|
|
283
|
+
label,
|
|
284
|
+
labelIndex,
|
|
285
|
+
orientation,
|
|
286
|
+
direction,
|
|
287
|
+
baseAnchor,
|
|
288
|
+
maxSearchDistance,
|
|
289
|
+
outwardDistance,
|
|
290
|
+
} = params
|
|
291
|
+
|
|
292
|
+
for (
|
|
293
|
+
let distance = LABEL_SEARCH_STEP;
|
|
294
|
+
distance <= maxSearchDistance + EPS;
|
|
295
|
+
distance += LABEL_SEARCH_STEP
|
|
296
|
+
) {
|
|
297
|
+
const anchorPoint = {
|
|
298
|
+
x: baseAnchor.x + direction.x * distance,
|
|
299
|
+
y: baseAnchor.y + direction.y * distance,
|
|
300
|
+
}
|
|
301
|
+
const candidate = this.createCandidate(label, anchorPoint, orientation)
|
|
302
|
+
const result = this.evaluateCandidate(
|
|
303
|
+
candidate,
|
|
304
|
+
label,
|
|
305
|
+
labelIndex,
|
|
306
|
+
"shift",
|
|
307
|
+
distance,
|
|
308
|
+
outwardDistance,
|
|
309
|
+
)
|
|
310
|
+
this.currentCandidateResults.push(result)
|
|
311
|
+
|
|
312
|
+
if (result.status === "valid") {
|
|
313
|
+
result.selected = true
|
|
314
|
+
return result
|
|
315
|
+
}
|
|
316
|
+
if (result.status === "trace-collision") break
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return null
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private evaluateCandidate(
|
|
323
|
+
candidate: CandidateLabel,
|
|
324
|
+
label: NetLabelPlacement,
|
|
325
|
+
labelIndex: number,
|
|
326
|
+
phase: CandidatePhase,
|
|
327
|
+
distance?: number,
|
|
328
|
+
outwardDistance?: number,
|
|
329
|
+
): EvaluatedCandidate {
|
|
330
|
+
return {
|
|
331
|
+
...candidate,
|
|
332
|
+
phase,
|
|
333
|
+
distance,
|
|
334
|
+
outwardDistance,
|
|
335
|
+
selected: false,
|
|
336
|
+
status: this.getCandidateStatus(candidate, label, labelIndex),
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private getSearchStartAnchor(
|
|
341
|
+
label: NetLabelPlacement,
|
|
342
|
+
orientation: FacingDirection,
|
|
343
|
+
) {
|
|
344
|
+
const anchorPoint = this.getWickOffsetAnchor(label.anchorPoint, orientation)
|
|
345
|
+
const { width, height } = getDimsForOrientation({
|
|
346
|
+
orientation,
|
|
347
|
+
netLabelWidth: this.getNetLabelWidth(label),
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
return this.getSideOffsetAnchor({
|
|
351
|
+
anchorPoint,
|
|
352
|
+
labelAnchorPoint: label.anchorPoint,
|
|
353
|
+
orientation,
|
|
354
|
+
width,
|
|
355
|
+
height,
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private getWickOffsetAnchor(
|
|
360
|
+
anchorPoint: Point,
|
|
361
|
+
orientation: FacingDirection,
|
|
362
|
+
) {
|
|
363
|
+
const direction = dir(orientation)
|
|
364
|
+
return {
|
|
365
|
+
x: anchorPoint.x + direction.x * WICK_CLEARANCE,
|
|
366
|
+
y: anchorPoint.y + direction.y * WICK_CLEARANCE,
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private getSideOffsetAnchor(params: {
|
|
371
|
+
anchorPoint: Point
|
|
372
|
+
labelAnchorPoint: Point
|
|
373
|
+
orientation: FacingDirection
|
|
374
|
+
width: number
|
|
375
|
+
height: number
|
|
376
|
+
}) {
|
|
377
|
+
const { anchorPoint, labelAnchorPoint, orientation, width, height } = params
|
|
378
|
+
const chipSide = this.getChipSideForPoint(labelAnchorPoint)
|
|
379
|
+
if (isYOrientation(orientation) && chipSide === "left") {
|
|
380
|
+
return {
|
|
381
|
+
x: anchorPoint.x - width / 2 - WICK_CLEARANCE,
|
|
382
|
+
y: anchorPoint.y,
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (isYOrientation(orientation) && chipSide === "right") {
|
|
386
|
+
return {
|
|
387
|
+
x: anchorPoint.x + width / 2 + WICK_CLEARANCE,
|
|
388
|
+
y: anchorPoint.y,
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (isXOrientation(orientation) && chipSide === "bottom") {
|
|
392
|
+
return {
|
|
393
|
+
x: anchorPoint.x,
|
|
394
|
+
y: anchorPoint.y - height / 2 - WICK_CLEARANCE,
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (isXOrientation(orientation) && chipSide === "top") {
|
|
398
|
+
return {
|
|
399
|
+
x: anchorPoint.x,
|
|
400
|
+
y: anchorPoint.y + height / 2 + WICK_CLEARANCE,
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return anchorPoint
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
private getSearchDistanceLimit(
|
|
407
|
+
label: NetLabelPlacement,
|
|
408
|
+
orientation: FacingDirection,
|
|
409
|
+
) {
|
|
410
|
+
const { width, height } = getDimsForOrientation({
|
|
411
|
+
orientation,
|
|
412
|
+
netLabelWidth: this.getNetLabelWidth(label),
|
|
413
|
+
})
|
|
414
|
+
const labelLength =
|
|
415
|
+
orientation === "y+" || orientation === "y-" ? height : width
|
|
416
|
+
return Math.min(this.maxSearchDistance, labelLength * 2)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private createCandidate(
|
|
420
|
+
label: NetLabelPlacement,
|
|
421
|
+
anchorPoint: Point,
|
|
422
|
+
orientation: FacingDirection,
|
|
423
|
+
): CandidateLabel {
|
|
424
|
+
const { width, height } = getDimsForOrientation({
|
|
425
|
+
orientation,
|
|
426
|
+
netLabelWidth: this.getNetLabelWidth(label),
|
|
427
|
+
})
|
|
428
|
+
return {
|
|
429
|
+
orientation,
|
|
430
|
+
anchorPoint,
|
|
431
|
+
width,
|
|
432
|
+
height,
|
|
433
|
+
center: getCenterFromAnchor(anchorPoint, orientation, width, height),
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private getNetLabelWidth(label: NetLabelPlacement) {
|
|
438
|
+
if (!label.netId) return undefined
|
|
439
|
+
return this.inputProblem.netConnections.find(
|
|
440
|
+
(connection) => connection.netId === label.netId,
|
|
441
|
+
)?.netLabelWidth
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private isLabelPlacementValid(label: NetLabelPlacement, labelIndex: number) {
|
|
445
|
+
return this.isCandidateValidWithOffset(
|
|
446
|
+
{
|
|
447
|
+
orientation: label.orientation,
|
|
448
|
+
anchorPoint: label.anchorPoint,
|
|
449
|
+
center: label.center,
|
|
450
|
+
width: label.width,
|
|
451
|
+
height: label.height,
|
|
452
|
+
},
|
|
453
|
+
labelIndex,
|
|
454
|
+
)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private isCandidateValidWithOffset(
|
|
458
|
+
candidate: CandidateLabel,
|
|
459
|
+
labelIndex: number,
|
|
460
|
+
) {
|
|
461
|
+
const bounds = this.getOffsetCollisionBounds(candidate)
|
|
462
|
+
return this.getBoundsStatus(bounds, labelIndex) === "valid"
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private getCandidateStatus(
|
|
466
|
+
candidate: CandidateLabel,
|
|
467
|
+
label: NetLabelPlacement,
|
|
468
|
+
labelIndex: number,
|
|
469
|
+
) {
|
|
470
|
+
const boundsStatus = this.getBoundsStatus(
|
|
471
|
+
getRectBounds(candidate.center, candidate.width, candidate.height),
|
|
472
|
+
labelIndex,
|
|
473
|
+
)
|
|
474
|
+
if (boundsStatus !== "valid") return boundsStatus
|
|
475
|
+
|
|
476
|
+
if (
|
|
477
|
+
tracePathCrossesAnyTrace(
|
|
478
|
+
getConnectorTracePath(
|
|
479
|
+
label.anchorPoint,
|
|
480
|
+
candidate.anchorPoint,
|
|
481
|
+
candidate.orientation,
|
|
482
|
+
),
|
|
483
|
+
this.traceMap,
|
|
484
|
+
)
|
|
485
|
+
) {
|
|
486
|
+
return "trace-collision"
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return "valid"
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
private getBoundsStatus(bounds: Bounds, labelIndex: number): CandidateStatus {
|
|
493
|
+
if (this.chipObstacleSpatialIndex.getChipsInBounds(bounds).length > 0) {
|
|
494
|
+
return "chip-collision"
|
|
495
|
+
}
|
|
496
|
+
if (this.sharesChipBoundary(bounds)) {
|
|
497
|
+
return "chip-collision"
|
|
498
|
+
}
|
|
499
|
+
if (traceCrossesBoundsInterior(bounds, this.traceMap)) {
|
|
500
|
+
return "trace-collision"
|
|
501
|
+
}
|
|
502
|
+
if (this.intersectsAnyOtherNetLabel(bounds, labelIndex)) {
|
|
503
|
+
return "netlabel-collision"
|
|
504
|
+
}
|
|
505
|
+
return "valid"
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private getOffsetCollisionBounds(candidate: CandidateLabel) {
|
|
509
|
+
const direction = dir(candidate.orientation)
|
|
510
|
+
const collisionTestOffset = 1e-4
|
|
511
|
+
const testCenter = {
|
|
512
|
+
x: candidate.center.x + direction.x * collisionTestOffset,
|
|
513
|
+
y: candidate.center.y + direction.y * collisionTestOffset,
|
|
514
|
+
}
|
|
515
|
+
return getRectBounds(testCenter, candidate.width, candidate.height)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
private intersectsAnyOtherNetLabel(bounds: Bounds, labelIndex: number) {
|
|
519
|
+
for (let i = 0; i < this.outputNetLabelPlacements.length; i++) {
|
|
520
|
+
if (i === labelIndex) continue
|
|
521
|
+
const label = this.outputNetLabelPlacements[i]!
|
|
522
|
+
const otherBounds = getRectBounds(label.center, label.width, label.height)
|
|
523
|
+
if (rectsOverlap(bounds, otherBounds)) return true
|
|
524
|
+
}
|
|
525
|
+
return false
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
private sharesChipBoundary(bounds: Bounds) {
|
|
529
|
+
for (const chip of this.chipObstacleSpatialIndex.chips) {
|
|
530
|
+
const chipBounds = chip.bounds
|
|
531
|
+
const adjacentToVerticalSide =
|
|
532
|
+
Math.abs(bounds.minX - chipBounds.maxX) <= WICK_CLEARANCE + EPS ||
|
|
533
|
+
Math.abs(bounds.maxX - chipBounds.minX) <= WICK_CLEARANCE + EPS
|
|
534
|
+
const adjacentToHorizontalSide =
|
|
535
|
+
Math.abs(bounds.minY - chipBounds.maxY) <= WICK_CLEARANCE + EPS ||
|
|
536
|
+
Math.abs(bounds.maxY - chipBounds.minY) <= WICK_CLEARANCE + EPS
|
|
537
|
+
|
|
538
|
+
if (
|
|
539
|
+
adjacentToVerticalSide &&
|
|
540
|
+
rangesOverlap(
|
|
541
|
+
bounds.minY,
|
|
542
|
+
bounds.maxY,
|
|
543
|
+
chipBounds.minY,
|
|
544
|
+
chipBounds.maxY,
|
|
545
|
+
)
|
|
546
|
+
) {
|
|
547
|
+
return true
|
|
548
|
+
}
|
|
549
|
+
if (
|
|
550
|
+
adjacentToHorizontalSide &&
|
|
551
|
+
rangesOverlap(
|
|
552
|
+
bounds.minX,
|
|
553
|
+
bounds.maxX,
|
|
554
|
+
chipBounds.minX,
|
|
555
|
+
chipBounds.maxX,
|
|
556
|
+
)
|
|
557
|
+
) {
|
|
558
|
+
return true
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return false
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
private getChipSideForPoint(point: Point) {
|
|
566
|
+
const containingSide = this.getContainingChipSide(point)
|
|
567
|
+
if (containingSide) return containingSide
|
|
568
|
+
|
|
569
|
+
const side = this.getOutsideChipSide(point)
|
|
570
|
+
if (side) return side
|
|
571
|
+
|
|
572
|
+
return this.getNearestChipSide(point)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
private getPerpendicularOutwardDirection(
|
|
576
|
+
point: Point,
|
|
577
|
+
orientation: FacingDirection,
|
|
578
|
+
) {
|
|
579
|
+
const chipSide = this.getChipSideForPoint(point)
|
|
580
|
+
if (isYOrientation(orientation) && chipSide === "left") {
|
|
581
|
+
return { x: -1, y: 0 }
|
|
582
|
+
}
|
|
583
|
+
if (isYOrientation(orientation) && chipSide === "right") {
|
|
584
|
+
return { x: 1, y: 0 }
|
|
585
|
+
}
|
|
586
|
+
if (isXOrientation(orientation) && chipSide === "bottom") {
|
|
587
|
+
return { x: 0, y: -1 }
|
|
588
|
+
}
|
|
589
|
+
if (isXOrientation(orientation) && chipSide === "top") {
|
|
590
|
+
return { x: 0, y: 1 }
|
|
591
|
+
}
|
|
592
|
+
return { x: 0, y: 0 }
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
private getContainingChipSide(point: Point) {
|
|
596
|
+
let nearestSide: ChipSide | null = null
|
|
597
|
+
let nearestDistance = Number.POSITIVE_INFINITY
|
|
598
|
+
|
|
599
|
+
for (const chip of this.chipObstacleSpatialIndex.chips) {
|
|
600
|
+
const bounds = chip.bounds
|
|
601
|
+
if (
|
|
602
|
+
point.x < bounds.minX - EPS ||
|
|
603
|
+
point.x > bounds.maxX + EPS ||
|
|
604
|
+
point.y < bounds.minY - EPS ||
|
|
605
|
+
point.y > bounds.maxY + EPS
|
|
606
|
+
) {
|
|
607
|
+
continue
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
for (const [side, distance] of getSideDistances(point, bounds)) {
|
|
611
|
+
if (distance < nearestDistance) {
|
|
612
|
+
nearestSide = side
|
|
613
|
+
nearestDistance = distance
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
return nearestSide
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
private getOutsideChipSide(point: Point) {
|
|
622
|
+
let nearestSide: ChipSide | null = null
|
|
623
|
+
let nearestDistanceSq = Number.POSITIVE_INFINITY
|
|
624
|
+
|
|
625
|
+
for (const chip of this.chipObstacleSpatialIndex.chips) {
|
|
626
|
+
const bounds = chip.bounds
|
|
627
|
+
const dx =
|
|
628
|
+
point.x < bounds.minX - EPS
|
|
629
|
+
? bounds.minX - point.x
|
|
630
|
+
: point.x > bounds.maxX + EPS
|
|
631
|
+
? point.x - bounds.maxX
|
|
632
|
+
: 0
|
|
633
|
+
const dy =
|
|
634
|
+
point.y < bounds.minY - EPS
|
|
635
|
+
? bounds.minY - point.y
|
|
636
|
+
: point.y > bounds.maxY + EPS
|
|
637
|
+
? point.y - bounds.maxY
|
|
638
|
+
: 0
|
|
639
|
+
|
|
640
|
+
if (dx === 0 && dy === 0) continue
|
|
641
|
+
|
|
642
|
+
const distanceSq = dx ** 2 + dy ** 2
|
|
643
|
+
if (distanceSq >= nearestDistanceSq) continue
|
|
644
|
+
|
|
645
|
+
nearestDistanceSq = distanceSq
|
|
646
|
+
if (dx > 0) {
|
|
647
|
+
nearestSide = point.x < bounds.minX ? "left" : "right"
|
|
648
|
+
} else {
|
|
649
|
+
nearestSide = point.y < bounds.minY ? "bottom" : "top"
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return nearestSide
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
private getNearestChipSide(point: Point) {
|
|
657
|
+
let nearestSide: ChipSide | null = null
|
|
658
|
+
let nearestDistance = Number.POSITIVE_INFINITY
|
|
659
|
+
|
|
660
|
+
for (const chip of this.chipObstacleSpatialIndex.chips) {
|
|
661
|
+
for (const [side, distance] of getSideDistances(point, chip.bounds)) {
|
|
662
|
+
if (distance < nearestDistance) {
|
|
663
|
+
nearestSide = side
|
|
664
|
+
nearestDistance = distance
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return nearestSide
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
override visualize(): GraphicsObject {
|
|
673
|
+
return visualizeAvailableNetOrientationSolver({
|
|
674
|
+
inputProblem: this.inputProblem,
|
|
675
|
+
traces: this.traces,
|
|
676
|
+
outputNetLabelPlacements: this.outputNetLabelPlacements,
|
|
677
|
+
currentLabel: this.currentLabel,
|
|
678
|
+
currentCandidateResults: this.currentCandidateResults,
|
|
679
|
+
solved: this.solved,
|
|
680
|
+
})
|
|
681
|
+
}
|
|
682
|
+
}
|