@tscircuit/schematic-trace-solver 0.0.125 → 0.0.126

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.
Files changed (18) hide show
  1. package/dist/index.d.ts +45 -1
  2. package/dist/index.js +427 -35
  3. package/lib/solvers/NetLabelTraceCollisionSolver/NetLabelTraceCollisionSolver.ts +15 -0
  4. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +69 -11
  5. package/lib/solvers/TraceElbowTransitionSimplificationSolver/TraceElbowTransitionSimplificationSolver.ts +216 -0
  6. package/lib/solvers/TraceElbowTransitionSimplificationSolver/generateElbowTransitionSimplificationCandidates.ts +186 -0
  7. package/lib/solvers/TraceElbowTransitionSimplificationSolver/types.ts +10 -0
  8. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver.ts +3 -0
  9. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/OverlapAvoidanceStepSolver.ts +16 -0
  10. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts +56 -13
  11. package/package.json +1 -1
  12. package/site/bug-reports/bug-report-20260806T093501Z.page.tsx +4 -0
  13. package/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg +3 -3
  14. package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +6 -6
  15. package/tests/bug-reports/bug-report-20260806T093501Z/__snapshots__/bug-report-20260806T093501Z.snap.svg +135 -0
  16. package/tests/bug-reports/bug-report-20260806T093501Z/bug-report-20260806T093501Z.json +505 -0
  17. package/tests/bug-reports/bug-report-20260806T093501Z/bug-report-20260806T093501Z.test.ts +66 -0
  18. package/tests/examples/__snapshots__/example33.snap.svg +1 -1
@@ -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,7 @@ 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"
33
34
 
34
35
  type PipelineStep<T extends new (...args: any[]) => BaseSolver> = {
35
36
  solverName: string
@@ -88,6 +89,9 @@ export class SchematicTracePipelineSolver extends BaseSolver {
88
89
  netLabelTraceCollisionSolver?: NetLabelTraceCollisionSolver
89
90
  traceCleanupSolver2?: TraceCleanupSolver
90
91
  netLabelNetLabelCollisionSolver?: NetLabelNetLabelCollisionSolver
92
+ traceElbowTransitionSimplificationSolver?: TraceElbowTransitionSimplificationSolver
93
+ preAlignmentTraceElbowTransitionSimplificationSolver?: TraceElbowTransitionSimplificationSolver
94
+ finalTraceElbowTransitionSimplificationSolver?: TraceElbowTransitionSimplificationSolver
91
95
  sameNetJunctionAlignmentSolver?: SameNetJunctionAlignmentSolver
92
96
 
93
97
  startTimeOfPhase: Record<string, number>
@@ -224,9 +228,27 @@ export class SchematicTracePipelineSolver extends BaseSolver {
224
228
  ]
225
229
  },
226
230
  ),
231
+ definePipelineStep(
232
+ "traceElbowTransitionSimplificationSolver",
233
+ TraceElbowTransitionSimplificationSolver,
234
+ (instance) => {
235
+ const overlapAvoidanceOutput =
236
+ instance.traceLabelOverlapAvoidanceSolver!.getOutput()
237
+ return [
238
+ {
239
+ inputProblem: instance.inputProblem,
240
+ traces: overlapAvoidanceOutput.traces,
241
+ completedReroutes: overlapAvoidanceOutput.completedReroutes,
242
+ netLabelPlacements:
243
+ instance.traceLabelOverlapAvoidanceSolver!.netLabelPlacements,
244
+ paddingBuffer: 0.1,
245
+ },
246
+ ]
247
+ },
248
+ ),
227
249
  definePipelineStep("traceCleanupSolver", TraceCleanupSolver, (instance) => {
228
250
  const prevSolverOutput =
229
- instance.traceLabelOverlapAvoidanceSolver!.getOutput()
251
+ instance.traceElbowTransitionSimplificationSolver!.getOutput()
230
252
  const traces = prevSolverOutput.traces
231
253
 
232
254
  const labelMergingOutput =
@@ -348,12 +370,29 @@ export class SchematicTracePipelineSolver extends BaseSolver {
348
370
  },
349
371
  ],
350
372
  ),
