@tscircuit/schematic-trace-solver 0.0.64 → 0.0.66

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.
@@ -74,6 +74,7 @@ export class NetLabelPlacementSolver extends BaseSolver {
74
74
  declare activeSubSolver: SingleNetLabelPlacementSolver | null
75
75
 
76
76
  netLabelPlacements: Array<NetLabelPlacement> = []
77
+ failedGroups: Array<OverlappingSameNetTraceGroup> = []
77
78
  currentGroup: OverlappingSameNetTraceGroup | null = null
78
79
  triedAnyOrientationFallbackForCurrentGroup = false
79
80
 
@@ -336,8 +337,13 @@ export class NetLabelPlacementSolver extends BaseSolver {
336
337
  return
337
338
  }
338
339
 
339
- this.failed = true
340
- this.error = this.activeSubSolver.error
340
+ // Record the failure for this group and continue to the next one
341
+ if (this.currentGroup) {
342
+ this.failedGroups.push(this.currentGroup)
343
+ }
344
+ this.activeSubSolver = null
345
+ this.currentGroup = null
346
+ this.triedAnyOrientationFallbackForCurrentGroup = false
341
347
  return
342
348
  }
343
349
 
@@ -42,10 +42,13 @@ export class TraceLabelOverlapAvoidanceSolver extends BaseSolver {
42
42
 
43
43
  labelMergingSolver?: MergedNetLabelObstacleSolver
44
44
 
45
+ allInputTraces: SolvedTracePath[]
46
+
45
47
  constructor(solverInput: TraceLabelOverlapAvoidanceSolverInput) {
46
48
  super()
47
49
  this.inputProblem = solverInput.inputProblem
48
50
  this.unprocessedTraces = [...solverInput.traces]
51
+ this.allInputTraces = solverInput.traces
49
52
  this.netLabelPlacements = solverInput.netLabelPlacements
50
53
  this.cleanTraces = []
51
54
  this.subSolvers = []
@@ -88,6 +91,9 @@ export class TraceLabelOverlapAvoidanceSolver extends BaseSolver {
88
91
  mergedNetLabelPlacements: mergingOutput.netLabelPlacements,
89
92
  mergedLabelNetIdMap: mergingOutput.mergedLabelNetIdMap,
90
93
  detourCounts: this.detourCounts,
94
+ tracesToAvoidOverlapping: this.allInputTraces.filter(
95
+ (t) => t.mspPairId !== currentTargetTrace.mspPairId,
96
+ ),
91
97
  })
92
98
  this.subSolvers.push(subSolver)
93
99
  }
@@ -19,6 +19,7 @@ export interface OverlapCollectionSolverInput {
19
19
  mergedNetLabelPlacements: NetLabelPlacement[]
20
20
  mergedLabelNetIdMap: Record<string, Set<string>>
21
21
  detourCounts: Map<string, number>
22
+ tracesToAvoidOverlapping?: SolvedTracePath[]
22
23
  }
23
24
 
