@tscircuit/schematic-trace-solver 0.0.37 → 0.0.38

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 (32) hide show
  1. package/dist/index.d.ts +26 -1
  2. package/dist/index.js +927 -20
  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 +247 -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 +26 -0
  11. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/index.ts +1 -0
  12. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts +73 -0
  13. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/simplifyPath.ts +41 -0
  14. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/trySnipAndReconnect.ts +229 -0
  15. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/turnMinimization.ts +305 -0
  16. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/violation.ts +25 -0
  17. package/package.json +1 -1
  18. package/site/examples/example20.page.tsx +103 -0
  19. package/site/examples/example22.page.tsx +108 -0
  20. package/site/examples/example23.page.tsx +137 -0
  21. package/site/examples/example24.page.tsx +128 -0
  22. package/tests/examples/__snapshots__/example16.snap.svg +27 -27
  23. package/tests/examples/__snapshots__/example22.snap.svg +137 -0
  24. package/tests/examples/__snapshots__/example23.snap.svg +137 -0
  25. package/tests/examples/__snapshots__/example24.snap.svg +143 -0
  26. package/tests/examples/__snapshots__/example25.snap.svg +165 -0
  27. package/tests/examples/__snapshots__/example26.snap.svg +157 -0
  28. package/tests/examples/example22.test.tsx +109 -0
  29. package/tests/examples/example23.test.tsx +109 -0
  30. package/tests/examples/example24.test.tsx +114 -0
  31. package/tests/examples/example25.test.tsx +143 -0
  32. 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,247 @@
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 "./turnMinimization"
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 updatedTracesMap: Map<string, SolvedTracePath>
28
+ private mergedLabelNetIdMap: Map<string, Set<string>>
29
+ private detourCountByLabel: Map<string, number>
30
+ private readonly PADDING_BUFFER = 0.1
31
+
32
+ constructor(solverInput: TraceLabelOverlapAvoidanceSolverInput) {
33
+ super()
34
+ this.problem = solverInput.inputProblem
35
+ this.traces = solverInput.traces
36
+ this.updatedTraces = [...solverInput.traces]
37
+ this.updatedTracesMap = new Map()
38
+ this.mergedLabelNetIdMap = new Map()
39
+ this.detourCountByLabel = new Map()
40
+
41
+ const originalLabels = solverInput.netLabelPlacements
42
+ this.netLabelPlacements = originalLabels
43
+ if (!originalLabels || originalLabels.length === 0) {
44
+ this.netTempLabelPlacements = []
45
+ return
46
+ }
47
+
48
+ const labelGroups = new Map<string, NetLabelPlacement[]>()
49
+
50
+ for (const p of originalLabels) {
51
+ if (p.pinIds.length === 0) continue
52
+ const chipId = p.pinIds[0].split(".")[0]
53
+ if (!chipId) continue
54
+ const key = `${chipId}-${p.orientation}`
55
+ if (!labelGroups.has(key)) {
56
+ labelGroups.set(key, [])
57
+ }
58
+ labelGroups.get(key)!.push(p)
59
+ }
60
+
61
+ const finalPlacements: NetLabelPlacement[] = []
62
+ for (const [key, group] of labelGroups.entries()) {
63
+ if (group.length <= 1) {
64
+ finalPlacements.push(...group)
65
+ continue
66
+ }
67
+
68
+ let minX = Infinity,
69
+ minY = Infinity,
70
+ maxX = -Infinity,
71
+ maxY = -Infinity
72
+ for (const p of group) {
73
+ const bounds = getRectBounds(p.center, p.width, p.height)
74
+ minX = Math.min(minX, bounds.minX)
75
+ minY = Math.min(minY, bounds.minY)
76
+ maxX = Math.max(maxX, bounds.maxX)
77
+ maxY = Math.max(maxY, bounds.maxY)
78
+ }
79
+
80
+ const newWidth = maxX - minX
81
+ const newHeight = maxY - minY
82
+ const template = group[0]!
83
+ const syntheticId = `merged-group-${key}`
84
+ const originalNetIds = new Set(group.map((p) => p.globalConnNetId))
85
+ this.mergedLabelNetIdMap.set(syntheticId, originalNetIds)
86
+
87
+ finalPlacements.push({
88
+ ...template,
89
+ globalConnNetId: syntheticId,
90
+ width: newWidth,
91
+ height: newHeight,
92
+ center: { x: minX + newWidth / 2, y: minY + newHeight / 2 },
93
+ pinIds: [...new Set(group.flatMap((p) => p.pinIds))],
94
+ mspConnectionPairIds: [
95
+ ...new Set(group.flatMap((p) => p.mspConnectionPairIds)),
96
+ ],
97
+ })
98
+ }
99
+
100
+ this.netTempLabelPlacements = finalPlacements
101
+ }
102
+
103
+ override _step() {
104
+ if (
105
+ !this.traces ||
106
+ this.traces.length === 0 ||
107
+ !this.netTempLabelPlacements ||
108
+ this.netTempLabelPlacements.length === 0
109
+ ) {
110
+ this.solved = true
111
+ return
112
+ }
113
+
114
+ this.detourCountByLabel.clear()
115
+
116
+ const overlaps = detectTraceLabelOverlap(
117
+ this.traces,
118
+ this.netTempLabelPlacements,
119
+ )
120
+
121
+ if (overlaps.length === 0) {
122
+ this.solved = true
123
+ return
124
+ }
125
+
126
+ const unfriendlyOverlaps = overlaps.filter((o) => {
127
+ const originalNetIds = this.mergedLabelNetIdMap.get(
128
+ o.label.globalConnNetId,
129
+ )
130
+ if (originalNetIds) {
131
+ return !originalNetIds.has(o.trace.globalConnNetId)
132
+ }
133
+ return o.trace.globalConnNetId !== o.label.globalConnNetId
134
+ })
135
+
136
+ if (unfriendlyOverlaps.length === 0) {
137
+ this.solved = true
138
+ return
139
+ }
140
+
141
+ const updatedTracesMap = new Map<string, SolvedTracePath>()
142
+ for (const trace of this.traces) {
143
+ updatedTracesMap.set(trace.mspPairId, trace)
144
+ }
145
+
146
+ const processedTraceIds = new Set<string>()
147
+
148
+ for (const overlap of unfriendlyOverlaps) {
149
+ if (processedTraceIds.has(overlap.trace.mspPairId)) {
150
+ continue
151
+ }
152
+
153
+ const currentTraceState = updatedTracesMap.get(overlap.trace.mspPairId)!
154
+ const labelId = overlap.label.globalConnNetId
155
+ const detourCount = this.detourCountByLabel.get(labelId) || 0
156
+
157
+ const newTrace = rerouteCollidingTrace({
158
+ trace: currentTraceState,
159
+ label: overlap.label,
160
+ problem: this.problem,
161
+ paddingBuffer: this.PADDING_BUFFER,
162
+ detourCount,
163
+ })
164
+
165
+ if (newTrace.tracePath !== currentTraceState.tracePath) {
166
+ this.detourCountByLabel.set(labelId, detourCount + 1)
167
+ }
168
+
169
+ updatedTracesMap.set(currentTraceState.mspPairId, newTrace)
170
+ processedTraceIds.add(currentTraceState.mspPairId)
171
+ }
172
+
173
+ this.updatedTraces = Array.from(updatedTracesMap.values())
174
+
175
+ const minimizedTraces = minimizeTurnsWithFilteredLabels({
176
+ traces: this.updatedTraces,
177
+ problem: this.problem,
178
+ allLabelPlacements: this.netTempLabelPlacements, // Use temp labels which include merged ones
179
+ mergedLabelNetIdMap: this.mergedLabelNetIdMap,
180
+ paddingBuffer: this.PADDING_BUFFER,
181
+ })
182
+ if (minimizedTraces) {
183
+ this.updatedTraces = minimizedTraces
184
+ }
185
+ const balancedTraces = balanceLShapes({
186
+ traces: this.updatedTraces,
187
+ problem: this.problem,
188
+ allLabelPlacements: this.netLabelPlacements,
189
+ })
190
+ if (balancedTraces) {
191
+ this.updatedTraces = balancedTraces
192
+ }
193
+
194
+ this.updatedTracesMap.clear()
195
+ for (const trace of this.updatedTraces) {
196
+ this.updatedTracesMap.set(trace.mspPairId, trace)
197
+ }
198
+
199
+ const finalLabelPlacementSolver = new NetLabelPlacementSolver({
200
+ inputProblem: this.problem,
201
+ inputTraceMap: Object.fromEntries(this.updatedTracesMap),
202
+ })
203
+ finalLabelPlacementSolver.solve()
204
+ this.netLabelPlacements = finalLabelPlacementSolver.netLabelPlacements
205
+
206
+ this.solved = true
207
+ }
208
+
209
+ getOutput() {
210
+ return {
211
+ traceMap: this.updatedTracesMap,
212
+ netLabelPlacements: this.netLabelPlacements,
213
+ }
214
+ }
215
+
216
+ override visualize(): GraphicsObject {
217
+ const graphics = visualizeInputProblem(this.problem)
218
+
219
+ if (!graphics.lines) graphics.lines = []
220
+ if (!graphics.circles) graphics.circles = []
221
+ if (!graphics.texts) graphics.texts = []
222
+ if (!graphics.rects) graphics.rects = []
223
+
224
+ for (const trace of Object.values(this.updatedTraces)) {
225
+ graphics.lines!.push({
226
+ points: trace.tracePath,
227
+ strokeColor: "purple",
228
+ })
229
+ }
230
+
231
+ for (const p of this.netLabelPlacements) {
232
+ graphics.rects!.push({
233
+ center: p.center,
234
+ width: p.width,
235
+ height: p.height,
236
+ fill: getColorFromString(p.globalConnNetId, 0.35),
237
+ })
238
+ graphics.points!.push({
239
+ x: p.anchorPoint.x,
240
+ y: p.anchorPoint.y,
241
+ color: getColorFromString(p.globalConnNetId, 0.9),
242
+ })
243
+ }
244
+
245
+ return graphics
246
+ }
247
+ }
@@ -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,26 @@
1
+ import type { Point } from "graphics-debug"
2
+ import {
3
+ isVertical,
4
+ isHorizontal,
5
+ segmentIntersectsRect,
6
+ } from "../SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
7
+
8
+ export const hasCollisions = (
9
+ pathSegments: Point[],
10
+ obstacles: any[],
11
+ ): boolean => {
12
+ // Check each segment of the path
13
+ for (let i = 0; i < pathSegments.length - 1; i++) {
14
+ const p1 = pathSegments[i]
15
+ const p2 = pathSegments[i + 1]
16
+
17
+ // Check collision with each obstacle
18
+ for (const obstacle of obstacles) {
19
+ if (segmentIntersectsRect(p1, p2, obstacle)) {
20
+ return true
21
+ }
22
+ }
23
+ }
24
+
25
+ return false
26
+ }