@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.
Files changed (28) hide show
  1. package/README.md +28 -0
  2. package/dist/index.d.ts +159 -2
  3. package/dist/index.js +739 -36
  4. package/lib/index.ts +2 -0
  5. package/lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts +424 -0
  6. package/lib/solvers/InlineNetLabelSolver/getAxisAlignedSegments.ts +73 -0
  7. package/lib/solvers/NetLabelTraceCollisionSolver/NetLabelTraceCollisionSolver.ts +15 -0
  8. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +86 -11
  9. package/lib/solvers/TraceElbowTransitionSimplificationSolver/TraceElbowTransitionSimplificationSolver.ts +216 -0
  10. package/lib/solvers/TraceElbowTransitionSimplificationSolver/generateElbowTransitionSimplificationCandidates.ts +186 -0
  11. package/lib/solvers/TraceElbowTransitionSimplificationSolver/types.ts +10 -0
  12. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver.ts +3 -0
  13. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/OverlapAvoidanceStepSolver.ts +16 -0
  14. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts +56 -13
  15. package/lib/types/InputProblem.ts +24 -0
  16. package/package.json +1 -1
  17. package/site/bug-reports/bug-report-20260806T093501Z.page.tsx +4 -0
  18. package/site/examples/inline-net-label01.page.tsx +6 -0
  19. package/tests/assets/inline-net-label01.json +47 -0
  20. package/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg +3 -3
  21. package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +6 -6
  22. package/tests/bug-reports/bug-report-20260806T093501Z/__snapshots__/bug-report-20260806T093501Z.snap.svg +135 -0
  23. package/tests/bug-reports/bug-report-20260806T093501Z/bug-report-20260806T093501Z.json +505 -0
  24. package/tests/bug-reports/bug-report-20260806T093501Z/bug-report-20260806T093501Z.test.ts +66 -0
  25. package/tests/examples/__snapshots__/example33.snap.svg +1 -1
  26. package/tests/functions/getAxisAlignedSegments.test.ts +56 -0
  27. package/tests/solvers/InlineNetLabelSolver/__snapshots__/inline-net-label01.snap.svg +54 -0
  28. package/tests/solvers/InlineNetLabelSolver/inline-net-label01.test.ts +44 -0
@@ -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
  }
@@ -19,9 +19,22 @@ interface SingleOverlapSolverInput {
19
19
  paddingBuffer: number
20
20
  detourCount: number
21
21
  tracesToAvoidOverlapping?: SolvedTracePath[]
22
+ netLabelPlacements?: NetLabelPlacement[]
22
23
  }
23
24
 
24
25
  const MAX_TRIES = 5
