@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.
- package/dist/index.d.ts +25 -1
- package/dist/index.js +917 -10
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions.ts +17 -0
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/pathOps.ts +31 -12
- package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +40 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver.ts +240 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/balanceLShapes.ts +207 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/countTurns.ts +18 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/detectTraceLabelOverlap.ts +38 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/hasCollisions.ts +22 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/hasCollisionsWithLabels.ts +19 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/index.ts +1 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/minimizeTurnsWithFilteredLabels.ts +66 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts +73 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/simplifyPath.ts +41 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/tryConnectPoints.ts +14 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/trySnipAndReconnect.ts +229 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/turnMinimization.ts +211 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/violation.ts +25 -0
- package/package.json +1 -1
- package/site/examples/example20.page.tsx +103 -0
- package/site/examples/example22.page.tsx +108 -0
- package/site/examples/example23.page.tsx +137 -0
- package/site/examples/example24.page.tsx +128 -0
- package/tests/examples/__snapshots__/example16.snap.svg +27 -27
- package/tests/examples/__snapshots__/example22.snap.svg +137 -0
- package/tests/examples/__snapshots__/example23.snap.svg +137 -0
- package/tests/examples/__snapshots__/example24.snap.svg +143 -0
- package/tests/examples/__snapshots__/example25.snap.svg +165 -0
- package/tests/examples/__snapshots__/example26.snap.svg +157 -0
- package/tests/examples/example22.test.tsx +109 -0
- package/tests/examples/example23.test.tsx +109 -0
- package/tests/examples/example24.test.tsx +114 -0
- package/tests/examples/example25.test.tsx +143 -0
- 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)
|
|
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)
|
|
22
|
+
if (!vert && !horz) {
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
21
25
|
|
|
22
26
|
// Ensure axis matches orthogonal shift
|
|
23
|
-
if (vert && axis !== "x")
|
|
24
|
-
|
|
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)
|
|
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)
|
|
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))
|
|
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
|
-
|
|
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
|
+
}
|