@tscircuit/schematic-trace-solver 0.0.60 → 0.0.62
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 +77 -6
- package/dist/index.js +445 -8
- package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +131 -11
- package/lib/solvers/AvailableNetOrientationSolver/geometry.ts +1 -1
- package/lib/solvers/AvailableNetOrientationSolver/types.ts +1 -1
- package/lib/solvers/NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver.ts +441 -0
- package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +15 -0
- package/package.json +1 -1
- package/site/examples/example37.page.tsx +4 -0
- package/site/examples/example38.page.tsx +4 -0
- package/site/examples/example39.page.tsx +4 -0
- package/site/examples/example40.page.tsx +4 -0
- package/site/examples/example41.page.tsx +4 -0
- package/tests/assets/example37.json +173 -0
- package/tests/assets/example38.json +32 -0
- package/tests/assets/example39.json +71 -0
- package/tests/assets/example40.json +157 -0
- package/tests/assets/example41.json +81 -0
- package/tests/examples/__snapshots__/example04.snap.svg +16 -17
- package/tests/examples/__snapshots__/example37.snap.svg +320 -0
- package/tests/examples/__snapshots__/example38.snap.svg +77 -0
- package/tests/examples/__snapshots__/example39.snap.svg +107 -0
- package/tests/examples/__snapshots__/example40.snap.svg +231 -0
- package/tests/examples/__snapshots__/example41.snap.svg +119 -0
- package/tests/examples/example37.test.ts +12 -0
- package/tests/examples/example38.test.ts +12 -0
- package/tests/examples/example39.test.ts +12 -0
- package/tests/examples/example40.test.ts +12 -0
- package/tests/examples/example41.test.ts +12 -0
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
isYOrientation,
|
|
21
21
|
rangesOverlap,
|
|
22
22
|
rectsOverlap,
|
|
23
|
+
simplifyOrthogonalPath,
|
|
23
24
|
traceCrossesBoundsInterior,
|
|
24
25
|
tracePathCrossesAnyBounds,
|
|
25
26
|
tracePathCrossesAnyTrace,
|
|
@@ -127,7 +128,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
127
128
|
|
|
128
129
|
private applyCandidate(
|
|
129
130
|
label: NetLabelPlacement,
|
|
130
|
-
candidate:
|
|
131
|
+
candidate: EvaluatedCandidate,
|
|
131
132
|
labelIndex: number,
|
|
132
133
|
) {
|
|
133
134
|
this.outputNetLabelPlacements[labelIndex] = {
|
|
@@ -139,14 +140,33 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
139
140
|
|
|
140
141
|
private addConnectorTrace(
|
|
141
142
|
label: NetLabelPlacement,
|
|
142
|
-
candidate:
|
|
143
|
+
candidate: EvaluatedCandidate,
|
|
143
144
|
labelIndex: number,
|
|
144
145
|
) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
candidate.orientation
|
|
149
|
-
|
|
146
|
+
let tracePath: Point[]
|
|
147
|
+
|
|
148
|
+
if (candidate.phase === "lateral-shift") {
|
|
149
|
+
const orientDir = dir(candidate.orientation)
|
|
150
|
+
const kickedSource = {
|
|
151
|
+
x: label.anchorPoint.x - orientDir.x * LABEL_SEARCH_STEP,
|
|
152
|
+
y: label.anchorPoint.y - orientDir.y * LABEL_SEARCH_STEP,
|
|
153
|
+
}
|
|
154
|
+
tracePath = simplifyOrthogonalPath([
|
|
155
|
+
label.anchorPoint,
|
|
156
|
+
...getConnectorTracePath(
|
|
157
|
+
kickedSource,
|
|
158
|
+
candidate.anchorPoint,
|
|
159
|
+
candidate.orientation,
|
|
160
|
+
),
|
|
161
|
+
])
|
|
162
|
+
} else {
|
|
163
|
+
tracePath = getConnectorTracePath(
|
|
164
|
+
label.anchorPoint,
|
|
165
|
+
candidate.anchorPoint,
|
|
166
|
+
candidate.orientation,
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
150
170
|
if (tracePath.length < 2) return
|
|
151
171
|
|
|
152
172
|
const mspPairId = `available-net-orientation-${labelIndex}-${label.netId ?? label.globalConnNetId}`
|
|
@@ -185,10 +205,19 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
185
205
|
labelIndex,
|
|
186
206
|
orientations,
|
|
187
207
|
)
|
|
208
|
+
if (rotatedCandidate) return rotatedCandidate
|
|
209
|
+
|
|
210
|
+
const shiftedCandidate = this.findValidShiftedCandidate(
|
|
211
|
+
label,
|
|
212
|
+
orientations[0]!,
|
|
213
|
+
labelIndex,
|
|
214
|
+
)
|
|
215
|
+
if (shiftedCandidate) return shiftedCandidate
|
|
188
216
|
|
|
189
|
-
return (
|
|
190
|
-
|
|
191
|
-
|
|
217
|
+
return this.findValidLateralShiftedCandidate(
|
|
218
|
+
label,
|
|
219
|
+
orientations[0]!,
|
|
220
|
+
labelIndex,
|
|
192
221
|
)
|
|
193
222
|
}
|
|
194
223
|
|
|
@@ -266,6 +295,63 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
266
295
|
return null
|
|
267
296
|
}
|
|
268
297
|
|
|
298
|
+
/**
|
|
299
|
+
* When all candidates fail for the current (unshifted) position, try
|
|
300
|
+
* shifting the label anchor laterally — x for y-orientations, y for
|
|
301
|
+
* x-orientations — and re-attempting the required orientation.
|
|
302
|
+
*
|
|
303
|
+
* Offsets are tried in alternating sign order:
|
|
304
|
+
* -1·step, +1·step, -2·step, +2·step, …
|
|
305
|
+
* so the nearest escape routes are tested first.
|
|
306
|
+
*/
|
|
307
|
+
private findValidLateralShiftedCandidate(
|
|
308
|
+
label: NetLabelPlacement,
|
|
309
|
+
orientation: FacingDirection,
|
|
310
|
+
labelIndex: number,
|
|
311
|
+
): EvaluatedCandidate | null {
|
|
312
|
+
const direction = dir(orientation)
|
|
313
|
+
const initialBaseAnchor = this.getSearchStartAnchor(label, orientation)
|
|
314
|
+
|
|
315
|
+
// Lateral axis: perpendicular to the orientation direction
|
|
316
|
+
const lateralDir: Point = {
|
|
317
|
+
x: isYOrientation(orientation) ? 1 : 0,
|
|
318
|
+
y: isXOrientation(orientation) ? 1 : 0,
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const maxSteps = Math.ceil(this.maxSearchDistance / LABEL_SEARCH_STEP)
|
|
322
|
+
|
|
323
|
+
for (let step = 1; step <= maxSteps; step++) {
|
|
324
|
+
for (const sign of [-1, 1]) {
|
|
325
|
+
const lateralOffset = sign * step * LABEL_SEARCH_STEP
|
|
326
|
+
const baseAnchor = {
|
|
327
|
+
x: initialBaseAnchor.x + lateralDir.x * lateralOffset,
|
|
328
|
+
y: initialBaseAnchor.y + lateralDir.y * lateralOffset,
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const maxSearchDistance = this.getLateralColumnMaxDistance(
|
|
332
|
+
label,
|
|
333
|
+
orientation,
|
|
334
|
+
baseAnchor,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
const candidate = this.findValidCandidateInShiftColumn({
|
|
338
|
+
label,
|
|
339
|
+
labelIndex,
|
|
340
|
+
orientation,
|
|
341
|
+
direction,
|
|
342
|
+
baseAnchor,
|
|
343
|
+
maxSearchDistance,
|
|
344
|
+
outwardDistance: lateralOffset,
|
|
345
|
+
phase: "lateral-shift",
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
if (candidate) return candidate
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return null
|
|
353
|
+
}
|
|
354
|
+
|
|
269
355
|
private findValidCandidateInShiftColumn(params: {
|
|
270
356
|
label: NetLabelPlacement
|
|
271
357
|
labelIndex: number
|
|
@@ -274,6 +360,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
274
360
|
baseAnchor: Point
|
|
275
361
|
maxSearchDistance: number
|
|
276
362
|
outwardDistance: number
|
|
363
|
+
phase?: CandidatePhase
|
|
277
364
|
}) {
|
|
278
365
|
const {
|
|
279
366
|
label,
|
|
@@ -283,6 +370,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
283
370
|
baseAnchor,
|
|
284
371
|
maxSearchDistance,
|
|
285
372
|
outwardDistance,
|
|
373
|
+
phase = "shift",
|
|
286
374
|
} = params
|
|
287
375
|
|
|
288
376
|
for (
|
|
@@ -299,7 +387,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
299
387
|
candidate,
|
|
300
388
|
label,
|
|
301
389
|
labelIndex,
|
|
302
|
-
|
|
390
|
+
phase,
|
|
303
391
|
distance,
|
|
304
392
|
outwardDistance,
|
|
305
393
|
)
|
|
@@ -412,6 +500,32 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
412
500
|
return Math.min(this.maxSearchDistance, labelLength * 2)
|
|
413
501
|
}
|
|
414
502
|
|
|
503
|
+
private getLateralColumnMaxDistance(
|
|
504
|
+
label: NetLabelPlacement,
|
|
505
|
+
orientation: FacingDirection,
|
|
506
|
+
baseAnchor: Point,
|
|
507
|
+
) {
|
|
508
|
+
const chipId = label.pinIds
|
|
509
|
+
.map((pid) => this.pinMap[pid]?.chipId)
|
|
510
|
+
.find(Boolean)
|
|
511
|
+
const chip = chipId
|
|
512
|
+
? this.chipObstacleSpatialIndex.chips.find((c) => c.chipId === chipId)
|
|
513
|
+
: null
|
|
514
|
+
|
|
515
|
+
if (chip) {
|
|
516
|
+
if (orientation === "y-")
|
|
517
|
+
return Math.max(0, baseAnchor.y - chip.bounds.minY)
|
|
518
|
+
if (orientation === "y+")
|
|
519
|
+
return Math.max(0, chip.bounds.maxY - baseAnchor.y)
|
|
520
|
+
if (orientation === "x-")
|
|
521
|
+
return Math.max(0, baseAnchor.x - chip.bounds.minX)
|
|
522
|
+
if (orientation === "x+")
|
|
523
|
+
return Math.max(0, chip.bounds.maxX - baseAnchor.x)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return this.getSearchDistanceLimit(label, orientation)
|
|
527
|
+
}
|
|
528
|
+
|
|
415
529
|
private createCandidate(
|
|
416
530
|
label: NetLabelPlacement,
|
|
417
531
|
anchorPoint: Point,
|
|
@@ -474,6 +588,12 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
474
588
|
return "trace-collision"
|
|
475
589
|
}
|
|
476
590
|
|
|
591
|
+
for (const chip of this.chipObstacleSpatialIndex.chips) {
|
|
592
|
+
if (tracePathCrossesAnyBounds(connectorTrace, chip.bounds)) {
|
|
593
|
+
return "chip-collision"
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
477
597
|
for (let i = 0; i < this.outputNetLabelPlacements.length; i++) {
|
|
478
598
|
if (i === labelIndex) continue
|
|
479
599
|
const otherLabel = this.outputNetLabelPlacements[i]!
|
|
@@ -168,7 +168,7 @@ export const getConnectorTracePath = (
|
|
|
168
168
|
: [source, { x: source.x, y: target.y }, target],
|
|
169
169
|
)
|
|
170
170
|
|
|
171
|
-
const simplifyOrthogonalPath = (path: Point[]) => {
|
|
171
|
+
export const simplifyOrthogonalPath = (path: Point[]) => {
|
|
172
172
|
const deduped = path.filter(
|
|
173
173
|
(point, index) => index === 0 || !pointsEqual(point, path[index - 1]!),
|
|
174
174
|
)
|
|
@@ -33,7 +33,7 @@ export type CandidateStatus =
|
|
|
33
33
|
| "trace-collision"
|
|
34
34
|
| "netlabel-collision"
|
|
35
35
|
|
|
36
|
-
export type CandidatePhase = "rotate" | "shift"
|
|
36
|
+
export type CandidatePhase = "rotate" | "shift" | "lateral-shift"
|
|
37
37
|
|
|
38
38
|
export type EvaluatedCandidate = CandidateLabel & {
|
|
39
39
|
status: CandidateStatus
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import type { GraphicsObject } from "graphics-debug"
|
|
2
|
+
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
3
|
+
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
4
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
5
|
+
import type { MspConnectionPairId } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver"
|
|
6
|
+
import type { InputProblem } from "lib/types/InputProblem"
|
|
7
|
+
import type { FacingDirection } from "lib/utils/dir"
|
|
8
|
+
import {
|
|
9
|
+
getDimsForOrientation,
|
|
10
|
+
getCenterFromAnchor,
|
|
11
|
+
getRectBounds,
|
|
12
|
+
} from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
|
|
13
|
+
import { rectIntersectsAnyTrace } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/collisions"
|
|
14
|
+
import { ChipObstacleSpatialIndex } from "lib/data-structures/ChipObstacleSpatialIndex"
|
|
15
|
+
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
|
|
16
|
+
import { getColorFromString } from "lib/utils/getColorFromString"
|
|
17
|
+
|
|
18
|
+
type CandidateStatus =
|
|
19
|
+
| "ok"
|
|
20
|
+
| "chip-collision"
|
|
21
|
+
| "trace-collision"
|
|
22
|
+
| "label-collision"
|
|
23
|
+
|
|
24
|
+
const ANCHOR_TRACE_CLEARANCE = 1e-4
|
|
25
|
+
const SEGMENT_PARALLEL_EPS = 1e-6
|
|
26
|
+
const CANDIDATE_STEP = 0.1
|
|
27
|
+
|
|
28
|
+
const OUTWARD_DIR: Record<FacingDirection, { x: number; y: number }> = {
|
|
29
|
+
"x+": { x: 1, y: 0 },
|
|
30
|
+
"x-": { x: -1, y: 0 },
|
|
31
|
+
"y+": { x: 0, y: 1 },
|
|
32
|
+
"y-": { x: 0, y: -1 },
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const CANDIDATE_STATUS_COLOR: Record<CandidateStatus, string> = {
|
|
36
|
+
ok: "green",
|
|
37
|
+
"label-collision": "orange",
|
|
38
|
+
"trace-collision": "darkorange",
|
|
39
|
+
"chip-collision": "red",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const CANDIDATE_STATUS_FILL: Record<CandidateStatus, string> = {
|
|
43
|
+
ok: "rgba(0, 200, 0, 0.25)",
|
|
44
|
+
"label-collision": "rgba(255, 160, 0, 0.2)",
|
|
45
|
+
"trace-collision": "rgba(200, 80, 0, 0.2)",
|
|
46
|
+
"chip-collision": "rgba(220, 0, 0, 0.15)",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type Candidate = {
|
|
50
|
+
orientation: FacingDirection
|
|
51
|
+
anchor: { x: number; y: number }
|
|
52
|
+
center: { x: number; y: number }
|
|
53
|
+
width: number
|
|
54
|
+
height: number
|
|
55
|
+
bounds: { minX: number; minY: number; maxX: number; maxY: number }
|
|
56
|
+
hostPairId?: MspConnectionPairId
|
|
57
|
+
hostSegIndex?: number
|
|
58
|
+
status: CandidateStatus | null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function boundsOverlap(
|
|
62
|
+
a: { minX: number; minY: number; maxX: number; maxY: number },
|
|
63
|
+
b: { minX: number; minY: number; maxX: number; maxY: number },
|
|
64
|
+
): boolean {
|
|
65
|
+
return (
|
|
66
|
+
a.minX < b.maxX - 1e-9 &&
|
|
67
|
+
a.maxX > b.minX + 1e-9 &&
|
|
68
|
+
a.minY < b.maxY - 1e-9 &&
|
|
69
|
+
a.maxY > b.minY + 1e-9
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function sampleAnchorsAlongSegment(
|
|
74
|
+
a: { x: number; y: number },
|
|
75
|
+
b: { x: number; y: number },
|
|
76
|
+
): Array<{ x: number; y: number }> {
|
|
77
|
+
const dx = b.x - a.x
|
|
78
|
+
const dy = b.y - a.y
|
|
79
|
+
const len = Math.sqrt(dx * dx + dy * dy)
|
|
80
|
+
const steps = Math.max(1, Math.round(len / CANDIDATE_STEP))
|
|
81
|
+
const anchors: Array<{ x: number; y: number }> = []
|
|
82
|
+
for (let k = 0; k <= steps; k++) {
|
|
83
|
+
const t = k / steps
|
|
84
|
+
anchors.push({ x: a.x + t * dx, y: a.y + t * dy })
|
|
85
|
+
}
|
|
86
|
+
return anchors
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface NetLabelNetLabelCollisionSolverParams {
|
|
90
|
+
inputProblem: InputProblem
|
|
91
|
+
traces: SolvedTracePath[]
|
|
92
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export class NetLabelNetLabelCollisionSolver extends BaseSolver {
|
|
96
|
+
inputProblem: InputProblem
|
|
97
|
+
traces: SolvedTracePath[]
|
|
98
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
99
|
+
|
|
100
|
+
outputNetLabelPlacements: NetLabelPlacement[]
|
|
101
|
+
|
|
102
|
+
currentCollision: [NetLabelPlacement, NetLabelPlacement] | null = null
|
|
103
|
+
currentLabelToMove: NetLabelPlacement | null = null
|
|
104
|
+
candidateResults: Candidate[] = []
|
|
105
|
+
|
|
106
|
+
private chipIndex: ChipObstacleSpatialIndex
|
|
107
|
+
private traceMap: Record<MspConnectionPairId, SolvedTracePath>
|
|
108
|
+
private skippedCollisionKeys = new Set<string>()
|
|
109
|
+
|
|
110
|
+
private labelsToTry: NetLabelPlacement[] = []
|
|
111
|
+
private candidateQueue: Candidate[] = []
|
|
112
|
+
private candidateIndex = 0
|
|
113
|
+
|
|
114
|
+
constructor(params: NetLabelNetLabelCollisionSolverParams) {
|
|
115
|
+
super()
|
|
116
|
+
this.inputProblem = params.inputProblem
|
|
117
|
+
this.traces = params.traces
|
|
118
|
+
this.netLabelPlacements = params.netLabelPlacements
|
|
119
|
+
this.outputNetLabelPlacements = [...params.netLabelPlacements]
|
|
120
|
+
this.chipIndex =
|
|
121
|
+
params.inputProblem._chipObstacleSpatialIndex ??
|
|
122
|
+
new ChipObstacleSpatialIndex(params.inputProblem.chips)
|
|
123
|
+
this.traceMap = Object.fromEntries(
|
|
124
|
+
params.traces.map((t) => [t.mspPairId, t]),
|
|
125
|
+
) as Record<MspConnectionPairId, SolvedTracePath>
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
override getConstructorParams(): ConstructorParameters<
|
|
129
|
+
typeof NetLabelNetLabelCollisionSolver
|
|
130
|
+
>[0] {
|
|
131
|
+
return {
|
|
132
|
+
inputProblem: this.inputProblem,
|
|
133
|
+
traces: this.traces,
|
|
134
|
+
netLabelPlacements: this.netLabelPlacements,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
getOutput() {
|
|
139
|
+
return { netLabelPlacements: this.outputNetLabelPlacements }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private labelBounds(label: NetLabelPlacement) {
|
|
143
|
+
return getRectBounds(label.center, label.width, label.height)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private collisionKey(a: NetLabelPlacement, b: NetLabelPlacement) {
|
|
147
|
+
return [a.globalConnNetId, b.globalConnNetId].sort().join("::")
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private findNextCollidingPair():
|
|
151
|
+
| [NetLabelPlacement, NetLabelPlacement]
|
|
152
|
+
| null {
|
|
153
|
+
const labels = this.outputNetLabelPlacements
|
|
154
|
+
for (let i = 0; i < labels.length; i++) {
|
|
155
|
+
for (let j = i + 1; j < labels.length; j++) {
|
|
156
|
+
const a = labels[i]!
|
|
157
|
+
const b = labels[j]!
|
|
158
|
+
if (a.globalConnNetId === b.globalConnNetId) continue
|
|
159
|
+
if (this.skippedCollisionKeys.has(this.collisionKey(a, b))) continue
|
|
160
|
+
if (boundsOverlap(this.labelBounds(a), this.labelBounds(b)))
|
|
161
|
+
return [a, b]
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return null
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private netLabelWidthOf(label: NetLabelPlacement): number | undefined {
|
|
168
|
+
if (label.orientation === "x+" || label.orientation === "x-")
|
|
169
|
+
return label.width
|
|
170
|
+
return label.height
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private buildCandidatesForLabel(label: NetLabelPlacement): Candidate[] {
|
|
174
|
+
const netLabelWidth = this.netLabelWidthOf(label)
|
|
175
|
+
const candidates: Candidate[] = []
|
|
176
|
+
|
|
177
|
+
const buildCandidate = (
|
|
178
|
+
orientation: FacingDirection,
|
|
179
|
+
anchor: { x: number; y: number },
|
|
180
|
+
hostPairId?: MspConnectionPairId,
|
|
181
|
+
hostSegIndex?: number,
|
|
182
|
+
): Candidate => {
|
|
183
|
+
const { width, height } = getDimsForOrientation({
|
|
184
|
+
orientation,
|
|
185
|
+
netLabelWidth,
|
|
186
|
+
})
|
|
187
|
+
const baseCenter = getCenterFromAnchor(anchor, orientation, width, height)
|
|
188
|
+
const outwardDir = OUTWARD_DIR[orientation]
|
|
189
|
+
const center = {
|
|
190
|
+
x: baseCenter.x + outwardDir.x * ANCHOR_TRACE_CLEARANCE,
|
|
191
|
+
y: baseCenter.y + outwardDir.y * ANCHOR_TRACE_CLEARANCE,
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
orientation,
|
|
195
|
+
anchor,
|
|
196
|
+
center,
|
|
197
|
+
width,
|
|
198
|
+
height,
|
|
199
|
+
bounds: getRectBounds(center, width, height),
|
|
200
|
+
hostPairId,
|
|
201
|
+
hostSegIndex,
|
|
202
|
+
status: null,
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const isPortOnly = label.mspConnectionPairIds.length === 0
|
|
207
|
+
|
|
208
|
+
if (isPortOnly) {
|
|
209
|
+
const allOrientations: FacingDirection[] = ["x+", "x-", "y+", "y-"]
|
|
210
|
+
const orderedOrientations = [
|
|
211
|
+
label.orientation,
|
|
212
|
+
...allOrientations.filter((o) => o !== label.orientation),
|
|
213
|
+
]
|
|
214
|
+
for (const orientation of orderedOrientations) {
|
|
215
|
+
candidates.push(buildCandidate(orientation, label.anchorPoint))
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
for (const mspPairId of label.mspConnectionPairIds) {
|
|
219
|
+
const trace = this.traceMap[mspPairId]
|
|
220
|
+
if (!trace) continue
|
|
221
|
+
const pts = trace.tracePath
|
|
222
|
+
for (let si = 0; si < pts.length - 1; si++) {
|
|
223
|
+
const segStart = pts[si]!
|
|
224
|
+
const segEnd = pts[si + 1]!
|
|
225
|
+
const isHorizontal =
|
|
226
|
+
Math.abs(segStart.y - segEnd.y) < SEGMENT_PARALLEL_EPS
|
|
227
|
+
const isVertical =
|
|
228
|
+
Math.abs(segStart.x - segEnd.x) < SEGMENT_PARALLEL_EPS
|
|
229
|
+
if (!isHorizontal && !isVertical) continue
|
|
230
|
+
let perpendicularOrientations: FacingDirection[]
|
|
231
|
+
if (isHorizontal) {
|
|
232
|
+
perpendicularOrientations = ["y+", "y-"]
|
|
233
|
+
} else {
|
|
234
|
+
perpendicularOrientations = ["x+", "x-"]
|
|
235
|
+
}
|
|
236
|
+
for (const anchor of sampleAnchorsAlongSegment(segStart, segEnd)) {
|
|
237
|
+
for (const orientation of perpendicularOrientations) {
|
|
238
|
+
candidates.push(
|
|
239
|
+
buildCandidate(
|
|
240
|
+
orientation,
|
|
241
|
+
anchor,
|
|
242
|
+
mspPairId as MspConnectionPairId,
|
|
243
|
+
si,
|
|
244
|
+
),
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return candidates
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private checkCandidate(
|
|
256
|
+
candidate: Candidate,
|
|
257
|
+
movingLabelNetId: string,
|
|
258
|
+
obstacleLabels: NetLabelPlacement[],
|
|
259
|
+
): CandidateStatus {
|
|
260
|
+
const { bounds, hostPairId, hostSegIndex } = candidate
|
|
261
|
+
|
|
262
|
+
if (this.chipIndex.getChipsInBounds(bounds).length > 0)
|
|
263
|
+
return "chip-collision"
|
|
264
|
+
|
|
265
|
+
if (
|
|
266
|
+
rectIntersectsAnyTrace(bounds, this.traceMap, hostPairId, hostSegIndex)
|
|
267
|
+
.hasIntersection
|
|
268
|
+
) {
|
|
269
|
+
return "trace-collision"
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
for (const obstacle of obstacleLabels) {
|
|
273
|
+
if (obstacle.globalConnNetId === movingLabelNetId) continue
|
|
274
|
+
if (boundsOverlap(bounds, this.labelBounds(obstacle)))
|
|
275
|
+
return "label-collision"
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return "ok"
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private beginSearchForLabel(label: NetLabelPlacement) {
|
|
282
|
+
this.currentLabelToMove = label
|
|
283
|
+
this.candidateQueue = this.buildCandidatesForLabel(label)
|
|
284
|
+
this.candidateIndex = 0
|
|
285
|
+
this.candidateResults = []
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private clearActiveSearch() {
|
|
289
|
+
this.currentCollision = null
|
|
290
|
+
this.currentLabelToMove = null
|
|
291
|
+
this.labelsToTry = []
|
|
292
|
+
this.candidateQueue = []
|
|
293
|
+
this.candidateIndex = 0
|
|
294
|
+
this.candidateResults = []
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
override _step() {
|
|
298
|
+
if (!this.currentCollision) {
|
|
299
|
+
const pair = this.findNextCollidingPair()
|
|
300
|
+
if (!pair) {
|
|
301
|
+
this.solved = true
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
this.currentCollision = pair
|
|
305
|
+
this.labelsToTry = [pair[1], pair[0]]
|
|
306
|
+
this.beginSearchForLabel(this.labelsToTry.shift()!)
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (this.candidateIndex >= this.candidateQueue.length) {
|
|
311
|
+
if (this.labelsToTry.length > 0) {
|
|
312
|
+
this.beginSearchForLabel(this.labelsToTry.shift()!)
|
|
313
|
+
} else {
|
|
314
|
+
this.skippedCollisionKeys.add(
|
|
315
|
+
this.collisionKey(this.currentCollision[0], this.currentCollision[1]),
|
|
316
|
+
)
|
|
317
|
+
this.clearActiveSearch()
|
|
318
|
+
}
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const candidate = this.candidateQueue[this.candidateIndex++]!
|
|
323
|
+
const [labelA, labelB] = this.currentCollision
|
|
324
|
+
let fixedLabel: NetLabelPlacement
|
|
325
|
+
if (this.currentLabelToMove === labelB) {
|
|
326
|
+
fixedLabel = labelA
|
|
327
|
+
} else {
|
|
328
|
+
fixedLabel = labelB
|
|
329
|
+
}
|
|
330
|
+
const obstacleLabels = [
|
|
331
|
+
...this.outputNetLabelPlacements.filter(
|
|
332
|
+
(l) => l !== labelA && l !== labelB,
|
|
333
|
+
),
|
|
334
|
+
fixedLabel,
|
|
335
|
+
]
|
|
336
|
+
|
|
337
|
+
const status = this.checkCandidate(
|
|
338
|
+
candidate,
|
|
339
|
+
this.currentLabelToMove!.globalConnNetId,
|
|
340
|
+
obstacleLabels,
|
|
341
|
+
)
|
|
342
|
+
candidate.status = status
|
|
343
|
+
this.candidateResults.push({ ...candidate })
|
|
344
|
+
|
|
345
|
+
if (status === "ok") {
|
|
346
|
+
const idx = this.outputNetLabelPlacements.indexOf(
|
|
347
|
+
this.currentLabelToMove!,
|
|
348
|
+
)
|
|
349
|
+
if (idx !== -1) {
|
|
350
|
+
this.outputNetLabelPlacements[idx] = {
|
|
351
|
+
...this.currentLabelToMove!,
|
|
352
|
+
orientation: candidate.orientation,
|
|
353
|
+
anchorPoint: candidate.anchor,
|
|
354
|
+
width: candidate.width,
|
|
355
|
+
height: candidate.height,
|
|
356
|
+
center: candidate.center,
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
this.clearActiveSearch()
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
override visualize(): GraphicsObject {
|
|
364
|
+
const graphics = visualizeInputProblem(this.inputProblem)
|
|
365
|
+
if (!graphics.lines) graphics.lines = []
|
|
366
|
+
if (!graphics.rects) graphics.rects = []
|
|
367
|
+
if (!graphics.points) graphics.points = []
|
|
368
|
+
|
|
369
|
+
for (const trace of this.traces) {
|
|
370
|
+
graphics.lines.push({
|
|
371
|
+
points: trace.tracePath,
|
|
372
|
+
strokeColor: "purple",
|
|
373
|
+
} as any)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
for (const label of this.outputNetLabelPlacements) {
|
|
377
|
+
const isInActiveCollision =
|
|
378
|
+
this.currentCollision != null &&
|
|
379
|
+
(label === this.currentCollision[0] ||
|
|
380
|
+
label === this.currentCollision[1])
|
|
381
|
+
|
|
382
|
+
let labelFill: string
|
|
383
|
+
let labelStroke: string
|
|
384
|
+
let labelText: string
|
|
385
|
+
let pointColor: string
|
|
386
|
+
if (isInActiveCollision) {
|
|
387
|
+
labelFill = "rgba(255, 0, 0, 0.2)"
|
|
388
|
+
labelStroke = "red"
|
|
389
|
+
labelText = `netId: ${label.netId}\nglobalConnNetId: ${label.globalConnNetId}\n⚠ COLLIDING`
|
|
390
|
+
pointColor = "red"
|
|
391
|
+
} else {
|
|
392
|
+
labelFill = getColorFromString(label.globalConnNetId, 0.35)
|
|
393
|
+
labelStroke = getColorFromString(label.globalConnNetId, 0.9)
|
|
394
|
+
labelText = `netId: ${label.netId}\nglobalConnNetId: ${label.globalConnNetId}`
|
|
395
|
+
pointColor = getColorFromString(label.globalConnNetId, 0.9)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
graphics.rects.push({
|
|
399
|
+
center: label.center,
|
|
400
|
+
width: label.width,
|
|
401
|
+
height: label.height,
|
|
402
|
+
fill: labelFill,
|
|
403
|
+
strokeColor: labelStroke,
|
|
404
|
+
label: labelText,
|
|
405
|
+
} as any)
|
|
406
|
+
graphics.points.push({
|
|
407
|
+
x: label.anchorPoint.x,
|
|
408
|
+
y: label.anchorPoint.y,
|
|
409
|
+
color: pointColor,
|
|
410
|
+
label: `anchorPoint\norientation: ${label.orientation}`,
|
|
411
|
+
} as any)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const movingNetId = this.currentLabelToMove
|
|
415
|
+
? this.currentLabelToMove.netId
|
|
416
|
+
: "?"
|
|
417
|
+
for (const c of this.candidateResults) {
|
|
418
|
+
const statusColor = CANDIDATE_STATUS_COLOR[c.status!]
|
|
419
|
+
const statusFill = CANDIDATE_STATUS_FILL[c.status!]
|
|
420
|
+
let strokeDash: string | undefined
|
|
421
|
+
if (c.status !== "ok") strokeDash = "4 2"
|
|
422
|
+
graphics.rects.push({
|
|
423
|
+
center: c.center,
|
|
424
|
+
width: c.width,
|
|
425
|
+
height: c.height,
|
|
426
|
+
fill: statusFill,
|
|
427
|
+
strokeColor: statusColor,
|
|
428
|
+
strokeDash,
|
|
429
|
+
label: `candidate: ${c.status}\norientation: ${c.orientation}\nmoving: ${movingNetId}`,
|
|
430
|
+
} as any)
|
|
431
|
+
graphics.points.push({
|
|
432
|
+
x: c.anchor.x,
|
|
433
|
+
y: c.anchor.y,
|
|
434
|
+
color: statusColor,
|
|
435
|
+
label: `candidate anchor\n${c.status}`,
|
|
436
|
+
} as any)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return graphics
|
|
440
|
+
}
|
|
441
|
+
}
|
|
@@ -26,6 +26,7 @@ import { AvailableNetOrientationSolver } from "../AvailableNetOrientationSolver/
|
|
|
26
26
|
import { VccNetLabelCornerPlacementSolver } from "../VccNetLabelCornerPlacementSolver/VccNetLabelCornerPlacementSolver"
|
|
27
27
|
import { TraceAnchoredNetLabelOverlapSolver } from "../TraceAnchoredNetLabelOverlapSolver/TraceAnchoredNetLabelOverlapSolver"
|
|
28
28
|
import { NetLabelTraceCollisionSolver } from "../NetLabelTraceCollisionSolver/NetLabelTraceCollisionSolver"
|
|
29
|
+
import { NetLabelNetLabelCollisionSolver } from "../NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver"
|
|
29
30
|
|
|
30
31
|
type PipelineStep<T extends new (...args: any[]) => BaseSolver> = {
|
|
31
32
|
solverName: string
|
|
@@ -80,6 +81,7 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
80
81
|
vccNetLabelCornerPlacementSolver?: VccNetLabelCornerPlacementSolver
|
|
81
82
|
traceAnchoredNetLabelOverlapSolver?: TraceAnchoredNetLabelOverlapSolver
|
|
82
83
|
netLabelTraceCollisionSolver?: NetLabelTraceCollisionSolver
|
|
84
|
+
netLabelNetLabelCollisionSolver?: NetLabelNetLabelCollisionSolver
|
|
83
85
|
|
|
84
86
|
startTimeOfPhase: Record<string, number>
|
|
85
87
|
endTimeOfPhase: Record<string, number>
|
|
@@ -300,6 +302,19 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
300
302
|
},
|
|
301
303
|
],
|
|
302
304
|
),
|
|
305
|
+
definePipelineStep(
|
|
306
|
+
"netLabelNetLabelCollisionSolver",
|
|
307
|
+
NetLabelNetLabelCollisionSolver,
|
|
308
|
+
(instance) => [
|
|
309
|
+
{
|
|
310
|
+
inputProblem: instance.inputProblem,
|
|
311
|
+
traces: instance.netLabelTraceCollisionSolver!.getOutput().traces,
|
|
312
|
+
netLabelPlacements:
|
|
313
|
+
instance.netLabelTraceCollisionSolver!.getOutput()
|
|
314
|
+
.netLabelPlacements,
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
),
|
|
303
318
|
]
|
|
304
319
|
|
|
305
320
|
constructor(inputProblem: InputProblem) {
|