26
+ const PATH_LENGTH_EPSILON = 1e-9
27
+
28
+ const getPathLength = (points: Point[]) => {
29
+ let length = 0
30
+ for (let pointIndex = 0; pointIndex < points.length - 1; pointIndex++) {
31
+ const point = points[pointIndex]!
32
+ const nextPoint = points[pointIndex + 1]!
33
+ length += Math.abs(nextPoint.x - point.x) + Math.abs(nextPoint.y - point.y)
34
+ }
35
+ return length
36
+ }
37
+
25
38
  /**
26
39
  * This solver attempts to find a valid rerouting for a single trace that is
27
40
  * overlapping with a net label. It tries various candidate paths until it
@@ -35,6 +48,8 @@ export class SingleOverlapSolver extends BaseSolver {
35
48
  obstacles: ReturnType<typeof getObstacleRects>
36
49
  label: NetLabelPlacement
37
50
  tracesToAvoidOverlapping: SolvedTracePath[]
51
+ netLabelPlacements: NetLabelPlacement[]
52
+ detourCount: number
38
53
  _tried: number = 0
39
54
 
40
55
  constructor(solverInput: SingleOverlapSolverInput) {
@@ -42,9 +57,14 @@ export class SingleOverlapSolver extends BaseSolver {
42
57
  this.initialTrace = solverInput.trace
43
58
  this.problem = solverInput.problem
44
59
  this.label = solverInput.label
60
+ this.detourCount = solverInput.detourCount
45
61
  this.tracesToAvoidOverlapping = (
46
62
  solverInput.tracesToAvoidOverlapping ?? []
47
63
  ).filter((t) => t.globalConnNetId !== solverInput.trace.globalConnNetId)
64
+ this.netLabelPlacements = solverInput.netLabelPlacements ?? [
65
+ solverInput.label,
66
+ ]
67
+ this.obstacles = getObstacleRects(this.problem)
48
68
 
49
69
  // Calculate an effective padding for this specific run based on the detourCount.
50
70
  const effectivePadding =
@@ -56,20 +76,33 @@ export class SingleOverlapSolver extends BaseSolver {
56
76
  paddingBuffer: effectivePadding, // Use the calculated, larger padding
57
77
  })
58
78
 
59
- const getPathLength = (pts: Point[]) => {
60
- let len = 0
61
- for (let i = 0; i < pts.length - 1; i++) {
62
- const dx = pts[i + 1].x - pts[i].x
63
- const dy = pts[i + 1].y - pts[i].y
64
- len += Math.sqrt(dx * dx + dy * dy)
65
- }
66
- return len
79
+ const getLabelOverlapCount = (path: Point[]) =>
80
+ detectTraceLabelOverlap({
81
+ traces: [{ ...this.initialTrace, tracePath: path }],
82
+ netLabels: this.netLabelPlacements,
83
+ }).length
84
+
85
+ const candidateByPath = new Map<string, Point[]>()
86
+ for (const candidate of candidates) {
87
+ const simplifiedCandidate = simplifyPath(candidate)
88
+ candidateByPath.set(
89
+ simplifiedCandidate.map((point) => `${point.x},${point.y}`).join(";"),
90
+ simplifiedCandidate,
91
+ )
67
92
  }
68
93
 
69
- this.queuedCandidatePaths = candidates.sort(
70
- (a, b) => getPathLength(a) - getPathLength(b),
71
- )
72
- this.obstacles = getObstacleRects(this.problem)
94
+ this.queuedCandidatePaths = [...candidateByPath.values()].sort((a, b) => {
95
+ const pathLengthDifference = getPathLength(a) - getPathLength(b)
96
+ if (Math.abs(pathLengthDifference) >= PATH_LENGTH_EPSILON) {
97
+ return pathLengthDifference
98
+ }
99
+
100
+ const overlapCountDifference =
101
+ getLabelOverlapCount(a) - getLabelOverlapCount(b)
102
+ if (overlapCountDifference !== 0) return overlapCountDifference
103
+
104
+ return a.length - b.length
105
+ })
73
106
  }
74
107
 
75
108
  override _step() {
@@ -91,9 +124,19 @@ export class SingleOverlapSolver extends BaseSolver {
91
124
  traces: [{ ...this.initialTrace, tracePath: simplifiedPath }],
92
125
  netLabels: [this.label],
93
126
  }).length > 0
127
+ const initialPath = simplifyPath(this.initialTrace.tracePath)
128
+ const initialLabelOverlaps = detectTraceLabelOverlap({
129
+ traces: [{ ...this.initialTrace, tracePath: initialPath }],
130
+ netLabels: this.netLabelPlacements,
131
+ })
132
+ const candidateLabelOverlaps = detectTraceLabelOverlap({
133
+ traces: [{ ...this.initialTrace, tracePath: simplifiedPath }],
134
+ netLabels: this.netLabelPlacements,
135
+ })
94
136
 
95
137
  if (
96
138
  !stillOverlapsLabel &&
139
+ candidateLabelOverlaps.length <= initialLabelOverlaps.length &&
97
140
  !isPathCollidingWithObstacles(simplifiedPath, this.obstacles) &&
98
141
  !doesPathCoincideWithTraces(simplifiedPath, this.tracesToAvoidOverlapping)
99
142
  ) {
@@ -128,7 +171,7 @@ export class SingleOverlapSolver extends BaseSolver {
128
171
  })
129
172
 
130
173
  // Draw next candidate
131
- if (this.queuedCandidatePaths.length > 0) {
174
+ if (!this.solvedTracePath && this.queuedCandidatePaths.length > 0) {
132
175
  graphics.lines.push({
133
176
  points: this.queuedCandidatePaths[0],
134
177
  strokeColor: "orange",
@@ -34,6 +34,30 @@ export interface InputDirectConnection {
34
34
  pinIds: [PinId, PinId]
35
35
  netId?: string
36
36
  netLabelWidth?: number
37
+
38
+ /**
39
+ * When true, this point-to-point connection may be labeled with an "inline
40
+ * net label": the net name is drawn parallel to (and offset from) the routed
41
+ * trace instead of being placed as a separate anchored net label at the end
42
+ * of the trace.
43
+ *
44
+ * Only set this for connections whose net name is worth showing on the wire -
45
+ * the solver trusts the caller (e.g. @tscircuit/core) to make that decision.
46
+ * An inline label is only emitted when the connection actually got routed.
47
+ */
48
+ allowInlineNetLabel?: boolean
49
+
50
+ /**
51
+ * Extent of the inline net label along the trace. Falls back to
52
+ * `netLabelWidth`, then to an estimate from the netId text.
53
+ */
54
+ inlineNetLabelWidth?: number
55
+
56
+ /**
57
+ * Height of the inline net label text. Defaults to
58
+ * DEFAULT_INLINE_NET_LABEL_HEIGHT.
59
+ */
60
+ inlineNetLabelHeight?: number
37
61
  }
38
62
 
39
63
  export interface InputNetConnection {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tscircuit/schematic-trace-solver",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.125",
4
+ "version": "0.0.127",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",
@@ -0,0 +1,4 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/bug-reports/bug-report-20260806T093501Z/bug-report-20260806T093501Z.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />
@@ -0,0 +1,6 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/assets/inline-net-label01.json"
3
+
4
+ export { inputProblem }
5
+
6
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />