@tscircuit/schematic-trace-solver 0.0.37 → 0.0.39

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 (35) hide show
  1. package/dist/index.d.ts +25 -1
  2. package/dist/index.js +917 -10
  3. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions.ts +17 -0
  4. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/pathOps.ts +31 -12
  5. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +40 -0
  6. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver.ts +240 -0
  7. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/balanceLShapes.ts +207 -0
  8. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/countTurns.ts +18 -0
  9. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/detectTraceLabelOverlap.ts +38 -0
  10. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/hasCollisions.ts +22 -0
  11. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/hasCollisionsWithLabels.ts +19 -0
  12. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/index.ts +1 -0
  13. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/minimizeTurnsWithFilteredLabels.ts +66 -0
  14. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts +73 -0
  15. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/simplifyPath.ts +41 -0
  16. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/tryConnectPoints.ts +14 -0
  17. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/trySnipAndReconnect.ts +229 -0
  18. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/turnMinimization.ts +211 -0
  19. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/violation.ts +25 -0
  20. package/package.json +1 -1
  21. package/site/examples/example20.page.tsx +103 -0
  22. package/site/examples/example22.page.tsx +108 -0
  23. package/site/examples/example23.page.tsx +137 -0
  24. package/site/examples/example24.page.tsx +128 -0
  25. package/tests/examples/__snapshots__/example16.snap.svg +27 -27
  26. package/tests/examples/__snapshots__/example22.snap.svg +137 -0
  27. package/tests/examples/__snapshots__/example23.snap.svg +137 -0
  28. package/tests/examples/__snapshots__/example24.snap.svg +143 -0
  29. package/tests/examples/__snapshots__/example25.snap.svg +165 -0
  30. package/tests/examples/__snapshots__/example26.snap.svg +157 -0
  31. package/tests/examples/example22.test.tsx +109 -0
  32. package/tests/examples/example23.test.tsx +109 -0
  33. package/tests/examples/example24.test.tsx +114 -0
  34. package/tests/examples/example25.test.tsx +143 -0
  35. package/tests/examples/example26.test.tsx +134 -0
@@ -55,3 +55,20 @@ export const findFirstCollision = (
55
55
  }
56
56
  return null
57
57
  }