24
25
  /**
@@ -32,6 +33,7 @@ export class OverlapAvoidanceStepSolver extends BaseSolver {
32
33
  mergedLabelNetIdMap: Record<string, Set<string>>
33
34
 
34
35
  allTraces: SolvedTracePath[]
36
+ tracesToAvoidOverlapping: SolvedTracePath[]
35
37
  modifiedTraces: SolvedTracePath[] = []
36
38
 
37
39
  private readonly PADDING_BUFFER = 0.1
@@ -51,6 +53,7 @@ export class OverlapAvoidanceStepSolver extends BaseSolver {
51
53
  this.mergedNetLabelPlacements = solverInput.mergedNetLabelPlacements
52
54
  this.mergedLabelNetIdMap = solverInput.mergedLabelNetIdMap
53
55
  this.allTraces = [...solverInput.traces]
56
+ this.tracesToAvoidOverlapping = solverInput.tracesToAvoidOverlapping ?? []
54
57
  this.detourCounts = solverInput.detourCounts
55
58
  }
56
59
 
@@ -150,6 +153,7 @@ export class OverlapAvoidanceStepSolver extends BaseSolver {
150
153
  problem: this.inputProblem,
151
154
  paddingBuffer: this.PADDING_BUFFER,
152
155
  detourCount: detourCount,
156
+ tracesToAvoidOverlapping: this.tracesToAvoidOverlapping,
153
157
  })
154
158
  } else {
155
159
  const overlapId = `${traceToFix.mspPairId}-${labelToAvoid.globalConnNetId}`
@@ -192,6 +196,7 @@ export class OverlapAvoidanceStepSolver extends BaseSolver {
192
196
  problem: this.inputProblem,
193
197
  paddingBuffer: this.PADDING_BUFFER,
194
198
  detourCount: detourCount,
199
+ tracesToAvoidOverlapping: this.tracesToAvoidOverlapping,
195
200
  })
196
201
  } else {
197
202
  const overlapId = `${traceToFix.mspPairId}-${labelToAvoid.globalConnNetId}`
@@ -211,6 +216,7 @@ export class OverlapAvoidanceStepSolver extends BaseSolver {
211
216
  problem: this.inputProblem,
212
217
  paddingBuffer: this.PADDING_BUFFER,
213
218
  detourCount: detourCount,
219
+ tracesToAvoidOverlapping: this.tracesToAvoidOverlapping,
214
220
  })
215
221
  }
216
222
  }
@@ -16,9 +16,69 @@ interface SingleOverlapSolverInput {
16
16
  problem: InputProblem
17
17
  paddingBuffer: number
18
18
  detourCount: number
19
+ tracesToAvoidOverlapping?: SolvedTracePath[]
19
20
  }
20
21
 
21
22
  const MAX_TRIES = 5
23
+ const COINCIDENT_EPS = 2e-3
24
+
25
+ /**
26
+ * Returns true if any segment of the path is parallel and coincident
27
+ * (within COINCIDENT_EPS) with a segment of one of the given traces.
28
+ * Perpendicular crossings are allowed.
29
+ */
30
+ const doesPathCoincideWithTraces = (
31
+ path: Point[],
32
+ traces: SolvedTracePath[],
33
+ ): boolean => {
34
+ const rangesOverlap1D = (a1: number, a2: number, b1: number, b2: number) =>
35
+ Math.min(Math.max(a1, a2), Math.max(b1, b2)) -
36
+ Math.max(Math.min(a1, a2), Math.min(b1, b2)) >
37
+ COINCIDENT_EPS
38
+
39
+ for (let i = 0; i < path.length - 1; i++) {
40
+ const pathSegStart = path[i]!
41
+ const pathSegEnd = path[i + 1]!
42
+ const isVertical = Math.abs(pathSegStart.x - pathSegEnd.x) < COINCIDENT_EPS
43
+ const isHorizontal =
44
+ Math.abs(pathSegStart.y - pathSegEnd.y) < COINCIDENT_EPS
45
+ if (!isVertical && !isHorizontal) continue
46
+
47
+ // For a vertical segment, coincidence is measured in x and overlap in y
48
+ // (and vice versa for horizontal)
49
+ const crossAxis = isVertical ? "x" : "y"
50
+ const alongAxis = isVertical ? "y" : "x"
51
+
52
+ for (const trace of traces) {
53
+ for (let j = 0; j < trace.tracePath.length - 1; j++) {
54
+ const traceSegStart = trace.tracePath[j]!
55
+ const traceSegEnd = trace.tracePath[j + 1]!
56
+
57
+ const isParallel =
58
+ Math.abs(traceSegStart[crossAxis] - traceSegEnd[crossAxis]) <
59
+ COINCIDENT_EPS
60
+ if (!isParallel) continue
61
+
62
+ const isCoincident =
63
+ Math.abs(pathSegStart[crossAxis] - traceSegStart[crossAxis]) <
64
+ COINCIDENT_EPS
65
+ if (!isCoincident) continue
66
+
67
+ if (
68
+ rangesOverlap1D(
69
+ pathSegStart[alongAxis],
70
+ pathSegEnd[alongAxis],
71
+ traceSegStart[alongAxis],
72
+ traceSegEnd[alongAxis],
73
+ )
74
+ ) {
75
+ return true
76
+ }
77
+ }
78
+ }
79
+ }
80
+ return false
81
+ }
22
82
 
