@tscircuit/schematic-trace-solver 0.0.125 → 0.0.127
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/README.md +28 -0
- package/dist/index.d.ts +159 -2
- package/dist/index.js +739 -36
- package/lib/index.ts +2 -0
- package/lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts +424 -0
- package/lib/solvers/InlineNetLabelSolver/getAxisAlignedSegments.ts +73 -0
- package/lib/solvers/NetLabelTraceCollisionSolver/NetLabelTraceCollisionSolver.ts +15 -0
- package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +86 -11
- package/lib/solvers/TraceElbowTransitionSimplificationSolver/TraceElbowTransitionSimplificationSolver.ts +216 -0
- package/lib/solvers/TraceElbowTransitionSimplificationSolver/generateElbowTransitionSimplificationCandidates.ts +186 -0
- package/lib/solvers/TraceElbowTransitionSimplificationSolver/types.ts +10 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver.ts +3 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/OverlapAvoidanceStepSolver.ts +16 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts +56 -13
- package/lib/types/InputProblem.ts +24 -0
- package/package.json +1 -1
- package/site/bug-reports/bug-report-20260806T093501Z.page.tsx +4 -0
- package/site/examples/inline-net-label01.page.tsx +6 -0
- package/tests/assets/inline-net-label01.json +47 -0
- package/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg +3 -3
- package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +6 -6
- package/tests/bug-reports/bug-report-20260806T093501Z/__snapshots__/bug-report-20260806T093501Z.snap.svg +135 -0
- package/tests/bug-reports/bug-report-20260806T093501Z/bug-report-20260806T093501Z.json +505 -0
- package/tests/bug-reports/bug-report-20260806T093501Z/bug-report-20260806T093501Z.test.ts +66 -0
- package/tests/examples/__snapshots__/example33.snap.svg +1 -1
- package/tests/functions/getAxisAlignedSegments.test.ts +56 -0
- package/tests/solvers/InlineNetLabelSolver/__snapshots__/inline-net-label01.snap.svg +54 -0
- package/tests/solvers/InlineNetLabelSolver/inline-net-label01.test.ts +44 -0
package/lib/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export * from "./solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
|
|
2
2
|
export * from "./types/InputProblem"
|
|
3
|
+
export * from "./solvers/InlineNetLabelSolver/InlineNetLabelSolver"
|
|
4
|
+
export type { NetLabelPlacement } from "./solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
3
5
|
export { SchematicTraceSingleLineSolver2 } from "./solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2"
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import type { Bounds, Point } from "@tscircuit/math-utils"
|
|
2
|
+
import type { GraphicsObject, Rect } from "graphics-debug"
|
|
3
|
+
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
4
|
+
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
5
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
6
|
+
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
|
|
7
|
+
import type {
|
|
8
|
+
InputDirectConnection,
|
|
9
|
+
InputProblem,
|
|
10
|
+
PinId,
|
|
11
|
+
} from "lib/types/InputProblem"
|
|
12
|
+
import { getColorFromString } from "lib/utils/getColorFromString"
|
|
13
|
+
import { boundsOverlap, getTextBoxBounds } from "lib/utils/textBoxBounds"
|
|
14
|
+
import {
|
|
15
|
+
type AxisAlignedSegment,
|
|
16
|
+
getAxisAlignedSegments,
|
|
17
|
+
} from "./getAxisAlignedSegments"
|
|
18
|
+
|
|
19
|
+
export const DEFAULT_INLINE_NET_LABEL_HEIGHT = 0.18
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Gap between the trace and the near edge of the inline label text.
|
|
23
|
+
*/
|
|
24
|
+
export const INLINE_NET_LABEL_TRACE_MARGIN = 0.05
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* How much of the label is allowed to hang off the end of the wire it names,
|
|
28
|
+
* as a fraction of the label's length. A short elbow stub is technically clear
|
|
29
|
+
* of every obstacle but reads as a label floating in space, so runs shorter
|
|
30
|
+
* than this are not considered.
|
|
31
|
+
*/
|
|
32
|
+
export const MIN_INLINE_NET_LABEL_SEGMENT_RATIO = 0.5
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A net label drawn parallel to the trace it names, rather than anchored to the
|
|
36
|
+
* end of it. Used for point-to-point signal traces, where an anchored label
|
|
37
|
+
* would break the visual line between the two pins.
|
|
38
|
+
*
|
|
39
|
+
* `center` is the center of the text box in the schematic's own (unrotated)
|
|
40
|
+
* coordinate space. `width` is the extent of the text along the trace and
|
|
41
|
+
* `height` is its extent perpendicular to the trace, so a vertical label has
|
|
42
|
+
* its `width` running along y.
|
|
43
|
+
*/
|
|
44
|
+
export interface InlineNetLabelPlacement {
|
|
45
|
+
globalConnNetId: string
|
|
46
|
+
netId?: string
|
|
47
|
+
mspPairId: string
|
|
48
|
+
pinIds: PinId[]
|
|
49
|
+
|
|
50
|
+
/** Axis the text runs along: "x" reads left-to-right, "y" reads bottom-to-top */
|
|
51
|
+
axis: "x" | "y"
|
|
52
|
+
|
|
53
|
+
/** Midpoint of the trace segment the label is attached to */
|
|
54
|
+
anchorPoint: Point
|
|
55
|
+
|
|
56
|
+
/** Center of the label text, offset perpendicular to the trace */
|
|
57
|
+
center: Point
|
|
58
|
+
|
|
59
|
+
/** Extent along the trace */
|
|
60
|
+
width: number
|
|
61
|
+
/** Extent perpendicular to the trace */
|
|
62
|
+
height: number
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Which side of the trace the label sits on. For a horizontal trace, "y+" is
|
|
66
|
+
* above; for a vertical trace, "x-" is the left side (which is "above" once
|
|
67
|
+
* the text is rotated).
|
|
68
|
+
*/
|
|
69
|
+
side: "x+" | "x-" | "y+" | "y-"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface InlineNetLabelSolverInput {
|
|
73
|
+
inputProblem: InputProblem
|
|
74
|
+
traces: SolvedTracePath[]
|
|
75
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const getPinPairKey = (pinIds: readonly string[]) =>
|
|
79
|
+
[...pinIds].sort().join("::")
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Places "inline net labels" - net names drawn alongside the trace they belong
|
|
83
|
+
* to - for direct connections that opted in via `allowInlineNetLabel`.
|
|
84
|
+
*
|
|
85
|
+
* Any regular (anchored) net label placement for the same net is dropped, so a
|
|
86
|
+
* net is never labeled twice.
|
|
87
|
+
*/
|
|
88
|
+
export class InlineNetLabelSolver extends BaseSolver {
|
|
89
|
+
inputProblem: InputProblem
|
|
90
|
+
traces: SolvedTracePath[]
|
|
91
|
+
inputNetLabelPlacements: NetLabelPlacement[]
|
|
92
|
+
|
|
93
|
+
inlineNetLabelPlacements: InlineNetLabelPlacement[] = []
|
|
94
|
+
|
|
95
|
+
/** Direct connections that opted in, still waiting to be processed */
|
|
96
|
+
queuedDirectConnections: InputDirectConnection[]
|
|
97
|
+
|
|
98
|
+
private tracesByPinPairKey: Map<string, SolvedTracePath[]>
|
|
99
|
+
|
|
100
|
+
constructor(input: InlineNetLabelSolverInput) {
|
|
101
|
+
super()
|
|
102
|
+
this.inputProblem = input.inputProblem
|
|
103
|
+
this.traces = input.traces
|
|
104
|
+
this.inputNetLabelPlacements = input.netLabelPlacements
|
|
105
|
+
|
|
106
|
+
this.queuedDirectConnections = this.inputProblem.directConnections.filter(
|
|
107
|
+
(dc) => dc.allowInlineNetLabel && dc.netId,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
this.tracesByPinPairKey = new Map()
|
|
111
|
+
for (const trace of this.traces) {
|
|
112
|
+
const key = getPinPairKey(trace.pins.map((p) => p.pinId))
|
|
113
|
+
const existing = this.tracesByPinPairKey.get(key)
|
|
114
|
+
if (existing) {
|
|
115
|
+
existing.push(trace)
|
|
116
|
+
} else {
|
|
117
|
+
this.tracesByPinPairKey.set(key, [trace])
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
override getConstructorParams(): [InlineNetLabelSolverInput] {
|
|
123
|
+
return [
|
|
124
|
+
{
|
|
125
|
+
inputProblem: this.inputProblem,
|
|
126
|
+
traces: this.traces,
|
|
127
|
+
netLabelPlacements: this.inputNetLabelPlacements,
|
|
128
|
+
},
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
override _step() {
|
|
133
|
+
const directConnection = this.queuedDirectConnections.shift()
|
|
134
|
+
if (!directConnection) {
|
|
135
|
+
this.solved = true
|
|
136
|
+
this.stats.inlineNetLabelCount = this.inlineNetLabelPlacements.length
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const placement = this.computeInlinePlacement(directConnection)
|
|
141
|
+
if (placement) {
|
|
142
|
+
this.inlineNetLabelPlacements.push(placement)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private computeInlinePlacement(
|
|
147
|
+
directConnection: InputDirectConnection,
|
|
148
|
+
): InlineNetLabelPlacement | null {
|
|
149
|
+
// Only connections the router actually drew a trace for can carry an inline
|
|
150
|
+
// label - there's nothing to run parallel to otherwise.
|
|
151
|
+
const traces =
|
|
152
|
+
this.tracesByPinPairKey.get(getPinPairKey(directConnection.pinIds)) ?? []
|
|
153
|
+
if (traces.length === 0) return null
|
|
154
|
+
|
|
155
|
+
const trace = traces[0]!
|
|
156
|
+
const segments = getAxisAlignedSegments(trace.tracePath)
|
|
157
|
+
if (segments.length === 0) return null
|
|
158
|
+
|
|
159
|
+
const height =
|
|
160
|
+
directConnection.inlineNetLabelHeight ?? DEFAULT_INLINE_NET_LABEL_HEIGHT
|
|
161
|
+
const width =
|
|
162
|
+
directConnection.inlineNetLabelWidth ??
|
|
163
|
+
directConnection.netLabelWidth ??
|
|
164
|
+
estimateInlineNetLabelWidth(directConnection.netId!, height)
|
|
165
|
+
|
|
166
|
+
const offset = height / 2 + INLINE_NET_LABEL_TRACE_MARGIN
|
|
167
|
+
|
|
168
|
+
// Runs the label fully fits on come first, then longer-to-shorter among the
|
|
169
|
+
// runs it may overhang. Within a run, try the preferred side before the far
|
|
170
|
+
// side, and positions near the middle before ones near the ends.
|
|
171
|
+
const usableSegments = segments
|
|
172
|
+
.filter(
|
|
173
|
+
(segment) =>
|
|
174
|
+
segment.length >= width * MIN_INLINE_NET_LABEL_SEGMENT_RATIO,
|
|
175
|
+
)
|
|
176
|
+
.sort((a, b) => {
|
|
177
|
+
const aFits = a.length >= width
|
|
178
|
+
const bFits = b.length >= width
|
|
179
|
+
if (aFits !== bFits) return aFits ? -1 : 1
|
|
180
|
+
return b.length - a.length
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
for (const segment of usableSegments) {
|
|
184
|
+
// "Above" the trace: +y for a horizontal trace, and -x for a vertical one
|
|
185
|
+
// (rotating the text 90deg counter-clockwise maps "above" onto the left).
|
|
186
|
+
const sides: InlineNetLabelPlacement["side"][] =
|
|
187
|
+
segment.axis === "x" ? ["y+", "y-"] : ["x-", "x+"]
|
|
188
|
+
|
|
189
|
+
for (const side of sides) {
|
|
190
|
+
for (const anchorPoint of getAnchorCandidates(segment, width)) {
|
|
191
|
+
const center =
|
|
192
|
+
side === "y+"
|
|
193
|
+
? { x: anchorPoint.x, y: anchorPoint.y + offset }
|
|
194
|
+
: side === "y-"
|
|
195
|
+
? { x: anchorPoint.x, y: anchorPoint.y - offset }
|
|
196
|
+
: side === "x-"
|
|
197
|
+
? { x: anchorPoint.x - offset, y: anchorPoint.y }
|
|
198
|
+
: { x: anchorPoint.x + offset, y: anchorPoint.y }
|
|
199
|
+
|
|
200
|
+
const halfAlong = width / 2
|
|
201
|
+
const halfAcross = height / 2
|
|
202
|
+
const bounds: Bounds =
|
|
203
|
+
segment.axis === "x"
|
|
204
|
+
? {
|
|
205
|
+
minX: center.x - halfAlong,
|
|
206
|
+
maxX: center.x + halfAlong,
|
|
207
|
+
minY: center.y - halfAcross,
|
|
208
|
+
maxY: center.y + halfAcross,
|
|
209
|
+
}
|
|
210
|
+
: {
|
|
211
|
+
minX: center.x - halfAcross,
|
|
212
|
+
maxX: center.x + halfAcross,
|
|
213
|
+
minY: center.y - halfAlong,
|
|
214
|
+
maxY: center.y + halfAlong,
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (this.isObstructed(bounds, trace)) continue
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
globalConnNetId: trace.globalConnNetId,
|
|
221
|
+
netId: directConnection.netId,
|
|
222
|
+
mspPairId: trace.mspPairId,
|
|
223
|
+
pinIds: [...directConnection.pinIds],
|
|
224
|
+
axis: segment.axis,
|
|
225
|
+
anchorPoint,
|
|
226
|
+
center,
|
|
227
|
+
width,
|
|
228
|
+
height,
|
|
229
|
+
side,
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// No room anywhere along the trace. Leave the net to the regular anchored
|
|
236
|
+
// net label rather than drawing the name over a chip.
|
|
237
|
+
return null
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* An inline label may not sit on top of a chip, a component's text, or a
|
|
242
|
+
* trace belonging to another net.
|
|
243
|
+
*/
|
|
244
|
+
private isObstructed(bounds: Bounds, ownTrace: SolvedTracePath): boolean {
|
|
245
|
+
for (const chip of this.inputProblem.chips) {
|
|
246
|
+
const chipBounds: Bounds = {
|
|
247
|
+
minX: chip.center.x - chip.width / 2,
|
|
248
|
+
maxX: chip.center.x + chip.width / 2,
|
|
249
|
+
minY: chip.center.y - chip.height / 2,
|
|
250
|
+
maxY: chip.center.y + chip.height / 2,
|
|
251
|
+
}
|
|
252
|
+
if (boundsOverlap(bounds, chipBounds)) return true
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
for (const textBox of this.inputProblem.textBoxes ?? []) {
|
|
256
|
+
if (boundsOverlap(bounds, getTextBoxBounds(textBox))) return true
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
for (const trace of this.traces) {
|
|
260
|
+
if (trace.mspPairId === ownTrace.mspPairId) continue
|
|
261
|
+
if (trace.globalConnNetId === ownTrace.globalConnNetId) continue
|
|
262
|
+
if (doesPathIntersectBounds(trace.tracePath, bounds)) return true
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return false
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Net label placements superseded by an inline label. A net gets one label or
|
|
270
|
+
* the other, never both.
|
|
271
|
+
*/
|
|
272
|
+
private getSupersededNetLabelKeys(): Set<string> {
|
|
273
|
+
const keys = new Set<string>()
|
|
274
|
+
for (const placement of this.inlineNetLabelPlacements) {
|
|
275
|
+
keys.add(placement.globalConnNetId)
|
|
276
|
+
}
|
|
277
|
+
return keys
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
getOutput() {
|
|
281
|
+
const superseded = this.getSupersededNetLabelKeys()
|
|
282
|
+
return {
|
|
283
|
+
traces: this.traces,
|
|
284
|
+
netLabelPlacements: this.inputNetLabelPlacements.filter(
|
|
285
|
+
(placement) => !superseded.has(placement.globalConnNetId),
|
|
286
|
+
),
|
|
287
|
+
inlineNetLabelPlacements: this.inlineNetLabelPlacements,
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
override visualize(): GraphicsObject {
|
|
292
|
+
// Mirrors the previous pipeline stage's visualization so that a problem
|
|
293
|
+
// with no inline labels renders identically, then layers the inline labels
|
|
294
|
+
// on top.
|
|
295
|
+
const graphics = visualizeInputProblem(this.inputProblem)
|
|
296
|
+
graphics.lines ??= []
|
|
297
|
+
graphics.rects ??= []
|
|
298
|
+
graphics.points ??= []
|
|
299
|
+
graphics.texts ??= []
|
|
300
|
+
|
|
301
|
+
for (const trace of this.traces) {
|
|
302
|
+
graphics.lines.push({
|
|
303
|
+
points: trace.tracePath,
|
|
304
|
+
strokeColor: "purple",
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const { netLabelPlacements } = this.getOutput()
|
|
309
|
+
for (const label of netLabelPlacements) {
|
|
310
|
+
graphics.rects.push({
|
|
311
|
+
center: label.center,
|
|
312
|
+
width: label.width,
|
|
313
|
+
height: label.height,
|
|
314
|
+
fill: getColorFromString(label.globalConnNetId, 0.35),
|
|
315
|
+
strokeColor: getColorFromString(label.globalConnNetId, 0.9),
|
|
316
|
+
label: `netId: ${label.netId}\nglobalConnNetId: ${label.globalConnNetId}`,
|
|
317
|
+
} as Rect & { strokeColor: string })
|
|
318
|
+
graphics.points.push({
|
|
319
|
+
x: label.anchorPoint.x,
|
|
320
|
+
y: label.anchorPoint.y,
|
|
321
|
+
color: getColorFromString(label.globalConnNetId, 0.9),
|
|
322
|
+
label: `anchorPoint\norientation: ${label.orientation}`,
|
|
323
|
+
})
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
for (const inlineLabel of this.inlineNetLabelPlacements) {
|
|
327
|
+
const isHorizontal = inlineLabel.axis === "x"
|
|
328
|
+
graphics.rects.push({
|
|
329
|
+
center: inlineLabel.center,
|
|
330
|
+
width: isHorizontal ? inlineLabel.width : inlineLabel.height,
|
|
331
|
+
height: isHorizontal ? inlineLabel.height : inlineLabel.width,
|
|
332
|
+
fill: getColorFromString(inlineLabel.globalConnNetId, 0.35),
|
|
333
|
+
strokeColor: "green",
|
|
334
|
+
label: [
|
|
335
|
+
`INLINE netId: ${inlineLabel.netId}`,
|
|
336
|
+
`axis: ${inlineLabel.axis}`,
|
|
337
|
+
`side: ${inlineLabel.side}`,
|
|
338
|
+
].join("\n"),
|
|
339
|
+
} as Rect & { strokeColor: string })
|
|
340
|
+
graphics.texts.push({
|
|
341
|
+
x: inlineLabel.center.x,
|
|
342
|
+
y: inlineLabel.center.y,
|
|
343
|
+
text: inlineLabel.netId ?? "",
|
|
344
|
+
color: "green",
|
|
345
|
+
fontSize: inlineLabel.height,
|
|
346
|
+
anchorSide: "center",
|
|
347
|
+
})
|
|
348
|
+
graphics.points.push({
|
|
349
|
+
x: inlineLabel.anchorPoint.x,
|
|
350
|
+
y: inlineLabel.anchorPoint.y,
|
|
351
|
+
color: "green",
|
|
352
|
+
label: `inline anchor\n${inlineLabel.netId}`,
|
|
353
|
+
})
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return graphics
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Mirrors the net label text width used by @tscircuit/core so a label that core
|
|
362
|
+
* did not measure for us still reserves a sane amount of space.
|
|
363
|
+
*/
|
|
364
|
+
export const estimateInlineNetLabelWidth = (
|
|
365
|
+
text: string,
|
|
366
|
+
fontSize = DEFAULT_INLINE_NET_LABEL_HEIGHT,
|
|
367
|
+
) => {
|
|
368
|
+
const fontScale = fontSize / 0.18
|
|
369
|
+
return text.length * 0.12 * fontScale + 0.12 * fontScale
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Points along a segment where the label's midpoint could sit, ordered from the
|
|
374
|
+
* middle of the segment outwards. Sliding the label along the wire lets it
|
|
375
|
+
* dodge a chip that only crowds one end.
|
|
376
|
+
*
|
|
377
|
+
* When the label is shorter than the segment, candidates are limited to
|
|
378
|
+
* positions that keep it fully on the wire; when it is longer, only the
|
|
379
|
+
* midpoint is offered (it will overhang either way).
|
|
380
|
+
*/
|
|
381
|
+
const getAnchorCandidates = (
|
|
382
|
+
segment: AxisAlignedSegment,
|
|
383
|
+
labelWidth: number,
|
|
384
|
+
step = 0.1,
|
|
385
|
+
): Point[] => {
|
|
386
|
+
const along = segment.axis === "x" ? "x" : "y"
|
|
387
|
+
const start = segment.start[along]
|
|
388
|
+
const end = segment.end[along]
|
|
389
|
+
const mid = (start + end) / 2
|
|
390
|
+
const direction = Math.sign(end - start) || 1
|
|
391
|
+
|
|
392
|
+
const pointAt = (value: number): Point =>
|
|
393
|
+
segment.axis === "x"
|
|
394
|
+
? { x: value, y: segment.start.y }
|
|
395
|
+
: { x: segment.start.x, y: segment.start.y + (value - start) }
|
|
396
|
+
|
|
397
|
+
const slack = segment.length - labelWidth
|
|
398
|
+
if (slack <= 0) return [pointAt(mid)]
|
|
399
|
+
|
|
400
|
+
const offsets: number[] = [0]
|
|
401
|
+
for (let offset = step; offset <= slack / 2 + 1e-9; offset += step) {
|
|
402
|
+
offsets.push(offset, -offset)
|
|
403
|
+
}
|
|
404
|
+
// Always consider both extremes, even when they fall between samples.
|
|
405
|
+
offsets.push(slack / 2, -slack / 2)
|
|
406
|
+
|
|
407
|
+
return offsets.map((offset) => pointAt(mid + offset * direction))
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const doesPathIntersectBounds = (path: Point[], bounds: Bounds): boolean => {
|
|
411
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
412
|
+
const a = path[i]!
|
|
413
|
+
const b = path[i + 1]!
|
|
414
|
+
const segmentBounds: Bounds = {
|
|
415
|
+
minX: Math.min(a.x, b.x),
|
|
416
|
+
maxX: Math.max(a.x, b.x),
|
|
417
|
+
minY: Math.min(a.y, b.y),
|
|
418
|
+
maxY: Math.max(a.y, b.y),
|
|
419
|
+
}
|
|
420
|
+
// Trace segments are axis-aligned, so their bounding box is the segment.
|
|
421
|
+
if (boundsOverlap(segmentBounds, bounds)) return true
|
|
422
|
+
}
|
|
423
|
+
return false
|
|
424
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
|
|
3
|
+
export interface AxisAlignedSegment {
|
|
4
|
+
start: Point
|
|
5
|
+
end: Point
|
|
6
|
+
axis: "x" | "y"
|
|
7
|
+
length: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Splits a trace path into its axis-aligned runs - the straight stretches of
|
|
12
|
+
* wire an inline net label can sit alongside without crossing a corner -
|
|
13
|
+
* ordered longest first.
|
|
14
|
+
*
|
|
15
|
+
* Segments that continue in the same direction are merged, so a straight
|
|
16
|
+
* stretch split into several points counts as one run. A reversal (a trace
|
|
17
|
+
* doubling back on itself) starts a new run, and diagonal segments are dropped
|
|
18
|
+
* since they have no well-defined "parallel" direction.
|
|
19
|
+
*/
|
|
20
|
+
export const getAxisAlignedSegments = (
|
|
21
|
+
path: Point[],
|
|
22
|
+
epsilon = 1e-6,
|
|
23
|
+
): AxisAlignedSegment[] => {
|
|
24
|
+
const segments: AxisAlignedSegment[] = []
|
|
25
|
+
|
|
26
|
+
let runStart: Point | null = null
|
|
27
|
+
let runAxis: "x" | "y" | null = null
|
|
28
|
+
let runSign = 0
|
|
29
|
+
|
|
30
|
+
const closeRun = (runEnd: Point) => {
|
|
31
|
+
if (runStart && runAxis) {
|
|
32
|
+
const length =
|
|
33
|
+
runAxis === "x"
|
|
34
|
+
? Math.abs(runEnd.x - runStart.x)
|
|
35
|
+
: Math.abs(runEnd.y - runStart.y)
|
|
36
|
+
if (length > epsilon) {
|
|
37
|
+
segments.push({ start: runStart, end: runEnd, axis: runAxis, length })
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
runStart = null
|
|
41
|
+
runAxis = null
|
|
42
|
+
runSign = 0
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
46
|
+
const a = path[i]!
|
|
47
|
+
const b = path[i + 1]!
|
|
48
|
+
const dx = b.x - a.x
|
|
49
|
+
const dy = b.y - a.y
|
|
50
|
+
|
|
51
|
+
if (Math.abs(dx) <= epsilon && Math.abs(dy) <= epsilon) continue
|
|
52
|
+
const axis: "x" | "y" | null =
|
|
53
|
+
Math.abs(dy) <= epsilon ? "x" : Math.abs(dx) <= epsilon ? "y" : null
|
|
54
|
+
if (!axis) {
|
|
55
|
+
closeRun(a)
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
const sign = Math.sign(axis === "x" ? dx : dy)
|
|
59
|
+
|
|
60
|
+
if (runStart && runAxis === axis && runSign === sign) continue
|
|
61
|
+
|
|
62
|
+
closeRun(a)
|
|
63
|
+
runStart = a
|
|
64
|
+
runAxis = axis
|
|
65
|
+
runSign = sign
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (path.length >= 2) {
|
|
69
|
+
closeRun(path[path.length - 1]!)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return segments.sort((a, b) => b.length - a.length)
|
|
73
|
+
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { segmentIntersectsRect } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/collisions"
|
|
13
13
|
import { getColorFromString } from "lib/utils/getColorFromString"
|
|
14
14
|
import type { InputProblem } from "lib/types/InputProblem"
|
|
15
|
+
import type { CompletedTraceReroute } from "lib/solvers/TraceElbowTransitionSimplificationSolver/types"
|
|
15
16
|
|
|
16
17
|
const ON_PATH_EPS = 1e-6
|
|
17
18
|
|
|
@@ -172,6 +173,7 @@ export class NetLabelTraceCollisionSolver extends BaseSolver {
|
|
|
172
173
|
|
|
173
174
|
outputTraces: SolvedTracePath[]
|
|
174
175
|
outputNetLabelPlacements: NetLabelPlacement[]
|
|
176
|
+
completedReroutes: CompletedTraceReroute[] = []
|
|
175
177
|
|
|
176
178
|
override activeSubSolver: SingleOverlapSolver | null = null
|
|
177
179
|
private recentlyFailed = new Set<string>()
|
|
@@ -208,6 +210,17 @@ export class NetLabelTraceCollisionSolver extends BaseSolver {
|
|
|
208
210
|
if (this.activeSubSolver.solved) {
|
|
209
211
|
const solvedPath = this.activeSubSolver.solvedTracePath
|
|
210
212
|
if (solvedPath) {
|
|
213
|
+
this.completedReroutes.push({
|
|
214
|
+
initialTrace: {
|
|
215
|
+
...this.activeSubSolver.initialTrace,
|
|
216
|
+
tracePath: this.activeSubSolver.initialTrace.tracePath.map(
|
|
217
|
+
(point) => ({ ...point }),
|
|
218
|
+
),
|
|
219
|
+
},
|
|
220
|
+
reroutedTracePath: solvedPath.map((point) => ({ ...point })),
|
|
221
|
+
label: this.activeSubSolver.label,
|
|
222
|
+
detourCount: this.activeSubSolver.detourCount,
|
|
223
|
+
})
|
|
211
224
|
const idx = this.outputTraces.findIndex(
|
|
212
225
|
(t) => t.mspPairId === this.activeSubSolver!.initialTrace.mspPairId,
|
|
213
226
|
)
|
|
@@ -282,6 +295,7 @@ export class NetLabelTraceCollisionSolver extends BaseSolver {
|
|
|
282
295
|
paddingBuffer: PADDING_BUFFER,
|
|
283
296
|
detourCount,
|
|
284
297
|
tracesToAvoidOverlapping: this.outputTraces,
|
|
298
|
+
netLabelPlacements: this.outputNetLabelPlacements,
|
|
285
299
|
})
|
|
286
300
|
}
|
|
287
301
|
|
|
@@ -319,6 +333,7 @@ export class NetLabelTraceCollisionSolver extends BaseSolver {
|
|
|
319
333
|
getOutput() {
|
|
320
334
|
return {
|
|
321
335
|
traces: this.outputTraces,
|
|
336
|
+
completedReroutes: this.completedReroutes,
|
|
322
337
|
netLabelPlacements: this.outputNetLabelPlacements,
|
|
323
338
|
}
|
|
324
339
|
}
|
|
@@ -30,6 +30,8 @@ import { NetLabelTraceCollisionSolver } from "../NetLabelTraceCollisionSolver/Ne
|
|
|
30
30
|
import { NetLabelNetLabelCollisionSolver } from "../NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver"
|
|
31
31
|
import { UnroutedTraceRecoverySolver } from "../UnroutedTraceRecoverySolver/UnroutedTraceRecoverySolver"
|
|
32
32
|
import { SameNetJunctionAlignmentSolver } from "../SameNetJunctionAlignmentSolver/SameNetJunctionAlignmentSolver"
|
|
33
|
+
import { TraceElbowTransitionSimplificationSolver } from "../TraceElbowTransitionSimplificationSolver/TraceElbowTransitionSimplificationSolver"
|
|
34
|
+
import { InlineNetLabelSolver } from "../InlineNetLabelSolver/InlineNetLabelSolver"
|
|
33
35
|
|
|
34
36
|
type PipelineStep<T extends new (...args: any[]) => BaseSolver> = {
|
|
35
37
|
solverName: string
|
|
@@ -88,7 +90,11 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
88
90
|
netLabelTraceCollisionSolver?: NetLabelTraceCollisionSolver
|
|
89
91
|
traceCleanupSolver2?: TraceCleanupSolver
|
|
90
92
|
netLabelNetLabelCollisionSolver?: NetLabelNetLabelCollisionSolver
|
|
93
|
+
traceElbowTransitionSimplificationSolver?: TraceElbowTransitionSimplificationSolver
|
|
94
|
+
preAlignmentTraceElbowTransitionSimplificationSolver?: TraceElbowTransitionSimplificationSolver
|
|
95
|
+
finalTraceElbowTransitionSimplificationSolver?: TraceElbowTransitionSimplificationSolver
|
|
91
96
|
sameNetJunctionAlignmentSolver?: SameNetJunctionAlignmentSolver
|
|
97
|
+
inlineNetLabelSolver?: InlineNetLabelSolver
|
|
92
98
|
|
|
93
99
|
startTimeOfPhase: Record<string, number>
|
|
94
100
|
endTimeOfPhase: Record<string, number>
|
|
@@ -224,9 +230,27 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
224
230
|
]
|
|
225
231
|
},
|
|
226
232
|
),
|
|
233
|
+
definePipelineStep(
|
|
234
|
+
"traceElbowTransitionSimplificationSolver",
|
|
235
|
+
TraceElbowTransitionSimplificationSolver,
|
|
236
|
+
(instance) => {
|
|
237
|
+
const overlapAvoidanceOutput =
|
|
238
|
+
instance.traceLabelOverlapAvoidanceSolver!.getOutput()
|
|
239
|
+
return [
|
|
240
|
+
{
|
|
241
|
+
inputProblem: instance.inputProblem,
|
|
242
|
+
traces: overlapAvoidanceOutput.traces,
|
|
243
|
+
completedReroutes: overlapAvoidanceOutput.completedReroutes,
|
|
244
|
+
netLabelPlacements:
|
|
245
|
+
instance.traceLabelOverlapAvoidanceSolver!.netLabelPlacements,
|
|
246
|
+
paddingBuffer: 0.1,
|
|
247
|
+
},
|
|
248
|
+
]
|
|
249
|
+
},
|
|
250
|
+
),
|
|
227
251
|
definePipelineStep("traceCleanupSolver", TraceCleanupSolver, (instance) => {
|
|
228
252
|
const prevSolverOutput =
|
|
229
|
-
instance.
|
|
253
|
+
instance.traceElbowTransitionSimplificationSolver!.getOutput()
|
|
230
254
|
const traces = prevSolverOutput.traces
|
|
231
255
|
|
|
232
256
|
const labelMergingOutput =
|
|
@@ -348,12 +372,29 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
348
372
|
},
|
|
349
373
|
],
|
|
350
374
|
),
|
|
375
|
+
definePipelineStep(
|
|
376
|
+
"preAlignmentTraceElbowTransitionSimplificationSolver",
|
|
377
|
+
TraceElbowTransitionSimplificationSolver,
|
|
378
|
+
(instance) => {
|
|
379
|
+
const collisionOutput =
|
|
380
|
+
instance.preAlignmentNetLabelTraceCollisionSolver!.getOutput()
|
|
381
|
+
return [
|
|
382
|
+
{
|
|
383
|
+
inputProblem: instance.inputProblem,
|
|
384
|
+
traces: collisionOutput.traces,
|
|
385
|
+
completedReroutes: collisionOutput.completedReroutes,
|
|
386
|
+
netLabelPlacements: collisionOutput.netLabelPlacements,
|
|
387
|
+
paddingBuffer: 0.1,
|
|
388
|
+
},
|
|
389
|
+
]
|
|
390
|
+
},
|
|
391
|
+
),
|
|
351
392
|
definePipelineStep(
|
|
352
393
|
"traceCleanupSolver2",
|
|
353
394
|
TraceCleanupSolver,
|
|
354
395
|
(instance) => {
|
|
355
396
|
const collisionOutput =
|
|
356
|
-
instance.
|
|
397
|
+
instance.preAlignmentTraceElbowTransitionSimplificationSolver!.getOutput()
|
|
357
398
|
const labelMergingOutput =
|
|
358
399
|
instance.traceLabelOverlapAvoidanceSolver!.labelMergingSolver!.getOutput()
|
|
359
400
|
|
|
@@ -416,18 +457,37 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
416
457
|
]
|
|
417
458
|
},
|
|
418
459
|
),
|
|
460
|
+
definePipelineStep(
|
|
461
|
+
"finalTraceElbowTransitionSimplificationSolver",
|
|
462
|
+
TraceElbowTransitionSimplificationSolver,
|
|
463
|
+
(instance) => {
|
|
464
|
+
const collisionOutput =
|
|
465
|
+
instance.netLabelTraceCollisionSolver!.getOutput()
|
|
466
|
+
return [
|
|
467
|
+
{
|
|
468
|
+
inputProblem: instance.inputProblem,
|
|
469
|
+
traces: collisionOutput.traces,
|
|
470
|
+
completedReroutes: collisionOutput.completedReroutes,
|
|
471
|
+
netLabelPlacements: collisionOutput.netLabelPlacements,
|
|
472
|
+
paddingBuffer: 0.1,
|
|
473
|
+
},
|
|
474
|
+
]
|
|
475
|
+
},
|
|
476
|
+
),
|
|
419
477
|
definePipelineStep(
|
|
420
478
|
"netLabelNetLabelCollisionSolver",
|
|
421
479
|
NetLabelNetLabelCollisionSolver,
|
|
422
|
-
(instance) =>
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
instance.
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
480
|
+
(instance) => {
|
|
481
|
+
const simplificationOutput =
|
|
482
|
+
instance.finalTraceElbowTransitionSimplificationSolver!.getOutput()
|
|
483
|
+
return [
|
|
484
|
+
{
|
|
485
|
+
inputProblem: instance.inputProblem,
|
|
486
|
+
traces: simplificationOutput.traces,
|
|
487
|
+
netLabelPlacements: simplificationOutput.netLabelPlacements,
|
|
488
|
+
},
|
|
489
|
+
]
|
|
490
|
+
},
|
|
431
491
|
),
|
|
432
492
|
definePipelineStep(
|
|
433
493
|
"sameNetJunctionAlignmentSolver",
|
|
@@ -444,6 +504,21 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
444
504
|
]
|
|
445
505
|
},
|
|
446
506
|
),
|
|
507
|
+
definePipelineStep(
|
|
508
|
+
"inlineNetLabelSolver",
|
|
509
|
+
InlineNetLabelSolver,
|
|
510
|
+
(instance) => {
|
|
511
|
+
const junctionOutput =
|
|
512
|
+
instance.sameNetJunctionAlignmentSolver!.getOutput()
|
|
513
|
+
return [
|
|
514
|
+
{
|
|
515
|
+
inputProblem: instance.inputProblem,
|
|
516
|
+
traces: junctionOutput.traces,
|
|
517
|
+
netLabelPlacements: junctionOutput.netLabelPlacements,
|
|
518
|
+
},
|
|
519
|
+
]
|
|
520
|
+
},
|
|
521
|
+
),
|
|
447
522
|
]
|
|
448
523
|
|
|
449
524
|
constructor(inputProblem: InputProblem, opts?: Options) {
|