58
+
59
+ /**
60
+ * Checks if a given path has any intersections with a set of chip obstacles.
61
+ */
62
+ export const isPathCollidingWithObstacles = (
63
+ path: Point[],
64
+ obstacles: ChipWithBounds[],
65
+ ): boolean => {
66
+ for (let i = 0; i < path.length - 1; i++) {
67
+ for (const obstacle of obstacles) {
68
+ if (segmentIntersectsRect(path[i], path[i + 1], obstacle)) {
69
+ return true // Found a collision
70
+ }
71
+ }
72
+ }
73
+ return false // No collisions found
74
+ }
@@ -12,27 +12,37 @@ export const shiftSegmentOrth = (
12
12
  newCoord: number,
13
13
  eps = EPS,
14
14
  ): Point[] | null => {
15
- if (segIndex < 0 || segIndex >= pts.length - 1) return null
15
+ if (segIndex < 0 || segIndex >= pts.length - 1) {
16
+ return null
17
+ }
16
18
  const a = pts[segIndex]!
17
19
  const b = pts[segIndex + 1]!
18
20
  const vert = isVertical(a, b, eps)
19
21
  const horz = isHorizontal(a, b, eps)
20
- if (!vert && !horz) return null
22
+ if (!vert && !horz) {
23
+ return null
24
+ }
21
25
 
22
26
  // Ensure axis matches orthogonal shift
23
- if (vert && axis !== "x") return null
24
- if (horz && axis !== "y") return null
27
+ if (vert && axis !== "x") {
28
+ return null
29
+ }
30
+ if (horz && axis !== "y") {
31
+ return null
32
+ }
25
33
 
26
34
  const out = pts.map((p) => ({ ...p }))
27
35
 
28
36
  if (axis === "x") {
29
- if (Math.abs(a.x - newCoord) < eps && Math.abs(b.x - newCoord) < eps)
37
+ if (Math.abs(a.x - newCoord) < eps && Math.abs(b.x - newCoord) < eps) {
30
38
  return null
39
+ }
31
40
  out[segIndex] = { ...out[segIndex], x: newCoord }
32
41
  out[segIndex + 1] = { ...out[segIndex + 1], x: newCoord }
33
42
  } else {
34
- if (Math.abs(a.y - newCoord) < eps && Math.abs(b.y - newCoord) < eps)
43
+ if (Math.abs(a.y - newCoord) < eps && Math.abs(b.y - newCoord) < eps) {
35
44
  return null
45
+ }
36
46
  out[segIndex] = { ...out[segIndex], y: newCoord }
37
47
  out[segIndex + 1] = { ...out[segIndex + 1], y: newCoord }
38
48
  }
@@ -42,24 +52,33 @@ export const shiftSegmentOrth = (
42
52
  const p = out[segIndex - 1]!
43
53
  const q = out[segIndex]!
44
54
  const manhattan = Math.abs(p.x - q.x) + Math.abs(p.y - q.y)
45
- if (manhattan < eps) return null
55
+ if (manhattan < eps) {
56
+ return null
57
+ }
46
58
  }
47
59
  if (segIndex + 2 <= out.length - 1) {
48
60
  const p = out[segIndex + 1]!
49
61
  const q = out[segIndex + 2]!
50
62
  const manhattan = Math.abs(p.x - q.x) + Math.abs(p.y - q.y)
51
- if (manhattan < eps) return null
63
+ if (manhattan < eps) {
64
+ return null
65
+ }
52
66
  }
53
67
 
54
68
  // Sanity: still orthogonal
55
69
  for (let i = 0; i < out.length - 1; i++) {
56
70
  const u = out[i]!
57
71
  const v = out[i + 1]!
58
- if (!isHorizontal(u, v, eps) && !isVertical(u, v, eps)) return null
72
+ if (!isHorizontal(u, v, eps) && !isVertical(u, v, eps)) {
73
+ return null
74
+ }
59
75
  }
60
-
61
76
  return out
62
77
  }
63
78
 
64
- export const pathKey = (pts: Point[], decimals = 6) =>
65
- pts.map((p) => `${p.x.toFixed(decimals)},${p.y.toFixed(decimals)}`).join("|")
79
+ export const pathKey = (pts: Point[], decimals = 6) => {
80
+ const key = pts
81
+ .map((p) => `${p.x.toFixed(decimals)},${p.y.toFixed(decimals)}`)
82
+ .join("|")
83
+ return key
84
+ }
@@ -12,6 +12,7 @@ import { TraceOverlapShiftSolver } from "../TraceOverlapShiftSolver/TraceOverlap
12
12
  import { NetLabelPlacementSolver } from "../NetLabelPlacementSolver/NetLabelPlacementSolver"
13
13
  import { visualizeInputProblem } from "./visualizeInputProblem"
14
14
  import { GuidelinesSolver } from "../GuidelinesSolver/GuidelinesSolver"
15
+ import { TraceLabelOverlapAvoidanceSolver } from "../TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver"
15
16
  import { getInputChipBounds } from "../GuidelinesSolver/getInputChipBounds"
16
17
  import { correctPinsInsideChips } from "./correctPinsInsideChip"
17
18
  import { expandChipsToFitPins } from "./expandChipsToFitPins"
@@ -52,6 +53,7 @@ export class SchematicTracePipelineSolver extends BaseSolver {
52
53
  schematicTraceLinesSolver?: SchematicTraceLinesSolver
53
54
  traceOverlapShiftSolver?: TraceOverlapShiftSolver
54
55
  netLabelPlacementSolver?: NetLabelPlacementSolver
56
+ traceLabelOverlapAvoidanceSolver?: TraceLabelOverlapAvoidanceSolver
55
57
 
56
58
  startTimeOfPhase: Record<string, number>
57
59
  endTimeOfPhase: Record<string, number>
@@ -134,6 +136,44 @@ export class SchematicTracePipelineSolver extends BaseSolver {
134
136
  },
135
137
  },