23
83
  /**
24
84
  * This solver attempts to find a valid rerouting for a single trace that is
@@ -32,6 +92,7 @@ export class SingleOverlapSolver extends BaseSolver {
32
92
  problem: InputProblem
33
93
  obstacles: ReturnType<typeof getObstacleRects>
34
94
  label: NetLabelPlacement
95
+ tracesToAvoidOverlapping: SolvedTracePath[]
35
96
  _tried: number = 0
36
97
 
37
98
  constructor(solverInput: SingleOverlapSolverInput) {
@@ -39,6 +100,9 @@ export class SingleOverlapSolver extends BaseSolver {
39
100
  this.initialTrace = solverInput.trace
40
101
  this.problem = solverInput.problem
41
102
  this.label = solverInput.label
103
+ this.tracesToAvoidOverlapping = (
104
+ solverInput.tracesToAvoidOverlapping ?? []
105
+ ).filter((t) => t.globalConnNetId !== solverInput.trace.globalConnNetId)
42
106
 
43
107
  // Calculate an effective padding for this specific run based on the detourCount.
44
108
  const effectivePadding =
@@ -77,7 +141,10 @@ export class SingleOverlapSolver extends BaseSolver {
77
141
  const nextCandidatePath = this.queuedCandidatePaths.shift()!
78
142
  const simplifiedPath = simplifyPath(nextCandidatePath)
79
143
 
80
- if (!isPathCollidingWithObstacles(simplifiedPath, this.obstacles)) {
144
+ if (
145
+ !isPathCollidingWithObstacles(simplifiedPath, this.obstacles) &&
146
+ !doesPathCoincideWithTraces(simplifiedPath, this.tracesToAvoidOverlapping)
147
+ ) {
81
148
  this.solvedTracePath = simplifiedPath
82
149
  this.solved = true
83
150
  }
@@ -1,11 +1,16 @@
1
1
  import type { GraphicsObject } from "graphics-debug"
2
2
  import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
3
3
  import type { MspConnectionPairId } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver"
4
+ import { findFirstCollision } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
5
+ import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
4
6
  import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
7
+ import type { InputProblem } from "lib/types/InputProblem"
5
8
  import { applyJogToTerminalSegment } from "./applyJogToTrace"
6
9
 
7
10
  type ConnNetId = string
8
11
 
12
+ const EPS = 1e-6
13
+
9
14
  export interface OverlappingTraceSegmentLocator {
10
15
  connNetId: string
11
16
  pathsWithOverlap: Array<{
@@ -17,18 +22,21 @@ export interface OverlappingTraceSegmentLocator {
17
22
  export class TraceOverlapIssueSolver extends BaseSolver {
18
23
  overlappingTraceSegments: OverlappingTraceSegmentLocator[]
19
24
  traceNetIslands: Record<ConnNetId, Array<SolvedTracePath>>
25
+ obstacleRects: ReturnType<typeof getObstacleRects>
20
26
 
21
27
  SHIFT_DISTANCE = 0.1
22
28
 
23
29
  correctedTraceMap: Record<MspConnectionPairId, SolvedTracePath> = {}
24
30
 
25
31
  constructor(params: {
32
+ inputProblem: InputProblem
26
33
  overlappingTraceSegments: OverlappingTraceSegmentLocator[]
27
34
  traceNetIslands: Record<ConnNetId, Array<SolvedTracePath>>
28
35
  }) {
29
36
  super()
30
37
  this.overlappingTraceSegments = params.overlappingTraceSegments
31
38
  this.traceNetIslands = params.traceNetIslands
39
+ this.obstacleRects = getObstacleRects(params.inputProblem)
32
40
 
33
41
  // Only add the relevant traces to the correctedTraceMap
34
42
  for (const { connNetId, pathsWithOverlap } of this
@@ -49,13 +57,34 @@ export class TraceOverlapIssueSolver extends BaseSolver {
49
57
  // Shift only the overlapping segments, and move the shared endpoints
50
58
  // (the last point of the previous segment and the first point of the next
51
59
  // segment) so the polyline remains orthogonal without self-overlap.
52
- const EPS = 1e-6
60
+ // Groups containing a straight pin-to-pin trace (2 points, both endpoints
61
+ // are pins) keep their position when another group can shift instead:
62
+ // shifting such a trace would force jogs on an otherwise straight line.
63
+ const containsStraightPinToPinTrace = (
64
+ group: OverlappingTraceSegmentLocator,
65
+ ) =>
66
+ group.pathsWithOverlap.some(({ solvedTracePathIndex }) => {
67
+ const path =
68
+ this.traceNetIslands[group.connNetId][solvedTracePathIndex]!
69
+ return path.tracePath.length === 2
70
+ })
71
+
72
+ const groupShouldStayInPlace = this.overlappingTraceSegments.map(
73
+ containsStraightPinToPinTrace,
74
+ )
75
+ const someGroupCanShift = groupShouldStayInPlace.some(
76
+ (shouldStay) => !shouldStay,
77
+ )
53
78
 
54
79
  // Compute offsets for each island involved: alternate directions
55
- const offsets = this.overlappingTraceSegments.map((_, idx) => {
80
+ const offsets = this.overlappingTraceSegments.map((group, idx) => {
81
+ if (someGroupCanShift && groupShouldStayInPlace[idx]) return 0
56
82
  const n = Math.floor(idx / 2) + 1
57
83
  const signed = idx % 2 === 0 ? -n : n
58
- return signed * this.SHIFT_DISTANCE
84
+ return this.getObstacleAwareOffset({
85
+ group,
86
+ offset: signed * this.SHIFT_DISTANCE,
87
+ })
59
88
  })
60
89
 
61
90
  const eq = (a: number, b: number) => Math.abs(a - b) < EPS
@@ -67,6 +96,7 @@ export class TraceOverlapIssueSolver extends BaseSolver {
67
96
  // For each net island group, shift only its overlapping segments and adjust adjacent joints
68
97
  this.overlappingTraceSegments.forEach((group, gidx) => {
69
98
  const offset = offsets[gidx]!
99
+ if (offset === 0) return
70
100
 
71
101
  // Gather unique segment indices per path
72
102
  const byPath: Map<number, Set<number>> = new Map()
@@ -82,8 +112,6 @@ export class TraceOverlapIssueSolver extends BaseSolver {
82
112
  const current = this.correctedTraceMap[original.mspPairId] ?? original
83
113
  const pts = current.tracePath.map((p) => ({ ...p }))
84
114
 
85
- const segIdxs = Array.from(segIdxSet).sort((a, b) => a - b)
86
-
87
115
  const segIdxsRev = Array.from(segIdxSet)
88
116
  .sort((a, b) => a - b)
89
117
  .reverse()
@@ -142,6 +170,82 @@ export class TraceOverlapIssueSolver extends BaseSolver {
142
170
  this.solved = true
143
171
  }
144
172
 
173
+ private getObstacleAwareOffset({
174
+ group,
175
+ offset,
176
+ }: {
177
+ group: OverlappingTraceSegmentLocator
178
+ offset: number
179
+ }) {
180
+ const blockingCollisions = this.getBlockingCollisions({ group, offset })
181
+ if (blockingCollisions.length === 0) return offset
182
+
183
+ return (
184
+ blockingCollisions
185
+ .flatMap(({ start, isVertical, obstacle }) => [
186
+ (isVertical ? obstacle.minX - start.x : obstacle.minY - start.y) -
187
+ this.SHIFT_DISTANCE,
188
+ (isVertical ? obstacle.maxX - start.x : obstacle.maxY - start.y) +
189
+ this.SHIFT_DISTANCE,
190
+ ])
191
+ .filter(
192
+ (candidateOffset) =>
193
+ this.getBlockingCollisions({ group, offset: candidateOffset })
194
+ .length === 0,
195
+ )
196
+ .sort((a, b) => Math.abs(a - offset) - Math.abs(b - offset))[0] ??
197
+ offset
198
+ )
199
+ }
200
+
201
+ private getBlockingCollisions({
202
+ group,
203
+ offset,
204
+ }: {
205
+ group: OverlappingTraceSegmentLocator
206
+ offset: number
207
+ }) {
208
+ const blockingCollisions: Array<{
209
+ start: SolvedTracePath["tracePath"][number]
210
+ isVertical: boolean
211
+ obstacle: ReturnType<typeof getObstacleRects>[number]
212
+ }> = []
213
+
214
+ for (const {
215
+ solvedTracePathIndex,
216
+ traceSegmentIndex,
217
+ } of group.pathsWithOverlap) {
218
+ const trace = this.traceNetIslands[group.connNetId][solvedTracePathIndex]!
219
+ const start = trace.tracePath[traceSegmentIndex]
220
+ const end = trace.tracePath[traceSegmentIndex + 1]
221
+ if (!start || !end) continue
222
+
223
+ const isVertical = Math.abs(start.x - end.x) < EPS
224
+ const isHorizontal = Math.abs(start.y - end.y) < EPS
225
+ if (!isVertical && !isHorizontal) continue
226
+
227
+ const shiftedSegment = isVertical
228
+ ? [
229
+ { ...start, x: start.x + offset },
230
+ { ...end, x: end.x + offset },
231
+ ]
232
+ : [
233
+ { ...start, y: start.y + offset },
234
+ { ...end, y: end.y + offset },
235
+ ]
236
+ const collision = findFirstCollision(shiftedSegment, this.obstacleRects)
237
+ if (collision) {
238
+ blockingCollisions.push({
239
+ start,
240
+ isVertical,
241
+ obstacle: collision.rect,
242
+ })
243
+ }
244
+ }
245
+
246
+ return blockingCollisions
247
+ }
248
+
145
249
  override visualize(): GraphicsObject {
146
250
  // Visualize overlapped segments and proposed corrections
147
251
  const graphics: GraphicsObject = {
@@ -29,6 +29,36 @@ export const applyJogToTerminalSegment = ({
29
29
  ? 1
30
30
  : -1
31
31
 
32
+ if (pts.length === 2) {
33
+ // The segment is both the first and last segment: both endpoints are
34
+ // pins, so jog inward on both sides to keep the endpoints anchored
35
+ if (isVertical) {
36
+ const jogYNearStart = start.y + segDir * JOG_SIZE
37
+ const jogYNearEnd = end.y - segDir * JOG_SIZE
38
+ pts.splice(
39
+ 1,
40
+ 0,
41
+ { x: start.x, y: jogYNearStart },
42
+ { x: start.x + offset, y: jogYNearStart },
43
+ { x: end.x + offset, y: jogYNearEnd },
44
+ { x: end.x, y: jogYNearEnd },
45
+ )
46
+ } else {
47
+ // Horizontal
48
+ const jogXNearStart = start.x + segDir * JOG_SIZE
49
+ const jogXNearEnd = end.x - segDir * JOG_SIZE
50
+ pts.splice(
51
+ 1,
52
+ 0,
53
+ { x: jogXNearStart, y: start.y },
54
+ { x: jogXNearStart, y: start.y + offset },
55
+ { x: jogXNearEnd, y: end.y + offset },
56
+ { x: jogXNearEnd, y: end.y },
57
+ )
58
+ }
59
+ return
60
+ }
61
+
32
62
  if (si === 0) {
33
63
  if (isVertical) {
34
64
  const jogY = start.y + segDir * JOG_SIZE
@@ -309,6 +309,7 @@ export class TraceOverlapShiftSolver extends BaseSolver {
309
309
  const { overlappingTraceSegments } = overlapIssue
310
310
 
311
311
  this.activeSubSolver = new TraceOverlapIssueSolver({
312
+ inputProblem: this.inputProblem,
312
313
  overlappingTraceSegments,
313
314
  traceNetIslands: this.traceNetIslands,
314
315
  })
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.64",
4
+ "version": "0.0.66",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",