@tscircuit/schematic-trace-solver 0.0.124 → 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 (29) hide show
  1. package/dist/index.d.ts +46 -1
  2. package/dist/index.js +461 -37
  3. package/lib/solvers/AvailableNetOrientationSolver/geometry.ts +5 -1
  4. package/lib/solvers/NetLabelTraceCollisionSolver/NetLabelTraceCollisionSolver.ts +15 -0
  5. package/lib/solvers/RailNetLabelCornerPlacementSolver/RailNetLabelCornerPlacementSolver.ts +49 -6
  6. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +69 -11
  7. package/lib/solvers/TraceElbowTransitionSimplificationSolver/TraceElbowTransitionSimplificationSolver.ts +216 -0
  8. package/lib/solvers/TraceElbowTransitionSimplificationSolver/generateElbowTransitionSimplificationCandidates.ts +186 -0
  9. package/lib/solvers/TraceElbowTransitionSimplificationSolver/types.ts +10 -0
  10. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver.ts +3 -0
  11. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/OverlapAvoidanceStepSolver.ts +16 -0
  12. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts +56 -13
  13. package/package.json +1 -1
  14. package/site/bug-reports/bug-report-20260805T061316Z.page.tsx +4 -0
  15. package/site/bug-reports/bug-report-20260806T061548Z.page.tsx +4 -0
  16. package/site/bug-reports/bug-report-20260806T093501Z.page.tsx +4 -0
  17. package/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg +3 -3
  18. package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +6 -6
  19. package/tests/bug-reports/bug-report-20260805T061316Z/__snapshots__/bug-report-20260805T061316Z.snap.svg +212 -0
  20. package/tests/bug-reports/bug-report-20260805T061316Z/bug-report-20260805T061316Z.json +889 -0
  21. package/tests/bug-reports/bug-report-20260805T061316Z/bug-report-20260805T061316Z.test.ts +12 -0
  22. package/tests/bug-reports/bug-report-20260806T061548Z/__snapshots__/bug-report-20260806T061548Z.snap.svg +212 -0
  23. package/tests/bug-reports/bug-report-20260806T061548Z/bug-report-20260806T061548Z.json +960 -0
  24. package/tests/bug-reports/bug-report-20260806T061548Z/bug-report-20260806T061548Z.test.ts +32 -0
  25. package/tests/bug-reports/bug-report-20260806T093501Z/__snapshots__/bug-report-20260806T093501Z.snap.svg +135 -0
  26. package/tests/bug-reports/bug-report-20260806T093501Z/bug-report-20260806T093501Z.json +505 -0
  27. package/tests/bug-reports/bug-report-20260806T093501Z/bug-report-20260806T093501Z.test.ts +66 -0
  28. package/tests/examples/__snapshots__/example01.snap.svg +18 -18
  29. package/tests/examples/__snapshots__/example33.snap.svg +1 -1
@@ -41,7 +41,11 @@ export const traceCrossesBoundsInterior = (
41
41
  return false
42
42
  }
43
43
 