373
+ definePipelineStep(
374
+ "preAlignmentTraceElbowTransitionSimplificationSolver",
375
+ TraceElbowTransitionSimplificationSolver,
376
+ (instance) => {
377
+ const collisionOutput =
378
+ instance.preAlignmentNetLabelTraceCollisionSolver!.getOutput()
379
+ return [
380
+ {
381
+ inputProblem: instance.inputProblem,
382
+ traces: collisionOutput.traces,
383
+ completedReroutes: collisionOutput.completedReroutes,
384
+ netLabelPlacements: collisionOutput.netLabelPlacements,
385
+ paddingBuffer: 0.1,
386
+ },
387
+ ]
388
+ },
389
+ ),
351
390
  definePipelineStep(
352
391
  "traceCleanupSolver2",
353
392
  TraceCleanupSolver,
354
393
  (instance) => {
355
394
  const collisionOutput =
356
- instance.preAlignmentNetLabelTraceCollisionSolver!.getOutput()
395
+ instance.preAlignmentTraceElbowTransitionSimplificationSolver!.getOutput()
357
396
  const labelMergingOutput =
358
397
  instance.traceLabelOverlapAvoidanceSolver!.labelMergingSolver!.getOutput()
359
398
 
@@ -416,18 +455,37 @@ export class SchematicTracePipelineSolver extends BaseSolver {
416
455
  ]
417
456
  },
418
457
  ),
458
+ definePipelineStep(
459
+ "finalTraceElbowTransitionSimplificationSolver",
460
+ TraceElbowTransitionSimplificationSolver,
461
+ (instance) => {
462
+ const collisionOutput =
463
+ instance.netLabelTraceCollisionSolver!.getOutput()
464
+ return [
465
+ {
466
+ inputProblem: instance.inputProblem,
467
+ traces: collisionOutput.traces,
468
+ completedReroutes: collisionOutput.completedReroutes,
469
+ netLabelPlacements: collisionOutput.netLabelPlacements,
470
+ paddingBuffer: 0.1,
471
+ },
472
+ ]
473
+ },
474
+ ),
419
475
  definePipelineStep(
420
476
  "netLabelNetLabelCollisionSolver",
421
477
  NetLabelNetLabelCollisionSolver,
422
- (instance) => [
423
- {
424
- inputProblem: instance.inputProblem,
425
- traces: instance.netLabelTraceCollisionSolver!.getOutput().traces,
426
- netLabelPlacements:
427
- instance.netLabelTraceCollisionSolver!.getOutput()
428
- .netLabelPlacements,
429
- },
430
- ],
478
+ (instance) => {
479
+ const simplificationOutput =
480
+ instance.finalTraceElbowTransitionSimplificationSolver!.getOutput()
481
+ return [
482
+ {
483
+ inputProblem: instance.inputProblem,
484
+ traces: simplificationOutput.traces,
485
+ netLabelPlacements: simplificationOutput.netLabelPlacements,
486
+ },
487
+ ]
488
+ },
431
489
  ),