136
138
  ),
139
+ definePipelineStep(
140
+ "traceLabelOverlapAvoidanceSolver",
141
+ TraceLabelOverlapAvoidanceSolver,
142
+ (instance) => {
143
+ const traceMap =
144
+ instance.traceOverlapShiftSolver?.correctedTraceMap ??
145
+ Object.fromEntries(
146
+ instance.schematicTraceLinesSolver!.solvedTracePaths.map((p) => [
147
+ p.mspPairId,
148
+ p,
149
+ ]),
150
+ )
151
+ const traces = Object.values(traceMap)
152
+ const netLabelPlacements =
153
+ instance.netLabelPlacementSolver!.netLabelPlacements
154
+
155
+ return [
156
+ {
157
+ inputProblem: instance.inputProblem,
158
+ traces,
159
+ netLabelPlacements,
160
+ },
161
+ ]
162
+ },
163
+ {
164
+ onSolved: (instance) => {
165
+ if (
166
+ instance.traceLabelOverlapAvoidanceSolver &&
167
+ instance.netLabelPlacementSolver
168
+ ) {
169
+ const { netLabelPlacements } =
170
+ instance.traceLabelOverlapAvoidanceSolver.getOutput()
171
+ instance.netLabelPlacementSolver.netLabelPlacements =
172
+ netLabelPlacements
173
+ }
174
+ },
175
+ },
176
+ ),
137
177
  ]
138
178
 
