@tscircuit/schematic-trace-solver 0.0.140 → 0.0.142
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 +31 -1
- package/dist/index.js +323 -22
- package/lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts +216 -17
- package/lib/solvers/InlineNetLabelSolver/alignPortOnlyInlineNetLabelStubs.ts +233 -0
- package/lib/solvers/TraceOverlapShiftSolver/TraceOverlapShiftSolver.ts +60 -4
- package/lib/types/InputProblem.ts +13 -0
- package/package.json +1 -1
- package/tests/assets/inline-net-label-port-stub.json +67 -0
- package/tests/repros/__snapshots__/board-1273-trace-overlap-cycle.snap.svg +542 -0
- package/tests/repros/__snapshots__/repro-tida010076-j4-ground-bus.snap.svg +52 -0
- package/tests/repros/__snapshots__/repro-tida010076-page02.snap.svg +624 -0
- package/tests/repros/assets/board-1273-trace-overlap-cycle.input.json +3519 -0
- package/tests/repros/assets/repro-tida010076-page02.input.json +2964 -0
- package/tests/repros/board-1273-trace-overlap-cycle.test.ts +30 -0
- package/tests/repros/repro-tida010076-j4-ground-bus.test.ts +78 -0
- package/tests/repros/repro-tida010076-page02.test.ts +27 -0
- package/tests/solvers/InlineNetLabelSolver/__snapshots__/inline-net-label-port-stub.snap.svg +64 -0
- package/tests/solvers/InlineNetLabelSolver/align-port-only-inline-net-label-stubs-conflict.test.ts +75 -0
- package/tests/solvers/InlineNetLabelSolver/inline-net-label-port-stub.test.ts +82 -0
|
@@ -3,9 +3,11 @@ import type { GraphicsObject, Rect } from "graphics-debug"
|
|
|
3
3
|
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
4
4
|
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
5
5
|
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
6
|
+
import { getPinDirection } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/getPinDirection"
|
|
6
7
|
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
|
|
7
8
|
import type {
|
|
8
9
|
InputDirectConnection,
|
|
10
|
+
InputNetConnection,
|
|
9
11
|
InputProblem,
|
|
10
12
|
PinId,
|
|
11
13
|
} from "lib/types/InputProblem"
|
|
@@ -15,6 +17,7 @@ import {
|
|
|
15
17
|
type AxisAlignedSegment,
|
|
16
18
|
getAxisAlignedSegments,
|
|
17
19
|
} from "./getAxisAlignedSegments"
|
|
20
|
+
import { alignPortOnlyInlineNetLabelStubs } from "./alignPortOnlyInlineNetLabelStubs"
|
|
18
21
|
|
|
19
22
|
export const DEFAULT_INLINE_NET_LABEL_HEIGHT = 0.18
|
|
20
23
|
|
|
@@ -45,9 +48,15 @@ export const INLINE_NET_LABEL_MAX_SPAN_JOG = 0.4
|
|
|
45
48
|
export interface InlineNetLabelPlacement {
|
|
46
49
|
globalConnNetId: string
|
|
47
50
|
netId?: string
|
|
48
|
-
mspPairId
|
|
51
|
+
mspPairId?: string
|
|
49
52
|
pinIds: PinId[]
|
|
50
53
|
|
|
54
|
+
/**
|
|
55
|
+
* A generated single-ended trace stub. Present only for an eligible
|
|
56
|
+
* single-pin net connection; routed point-to-point traces omit it.
|
|
57
|
+
*/
|
|
58
|
+
stubTracePath?: [Point, Point]
|
|
59
|
+
|
|
51
60
|
/** Axis the text runs along: "x" reads left-to-right, "y" reads bottom-to-top */
|
|
52
61
|
axis: "x" | "y"
|
|
53
62
|
|
|
@@ -96,7 +105,11 @@ export class InlineNetLabelSolver extends BaseSolver {
|
|
|
96
105
|
/** Direct connections that opted in, still waiting to be processed */
|
|
97
106
|
queuedDirectConnections: InputDirectConnection[]
|
|
98
107
|
|
|
108
|
+
/** Single-pin net connections that opted in, still waiting to be processed */
|
|
109
|
+
queuedPortOnlyNetConnections: InputNetConnection[]
|
|
110
|
+
|
|
99
111
|
private tracesByPinPairKey: Map<string, SolvedTracePath[]>
|
|
112
|
+
private hasAlignedPortOnlyStubs = false
|
|
100
113
|
|
|
101
114
|
constructor(input: InlineNetLabelSolverInput) {
|
|
102
115
|
super()
|
|
@@ -107,6 +120,9 @@ export class InlineNetLabelSolver extends BaseSolver {
|
|
|
107
120
|
this.queuedDirectConnections = this.inputProblem.directConnections.filter(
|
|
108
121
|
(dc) => dc.allowInlineNetLabel && dc.netId,
|
|
109
122
|
)
|
|
123
|
+
this.queuedPortOnlyNetConnections = this.inputProblem.netConnections.filter(
|
|
124
|
+
(nc) => nc.allowInlineNetLabel && nc.pinIds.length === 1 && nc.netId,
|
|
125
|
+
)
|
|
110
126
|
|
|
111
127
|
this.tracesByPinPairKey = new Map()
|
|
112
128
|
for (const trace of this.traces) {
|
|
@@ -132,15 +148,140 @@ export class InlineNetLabelSolver extends BaseSolver {
|
|
|
132
148
|
|
|
133
149
|
override _step() {
|
|
134
150
|
const directConnection = this.queuedDirectConnections.shift()
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
|
|
151
|
+
if (directConnection) {
|
|
152
|
+
const placement = this.computeInlinePlacement(directConnection)
|
|
153
|
+
if (placement) {
|
|
154
|
+
this.inlineNetLabelPlacements.push(placement)
|
|
155
|
+
}
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const portOnlyNetConnection = this.queuedPortOnlyNetConnections.shift()
|
|
160
|
+
if (portOnlyNetConnection) {
|
|
161
|
+
const placement = this.computePortOnlyInlinePlacement(
|
|
162
|
+
portOnlyNetConnection,
|
|
163
|
+
)
|
|
164
|
+
if (placement) {
|
|
165
|
+
this.inlineNetLabelPlacements.push(placement)
|
|
166
|
+
}
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!this.hasAlignedPortOnlyStubs) {
|
|
171
|
+
this.inlineNetLabelPlacements = alignPortOnlyInlineNetLabelStubs({
|
|
172
|
+
placements: this.inlineNetLabelPlacements,
|
|
173
|
+
inputProblem: this.inputProblem,
|
|
174
|
+
traces: this.traces,
|
|
175
|
+
netLabelPlacements: this.inputNetLabelPlacements,
|
|
176
|
+
})
|
|
177
|
+
this.hasAlignedPortOnlyStubs = true
|
|
138
178
|
return
|
|
139
179
|
}
|
|
140
180
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
181
|
+
this.solved = true
|
|
182
|
+
this.stats.inlineNetLabelCount = this.inlineNetLabelPlacements.length
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Converts a conventional port-only anchored placement into an inline label
|
|
187
|
+
* on a generated outward stub. The stub follows the pin's true facing
|
|
188
|
+
* direction; an anchored label may finish in another direction after an
|
|
189
|
+
* elbow, which is not the direction a terminal stub should leave the pin.
|
|
190
|
+
*/
|
|
191
|
+
private computePortOnlyInlinePlacement(
|
|
192
|
+
netConnection: InputNetConnection,
|
|
193
|
+
): InlineNetLabelPlacement | null {
|
|
194
|
+
const [pinId] = netConnection.pinIds
|
|
195
|
+
if (!pinId) return null
|
|
196
|
+
|
|
197
|
+
const anchoredPlacement = this.inputNetLabelPlacements.find(
|
|
198
|
+
(placement) =>
|
|
199
|
+
placement.netId === netConnection.netId &&
|
|
200
|
+
placement.pinIds.length === 1 &&
|
|
201
|
+
placement.pinIds[0] === pinId,
|
|
202
|
+
)
|
|
203
|
+
if (!anchoredPlacement) return null
|
|
204
|
+
|
|
205
|
+
const inputChip = this.inputProblem.chips.find((chip) =>
|
|
206
|
+
chip.pins.some((pin) => pin.pinId === pinId),
|
|
207
|
+
)
|
|
208
|
+
const inputPin = inputChip?.pins.find((pin) => pin.pinId === pinId)
|
|
209
|
+
|
|
210
|
+
const height =
|
|
211
|
+
netConnection.inlineNetLabelHeight ?? DEFAULT_INLINE_NET_LABEL_HEIGHT
|
|
212
|
+
const width =
|
|
213
|
+
netConnection.inlineNetLabelWidth ??
|
|
214
|
+
netConnection.netLabelWidth ??
|
|
215
|
+
estimateInlineNetLabelWidth(netConnection.netId, height)
|
|
216
|
+
|
|
217
|
+
// Leave a small wire tail at both ends of the text so it unmistakably
|
|
218
|
+
// reads as a label on a trace rather than free-standing text.
|
|
219
|
+
const stubLength = Math.max(width + 0.2, 0.6)
|
|
220
|
+
const start = inputPin
|
|
221
|
+
? { x: inputPin.x, y: inputPin.y }
|
|
222
|
+
: anchoredPlacement.anchorPoint
|
|
223
|
+
const direction =
|
|
224
|
+
inputPin && inputChip
|
|
225
|
+
? (inputPin._facingDirection ?? getPinDirection(inputPin, inputChip))
|
|
226
|
+
: anchoredPlacement.orientation
|
|
227
|
+
const end: Point =
|
|
228
|
+
direction === "x+"
|
|
229
|
+
? { x: start.x + stubLength, y: start.y }
|
|
230
|
+
: direction === "x-"
|
|
231
|
+
? { x: start.x - stubLength, y: start.y }
|
|
232
|
+
: direction === "y+"
|
|
233
|
+
? { x: start.x, y: start.y + stubLength }
|
|
234
|
+
: { x: start.x, y: start.y - stubLength }
|
|
235
|
+
|
|
236
|
+
const axis: InlineNetLabelPlacement["axis"] =
|
|
237
|
+
direction === "x+" || direction === "x-" ? "x" : "y"
|
|
238
|
+
const side: InlineNetLabelPlacement["side"] = axis === "x" ? "y+" : "x-"
|
|
239
|
+
const anchorPoint = {
|
|
240
|
+
x: (start.x + end.x) / 2,
|
|
241
|
+
y: (start.y + end.y) / 2,
|
|
242
|
+
}
|
|
243
|
+
const offset = height / 2 + INLINE_NET_LABEL_TRACE_MARGIN
|
|
244
|
+
const center: Point =
|
|
245
|
+
side === "y+"
|
|
246
|
+
? { x: anchorPoint.x, y: anchorPoint.y + offset }
|
|
247
|
+
: { x: anchorPoint.x - offset, y: anchorPoint.y }
|
|
248
|
+
|
|
249
|
+
const halfAlong = width / 2
|
|
250
|
+
const halfAcross = height / 2
|
|
251
|
+
const bounds: Bounds =
|
|
252
|
+
axis === "x"
|
|
253
|
+
? {
|
|
254
|
+
minX: center.x - halfAlong,
|
|
255
|
+
maxX: center.x + halfAlong,
|
|
256
|
+
minY: center.y - halfAcross,
|
|
257
|
+
maxY: center.y + halfAcross,
|
|
258
|
+
}
|
|
259
|
+
: {
|
|
260
|
+
minX: center.x - halfAcross,
|
|
261
|
+
maxX: center.x + halfAcross,
|
|
262
|
+
minY: center.y - halfAlong,
|
|
263
|
+
maxY: center.y + halfAlong,
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (
|
|
267
|
+
this.isObstructed(bounds, {
|
|
268
|
+
ownGlobalConnNetId: anchoredPlacement.globalConnNetId,
|
|
269
|
+
})
|
|
270
|
+
) {
|
|
271
|
+
return null
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
globalConnNetId: anchoredPlacement.globalConnNetId,
|
|
276
|
+
netId: netConnection.netId,
|
|
277
|
+
pinIds: [pinId],
|
|
278
|
+
stubTracePath: [start, end],
|
|
279
|
+
axis,
|
|
280
|
+
anchorPoint,
|
|
281
|
+
center,
|
|
282
|
+
width,
|
|
283
|
+
height,
|
|
284
|
+
side,
|
|
144
285
|
}
|
|
145
286
|
}
|
|
146
287
|
|
|
@@ -209,7 +350,13 @@ export class InlineNetLabelSolver extends BaseSolver {
|
|
|
209
350
|
maxY: center.y + halfAlong,
|
|
210
351
|
}
|
|
211
352
|
|
|
212
|
-
if (
|
|
353
|
+
if (
|
|
354
|
+
this.isObstructed(bounds, {
|
|
355
|
+
ownTrace: trace,
|
|
356
|
+
ownGlobalConnNetId: trace.globalConnNetId,
|
|
357
|
+
})
|
|
358
|
+
)
|
|
359
|
+
continue
|
|
213
360
|
|
|
214
361
|
return {
|
|
215
362
|
globalConnNetId: trace.globalConnNetId,
|
|
@@ -334,7 +481,13 @@ export class InlineNetLabelSolver extends BaseSolver {
|
|
|
334
481
|
maxY: center.y + halfAlong,
|
|
335
482
|
}
|
|
336
483
|
|
|
337
|
-
if (
|
|
484
|
+
if (
|
|
485
|
+
this.isObstructed(bounds, {
|
|
486
|
+
ownTrace: trace,
|
|
487
|
+
ownGlobalConnNetId: trace.globalConnNetId,
|
|
488
|
+
})
|
|
489
|
+
)
|
|
490
|
+
continue
|
|
338
491
|
|
|
339
492
|
const anchorPoint: Point =
|
|
340
493
|
axis === "x"
|
|
@@ -363,7 +516,16 @@ export class InlineNetLabelSolver extends BaseSolver {
|
|
|
363
516
|
* An inline label may not sit on top of a chip, a component's text, or a
|
|
364
517
|
* trace belonging to another net.
|
|
365
518
|
*/
|
|
366
|
-
private isObstructed(
|
|
519
|
+
private isObstructed(
|
|
520
|
+
bounds: Bounds,
|
|
521
|
+
{
|
|
522
|
+
ownTrace,
|
|
523
|
+
ownGlobalConnNetId,
|
|
524
|
+
}: {
|
|
525
|
+
ownTrace?: SolvedTracePath
|
|
526
|
+
ownGlobalConnNetId: string
|
|
527
|
+
},
|
|
528
|
+
): boolean {
|
|
367
529
|
for (const chip of this.inputProblem.chips) {
|
|
368
530
|
const chipBounds: Bounds = {
|
|
369
531
|
minX: chip.center.x - chip.width / 2,
|
|
@@ -379,8 +541,8 @@ export class InlineNetLabelSolver extends BaseSolver {
|
|
|
379
541
|
}
|
|
380
542
|
|
|
381
543
|
for (const trace of this.traces) {
|
|
382
|
-
if (trace.mspPairId === ownTrace.mspPairId) continue
|
|
383
|
-
if (trace.globalConnNetId ===
|
|
544
|
+
if (ownTrace && trace.mspPairId === ownTrace.mspPairId) continue
|
|
545
|
+
if (trace.globalConnNetId === ownGlobalConnNetId) continue
|
|
384
546
|
if (doesPathIntersectBounds(trace.tracePath, bounds)) return true
|
|
385
547
|
}
|
|
386
548
|
|
|
@@ -399,10 +561,23 @@ export class InlineNetLabelSolver extends BaseSolver {
|
|
|
399
561
|
return keys
|
|
400
562
|
}
|
|
401
563
|
|
|
564
|
+
private getOutputTraces(supersededNetLabelKeys: Set<string>) {
|
|
565
|
+
return this.traces.filter(
|
|
566
|
+
(trace) =>
|
|
567
|
+
!(
|
|
568
|
+
supersededNetLabelKeys.has(trace.globalConnNetId) &&
|
|
569
|
+
trace.mspPairId.startsWith("available-net-orientation-")
|
|
570
|
+
),
|
|
571
|
+
)
|
|
572
|
+
}
|
|
573
|
+
|
|
402
574
|
getOutput() {
|
|
403
575
|
const superseded = this.getSupersededNetLabelKeys()
|
|
404
576
|
return {
|
|
405
|
-
|
|
577
|
+
// AvailableNetOrientationSolver may have routed an elbow from a port to
|
|
578
|
+
// the anchored label that this inline placement supersedes. Keep the
|
|
579
|
+
// actual net trace, but discard that now-orphaned label connector.
|
|
580
|
+
traces: this.getOutputTraces(superseded),
|
|
406
581
|
netLabelPlacements: this.inputNetLabelPlacements.filter(
|
|
407
582
|
(placement) => !superseded.has(placement.globalConnNetId),
|
|
408
583
|
),
|
|
@@ -420,7 +595,8 @@ export class InlineNetLabelSolver extends BaseSolver {
|
|
|
420
595
|
graphics.points ??= []
|
|
421
596
|
graphics.texts ??= []
|
|
422
597
|
|
|
423
|
-
|
|
598
|
+
const output = this.getOutput()
|
|
599
|
+
for (const trace of output.traces) {
|
|
424
600
|
graphics.lines.push({
|
|
425
601
|
points: trace.tracePath,
|
|
426
602
|
strokeColor: "purple",
|
|
@@ -446,6 +622,12 @@ export class InlineNetLabelSolver extends BaseSolver {
|
|
|
446
622
|
}
|
|
447
623
|
|
|
448
624
|
for (const inlineLabel of this.inlineNetLabelPlacements) {
|
|
625
|
+
if (inlineLabel.stubTracePath) {
|
|
626
|
+
graphics.lines.push({
|
|
627
|
+
points: inlineLabel.stubTracePath,
|
|
628
|
+
strokeColor: "purple",
|
|
629
|
+
})
|
|
630
|
+
}
|
|
449
631
|
const isHorizontal = inlineLabel.axis === "x"
|
|
450
632
|
graphics.rects.push({
|
|
451
633
|
center: inlineLabel.center,
|
|
@@ -460,12 +642,29 @@ export class InlineNetLabelSolver extends BaseSolver {
|
|
|
460
642
|
].join("\n"),
|
|
461
643
|
} as Rect & { strokeColor: string })
|
|
462
644
|
graphics.texts.push({
|
|
463
|
-
x:
|
|
464
|
-
|
|
645
|
+
x:
|
|
646
|
+
inlineLabel.stubTracePath && inlineLabel.axis === "x"
|
|
647
|
+
? inlineLabel.center.x +
|
|
648
|
+
(inlineLabel.stubTracePath[1].x > inlineLabel.stubTracePath[0].x
|
|
649
|
+
? -inlineLabel.width / 2
|
|
650
|
+
: inlineLabel.width / 2)
|
|
651
|
+
: inlineLabel.center.x,
|
|
652
|
+
y:
|
|
653
|
+
inlineLabel.stubTracePath && inlineLabel.axis === "y"
|
|
654
|
+
? inlineLabel.center.y +
|
|
655
|
+
(inlineLabel.stubTracePath[1].y > inlineLabel.stubTracePath[0].y
|
|
656
|
+
? -inlineLabel.width / 2
|
|
657
|
+
: inlineLabel.width / 2)
|
|
658
|
+
: inlineLabel.center.y,
|
|
465
659
|
text: inlineLabel.netId ?? "",
|
|
466
660
|
color: "green",
|
|
467
661
|
fontSize: inlineLabel.height,
|
|
468
|
-
anchorSide:
|
|
662
|
+
anchorSide: inlineLabel.stubTracePath
|
|
663
|
+
? inlineLabel.stubTracePath[1][inlineLabel.axis] >
|
|
664
|
+
inlineLabel.stubTracePath[0][inlineLabel.axis]
|
|
665
|
+
? "center_left"
|
|
666
|
+
: "center_right"
|
|
667
|
+
: "center",
|
|
469
668
|
// Vertical labels read bottom-to-top, alongside the wire they name.
|
|
470
669
|
rotation: inlineLabel.axis === "y" ? 90 : undefined,
|
|
471
670
|
})
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import type { Bounds, Point } from "@tscircuit/math-utils"
|
|
2
|
+
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
3
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
4
|
+
import type { InputProblem } from "lib/types/InputProblem"
|
|
5
|
+
import { boundsOverlap, getTextBoxBounds } from "lib/utils/textBoxBounds"
|
|
6
|
+
import type { InlineNetLabelPlacement } from "./InlineNetLabelSolver"
|
|
7
|
+
|
|
8
|
+
type StubDirection = "x+" | "x-" | "y+" | "y-"
|
|
9
|
+
|
|
10
|
+
const getStubDirection = (path: [Point, Point]): StubDirection | undefined => {
|
|
11
|
+
const [start, end] = path
|
|
12
|
+
if (Math.abs(end.x - start.x) >= Math.abs(end.y - start.y)) {
|
|
13
|
+
return end.x >= start.x ? "x+" : "x-"
|
|
14
|
+
}
|
|
15
|
+
return end.y >= start.y ? "y+" : "y-"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const getStubLength = (path: [Point, Point]) => {
|
|
19
|
+
const [start, end] = path
|
|
20
|
+
return Math.abs(end.x - start.x) + Math.abs(end.y - start.y)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const getLabelBounds = (placement: {
|
|
24
|
+
center: Point
|
|
25
|
+
width: number
|
|
26
|
+
height: number
|
|
27
|
+
axis?: "x" | "y"
|
|
28
|
+
}): Bounds => {
|
|
29
|
+
const isVertical = placement.axis === "y"
|
|
30
|
+
const width = isVertical ? placement.height : placement.width
|
|
31
|
+
const height = isVertical ? placement.width : placement.height
|
|
32
|
+
return {
|
|
33
|
+
minX: placement.center.x - width / 2,
|
|
34
|
+
maxX: placement.center.x + width / 2,
|
|
35
|
+
minY: placement.center.y - height / 2,
|
|
36
|
+
maxY: placement.center.y + height / 2,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const getPathBounds = (path: Point[]): Bounds => ({
|
|
41
|
+
minX: Math.min(...path.map((point) => point.x)),
|
|
42
|
+
maxX: Math.max(...path.map((point) => point.x)),
|
|
43
|
+
minY: Math.min(...path.map((point) => point.y)),
|
|
44
|
+
maxY: Math.max(...path.map((point) => point.y)),
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const doesPathIntersectBounds = (path: Point[], bounds: Bounds): boolean => {
|
|
48
|
+
for (let index = 0; index < path.length - 1; index++) {
|
|
49
|
+
const segmentBounds = getPathBounds([path[index]!, path[index + 1]!])
|
|
50
|
+
if (boundsOverlap(segmentBounds, bounds)) return true
|
|
51
|
+
}
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const resizeStub = (
|
|
56
|
+
placement: InlineNetLabelPlacement,
|
|
57
|
+
direction: StubDirection,
|
|
58
|
+
targetLength: number,
|
|
59
|
+
): InlineNetLabelPlacement => {
|
|
60
|
+
const [start] = placement.stubTracePath!
|
|
61
|
+
const end: Point =
|
|
62
|
+
direction === "x+"
|
|
63
|
+
? { x: start.x + targetLength, y: start.y }
|
|
64
|
+
: direction === "x-"
|
|
65
|
+
? { x: start.x - targetLength, y: start.y }
|
|
66
|
+
: direction === "y+"
|
|
67
|
+
? { x: start.x, y: start.y + targetLength }
|
|
68
|
+
: { x: start.x, y: start.y - targetLength }
|
|
69
|
+
const anchorPoint = {
|
|
70
|
+
x: (start.x + end.x) / 2,
|
|
71
|
+
y: (start.y + end.y) / 2,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
...placement,
|
|
76
|
+
stubTracePath: [start, end],
|
|
77
|
+
anchorPoint,
|
|
78
|
+
// Extending a short row only adds wire at its free end. Keep the label
|
|
79
|
+
// itself next to the pin so terminal labels can be start/end aligned and
|
|
80
|
+
// collision checks continue to describe the rendered text box.
|
|
81
|
+
center: placement.center,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Aligns the free ends of port-only inline-label stubs on each component side.
|
|
87
|
+
*
|
|
88
|
+
* A group is changed atomically: if any longer stub or retained label would
|
|
89
|
+
* collide with a chip, component text, routed trace, retained anchored label,
|
|
90
|
+
* or another inline label/stub, every member keeps its original length.
|
|
91
|
+
*/
|
|
92
|
+
export const alignPortOnlyInlineNetLabelStubs = ({
|
|
93
|
+
placements,
|
|
94
|
+
inputProblem,
|
|
95
|
+
traces,
|
|
96
|
+
netLabelPlacements,
|
|
97
|
+
}: {
|
|
98
|
+
placements: InlineNetLabelPlacement[]
|
|
99
|
+
inputProblem: InputProblem
|
|
100
|
+
traces: SolvedTracePath[]
|
|
101
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
102
|
+
}): InlineNetLabelPlacement[] => {
|
|
103
|
+
const chipIdByPinId = new Map<string, string>()
|
|
104
|
+
for (const chip of inputProblem.chips) {
|
|
105
|
+
for (const pin of chip.pins) chipIdByPinId.set(pin.pinId, chip.chipId)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const groups = new Map<
|
|
109
|
+
string,
|
|
110
|
+
Array<{
|
|
111
|
+
placementIndex: number
|
|
112
|
+
placement: InlineNetLabelPlacement
|
|
113
|
+
direction: StubDirection
|
|
114
|
+
ownerChipId?: string
|
|
115
|
+
}>
|
|
116
|
+
>()
|
|
117
|
+
for (const [placementIndex, placement] of placements.entries()) {
|
|
118
|
+
if (!placement.stubTracePath || placement.pinIds.length !== 1) continue
|
|
119
|
+
const direction = getStubDirection(placement.stubTracePath)
|
|
120
|
+
if (!direction) continue
|
|
121
|
+
const ownerChipId = chipIdByPinId.get(placement.pinIds[0]!)
|
|
122
|
+
const groupKey = `${ownerChipId ?? placement.pinIds[0]}::${direction}`
|
|
123
|
+
const group = groups.get(groupKey) ?? []
|
|
124
|
+
group.push({ placementIndex, placement, direction, ownerChipId })
|
|
125
|
+
groups.set(groupKey, group)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const alignedPlacements = [...placements]
|
|
129
|
+
const supersededGlobalNetIds = new Set(
|
|
130
|
+
placements.map((placement) => placement.globalConnNetId),
|
|
131
|
+
)
|
|
132
|
+
const retainedAnchoredLabelBounds = netLabelPlacements
|
|
133
|
+
.filter(
|
|
134
|
+
(placement) => !supersededGlobalNetIds.has(placement.globalConnNetId),
|
|
135
|
+
)
|
|
136
|
+
.map(getLabelBounds)
|
|
137
|
+
|
|
138
|
+
for (const group of groups.values()) {
|
|
139
|
+
if (group.length < 2) continue
|
|
140
|
+
const targetLength = Math.max(
|
|
141
|
+
...group.map(({ placement }) => getStubLength(placement.stubTracePath!)),
|
|
142
|
+
)
|
|
143
|
+
const proposals = group.map(({ placement, direction }) =>
|
|
144
|
+
resizeStub(placement, direction, targetLength),
|
|
145
|
+
)
|
|
146
|
+
const groupPlacementIndices = new Set(
|
|
147
|
+
group.map(({ placementIndex }) => placementIndex),
|
|
148
|
+
)
|
|
149
|
+
const fixedInlinePlacements = placements.filter(
|
|
150
|
+
(_, placementIndex) => !groupPlacementIndices.has(placementIndex),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
const hasConflict = proposals.some((proposal, proposalIndex) => {
|
|
154
|
+
const labelBounds = getLabelBounds(proposal)
|
|
155
|
+
const stubPath = proposal.stubTracePath!
|
|
156
|
+
const stubBounds = getPathBounds(stubPath)
|
|
157
|
+
const ownerChipId = group[proposalIndex]!.ownerChipId
|
|
158
|
+
|
|
159
|
+
for (const chip of inputProblem.chips) {
|
|
160
|
+
const chipBounds: Bounds = {
|
|
161
|
+
minX: chip.center.x - chip.width / 2,
|
|
162
|
+
maxX: chip.center.x + chip.width / 2,
|
|
163
|
+
minY: chip.center.y - chip.height / 2,
|
|
164
|
+
maxY: chip.center.y + chip.height / 2,
|
|
165
|
+
}
|
|
166
|
+
if (boundsOverlap(labelBounds, chipBounds)) return true
|
|
167
|
+
if (
|
|
168
|
+
chip.chipId !== ownerChipId &&
|
|
169
|
+
boundsOverlap(stubBounds, chipBounds)
|
|
170
|
+
)
|
|
171
|
+
return true
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (const textBox of inputProblem.textBoxes ?? []) {
|
|
175
|
+
const textBounds = getTextBoxBounds(textBox)
|
|
176
|
+
if (
|
|
177
|
+
boundsOverlap(labelBounds, textBounds) ||
|
|
178
|
+
boundsOverlap(stubBounds, textBounds)
|
|
179
|
+
)
|
|
180
|
+
return true
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const trace of traces) {
|
|
184
|
+
if (trace.globalConnNetId === proposal.globalConnNetId) continue
|
|
185
|
+
if (
|
|
186
|
+
doesPathIntersectBounds(trace.tracePath, labelBounds) ||
|
|
187
|
+
doesPathIntersectBounds(trace.tracePath, stubBounds)
|
|
188
|
+
)
|
|
189
|
+
return true
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (
|
|
193
|
+
retainedAnchoredLabelBounds.some(
|
|
194
|
+
(bounds) =>
|
|
195
|
+
boundsOverlap(labelBounds, bounds) ||
|
|
196
|
+
boundsOverlap(stubBounds, bounds),
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
return true
|
|
200
|
+
|
|
201
|
+
for (const fixedPlacement of fixedInlinePlacements) {
|
|
202
|
+
const fixedLabelBounds = getLabelBounds(fixedPlacement)
|
|
203
|
+
if (
|
|
204
|
+
boundsOverlap(labelBounds, fixedLabelBounds) ||
|
|
205
|
+
boundsOverlap(stubBounds, fixedLabelBounds)
|
|
206
|
+
)
|
|
207
|
+
return true
|
|
208
|
+
if (
|
|
209
|
+
fixedPlacement.stubTracePath &&
|
|
210
|
+
(doesPathIntersectBounds(fixedPlacement.stubTracePath, labelBounds) ||
|
|
211
|
+
doesPathIntersectBounds(stubPath, fixedLabelBounds))
|
|
212
|
+
)
|
|
213
|
+
return true
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
for (const [otherIndex, otherProposal] of proposals.entries()) {
|
|
217
|
+
if (otherIndex === proposalIndex) continue
|
|
218
|
+
const otherLabelBounds = getLabelBounds(otherProposal)
|
|
219
|
+
if (boundsOverlap(labelBounds, otherLabelBounds)) return true
|
|
220
|
+
if (doesPathIntersectBounds(stubPath, otherLabelBounds)) return true
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return false
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
if (hasConflict) continue
|
|
227
|
+
for (const [groupIndex, { placementIndex }] of group.entries()) {
|
|
228
|
+
alignedPlacements[placementIndex] = proposals[groupIndex]!
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return alignedPlacements
|
|
233
|
+
}
|
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
import type { MspConnectionPairId } from "../MspConnectionPairSolver/MspConnectionPairSolver"
|
|
12
12
|
|
|
13
13
|
type ConnNetId = string
|
|
14
|
+
type TraceState = Record<MspConnectionPairId, SolvedTracePath>
|
|
15
|
+
|
|
16
|
+
const TRACE_STATE_POSITION_EPSILON = 1e-6
|
|
14
17
|
|
|
15
18
|
/**
|
|
16
19
|
* This solver finds traces that overlap or meet collinearly and aren't
|
|
@@ -44,6 +47,9 @@ export class TraceOverlapShiftSolver extends BaseSolver {
|
|
|
44
47
|
traceNetIslands: Record<ConnNetId, Array<SolvedTracePath>> = {}
|
|
45
48
|
|
|
46
49
|
correctedTraceMap: Record<MspConnectionPairId, SolvedTracePath> = {}
|
|
50
|
+
// Keep only the current and previous layouts to detect a two-state cycle
|
|
51
|
+
// without accumulating routing history for the whole solve.
|
|
52
|
+
recentTraceStates: TraceState[] = []
|
|
47
53
|
|
|
48
54
|
cleanupPhase: "diagonals" | "done" | null = null
|
|
49
55
|
|
|
@@ -64,6 +70,8 @@ export class TraceOverlapShiftSolver extends BaseSolver {
|
|
|
64
70
|
this.correctedTraceMap[mspPairId] = tracePath
|
|
65
71
|
}
|
|
66
72
|
|
|
73
|
+
this.rememberTraceState(this.correctedTraceMap)
|
|
74
|
+
|
|
67
75
|
this.traceNetIslands = this.computeTraceNetIslands()
|
|
68
76
|
}
|
|
69
77
|
|
|
@@ -361,11 +369,19 @@ export class TraceOverlapShiftSolver extends BaseSolver {
|
|
|
361
369
|
|
|
362
370
|
override _step() {
|
|
363
371
|
if (this.activeSubSolver?.solved) {
|
|
364
|
-
|
|
365
|
-
this.
|
|
366
|
-
|
|
367
|
-
this.correctedTraceMap[mspPairId] = newTrace
|
|
372
|
+
const nextTraceState = {
|
|
373
|
+
...this.correctedTraceMap,
|
|
374
|
+
...this.activeSubSolver.correctedTraceMap,
|
|
368
375
|
}
|
|
376
|
+
// Returning to the older retained layout means corrections are bouncing
|
|
377
|
+
// A -> B -> A. Keep B and finish this pass instead of retrying forever.
|
|
378
|
+
if (this.returnsToPreviousTraceState(nextTraceState)) {
|
|
379
|
+
this.activeSubSolver = null
|
|
380
|
+
this.solved = true
|
|
381
|
+
return
|
|
382
|
+
}
|
|
383
|
+
this.correctedTraceMap = nextTraceState
|
|
384
|
+
this.rememberTraceState(nextTraceState)
|
|
369
385
|
this.activeSubSolver = null
|
|
370
386
|
this.traceNetIslands = this.computeTraceNetIslands()
|
|
371
387
|
}
|
|
@@ -407,6 +423,46 @@ export class TraceOverlapShiftSolver extends BaseSolver {
|
|
|
407
423
|
})
|
|
408
424
|
}
|
|
409
425
|
|
|
426
|
+
private rememberTraceState(traceState: TraceState) {
|
|
427
|
+
this.recentTraceStates.push({ ...traceState })
|
|
428
|
+
if (this.recentTraceStates.length > 2) {
|
|
429
|
+
this.recentTraceStates.shift()
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private returnsToPreviousTraceState(candidateTraceState: TraceState) {
|
|
434
|
+
if (this.recentTraceStates.length < 2) return false
|
|
435
|
+
|
|
436
|
+
const previousTraceState = this.recentTraceStates[0]!
|
|
437
|
+
const traceIds = Object.keys(candidateTraceState)
|
|
438
|
+
if (traceIds.length !== Object.keys(previousTraceState).length) return false
|
|
439
|
+
|
|
440
|
+
// Corrections create new objects, so compare the trace geometry itself.
|
|
441
|
+
for (const traceId of traceIds) {
|
|
442
|
+
const candidatePath = candidateTraceState[traceId]
|
|
443
|
+
const previousPath = previousTraceState[traceId]
|
|
444
|
+
if (!candidatePath || !previousPath) return false
|
|
445
|
+
if (candidatePath.tracePath.length !== previousPath.tracePath.length) {
|
|
446
|
+
return false
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
for (let i = 0; i < candidatePath.tracePath.length; i++) {
|
|
450
|
+
const candidatePoint = candidatePath.tracePath[i]!
|
|
451
|
+
const previousPoint = previousPath.tracePath[i]!
|
|
452
|
+
if (
|
|
453
|
+
Math.abs(candidatePoint.x - previousPoint.x) >
|
|
454
|
+
TRACE_STATE_POSITION_EPSILON ||
|
|
455
|
+
Math.abs(candidatePoint.y - previousPoint.y) >
|
|
456
|
+
TRACE_STATE_POSITION_EPSILON
|
|
457
|
+
) {
|
|
458
|
+
return false
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return true
|
|
464
|
+
}
|
|
465
|
+
|
|
410
466
|
override visualize() {
|
|
411
467
|
if (this.activeSubSolver) {
|
|
412
468
|
return this.activeSubSolver.visualize()
|
|
@@ -65,6 +65,19 @@ export interface InputNetConnection {
|
|
|
65
65
|
pinIds: Array<PinId>
|
|
66
66
|
netLabelWidth?: number
|
|
67
67
|
netLabelHeight?: number
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* When true, a named single-pin net may be drawn as a short outward trace
|
|
71
|
+
* stub with its net name placed inline. Multi-pin net connections retain the
|
|
72
|
+
* regular anchored-label behavior.
|
|
73
|
+
*/
|
|
74
|
+
allowInlineNetLabel?: boolean
|
|
75
|
+
|
|
76
|
+
/** Extent of the inline text along the generated trace stub. */
|
|
77
|
+
inlineNetLabelWidth?: number
|
|
78
|
+
|
|
79
|
+
/** Height of the inline text perpendicular to the generated trace stub. */
|
|
80
|
+
inlineNetLabelHeight?: number
|
|
68
81
|
}
|
|
69
82
|
|
|
70
83
|
export interface InputProblem {
|