432
490
  definePipelineStep(
433
491
  "sameNetJunctionAlignmentSolver",
@@ -0,0 +1,216 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+ import type { GraphicsObject } 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 { isPathCollidingWithObstacles } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
7
+ import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
8
+ import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
9
+ import { preservesLabelAnchors } from "lib/solvers/TraceCleanupSolver/sameNetRailAlignment/preservesLabelAnchors"
10
+ import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath"
11
+ import { detectTraceLabelOverlap } from "lib/solvers/TraceLabelOverlapAvoidanceSolver/detectTraceLabelOverlap"
12
+ import type { InputProblem } from "lib/types/InputProblem"
13
+ import { doesPathCoincideWithTraces } from "lib/utils/doesPathCoincideWithTraces"
14
+ import type { CompletedTraceReroute } from "./types"
15
+ import { generateElbowTransitionSimplificationCandidates } from "./generateElbowTransitionSimplificationCandidates"
16
+
17
+ interface TraceElbowTransitionSimplificationSolverInput {
18
+ inputProblem: InputProblem
19
+ traces: SolvedTracePath[]
20
+ completedReroutes: CompletedTraceReroute[]
21
+ netLabelPlacements: NetLabelPlacement[]
22
+ paddingBuffer: number
23
+ }
24
+
25
+ const PATH_LENGTH_EPSILON = 1e-9
26
+
27
+ const getPathLength = (points: Point[]) =>
28
+ points.slice(1).reduce((length, point, pointIndex) => {
29
+ const previousPoint = points[pointIndex]!
30
+ return (
31
+ length +
32
+ Math.abs(point.x - previousPoint.x) +
33
+ Math.abs(point.y - previousPoint.y)
34
+ )
35
+ }, 0)
36
+
37
+ /**
38
+ * Post-processes traces after trace/label overlap avoidance. It removes
39
+ * redundant elbow transitions and, when needed, shifts a simplified elbow
40
+ * around a rendered label. Every replacement is revalidated against labels,
41
+ * components, other-net traces, and label anchors.
42
+ */
43
+ export class TraceElbowTransitionSimplificationSolver extends BaseSolver {
44
+ private input: TraceElbowTransitionSimplificationSolverInput
45
+ private outputTraces: SolvedTracePath[]
46
+ private traceIdQueue: string[]
47
+ private obstacles: ReturnType<typeof getObstacleRects>
48
+
49
+ constructor(input: TraceElbowTransitionSimplificationSolverInput) {
50
+ super()
51
+ this.input = input
52
+ this.outputTraces = [...input.traces]
53
+ const reroutedTraceIds = new Set(
54
+ input.completedReroutes.map(
55
+ (completedReroute) => completedReroute.initialTrace.mspPairId,
56
+ ),
57
+ )
58
+ this.traceIdQueue = input.traces
59
+ .map((trace) => trace.mspPairId)
60
+ .filter((traceId) => reroutedTraceIds.has(traceId))
61
+ this.obstacles = getObstacleRects(input.inputProblem)
62
+ }
63
+
64
+ override _step() {
65
+ const traceId = this.traceIdQueue.shift()
66
+ if (!traceId) {
67
+ this.solved = true
68
+ return
69
+ }
70
+
71
+ const traceIndex = this.outputTraces.findIndex(
72
+ (trace) => trace.mspPairId === traceId,
73
+ )
74
+ const trace = this.outputTraces[traceIndex]!
75
+ const tracePath = simplifyPath(trace.tracePath)
76
+ const initialOverlaps = detectTraceLabelOverlap({
77
+ traces: [{ ...trace, tracePath }],
78
+ netLabels: this.input.netLabelPlacements,
79
+ })
80
+ const otherNetTraces = this.outputTraces.filter(
81
+ (otherTrace) =>
82
+ otherTrace.mspPairId !== trace.mspPairId &&
83
+ otherTrace.globalConnNetId !== trace.globalConnNetId,
84
+ )
85
+ const candidateByPath = new Map<string, Point[]>()
86
+ const completedReroutes = this.input.completedReroutes.filter(
87
+ (completedReroute) =>
88
+ completedReroute.initialTrace.mspPairId === trace.mspPairId,
89
+ )
90
+
91
+ for (const completedReroute of completedReroutes) {
92
+ const initialReroutePath = simplifyPath(
93
+ completedReroute.initialTrace.tracePath,
94
+ )
95
+ const reroutedPath = simplifyPath(completedReroute.reroutedTracePath)
96
+ const initialRerouteOverlapCount = detectTraceLabelOverlap({
97
+ traces: [
98
+ { ...completedReroute.initialTrace, tracePath: initialReroutePath },
99
+ ],
100
+ netLabels: this.input.netLabelPlacements,
101
+ }).length
102
+
103
+ const candidates = generateElbowTransitionSimplificationCandidates({
104
+ trace: completedReroute.initialTrace,
105
+ label: completedReroute.label,
106
+ netLabelPlacements: this.input.netLabelPlacements,
107
+ paddingBuffer: this.input.paddingBuffer,
108
+ detourCount: completedReroute.detourCount,
109
+ })
110
+ for (const candidate of candidates) {
111
+ const simplifiedCandidate = simplifyPath(candidate)
112
+ const candidateTrace = {
113
+ ...completedReroute.initialTrace,
114
+ tracePath: simplifiedCandidate,
115
+ }
116
+ const candidateOverlapCount = detectTraceLabelOverlap({
117
+ traces: [candidateTrace],
118
+ netLabels: this.input.netLabelPlacements,
119
+ }).length
120
+ const isSimplerEquivalentReroute =
121
+ candidateOverlapCount < initialRerouteOverlapCount &&
122
+ Math.abs(
123
+ getPathLength(simplifiedCandidate) - getPathLength(reroutedPath),
124
+ ) < PATH_LENGTH_EPSILON &&
125
+ simplifiedCandidate.length < reroutedPath.length &&
126
+ preservesLabelAnchors(
127
+ this.input.netLabelPlacements,
128
+ [completedReroute.initialTrace],
129
+ [candidateTrace],
130
+ )
131
+
132
+ if (isSimplerEquivalentReroute) {
133
+ candidateByPath.set(
134
+ simplifiedCandidate
135
+ .map((point) => `${point.x},${point.y}`)
136
+ .join(";"),
137
+ simplifiedCandidate,
138
+ )
139
+ }
140
+ }
141
+ }
142
+
143
+ const initialOverlapIds = new Set(
144
+ initialOverlaps.map(
145
+ ({ label }) => `${label.globalConnNetId}:${label.netId}`,
146
+ ),
147
+ )
148
+ const initialPathLength = getPathLength(tracePath)
149
+
150
+ const validCandidates = [...candidateByPath.values()].filter(
151
+ (candidatePath) => {
152
+ const candidateTrace = { ...trace, tracePath: candidatePath }
153
+ const candidateOverlaps = detectTraceLabelOverlap({
154
+ traces: [candidateTrace],
155
+ netLabels: this.input.netLabelPlacements,
156
+ })
157
+ const candidateOnlyKeepsExistingOverlaps = candidateOverlaps.every(
158
+ ({ label }) =>
159
+ initialOverlapIds.has(`${label.globalConnNetId}:${label.netId}`),
160
+ )
161
+ const reducesCollisions =
162
+ candidateOverlaps.length < initialOverlaps.length
163
+ const simplifiesGeometry =
164
+ candidateOverlaps.length === initialOverlaps.length &&
165
+ getPathLength(candidatePath) <=
166
+ initialPathLength + PATH_LENGTH_EPSILON &&
167
+ candidatePath.length < tracePath.length
168
+
169
+ return (
170
+ candidateOnlyKeepsExistingOverlaps &&
171
+ (reducesCollisions || simplifiesGeometry) &&
172
+ preservesLabelAnchors(
173
+ this.input.netLabelPlacements,
174
+ [trace],
175
+ [candidateTrace],
176
+ ) &&
177
+ !isPathCollidingWithObstacles(candidatePath, this.obstacles) &&
178
+ !doesPathCoincideWithTraces(candidatePath, otherNetTraces)
179
+ )
180
+ },
181
+ )
182
+
183
+ const getOverlapCount = (candidatePath: Point[]) =>
184
+ detectTraceLabelOverlap({
185
+ traces: [{ ...trace, tracePath: candidatePath }],
186
+ netLabels: this.input.netLabelPlacements,
187
+ }).length
188
+ validCandidates.sort((a, b) => {
189
+ const overlapDifference = getOverlapCount(a) - getOverlapCount(b)
190
+ if (overlapDifference !== 0) return overlapDifference
191
+ return getPathLength(a) - getPathLength(b) || a.length - b.length
192
+ })
193
+
194
+ const bestCandidate = validCandidates[0]
195
+ if (!bestCandidate) return
196
+
197
+ this.outputTraces[traceIndex] = { ...trace, tracePath: bestCandidate }
198
+ this.stats.simplifiedTraceCount = (this.stats.simplifiedTraceCount ?? 0) + 1
199
+ }
200
+
201
+ getOutput() {
202
+ return {
203
+ traces: this.outputTraces,
204
+ netLabelPlacements: this.input.netLabelPlacements,
205
+ }
206
+ }
207
+
208
+ override visualize(): GraphicsObject {
209
+ const graphics = visualizeInputProblem(this.input.inputProblem)
210
+ graphics.lines ??= []
211
+ for (const trace of this.outputTraces) {
212
+ graphics.lines.push({ points: trace.tracePath, strokeColor: "purple" })
213
+ }
214
+ return graphics
215
+ }
216
+ }
@@ -0,0 +1,186 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
3
+ import { getRectBounds } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
4
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
5
+ import {
6
+ isHorizontal,
7
+ isVertical,
8
+ segmentIntersectsRect,
9
+ } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
10
+ import { shiftSegmentOrth } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/pathOps"
11
+ import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath"
12
+ import { detectTraceLabelOverlap } from "lib/solvers/TraceLabelOverlapAvoidanceSolver/detectTraceLabelOverlap"
13
+
14
+ export const isSimpleFiveSegmentElbow = (path: Point[]): boolean => {
15
+ const simplifiedPath = simplifyPath(path)
16
+ if (simplifiedPath.length !== 6) return false
17
+
18
+ const segmentIsHorizontal = simplifiedPath
19
+ .slice(0, -1)
20
+ .map((point, index) => isHorizontal(point, simplifiedPath[index + 1]!))
21
+
22
+ return segmentIsHorizontal.every(
23
+ (isHorizontalSegment, index) =>
24
+ index === 0 || isHorizontalSegment !== segmentIsHorizontal[index - 1],
25
+ )
26
+ }
27
+
28
+ const generateSegmentShiftCandidates = ({
29
+ trace,
30
+ label,
31
+ paddingBuffer,
32
+ detourCount,
33
+ }: {
34
+ trace: SolvedTracePath
35
+ label: NetLabelPlacement
36
+ paddingBuffer: number
37
+ detourCount: number
38
+ }): Point[][] => {
39
+ if (trace.globalConnNetId === label.globalConnNetId) return []
40
+
41
+ const path = simplifyPath(trace.tracePath)
42
+ if (!isSimpleFiveSegmentElbow(path)) return []
43
+
44
+ const labelBounds = getRectBounds(label.center, label.width, label.height)
45
+ const effectivePadding = paddingBuffer + detourCount * paddingBuffer
46
+ const paddedLabelBounds = {
47
+ minX: labelBounds.minX - effectivePadding,
48
+ maxX: labelBounds.maxX + effectivePadding,
49
+ minY: labelBounds.minY - effectivePadding,
50
+ maxY: labelBounds.maxY + effectivePadding,
51
+ }
52
+ const candidates: Point[][] = []
53
+
54
+ for (let segmentIndex = 1; segmentIndex < path.length - 2; segmentIndex++) {
55
+ const segmentStart = path[segmentIndex]!
56
+ const segmentEnd = path[segmentIndex + 1]!
57
+ if (!segmentIntersectsRect(segmentStart, segmentEnd, labelBounds)) continue
58
+
59
+ const isHorizontalSegment = isHorizontal(segmentStart, segmentEnd)
60
+ const isVerticalSegment = isVertical(segmentStart, segmentEnd)
61
+ if (!isHorizontalSegment && !isVerticalSegment) continue
62
+
63
+ const axis = isHorizontalSegment ? "y" : "x"
64
+ const coordinates = isHorizontalSegment
65
+ ? [paddedLabelBounds.minY, paddedLabelBounds.maxY]
66
+ : [paddedLabelBounds.minX, paddedLabelBounds.maxX]
67
+
68
+ for (const coordinate of coordinates) {
69
+ const shiftedPath = shiftSegmentOrth(path, segmentIndex, axis, coordinate)
70
+ if (shiftedPath) candidates.push(shiftedPath)
71
+ }
72
+ }
73
+
74
+ return candidates
75
+ }
76
+
77
+ const generateTransitionShiftCandidates = ({
78
+ trace,
79
+ label,
80
+ paddingBuffer,
81
+ detourCount,
82
+ }: {
83
+ trace: SolvedTracePath
84
+ label: NetLabelPlacement
85
+ paddingBuffer: number
86
+ detourCount: number
87
+ }): Point[][] => {
88
+ const path = simplifyPath(trace.tracePath)
89
+ if (!isSimpleFiveSegmentElbow(path)) return []
90
+
91
+ const labelBounds = getRectBounds(label.center, label.width, label.height)
92
+ const effectivePadding = paddingBuffer + detourCount * paddingBuffer
93
+ const paddedLabelBounds = {
94
+ minX: labelBounds.minX - effectivePadding,
95
+ maxX: labelBounds.maxX + effectivePadding,
96
+ minY: labelBounds.minY - effectivePadding,
97
+ maxY: labelBounds.maxY + effectivePadding,
98
+ }
99
+ const start = path[0]!
100
+ const end = path[path.length - 1]!
101
+ const middleSegmentIndex = 2
102
+ const candidates: Point[][] = []
103
+
104
+ for (let segmentIndex = 0; segmentIndex < path.length - 1; segmentIndex++) {
105
+ const segmentStart = path[segmentIndex]!
106
+ const segmentEnd = path[segmentIndex + 1]!
107
+ if (!segmentIntersectsRect(segmentStart, segmentEnd, labelBounds)) continue
108
+ if (Math.abs(segmentIndex - middleSegmentIndex) !== 1) continue
109
+
110
+ const isHorizontalSegment = isHorizontal(segmentStart, segmentEnd)
111
+ const isVerticalSegment = isVertical(segmentStart, segmentEnd)
112
+ if (!isHorizontalSegment && !isVerticalSegment) continue
113
+
114
+ const axis = isHorizontalSegment ? "x" : "y"
115
+ const min = isHorizontalSegment
116
+ ? paddedLabelBounds.minX
117
+ : paddedLabelBounds.minY
118
+ const max = isHorizontalSegment
119
+ ? paddedLabelBounds.maxX
120
+ : paddedLabelBounds.maxY
121
+ const startCoordinate = isHorizontalSegment ? start.x : start.y
122
+ const endCoordinate = isHorizontalSegment ? end.x : end.y
123
+ const corridorCoordinates: number[] = []
124
+
125
+ if (startCoordinate < min) {
126
+ corridorCoordinates.push((startCoordinate + min) / 2)
127
+ } else if (startCoordinate > max) {
128
+ corridorCoordinates.push((startCoordinate + max) / 2)
129
+ }
130
+ if (endCoordinate < min) {
131
+ corridorCoordinates.push((endCoordinate + min) / 2)
132
+ } else if (endCoordinate > max) {
133
+ corridorCoordinates.push((endCoordinate + max) / 2)
134
+ }
135
+
136
+ for (const coordinate of new Set(corridorCoordinates)) {
137
+ const shiftedPath = shiftSegmentOrth(
138
+ path,
139
+ middleSegmentIndex,
140
+ axis,
141
+ coordinate,
142
+ )
143
+ if (shiftedPath) candidates.push(shiftedPath)
144
+ }
145
+ }
146
+
147
+ return candidates
148
+ }
149
+
150
+ export const generateElbowTransitionSimplificationCandidates = ({
151
+ trace,
152
+ label,
153
+ netLabelPlacements,
154
+ paddingBuffer,
155
+ detourCount,
156
+ }: {
157
+ trace: SolvedTracePath
158
+ label: NetLabelPlacement
159
+ netLabelPlacements: NetLabelPlacement[]
160
+ paddingBuffer: number
161
+ detourCount: number
162
+ }): Point[][] => {
163
+ const transitionCandidates = generateTransitionShiftCandidates({
164
+ trace,
165
+ label,
166
+ paddingBuffer,
167
+ detourCount,
168
+ })
169
+
170
+ return transitionCandidates.flatMap((tracePath) => {
171
+ const shiftedTrace = { ...trace, tracePath }
172
+ const shiftedOverlaps = detectTraceLabelOverlap({
173
+ traces: [shiftedTrace],
174
+ netLabels: netLabelPlacements,
175
+ })
176
+
177
+ return shiftedOverlaps.flatMap(({ label: shiftedLabel }) =>
178
+ generateSegmentShiftCandidates({
179
+ trace: shiftedTrace,
180
+ label: shiftedLabel,
181
+ paddingBuffer,
182
+ detourCount,
183
+ }),
184
+ )
185
+ })
186
+ }
@@ -0,0 +1,10 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
3
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
4
+
5
+ export interface CompletedTraceReroute {
6
+ initialTrace: SolvedTracePath
7
+ reroutedTracePath: Point[]
8
+ label: NetLabelPlacement
9
+ detourCount: number
10
+ }
@@ -122,6 +122,9 @@ export class TraceLabelOverlapAvoidanceSolver extends BaseSolver {
122
122
  const solvedTraces = this.subSolvers.flatMap((s) => s.getOutput().allTraces)
123
123
  return {
124
124
  traces: [...this.cleanTraces, ...solvedTraces],
125
+ completedReroutes: this.subSolvers.flatMap(
126
+ (solver) => solver.completedReroutes,
127
+ ),
125
128
  netLabelPlacements:
126
129
  this.labelMergingSolver?.getOutput().netLabelPlacements ??
127
130
  this.netLabelPlacements,
@@ -8,6 +8,7 @@ import { detectTraceLabelOverlap } from "../../detectTraceLabelOverlap"
8
8
  import { SingleOverlapSolver } from "../SingleOverlapSolver/SingleOverlapSolver"
9
9
  import { doesTraceStartOrEndInLabel } from "./doesTraceStartOrEndInLabel"
10
10
  import { visualizeDecomposition } from "./visualizeDecomposition"
11
+ import type { CompletedTraceReroute } from "lib/solvers/TraceElbowTransitionSimplificationSolver/types"
11
12
 
12
13
  type Overlap = ReturnType<typeof detectTraceLabelOverlap>[0]
13
14
 
@@ -35,6 +36,7 @@ export class OverlapAvoidanceStepSolver extends BaseSolver {
35
36
  allTraces: SolvedTracePath[]
36
37
  tracesToAvoidOverlapping: SolvedTracePath[]
37
38
  modifiedTraces: SolvedTracePath[] = []
39
+ completedReroutes: CompletedTraceReroute[] = []
38
40
 
39
41
  private readonly PADDING_BUFFER = 0.1
40
42
  private detourCounts: Map<string, number> = new Map()
@@ -66,6 +68,17 @@ export class OverlapAvoidanceStepSolver extends BaseSolver {
66
68
  if (this.activeSubSolver.solved) {
67
69
  const solvedPath = this.activeSubSolver.solvedTracePath
68
70
  if (solvedPath) {
71
+ this.completedReroutes.push({
72
+ initialTrace: {
73
+ ...this.activeSubSolver.initialTrace,
74
+ tracePath: this.activeSubSolver.initialTrace.tracePath.map(
75
+ (point) => ({ ...point }),
76
+ ),
77
+ },
78
+ reroutedTracePath: solvedPath.map((point) => ({ ...point })),
79
+ label: this.activeSubSolver.label,
80
+ detourCount: this.activeSubSolver.detourCount,
81
+ })
69
82
  const traceIndex = this.allTraces.findIndex(
70
83
  (t) => t.mspPairId === this.activeSubSolver!.initialTrace.mspPairId,
71
84
  )
@@ -154,6 +167,7 @@ export class OverlapAvoidanceStepSolver extends BaseSolver {
154
167
  paddingBuffer: this.PADDING_BUFFER,
155
168
  detourCount: detourCount,
156
169
  tracesToAvoidOverlapping: this.tracesToAvoidOverlapping,
170
+ netLabelPlacements: this.initialNetLabelPlacements,
157
171
  })
158
172
  } else {
159
173
  const overlapId = `${traceToFix.mspPairId}-${labelToAvoid.globalConnNetId}`
@@ -197,6 +211,7 @@ export class OverlapAvoidanceStepSolver extends BaseSolver {
197
211
  paddingBuffer: this.PADDING_BUFFER,
198
212
  detourCount: detourCount,
199
213
  tracesToAvoidOverlapping: this.tracesToAvoidOverlapping,
214
+ netLabelPlacements: this.initialNetLabelPlacements,
200
215
  })
201
216
  } else {
202
217
  const overlapId = `${traceToFix.mspPairId}-${labelToAvoid.globalConnNetId}`
@@ -217,6 +232,7 @@ export class OverlapAvoidanceStepSolver extends BaseSolver {
217
232
  paddingBuffer: this.PADDING_BUFFER,
218
233
  detourCount: detourCount,
219
234
  tracesToAvoidOverlapping: this.tracesToAvoidOverlapping,
235
+ netLabelPlacements: this.initialNetLabelPlacements,
220
236
  })
221
237
  }
222
238
  }