139
179
  constructor(inputProblem: InputProblem) {
@@ -0,0 +1,240 @@
1
+ import { BaseSolver } from "../BaseSolver/BaseSolver"
2
+ import { detectTraceLabelOverlap } from "./detectTraceLabelOverlap"
3
+ import { rerouteCollidingTrace } from "./rerouteCollidingTrace"
4
+ import type { SolvedTracePath } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver"
5
+ import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver"
6
+ import type { GraphicsObject } from "graphics-debug"
7
+ import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualizeInputProblem"
8
+ import { getRectBounds } from "../NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
9
+ import { getColorFromString } from "lib/utils/getColorFromString"
10
+ import type { InputProblem } from "lib/types/InputProblem"
11
+ import { minimizeTurnsWithFilteredLabels } from "./minimizeTurnsWithFilteredLabels"
12
+ import { balanceLShapes } from "./balanceLShapes"
13
+ import { NetLabelPlacementSolver } from "../NetLabelPlacementSolver/NetLabelPlacementSolver"
14
+
15
+ interface TraceLabelOverlapAvoidanceSolverInput {
16
+ inputProblem: InputProblem
17
+ traces: SolvedTracePath[]
18
+ netLabelPlacements: NetLabelPlacement[]
19
+ }
20
+
21
+ export class TraceLabelOverlapAvoidanceSolver extends BaseSolver {
22
+ private problem: InputProblem
23
+ private traces: SolvedTracePath[]
24
+ private netTempLabelPlacements: NetLabelPlacement[]
25
+ private netLabelPlacements: NetLabelPlacement[]
26
+ private updatedTraces: SolvedTracePath[]
27
+ private mergedLabelNetIdMap: Record<string, Set<string>>
28
+ private detourCountByLabel: Record<string, number>
29
+ private readonly PADDING_BUFFER = 0.1
30
+
31
+ constructor(solverInput: TraceLabelOverlapAvoidanceSolverInput) {
32
+ super()
33
+ this.problem = solverInput.inputProblem
34
+ this.traces = solverInput.traces
35
+ this.updatedTraces = [...solverInput.traces]
36
+ this.mergedLabelNetIdMap = {}
37
+ this.detourCountByLabel = {}
38
+
39
+ const originalLabels = solverInput.netLabelPlacements
40
+ this.netLabelPlacements = originalLabels
41
+ if (!originalLabels || originalLabels.length === 0) {
42
+ this.netTempLabelPlacements = []
43
+ return
44
+ }
45
+
46
+ const labelGroups: Record<string, NetLabelPlacement[]> = {}
47
+
48
+ for (const p of originalLabels) {
49
+ if (p.pinIds.length === 0) continue
50
+ const chipId = p.pinIds[0].split(".")[0]
51
+ if (!chipId) continue
52
+ const key = `${chipId}-${p.orientation}`
53
+ if (!(key in labelGroups)) {
54
+ labelGroups[key] = []
55
+ }
56
+ labelGroups[key]!.push(p)
57
+ }
58
+
59
+ const finalPlacements: NetLabelPlacement[] = []
60
+ for (const [key, group] of Object.entries(labelGroups)) {
61
+ if (group.length <= 1) {
62
+ finalPlacements.push(...group)
63
+ continue
64
+ }
65
+
66
+ let minX = Infinity,
67
+ minY = Infinity,
68
+ maxX = -Infinity,
69
+ maxY = -Infinity
70
+ for (const p of group) {
71
+ const bounds = getRectBounds(p.center, p.width, p.height)
72
+ minX = Math.min(minX, bounds.minX)
73
+ minY = Math.min(minY, bounds.minY)
74
+ maxX = Math.max(maxX, bounds.maxX)
75
+ maxY = Math.max(maxY, bounds.maxY)
76
+ }
77
+
78
+ const newWidth = maxX - minX
79
+ const newHeight = maxY - minY
80
+ const template = group[0]!
81
+ const syntheticId = `merged-group-${key}`
82
+ const originalNetIds = new Set(group.map((p) => p.globalConnNetId))
83
+ this.mergedLabelNetIdMap[syntheticId] = originalNetIds
84
+
85
+ finalPlacements.push({
86
+ ...template,
87
+ globalConnNetId: syntheticId,
88
+ width: newWidth,
89
+ height: newHeight,
90
+ center: { x: minX + newWidth / 2, y: minY + newHeight / 2 },
91
+ pinIds: [...new Set(group.flatMap((p) => p.pinIds))],
92
+ mspConnectionPairIds: [
93
+ ...new Set(group.flatMap((p) => p.mspConnectionPairIds)),
94
+ ],
95
+ })
96
+ }
97
+
98
+ this.netTempLabelPlacements = finalPlacements
99
+ }
100
+
101
+ override _step() {
102
+ if (
103
+ !this.traces ||
104
+ this.traces.length === 0 ||
105
+ !this.netTempLabelPlacements ||
106
+ this.netTempLabelPlacements.length === 0
107
+ ) {
108
+ this.solved = true
109
+ return
110
+ }
111
+
112
+ this.detourCountByLabel = {}
113
+
114
+ const overlaps = detectTraceLabelOverlap(
115
+ this.traces,
116
+ this.netTempLabelPlacements,
117
+ )
118
+
119
+ if (overlaps.length === 0) {
120
+ this.solved = true
121
+ return
122
+ }
123
+
124
+ const unfriendlyOverlaps = overlaps.filter((o) => {
125
+ const originalNetIds = this.mergedLabelNetIdMap[o.label.globalConnNetId]
126
+ if (originalNetIds) {
127
+ return !originalNetIds.has(o.trace.globalConnNetId)
128
+ }
129
+ return o.trace.globalConnNetId !== o.label.globalConnNetId
130
+ })
131
+
132
+ if (unfriendlyOverlaps.length === 0) {
133
+ this.solved = true
134
+ return
135
+ }
136
+
137
+ const updatedTracesMap: Record<string, SolvedTracePath> = {}
138
+ for (const trace of this.traces) {
139
+ updatedTracesMap[trace.mspPairId] = trace
140
+ }
141
+
142
+ const processedTraceIds = new Set<string>()
143
+
144
+ for (const overlap of unfriendlyOverlaps) {
145
+ if (processedTraceIds.has(overlap.trace.mspPairId)) {
146
+ continue
147
+ }
148
+
149
+ const currentTraceState = updatedTracesMap[overlap.trace.mspPairId]!
150
+ const labelId = overlap.label.globalConnNetId
151
+ const detourCount = this.detourCountByLabel[labelId] || 0
152
+
153
+ const newTrace = rerouteCollidingTrace({
154
+ trace: currentTraceState,
155
+ label: overlap.label,
156
+ problem: this.problem,
157
+ paddingBuffer: this.PADDING_BUFFER,
158
+ detourCount,
159
+ })
160
+
161
+ if (newTrace.tracePath !== currentTraceState.tracePath) {
162
+ this.detourCountByLabel[labelId] = detourCount + 1
163
+ }
164
+
165
+ updatedTracesMap[currentTraceState.mspPairId] = newTrace
166
+ processedTraceIds.add(currentTraceState.mspPairId)
167
+ }
168
+
169
+ this.updatedTraces = Object.values(updatedTracesMap)
170
+
171
+ const minimizedTraces = minimizeTurnsWithFilteredLabels({
172
+ traces: this.updatedTraces,
173
+ problem: this.problem,
174
+ allLabelPlacements: this.netTempLabelPlacements, // Use temp labels which include merged ones
175
+ mergedLabelNetIdMap: this.mergedLabelNetIdMap,
176
+ paddingBuffer: this.PADDING_BUFFER,
177
+ })
178
+ if (minimizedTraces) {
179
+ this.updatedTraces = minimizedTraces
180
+ }
181
+ const balancedTraces = balanceLShapes({
182
+ traces: this.updatedTraces,
183
+ problem: this.problem,
184
+ allLabelPlacements: this.netLabelPlacements,
185
+ })
186
+ if (balancedTraces) {
187
+ this.updatedTraces = balancedTraces
188
+ }
189
+
190
+ const finalLabelPlacementSolver = new NetLabelPlacementSolver({
191
+ inputProblem: this.problem,
192
+ inputTraceMap: Object.fromEntries(
193
+ this.updatedTraces.map((trace) => [trace.mspPairId, trace]),
194
+ ),
195
+ })
196
+ finalLabelPlacementSolver.solve()
197
+ this.netLabelPlacements = finalLabelPlacementSolver.netLabelPlacements
198
+
199
+ this.solved = true
200
+ }
201
+
202
+ getOutput() {
203
+ return {
204
+ traces: this.updatedTraces,
205
+ netLabelPlacements: this.netLabelPlacements,
206
+ }
207
+ }
208
+
209
+ override visualize(): GraphicsObject {
210
+ const graphics = visualizeInputProblem(this.problem)
211
+
212
+ if (!graphics.lines) graphics.lines = []
213
+ if (!graphics.circles) graphics.circles = []
214
+ if (!graphics.texts) graphics.texts = []
215
+ if (!graphics.rects) graphics.rects = []
216
+
217
+ for (const trace of this.updatedTraces) {
218
+ graphics.lines!.push({
219
+ points: trace.tracePath,
220
+ strokeColor: "purple",
221
+ })
222
+ }
223
+
224
+ for (const p of this.netLabelPlacements) {
225
+ graphics.rects!.push({
226
+ center: p.center,
227
+ width: p.width,
228
+ height: p.height,
229
+ fill: getColorFromString(p.globalConnNetId, 0.35),
230
+ })
231
+ graphics.points!.push({
232
+ x: p.anchorPoint.x,
233
+ y: p.anchorPoint.y,
234
+ color: getColorFromString(p.globalConnNetId, 0.9),
235
+ })
236
+ }
237
+
238
+ return graphics
239
+ }
240
+ }
@@ -0,0 +1,207 @@
1
+ import type { Point } from "graphics-debug"
2
+ import type { InputProblem } from "lib/types/InputProblem"
3
+ import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver"
4
+ import { getObstacleRects } from "../SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
5
+ import type { SolvedTracePath } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver"
6
+ import { segmentIntersectsRect } from "../SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
7
+ import { simplifyPath } from "./simplifyPath"
8
+
9
+ export const balanceLShapes = ({
10
+ traces,
11
+ problem,
12
+ allLabelPlacements,
13
+ }: {
14
+ traces: SolvedTracePath[]
15
+ problem: InputProblem
16
+ allLabelPlacements: NetLabelPlacement[]
17
+ }): SolvedTracePath[] | null => {
18
+ const TOLERANCE = 1e-5
19
+ let changesMade = false
20
+
21
+ for (const trace of traces) {
22
+ if (trace.tracePath.length === 4) {
23
+ const [p0, p1, p2, p3] = trace.tracePath
24
+ const isHVHShape = p0.y === p1.y && p1.x === p2.x && p2.y === p3.y
25
+ const isVHVShape = p0.x === p1.x && p1.y === p2.y && p2.x === p3.x
26
+
27
+ const isCollinearHorizontal =
28
+ p0.y === p1.y && p1.y === p2.y && p2.y === p3.y
29
+ const isCollinearVertical =
30
+ p0.x === p1.x && p1.x === p2.x && p2.x === p3.x
31
+ const isCollinear = isCollinearHorizontal || isCollinearVertical
32
+
33
+ let isSameDirection = false
34
+ if (isHVHShape) {
35
+ isSameDirection = Math.sign(p1.x - p0.x) === Math.sign(p3.x - p2.x)
36
+ } else if (isVHVShape) {
37
+ isSameDirection = Math.sign(p1.y - p0.y) === Math.sign(p3.y - p2.y)
38
+ }
39
+
40
+ const isValidZShape =
41
+ (isHVHShape || isVHVShape) && !isCollinear && isSameDirection
42
+
43
+ if (!isValidZShape) {
44
+ return null
45
+ }
46
+ }
47
+ }
48
+
49
+ const obstacles = getObstacleRects(problem).map((obs) => ({
50
+ ...obs,
51
+ minX: obs.minX + TOLERANCE,
52
+ maxX: obs.maxX - TOLERANCE,
53
+ minY: obs.minY + TOLERANCE,
54
+ maxY: obs.maxY - TOLERANCE,
55
+ }))
56
+
57
+ const segmentIntersectsAnyRect = (
58
+ p1: Point,
59
+ p2: Point,
60
+ rects: any[],
61
+ ): boolean => {
62
+ for (const rect of rects) {
63
+ if (segmentIntersectsRect(p1, p2, rect)) {
64
+ return true
65
+ }
66
+ }
67
+ return false
68
+ }
69
+
70
+ const getLabelBounds = (labels: NetLabelPlacement[], traceNetId: string) => {
71
+ const filteredLabels = labels.filter(
72
+ (label) => label.globalConnNetId !== traceNetId,
73
+ )
74
+
75
+ return filteredLabels.map((nl) => ({
76
+ minX: nl.center.x - nl.width / 2 + TOLERANCE,
77
+ maxX: nl.center.x + nl.width / 2 - TOLERANCE,
78
+ minY: nl.center.y - nl.height / 2 + TOLERANCE,
79
+ maxY: nl.center.y + nl.height / 2 - TOLERANCE,
80
+ }))
81
+ }
82
+
83
+ const newTraces = traces.map((trace) => {
84
+ let newPath = [...trace.tracePath]
85
+
86
+ if (newPath.length < 4) {
87
+ return { ...trace }
88
+ }
89
+
90
+ const labelBounds = getLabelBounds(
91
+ allLabelPlacements,
92
+ trace.globalConnNetId,
93
+ )
94
+
95
+ if (newPath.length === 4) {
96
+ const [p0, p1, p2, p3] = newPath
97
+ let p1New: Point, p2New: Point
98
+
99
+ const isHVHShape = p0.y === p1.y && p1.x === p2.x && p2.y === p3.y
100
+
101
+ if (isHVHShape) {
102
+ const idealX = (p0.x + p3.x) / 2
103
+ p1New = { x: idealX, y: p1.y }
104
+ p2New = { x: idealX, y: p2.y }
105
+ } else {
106
+ const idealY = (p0.y + p3.y) / 2
107
+ p1New = { x: p1.x, y: idealY }
108
+ p2New = { x: p2.x, y: idealY }
109
+ }
110
+
111
+ const collides =
112
+ segmentIntersectsAnyRect(p0, p1New, obstacles) ||
113
+ segmentIntersectsAnyRect(p1New, p2New, obstacles) ||
114
+ segmentIntersectsAnyRect(p2New, p3, obstacles) ||
115
+ segmentIntersectsAnyRect(p0, p1New, labelBounds) ||
116
+ segmentIntersectsAnyRect(p1New, p2New, labelBounds) ||
117
+ segmentIntersectsAnyRect(p2New, p3, labelBounds)
118
+
119
+ if (!collides) {
120
+ newPath[1] = p1New
121
+ newPath[2] = p2New
122
+ changesMade = true
123
+ }
124
+
125
+ return { ...trace, tracePath: simplifyPath(newPath) }
126
+ }
127
+
128
+ for (let i = 1; i < newPath.length - 4; i++) {
129
+ const p1 = newPath[i]
130
+ const p2 = newPath[i + 1]
131
+ const p3 = newPath[i + 2]
132
+ const p4 = newPath[i + 3]
133
+
134
+ const isHVHZShape = p1.y === p2.y && p2.x === p3.x && p3.y === p4.y
135
+ const isVHVZShape = p1.x === p2.x && p2.y === p3.y && p3.x === p4.x
136
+
137
+ const isCollinearHorizontal =
138
+ p1.y === p2.y && p2.y === p3.y && p3.y === p4.y
139
+ const isCollinearVertical =
140
+ p1.x === p2.x && p2.x === p3.x && p3.x === p4.x
141
+ const isCollinear = isCollinearHorizontal || isCollinearVertical
142
+
143
+ let isSameDirection = false
144
+ if (isHVHZShape) {
145
+ isSameDirection = Math.sign(p2.x - p1.x) === Math.sign(p4.x - p3.x)
146
+ } else if (isVHVZShape) {
147
+ isSameDirection = Math.sign(p2.y - p1.y) === Math.sign(p4.y - p3.y)
148
+ }
149
+
150
+ const isValidZShape =
151
+ (isHVHZShape || isVHVZShape) && !isCollinear && isSameDirection
152
+
153
+ if (!isValidZShape) {
154
+ continue
155
+ }
156
+
157
+ let p2New: Point, p3New: Point
158
+ const len1Original = isHVHZShape
159
+ ? Math.abs(p1.x - p2.x)
160
+ : Math.abs(p1.y - p2.y)
161
+ const len2Original = isHVHZShape
162
+ ? Math.abs(p3.x - p4.x)
163
+ : Math.abs(p3.y - p4.y)
164
+
165
+ if (Math.abs(len1Original - len2Original) < 0.001) {
166
+ continue
167
+ }
168
+
169
+ if (isHVHZShape) {
170
+ const idealX = (p1.x + p4.x) / 2
171
+ p2New = { x: idealX, y: p2.y }
172
+ p3New = { x: idealX, y: p3.y }
173
+ } else {
174
+ const idealY = (p1.y + p4.y) / 2
175
+ p2New = { x: p2.x, y: idealY }
176
+ p3New = { x: p3.x, y: idealY }
177
+ }
178
+
179
+ const collides =
180
+ segmentIntersectsAnyRect(p1, p2New, obstacles) ||
181
+ segmentIntersectsAnyRect(p2New, p3New, obstacles) ||
182
+ segmentIntersectsAnyRect(p3New, p4, obstacles) ||
183
+ segmentIntersectsAnyRect(p1, p2New, labelBounds) ||
184
+ segmentIntersectsAnyRect(p2New, p3New, labelBounds) ||
185
+ segmentIntersectsAnyRect(p3New, p4, labelBounds)
186
+
187
+ if (!collides) {
188
+ newPath[i + 1] = p2New
189
+ newPath[i + 2] = p3New
190
+ changesMade = true
191
+ i = 0
192
+ }
193
+ }
194
+
195
+ const finalSimplifiedPath = simplifyPath(newPath)
196
+ return {
197
+ ...trace,
198
+ tracePath: finalSimplifiedPath,
199
+ }
200
+ })
201
+
202
+ if (changesMade) {
203
+ return newTraces
204
+ } else {
205
+ return null
206
+ }
207
+ }
@@ -0,0 +1,18 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+
3
+ export const countTurns = (points: Point[]): number => {
4
+ let turns = 0
5
+ for (let i = 1; i < points.length - 1; i++) {
6
+ const prev = points[i - 1]
7
+ const curr = points[i]
8
+ const next = points[i + 1]
9
+
10
+ const prevVertical = prev.x === curr.x
11
+ const nextVertical = curr.x === next.x
12
+
13
+ if (prevVertical !== nextVertical) {
14
+ turns++
15
+ }
16
+ }
17
+ return turns
18
+ }
@@ -0,0 +1,38 @@
1
+ import type { SolvedTracePath } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver"
2
+ import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver"
3
+ import { segmentIntersectsRect } from "../NetLabelPlacementSolver/SingleNetLabelPlacementSolver/collisions"
4
+ import { getRectBounds } from "../NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
5
+
6
+ export interface TraceLabelOverlap {
7
+ trace: SolvedTracePath
8
+ label: NetLabelPlacement
9
+ }
10
+
11
+ export const detectTraceLabelOverlap = (
12
+ traces: SolvedTracePath[],
13
+ netLabels: NetLabelPlacement[],
14
+ ): TraceLabelOverlap[] => {
15
+ const overlaps: TraceLabelOverlap[] = []
16
+
17
+ for (const trace of traces) {
18
+ for (const label of netLabels) {
19
+ const labelBounds = getRectBounds(label.center, label.width, label.height)
20
+
21
+ for (let i = 0; i < trace.tracePath.length - 1; i++) {
22
+ const p1 = trace.tracePath[i]
23
+ const p2 = trace.tracePath[i + 1]
24
+
25
+ if (segmentIntersectsRect(p1, p2, labelBounds)) {
26
+ // Check if the trace and label belong to the same net.
27
+ // If so, it's a self-attachment, not a collision.
28
+ if (trace.globalConnNetId === label.globalConnNetId) {
29
+ break // Move to the next label
30
+ }
31
+ overlaps.push({ trace, label })
32
+ break // Move to the next label
33
+ }
34
+ }
35
+ }
36
+ }
37
+ return overlaps
38
+ }
@@ -0,0 +1,22 @@
1
+ import type { Point } from "graphics-debug"
2
+ import { segmentIntersectsRect } from "../SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
3
+
4
+ export const hasCollisions = (
5
+ pathSegments: Point[],
6
+ obstacles: any[],
7
+ ): boolean => {
8
+ // Check each segment of the path
9
+ for (let i = 0; i < pathSegments.length - 1; i++) {
10
+ const p1 = pathSegments[i]
11
+ const p2 = pathSegments[i + 1]
12
+
13
+ // Check collision with each obstacle
14
+ for (const obstacle of obstacles) {
15
+ if (segmentIntersectsRect(p1, p2, obstacle)) {
16
+ return true
17
+ }
18
+ }
19
+ }
20
+
21
+ return false
22
+ }