@tscircuit/schematic-trace-solver 0.0.177 → 0.0.178

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.
@@ -127,6 +127,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
127
127
  return {
128
128
  traces: this.traces,
129
129
  netLabelPlacements: this.outputNetLabelPlacements,
130
+ netLabelConnectorTraceIds: this.netLabelConnectorTraceIds,
130
131
  }
131
132
  }
132
133
 
@@ -467,12 +467,17 @@ export class SchematicTracePipelineSolver extends BaseSolver {
467
467
  allLabelPlacements: collisionOutput.netLabelPlacements,
468
468
  mergedLabelNetIdMap: labelMergingOutput.mergedLabelNetIdMap,
469
469
  paddingBuffer: 0.1,
470
- operations: ["aligning_same_net_rails"],
470
+ operations: [
471
+ "rerouting_generated_net_label_connector_crossings",
472
+ "aligning_same_net_rails",
473
+ ],
471
474
  eligibleTraceIds: new Set(
472
475
  instance
473
476
  .traceCleanupSolver!.getOutput()
474
477
  .traces.map((trace) => trace.mspPairId),
475
478
  ),
479
+ netLabelConnectorTraceIds:
480
+ instance.availableNetOrientationSolver!.netLabelConnectorTraceIds,
476
481
  },
477
482
  ]
478
483
  },
@@ -10,6 +10,7 @@ import { alignSameNetRails } from "./alignSameNetRails"
10
10
 
11
11
  export type TraceCleanupOperation =
12
12
  | "untangling_traces"
13
+ | "rerouting_generated_net_label_connector_crossings"
13
14
  | "minimizing_turns"
14
15
  | "balancing_l_shapes"
15
16
  | "aligning_same_net_rails"
@@ -25,10 +26,12 @@ export interface TraceCleanupSolverInput {
25
26
  paddingBuffer: number
26
27
  operations?: readonly TraceCleanupOperation[]
27
28
  eligibleTraceIds?: ReadonlySet<string>
29
+ netLabelConnectorTraceIds?: ReadonlySet<string>
28
30
  }
29
31
 
30
32
  import { UntangleTraceSubsolver } from "./sub-solver/UntangleTraceSubsolver"
31
33
  import { is4PointRectangle } from "./is4PointRectangle"
34
+ import { rerouteGeneratedNetLabelConnectorCrossings } from "./rerouteGeneratedNetLabelConnectorCrossings"
32
35
 