44
- const segmentCrossesBoundsInterior = (p1: Point, p2: Point, bounds: Bounds) => {
44
+ export const segmentCrossesBoundsInterior = (
45
+ p1: Point,
46
+ p2: Point,
47
+ bounds: Bounds,
48
+ ) => {
45
49
  const interiorBounds = {
46
50
  minX: bounds.minX + TRACE_BOUNDARY_TOLERANCE,
47
51
  minY: bounds.minY + TRACE_BOUNDARY_TOLERANCE,
@@ -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
  }
@@ -1,6 +1,9 @@
1
1
  import type { GraphicsObject } from "graphics-debug"
2
2
  import type { Point } from "@tscircuit/math-utils"
3
- import { traceCrossesBoundsInterior } from "lib/solvers/AvailableNetOrientationSolver/geometry"
3
+ import {
4
+ segmentCrossesBoundsInterior,
5
+ traceCrossesBoundsInterior,
6
+ } from "lib/solvers/AvailableNetOrientationSolver/geometry"
4
7
  import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
5
8
  import { moveAttachedLabelsToReroutedTrace } from "lib/solvers/Example28Solver/labelMovement"
6
9
  import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
@@ -32,6 +35,8 @@ import type {
32
35
  import { visualizeRailNetLabelCornerPlacementSolver } from "./visualize"
33
36
  import { rectIntersectsAnyTextBox } from "lib/utils/textBoxBounds"
34
37
 
38
+ const LABEL_TRACE_CLEARANCE = 0.1
39
+
35
40
  export class RailNetLabelCornerPlacementSolver extends BaseSolver {
36
41
  inputProblem: InputProblem
37
42
  traces: SolvedTracePath[]
@@ -271,11 +276,12 @@ export class RailNetLabelCornerPlacementSolver extends BaseSolver {
271
276
  const labelTraces = this.getTraceLinesForLabel(label)
272
277
  const allowRailAlignedFallback =
273
278
  this.isConfiguredRailLabel(label) &&
274
- !labelTraces.some((trace) =>
275
- getTraceCorners(trace.tracePath).some((corner) =>
276
- this.pointsEqual(corner, label.anchorPoint),
277
- ),
278
- )
279
+ (this.isLabelCrossedByOtherNetTrace(label) ||
280
+ !labelTraces.some((trace) =>
281
+ getTraceCorners(trace.tracePath).some((corner) =>
282
+ this.pointsEqual(corner, label.anchorPoint),
283
+ ),
284
+ ))
279
285
 
280
286
  for (const trace of labelTraces) {
281
287
  const path = trace.tracePath
@@ -324,6 +330,17 @@ export class RailNetLabelCornerPlacementSolver extends BaseSolver {
324
330
  )
325
331
  }
326
332
 
333
+ private isLabelCrossedByOtherNetTrace(label: NetLabelPlacement) {
334
+ const bounds = getRectBounds(label.center, label.width, label.height)
335
+ const otherNetTraceMap = Object.fromEntries(
336
+ this.traces
337
+ .filter((trace) => trace.globalConnNetId !== label.globalConnNetId)
338
+ .map((trace) => [trace.mspPairId, trace]),
339
+ )
340
+
341
+ return traceCrossesBoundsInterior(bounds, otherNetTraceMap)
342
+ }
343
+
327
344
  private pointsEqual(a: Point, b: Point) {
328
345
  return Math.abs(a.x - b.x) <= EPS && Math.abs(a.y - b.y) <= EPS
329
346
  }
@@ -394,6 +411,32 @@ export class RailNetLabelCornerPlacementSolver extends BaseSolver {
394
411
  shiftedCoordinates.add(chipBounds.minX - label.width / 2)
395
412
  shiftedCoordinates.add(chipBounds.maxX + label.width / 2)
396
413
  }
414
+ for (const otherTrace of this.traces) {
415
+ if (otherTrace.globalConnNetId === label.globalConnNetId) continue
416
+
417
+ for (let i = 0; i < otherTrace.tracePath.length - 1; i++) {
418
+ const start = otherTrace.tracePath[i]!
419
+ const end = otherTrace.tracePath[i + 1]!
420
+ if (!segmentCrossesBoundsInterior(start, end, labelBounds)) continue
421
+
422
+ const halfLabelWidth = label.width / 2
423
+ if (Math.abs(start.x - end.x) <= EPS) {
424
+ shiftedCoordinates.add(
425
+ start.x - halfLabelWidth - LABEL_TRACE_CLEARANCE,
426
+ )
427
+ shiftedCoordinates.add(
428
+ start.x + halfLabelWidth + LABEL_TRACE_CLEARANCE,
429
+ )
430
+ } else {
431
+ shiftedCoordinates.add(
432
+ Math.min(start.x, end.x) - halfLabelWidth - LABEL_TRACE_CLEARANCE,
433
+ )
434
+ shiftedCoordinates.add(
435
+ Math.max(start.x, end.x) + halfLabelWidth + LABEL_TRACE_CLEARANCE,
436
+ )
437
+ }
438
+ }
439
+ }
397
440
 
398
441
  return [...shiftedCoordinates].flatMap((x) => {
399
442
  if (Math.abs(x - corner.x) <= EPS) return []
@@ -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
+ }