33
36
  /**
34
37
  * Represents the different stages or steps within the trace cleanup pipeline.
@@ -97,6 +100,9 @@ export class TraceCleanupSolver extends BaseSolver {
97
100
  case "untangling_traces":
98
101
  this._runUntangleTracesStep()
99
102
  break
103
+ case "rerouting_generated_net_label_connector_crossings":
104
+ this._runGeneratedNetLabelConnectorCrossingRerouteStep()
105
+ break
100
106
  case "minimizing_turns":
101
107
  this._runMinimizeTurnsStep()
102
108
  break
@@ -123,6 +129,25 @@ export class TraceCleanupSolver extends BaseSolver {
123
129
  })
124
130
  }
125
131
 
132
+ private _runGeneratedNetLabelConnectorCrossingRerouteStep() {
133
+ const result = rerouteGeneratedNetLabelConnectorCrossings({
134
+ inputProblem: this.input.inputProblem,
135
+ traces: this.outputTraces,
136
+ netLabelPlacements: this.input.allLabelPlacements,
137
+ mergedLabelNetIdMap: this.input.mergedLabelNetIdMap,
138
+ clearance: this.input.paddingBuffer,
139
+ eligibleTraceIds: this.input.eligibleTraceIds,
140
+ connectorTraceIds: this.input.netLabelConnectorTraceIds ?? new Set(),
141
+ })
142
+ this.outputTraces = result.traces
143
+ this.tracesMap = new Map(
144
+ this.outputTraces.map((trace) => [trace.mspPairId, trace]),
145
+ )
146
+ this.stats.reroutedGeneratedConnectorCrossingTraceCount =
147
+ result.reroutedTraceCount
148
+ this._advancePipeline()
149
+ }
150
+
126
151
  private _runMinimizeTurnsStep() {
127
152
  if (this.traceIdQueue.length === 0) {
128
153
  this._advancePipeline()
@@ -0,0 +1,194 @@
1
+ import type { Bounds } from "@tscircuit/math-utils"
2
+ import { getPathLength } from "lib/solvers/Example28Solver/geometry"
3
+ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
4
+ import { getRectBounds } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
5
+ import { tracePathContainsPoint } from "lib/solvers/RailNetLabelCornerPlacementSolver/geometry"
6
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
7
+ import { isPathCollidingWithObstacles } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
8
+ import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
9
+ import type { InputProblem } from "lib/types/InputProblem"
10
+ import { doesPathCoincideWithTraces } from "lib/utils/doesPathCoincideWithTraces"
11
+ import { countTurns } from "./countTurns"
12
+ import { hasCollisionsWithLabels } from "./hasCollisionsWithLabels"
13
+ import { findPerpendicularPathCrossings } from "./sub-solver/findIntersectionsWithObstacles"
14
+ import { generatePerpendicularTraceDetours } from "./sub-solver/generateLShapeRerouteCandidates"
15
+ import { isPathColliding } from "./sub-solver/isPathColliding"
16
+
17
+ const EPS = 1e-6
18
+
19
+ const getConnectorObstacleBounds = (
20
+ connector: SolvedTracePath,
21
+ segmentIndex: number,
22
+ labels: NetLabelPlacement[],
23
+ ): Bounds => {
24
+ const start = connector.tracePath[segmentIndex]!
25
+ const end = connector.tracePath[segmentIndex + 1]!
26
+ const bounds = {
27
+ minX: Math.min(start.x, end.x),
28
+ minY: Math.min(start.y, end.y),
29
+ maxX: Math.max(start.x, end.x),
30
+ maxY: Math.max(start.y, end.y),
31
+ }
32
+
33
+ for (const label of labels) {
34
+ if (label.globalConnNetId !== connector.globalConnNetId) continue
35
+ if (!label.pinIds.every((pinId) => connector.pinIds.includes(pinId)))
36
+ continue
37
+ if (!tracePathContainsPoint(connector.tracePath, label.anchorPoint))
38
+ continue
39
+
40
+ const labelBounds = getRectBounds(label.center, label.width, label.height)
41
+ bounds.minX = Math.min(bounds.minX, labelBounds.minX)
42
+ bounds.minY = Math.min(bounds.minY, labelBounds.minY)
43
+ bounds.maxX = Math.max(bounds.maxX, labelBounds.maxX)
44
+ bounds.maxY = Math.max(bounds.maxY, labelBounds.maxY)
45
+ }
46
+
47
+ return bounds
48
+ }
49
+
50
+ const getEligibleConnectorCrossings = (
51
+ traces: SolvedTracePath[],
52
+ connectorTraceIds: ReadonlySet<string>,
53
+ eligibleTraceIds?: ReadonlySet<string>,
54
+ ) => {
55
+ const crossings: Array<{
56
+ traceIndex: number
57
+ connector: SolvedTracePath
58
+ connectorSegmentIndex: number
59
+ traceSegmentIndex: number
60
+ }> = []
61
+
62
+ for (const connector of traces) {
63
+ if (!connectorTraceIds.has(connector.mspPairId)) continue
64
+
65
+ for (let traceIndex = 0; traceIndex < traces.length; traceIndex++) {
66
+ const trace = traces[traceIndex]!
67
+ if (trace.mspPairId === connector.mspPairId) continue
68
+ if (connectorTraceIds.has(trace.mspPairId)) continue
69
+ if (trace.globalConnNetId === connector.globalConnNetId) continue
70
+ if (eligibleTraceIds && !eligibleTraceIds.has(trace.mspPairId)) continue
71
+
72
+ for (const crossing of findPerpendicularPathCrossings(
73
+ trace.tracePath,
74
+ connector.tracePath,
75
+ { includeTerminalSegments: true },
76
+ )) {
77
+ crossings.push({
78
+ traceIndex,
79
+ connector,
80
+ connectorSegmentIndex: crossing.otherPathSegmentIndex,
81
+ traceSegmentIndex: crossing.pathSegmentIndex,
82
+ })
83
+ }
84
+ }
85
+ }
86
+
87
+ return crossings
88
+ }
89
+
90
+ // Generated label connectors are added after the component traces are routed.
91
+ // Reuse the normal perpendicular-detour candidates here, keeping connectors
92
+ // fixed and considering only traces that came from the original routing pass.
93
+ export const rerouteGeneratedNetLabelConnectorCrossings = ({
94
+ inputProblem,
95
+ traces,
96
+ netLabelPlacements,
97
+ mergedLabelNetIdMap,
98
+ clearance,
99
+ eligibleTraceIds,
100
+ connectorTraceIds,
101
+ }: {
102
+ inputProblem: InputProblem
103
+ traces: SolvedTracePath[]
104
+ netLabelPlacements: NetLabelPlacement[]
105
+ mergedLabelNetIdMap: Record<string, Set<string>>
106
+ clearance: number
107
+ eligibleTraceIds?: ReadonlySet<string>
108
+ connectorTraceIds: ReadonlySet<string>
109
+ }) => {
110
+ const outputTraces = [...traces]
111
+ const componentAndTextObstacles = getObstacleRects(inputProblem).filter(
112
+ (obstacle) => obstacle.kind === "chip" || obstacle.kind === "text_box",
113
+ )
114
+ let reroutedTraceCount = 0
115
+
116
+ while (true) {
117
+ const candidates = getEligibleConnectorCrossings(
118
+ outputTraces,
119
+ connectorTraceIds,
120
+ eligibleTraceIds,
121
+ ).flatMap((crossing) => {
122
+ const trace = outputTraces[crossing.traceIndex]!
123
+ const obstacleBounds = getConnectorObstacleBounds(
124
+ crossing.connector,
125
+ crossing.connectorSegmentIndex,
126
+ netLabelPlacements,
127
+ )
128
+ const foreignTraces = outputTraces.filter(
129
+ (otherTrace) =>
130
+ otherTrace.mspPairId !== trace.mspPairId &&
131
+ otherTrace.globalConnNetId !== trace.globalConnNetId,
132
+ )
133
+ const foreignLabelBounds = netLabelPlacements
134
+ .filter((label) => {
135
+ const mergedNetIds = mergedLabelNetIdMap[label.globalConnNetId]
136
+ return mergedNetIds
137
+ ? !mergedNetIds.has(trace.globalConnNetId)
138
+ : label.globalConnNetId !== trace.globalConnNetId
139
+ })
140
+ .map((label) => getRectBounds(label.center, label.width, label.height))
141
+
142
+ return generatePerpendicularTraceDetours({
143
+ trace,
144
+ segmentIndex: crossing.traceSegmentIndex,
145
+ obstacleStart:
146
+ crossing.connector.tracePath[crossing.connectorSegmentIndex]!,
147
+ obstacleEnd:
148
+ crossing.connector.tracePath[crossing.connectorSegmentIndex + 1]!,
149
+ obstacleBounds,
150
+ chipBounds: [],
151
+ clearance,
152
+ })
153
+ .filter(
154
+ (candidate) =>
155
+ getPathLength(candidate.path) <=
156
+ getPathLength(trace.tracePath) + EPS &&
157
+ countTurns(candidate.path) <= countTurns(trace.tracePath) + 2 &&
158
+ !isPathCollidingWithObstacles(
159
+ candidate.path,
160
+ componentAndTextObstacles,
161
+ ) &&
162
+ !hasCollisionsWithLabels(candidate.path, foreignLabelBounds) &&
163
+ !isPathColliding(candidate.path, foreignTraces, trace.mspPairId)
164
+ .isColliding &&
165
+ !doesPathCoincideWithTraces(candidate.path, foreignTraces),
166
+ )
167
+ .map((candidate) => ({
168
+ ...candidate,
169
+ traceIndex: crossing.traceIndex,
170
+ }))
171
+ })
172
+
173
+ candidates.sort((first, second) => {
174
+ const lengthDifference =
175
+ getPathLength(first.path) - getPathLength(second.path)
176
+ return Math.abs(lengthDifference) > EPS
177
+ ? lengthDifference
178
+ : countTurns(first.path) - countTurns(second.path)
179
+ })
180
+ const bestCandidate = candidates[0]
181
+ if (!bestCandidate) break
182
+
183
+ outputTraces[bestCandidate.traceIndex] = {
184
+ ...outputTraces[bestCandidate.traceIndex]!,
185
+ tracePath: bestCandidate.path,
186
+ }
187
+ reroutedTraceCount++
188
+ }
189
+
190
+ return {
191
+ traces: outputTraces,
192
+ reroutedTraceCount,
193
+ }
194
+ }
@@ -9,6 +9,10 @@ export interface PerpendicularPathCrossing {
9
9
  otherPathSegmentIndex: number
10
10
  }
11
11
 
12
+ interface FindPerpendicularPathCrossingsOptions {
13
+ includeTerminalSegments?: boolean
14
+ }
15
+
12
16
  /**
13
17
  * Finds all intersection points between a given line segment (p1-p2) and a list of trace obstacles.
14
18
  * It iterates through each segment of every obstacle and checks for intersections with the input segment.
@@ -48,14 +52,24 @@ const isSamePoint = (first: Point, second: Point) =>
48
52
  export const findPerpendicularPathCrossings = (
49
53
  path: Point[],
50
54
  otherPath: Point[],
55
+ options: FindPerpendicularPathCrossingsOptions = {},
51
56
  ): PerpendicularPathCrossing[] => {
52
57
  const crossings: PerpendicularPathCrossing[] = []
58
+ const firstPathSegmentIndex = options.includeTerminalSegments ? 0 : 1
59
+ const lastPathSegmentIndex = options.includeTerminalSegments
60
+ ? path.length - 1
61
+ : path.length - 2
62
+ const firstOtherPathSegmentIndex = options.includeTerminalSegments ? 0 : 1
63
+ const lastOtherPathSegmentIndex = options.includeTerminalSegments
64
+ ? otherPath.length - 1
65
+ : otherPath.length - 2
53
66
 
54
67
  // Terminal segments connect to pins and are allowed to meet other traces at
55
- // their endpoints. Only internal, strict crossings need to be untangled.
68
+ // their endpoints. Callers may include them when looking for strict
69
+ // crossings through a segment's interior after all connector traces exist.
56
70
  for (
57
- let pathSegmentIndex = 1;
58
- pathSegmentIndex < path.length - 2;
71
+ let pathSegmentIndex = firstPathSegmentIndex;
72
+ pathSegmentIndex < lastPathSegmentIndex;
59
73
  pathSegmentIndex++
60
74
  ) {
61
75
  const start = path[pathSegmentIndex]!
@@ -63,8 +77,8 @@ export const findPerpendicularPathCrossings = (
63
77
  const isVertical = Math.abs(start.x - end.x) < EPS
64
78
 
65
79
  for (
66
- let otherPathSegmentIndex = 1;
67
- otherPathSegmentIndex < otherPath.length - 2;
80
+ let otherPathSegmentIndex = firstOtherPathSegmentIndex;
81
+ otherPathSegmentIndex < lastOtherPathSegmentIndex;
68
82
  otherPathSegmentIndex++
69
83
  ) {
70
84
  const otherStart = otherPath[otherPathSegmentIndex]!
@@ -113,6 +113,7 @@ export interface PerpendicularTraceDetourInput {
113
113
  segmentIndex: number
114
114
  obstacleStart: Point
115
115
  obstacleEnd: Point
116
+ obstacleBounds?: Bounds
116
117
  chipBounds: Bounds[]
117
118
  clearance: number
118
119
  }
@@ -179,6 +180,7 @@ export const generatePerpendicularTraceDetours = ({
179
180
  segmentIndex,
180
181
  obstacleStart,
181
182
  obstacleEnd,
183
+ obstacleBounds,
182
184
  chipBounds,
183
185
  clearance,
184
186
  }: PerpendicularTraceDetourInput): TraceDetourCandidate[] => {
@@ -187,20 +189,48 @@ export const generatePerpendicularTraceDetours = ({
187
189
  const end = path[index + 1]!
188
190
  const movingAxis: "x" | "y" = Math.abs(start.x - end.x) < EPS ? "y" : "x"
189
191
  const detourAxis = movingAxis === "x" ? "y" : "x"
190
- const gate =
192
+ const bounds = obstacleBounds ?? {
193
+ minX: Math.min(obstacleStart.x, obstacleEnd.x),
194
+ minY: Math.min(obstacleStart.y, obstacleEnd.y),
195
+ maxX: Math.max(obstacleStart.x, obstacleEnd.x),
196
+ maxY: Math.max(obstacleStart.y, obstacleEnd.y),
197
+ }
198
+ const movingLowBound = movingAxis === "x" ? "minX" : "minY"
199
+ const movingHighBound = movingAxis === "x" ? "maxX" : "maxY"
200
+ let gate =
191
201
  obstacleStart[movingAxis] +
192
202
  Math.sign(start[movingAxis] - obstacleStart[movingAxis]) * clearance
193
- const obstacleRange = [obstacleStart[detourAxis], obstacleEnd[detourAxis]]
203
+ if (obstacleBounds) {
204
+ const startSide =
205
+ start[movingAxis] <
206
+ (bounds[movingLowBound] + bounds[movingHighBound]) / 2
207
+ ? -1
208
+ : 1
209
+ gate =
210
+ bounds[startSide < 0 ? movingLowBound : movingHighBound] +
211
+ startSide * clearance
212
+ }
194
213
  const lowBound = detourAxis === "x" ? "minX" : "minY"
195
214
  const highBound = detourAxis === "x" ? "maxX" : "maxY"
215
+ const minDetour = bounds[lowBound] - clearance
216
+ const maxDetour = bounds[highBound] + clearance
217
+ const nextAnchor = obstacleBounds ? path[index + 2] : undefined
218
+ const balancedDetour = nextAnchor
219
+ ? nextAnchor[detourAxis] < minDetour
220
+ ? (nextAnchor[detourAxis] + minDetour) / 2
221
+ : nextAnchor[detourAxis] > maxDetour
222
+ ? (nextAnchor[detourAxis] + maxDetour) / 2
223
+ : undefined
224
+ : undefined
196
225
  const detourCoordinates = [
197
- Math.min(...obstacleRange) - clearance,
198
- Math.max(...obstacleRange) + clearance,
226
+ balancedDetour,
227
+ minDetour,
228
+ maxDetour,
199
229
  ...chipBounds.flatMap((bounds) => [
200
230
  bounds[lowBound] - clearance,
201
231
  bounds[highBound] + clearance,
202
232
  ]),
203
- ]
233
+ ].filter((coordinate): coordinate is number => coordinate !== undefined)
204
234
 
205
235
  return [...new Set(detourCoordinates)].map((detour) =>
206
236
  simplifyPath([
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "url": "https://github.com/tscircuit/schematic-trace-solver.git"
6
6
  },
7
7
  "main": "dist/index.js",
8
- "version": "0.0.177",
8
+ "version": "0.0.178",
9
9
  "type": "module",
10
10
  "scripts": {
11
11
  "start": "cosmos",