@tscircuit/schematic-trace-solver 0.0.42 → 0.0.44
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 +31 -29
- package/dist/index.js +981 -926
- package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +20 -1
- package/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts +133 -0
- package/lib/solvers/TraceCleanupSolver/balanceZShapes.ts +200 -0
- package/lib/solvers/TraceCleanupSolver/minimizeTurnsWithFilteredLabels.ts +76 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver.ts +1 -21
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts +1 -1
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts +1 -1
- package/lib/solvers/TraceOverlapShiftSolver/TraceOverlapShiftSolver.ts +83 -0
- package/package.json +1 -1
- package/site/examples/example26.page.tsx +4 -0
- package/tests/assets/example26.json +1206 -0
- package/tests/examples/__snapshots__/example02.snap.svg +1 -1
- package/tests/examples/__snapshots__/example09.snap.svg +34 -34
- package/tests/examples/__snapshots__/example11.snap.svg +13 -13
- package/tests/examples/__snapshots__/example12.snap.svg +3 -3
- package/tests/examples/__snapshots__/example13.snap.svg +12 -12
- package/tests/examples/__snapshots__/example16.snap.svg +4 -4
- package/tests/examples/__snapshots__/example19.snap.svg +9 -9
- package/tests/examples/__snapshots__/example25.snap.svg +3 -3
- package/tests/examples/__snapshots__/example26.snap.svg +3 -3
- package/tests/examples/__snapshots__/example28.snap.svg +3 -3
- package/tests/examples/__snapshots__/example29.snap.svg +1208 -0
- package/tests/examples/example29.test.ts +13 -0
- package/tests/solvers/{TraceLabelOverlapAvoidanceSolver/sub-solver → TraceCleanupSolver}/TraceCleanupSolver.test.ts +2 -2
- package/tests/solvers/{TraceLabelOverlapAvoidanceSolver/sub-solver → TraceCleanupSolver}/__snapshots__/TraceCleanupSolver.snap.svg +1 -1
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/__snapshots__/renderComparisonView03.snap.svg +1 -1
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/renderComparisonView03.test.ts +1 -1
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/TraceCleanupSolver/TraceCleanupSolver.ts +0 -132
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/TraceCleanupSolver/balanceLShapes.ts +0 -209
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/TraceCleanupSolver/minimizeTurnsWithFilteredLabels.ts +0 -66
- /package/lib/solvers/{TraceLabelOverlapAvoidanceSolver/sub-solvers/TraceCleanupSolver → TraceCleanupSolver}/countTurns.ts +0 -0
- /package/lib/solvers/{TraceLabelOverlapAvoidanceSolver/sub-solvers/TraceCleanupSolver → TraceCleanupSolver}/hasCollisions.ts +0 -0
- /package/lib/solvers/{TraceLabelOverlapAvoidanceSolver/sub-solvers/TraceCleanupSolver → TraceCleanupSolver}/hasCollisionsWithLabels.ts +0 -0
- /package/lib/solvers/{TraceLabelOverlapAvoidanceSolver/sub-solvers/TraceCleanupSolver → TraceCleanupSolver}/simplifyPath.ts +0 -0
- /package/lib/solvers/{TraceLabelOverlapAvoidanceSolver/sub-solvers/TraceCleanupSolver → TraceCleanupSolver}/tryConnectPoints.ts +0 -0
- /package/lib/solvers/{TraceLabelOverlapAvoidanceSolver/sub-solvers/TraceCleanupSolver → TraceCleanupSolver}/turnMinimization.ts +0 -0
|
@@ -19,7 +19,7 @@ import { correctPinsInsideChips } from "./correctPinsInsideChip"
|
|
|
19
19
|
import { expandChipsToFitPins } from "./expandChipsToFitPins"
|
|
20
20
|
import { LongDistancePairSolver } from "../LongDistancePairSolver/LongDistancePairSolver"
|
|
21
21
|
import { MergedNetLabelObstacleSolver } from "../TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/LabelMergingSolver"
|
|
22
|
-
import { TraceCleanupSolver } from "../
|
|
22
|
+
import { TraceCleanupSolver } from "../TraceCleanupSolver/TraceCleanupSolver"
|
|
23
23
|
|
|
24
24
|
type PipelineStep<T extends new (...args: any[]) => BaseSolver> = {
|
|
25
25
|
solverName: string
|
|
@@ -188,6 +188,25 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
188
188
|
]
|
|
189
189
|
},
|
|
190
190
|
),
|
|
191
|
+
definePipelineStep("traceCleanupSolver", TraceCleanupSolver, (instance) => {
|
|
192
|
+
const prevSolverOutput =
|
|
193
|
+
instance.traceLabelOverlapAvoidanceSolver!.getOutput()
|
|
194
|
+
const traces = prevSolverOutput.traces
|
|
195
|
+
|
|
196
|
+
const labelMergingOutput =
|
|
197
|
+
instance.traceLabelOverlapAvoidanceSolver!.labelMergingSolver!.getOutput()
|
|
198
|
+
|
|
199
|
+
return [
|
|
200
|
+
{
|
|
201
|
+
inputProblem: instance.inputProblem,
|
|
202
|
+
allTraces: traces,
|
|
203
|
+
targetTraceIds: new Set(traces.map((t) => t.mspPairId)),
|
|
204
|
+
allLabelPlacements: labelMergingOutput.netLabelPlacements,
|
|
205
|
+
mergedLabelNetIdMap: labelMergingOutput.mergedLabelNetIdMap,
|
|
206
|
+
paddingBuffer: 0.1,
|
|
207
|
+
},
|
|
208
|
+
]
|
|
209
|
+
}),
|
|
191
210
|
definePipelineStep(
|
|
192
211
|
"netLabelPlacementSolver",
|
|
193
212
|
NetLabelPlacementSolver,
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { InputProblem } from "lib/types/InputProblem"
|
|
2
|
+
import type { GraphicsObject, Line } from "graphics-debug"
|
|
3
|
+
import { minimizeTurnsWithFilteredLabels } from "./minimizeTurnsWithFilteredLabels"
|
|
4
|
+
import { balanceZShapes } from "./balanceZShapes"
|
|
5
|
+
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
6
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
7
|
+
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
|
|
8
|
+
import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
9
|
+
|
|
10
|
+
interface TraceCleanupSolverInput {
|
|
11
|
+
inputProblem: InputProblem
|
|
12
|
+
allTraces: SolvedTracePath[]
|
|
13
|
+
targetTraceIds: Set<string>
|
|
14
|
+
allLabelPlacements: NetLabelPlacement[]
|
|
15
|
+
mergedLabelNetIdMap: Record<string, Set<string>>
|
|
16
|
+
paddingBuffer: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type PipelineStep = "minimizing_turns" | "balancing_l_shapes"
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Cleans up traces by minimizing turns and balancing L-shapes to improve
|
|
23
|
+
* the overall aesthetics and readability of the schematic.
|
|
24
|
+
*/
|
|
25
|
+
export class TraceCleanupSolver extends BaseSolver {
|
|
26
|
+
private input: TraceCleanupSolverInput
|
|
27
|
+
private outputTraces: SolvedTracePath[]
|
|
28
|
+
private traceIdQueue: string[]
|
|
29
|
+
private tracesMap: Map<string, SolvedTracePath>
|
|
30
|
+
private pipelineStep: PipelineStep = "minimizing_turns"
|
|
31
|
+
private activeTraceId: string | null = null // New property
|
|
32
|
+
|
|
33
|
+
constructor(solverInput: TraceCleanupSolverInput) {
|
|
34
|
+
super()
|
|
35
|
+
this.input = solverInput
|
|
36
|
+
this.outputTraces = [...solverInput.allTraces]
|
|
37
|
+
this.tracesMap = new Map(this.outputTraces.map((t) => [t.mspPairId, t]))
|
|
38
|
+
this.traceIdQueue = Array.from(solverInput.targetTraceIds)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
override _step() {
|
|
42
|
+
if (
|
|
43
|
+
this.pipelineStep === "minimizing_turns" &&
|
|
44
|
+
this.traceIdQueue.length === 0
|
|
45
|
+
) {
|
|
46
|
+
this.pipelineStep = "balancing_l_shapes"
|
|
47
|
+
this.traceIdQueue = Array.from(this.input.targetTraceIds)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (
|
|
51
|
+
this.pipelineStep === "balancing_l_shapes" &&
|
|
52
|
+
this.traceIdQueue.length === 0
|
|
53
|
+
) {
|
|
54
|
+
this.solved = true
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const targetMspConnectionPairId = this.traceIdQueue.shift()!
|
|
59
|
+
this.activeTraceId = targetMspConnectionPairId // Set active trace ID
|
|
60
|
+
const originalTrace = this.tracesMap.get(targetMspConnectionPairId)!
|
|
61
|
+
|
|
62
|
+
const { tracePath } = originalTrace
|
|
63
|
+
|
|
64
|
+
// Skip if the trace is a perfect rectangle
|
|
65
|
+
const is4PointRectangle = (path: typeof tracePath): boolean => {
|
|
66
|
+
if (path.length !== 4) return false
|
|
67
|
+
const [p0, p1, p2, p3] = path
|
|
68
|
+
// H-V-H "C" shape
|
|
69
|
+
const isHVHC =
|
|
70
|
+
p0.y === p1.y && p1.x === p2.x && p2.y === p3.y && p0.x === p3.x
|
|
71
|
+
// V-H-V "C" shape
|
|
72
|
+
const isVHVC =
|
|
73
|
+
p0.x === p1.x && p1.y === p2.y && p2.x === p3.x && p0.y === p3.y
|
|
74
|
+
return isHVHC || isVHVC
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (is4PointRectangle(tracePath)) {
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const allTraces = Array.from(this.tracesMap.values())
|
|
82
|
+
|
|
83
|
+
let updatedTrace: SolvedTracePath
|
|
84
|
+
|
|
85
|
+
if (this.pipelineStep === "minimizing_turns") {
|
|
86
|
+
updatedTrace = minimizeTurnsWithFilteredLabels({
|
|
87
|
+
...this.input,
|
|
88
|
+
targetMspConnectionPairId,
|
|
89
|
+
traces: allTraces,
|
|
90
|
+
})
|
|
91
|
+
} else {
|
|
92
|
+
updatedTrace = balanceZShapes({
|
|
93
|
+
...this.input,
|
|
94
|
+
targetMspConnectionPairId,
|
|
95
|
+
traces: allTraces,
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Update the state in the central map
|
|
100
|
+
this.tracesMap.set(targetMspConnectionPairId, updatedTrace)
|
|
101
|
+
|
|
102
|
+
// Update the output for visualization
|
|
103
|
+
this.outputTraces = Array.from(this.tracesMap.values())
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
getOutput() {
|
|
107
|
+
return {
|
|
108
|
+
traces: this.outputTraces,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
override visualize(): GraphicsObject {
|
|
113
|
+
const graphics = visualizeInputProblem(this.input.inputProblem, {
|
|
114
|
+
chipAlpha: 0.1,
|
|
115
|
+
connectionAlpha: 0.1,
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
if (!graphics.lines) graphics.lines = []
|
|
119
|
+
if (!graphics.points) graphics.points = []
|
|
120
|
+
if (!graphics.rects) graphics.rects = []
|
|
121
|
+
if (!graphics.circles) graphics.circles = []
|
|
122
|
+
if (!graphics.texts) graphics.texts = []
|
|
123
|
+
|
|
124
|
+
for (const trace of this.outputTraces) {
|
|
125
|
+
const line: Line = {
|
|
126
|
+
points: trace.tracePath.map((p) => ({ x: p.x, y: p.y })),
|
|
127
|
+
strokeColor: trace.mspPairId === this.activeTraceId ? "red" : "blue", // Highlight active trace
|
|
128
|
+
}
|
|
129
|
+
graphics.lines!.push(line)
|
|
130
|
+
}
|
|
131
|
+
return graphics
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import type { Point } from "graphics-debug"
|
|
2
|
+
import type { InputProblem } from "lib/types/InputProblem"
|
|
3
|
+
import { simplifyPath } from "./simplifyPath"
|
|
4
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
5
|
+
import { segmentIntersectsRect } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
|
|
6
|
+
import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
|
|
7
|
+
import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
8
|
+
|
|
9
|
+
export const balanceZShapes = ({
|
|
10
|
+
targetMspConnectionPairId,
|
|
11
|
+
traces,
|
|
12
|
+
inputProblem,
|
|
13
|
+
allLabelPlacements,
|
|
14
|
+
mergedLabelNetIdMap,
|
|
15
|
+
paddingBuffer,
|
|
16
|
+
}: {
|
|
17
|
+
targetMspConnectionPairId: string
|
|
18
|
+
traces: SolvedTracePath[]
|
|
19
|
+
inputProblem: InputProblem
|
|
20
|
+
allLabelPlacements: NetLabelPlacement[]
|
|
21
|
+
mergedLabelNetIdMap: Record<string, Set<string>>
|
|
22
|
+
paddingBuffer: number
|
|
23
|
+
}): SolvedTracePath => {
|
|
24
|
+
const targetTrace = traces.find(
|
|
25
|
+
(t) => t.mspPairId === targetMspConnectionPairId,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if (!targetTrace) {
|
|
29
|
+
throw new Error(`Target trace ${targetMspConnectionPairId} not found`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const TOLERANCE = 1e-5
|
|
33
|
+
|
|
34
|
+
const obstacleTraces = traces.filter(
|
|
35
|
+
(t) => t.mspPairId !== targetMspConnectionPairId,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
const TRACE_WIDTH = 0.01
|
|
39
|
+
const traceObstacles = obstacleTraces.flatMap((trace, i) =>
|
|
40
|
+
trace.tracePath.slice(0, -1).map((p1, pi) => {
|
|
41
|
+
const p2 = trace.tracePath[pi + 1]!
|
|
42
|
+
return {
|
|
43
|
+
chipId: `trace-obstacle-${i}-${pi}`,
|
|
44
|
+
minX: Math.min(p1.x, p2.x) - TRACE_WIDTH / 2,
|
|
45
|
+
minY: Math.min(p1.y, p2.y) - TRACE_WIDTH / 2,
|
|
46
|
+
maxX: Math.max(p1.x, p2.x) + TRACE_WIDTH / 2,
|
|
47
|
+
maxY: Math.max(p1.y, p2.y) + TRACE_WIDTH / 2,
|
|
48
|
+
}
|
|
49
|
+
}),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const staticObstacles = getObstacleRects(inputProblem).map((obs) => ({
|
|
53
|
+
...obs,
|
|
54
|
+
minX: obs.minX + TOLERANCE,
|
|
55
|
+
maxX: obs.maxX - TOLERANCE,
|
|
56
|
+
minY: obs.minY + TOLERANCE,
|
|
57
|
+
maxY: obs.maxY - TOLERANCE,
|
|
58
|
+
}))
|
|
59
|
+
|
|
60
|
+
const combinedObstacles = [...staticObstacles, ...traceObstacles]
|
|
61
|
+
|
|
62
|
+
const segmentIntersectsAnyRect = (
|
|
63
|
+
p1: Point,
|
|
64
|
+
p2: Point,
|
|
65
|
+
rects: any[],
|
|
66
|
+
): boolean => {
|
|
67
|
+
for (const rect of rects) {
|
|
68
|
+
if (segmentIntersectsRect(p1, p2, rect)) {
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const filteredLabels = allLabelPlacements.filter((label) => {
|
|
76
|
+
const originalNetIds = mergedLabelNetIdMap[label.globalConnNetId]
|
|
77
|
+
if (originalNetIds) {
|
|
78
|
+
return !originalNetIds.has(targetTrace.globalConnNetId)
|
|
79
|
+
}
|
|
80
|
+
return label.globalConnNetId !== targetTrace.globalConnNetId
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const labelBounds = filteredLabels.map((nl) => ({
|
|
84
|
+
minX: nl.center.x - nl.width / 2 + TOLERANCE,
|
|
85
|
+
maxX: nl.center.x + nl.width / 2 - TOLERANCE,
|
|
86
|
+
minY: nl.center.y - nl.height / 2 + TOLERANCE,
|
|
87
|
+
maxY: nl.center.y + nl.height / 2 - TOLERANCE,
|
|
88
|
+
}))
|
|
89
|
+
|
|
90
|
+
const newPath = [...targetTrace.tracePath]
|
|
91
|
+
|
|
92
|
+
if (newPath.length < 4) {
|
|
93
|
+
return { ...targetTrace }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (newPath.length === 4) {
|
|
97
|
+
const [p0, p1, p2, p3] = newPath
|
|
98
|
+
let p1New: Point
|
|
99
|
+
let p2New: Point
|
|
100
|
+
|
|
101
|
+
const isHVHShape = p0.y === p1.y && p1.x === p2.x && p2.y === p3.y
|
|
102
|
+
|
|
103
|
+
if (isHVHShape) {
|
|
104
|
+
const idealX = (p0.x + p3.x) / 2
|
|
105
|
+
p1New = { x: idealX, y: p1.y }
|
|
106
|
+
p2New = { x: idealX, y: p2.y }
|
|
107
|
+
} else {
|
|
108
|
+
const idealY = (p0.y + p3.y) / 2
|
|
109
|
+
p1New = { x: p1.x, y: idealY }
|
|
110
|
+
p2New = { x: p2.x, y: idealY }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const collides =
|
|
114
|
+
segmentIntersectsAnyRect(p0, p1New, combinedObstacles) ||
|
|
115
|
+
segmentIntersectsAnyRect(p1New, p2New, combinedObstacles) ||
|
|
116
|
+
segmentIntersectsAnyRect(p2New, p3, combinedObstacles) ||
|
|
117
|
+
segmentIntersectsAnyRect(p0, p1New, labelBounds) ||
|
|
118
|
+
segmentIntersectsAnyRect(p1New, p2New, labelBounds) ||
|
|
119
|
+
segmentIntersectsAnyRect(p2New, p3, labelBounds)
|
|
120
|
+
|
|
121
|
+
if (!collides) {
|
|
122
|
+
newPath[1] = p1New
|
|
123
|
+
newPath[2] = p2New
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { ...targetTrace, tracePath: simplifyPath(newPath) }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (let i = 1; i < newPath.length - 4; i++) {
|
|
130
|
+
const p1 = newPath[i]!
|
|
131
|
+
const p2 = newPath[i + 1]!
|
|
132
|
+
const p3 = newPath[i + 2]!
|
|
133
|
+
const p4 = newPath[i + 3]!
|
|
134
|
+
|
|
135
|
+
const isHVHZShape = p1.y === p2.y && p2.x === p3.x && p3.y === p4.y
|
|
136
|
+
const isVHVZShape = p1.x === p2.x && p2.y === p3.y && p3.x === p4.x
|
|
137
|
+
|
|
138
|
+
const isCollinearHorizontal =
|
|
139
|
+
p1.y === p2.y && p2.y === p3.y && p3.y === p4.y
|
|
140
|
+
const isCollinearVertical = 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
|
|
158
|
+
let p3New: Point
|
|
159
|
+
const len1Original = isHVHZShape
|
|
160
|
+
? Math.abs(p1.x - p2.x)
|
|
161
|
+
: Math.abs(p1.y - p2.y)
|
|
162
|
+
const len2Original = isHVHZShape
|
|
163
|
+
? Math.abs(p3.x - p4.x)
|
|
164
|
+
: Math.abs(p3.y - p4.y)
|
|
165
|
+
|
|
166
|
+
if (Math.abs(len1Original - len2Original) < 0.001) {
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (isHVHZShape) {
|
|
171
|
+
const idealX = (p1.x + p4.x) / 2
|
|
172
|
+
p2New = { x: idealX, y: p2.y }
|
|
173
|
+
p3New = { x: idealX, y: p3.y }
|
|
174
|
+
} else {
|
|
175
|
+
const idealY = (p1.y + p4.y) / 2
|
|
176
|
+
p2New = { x: p2.x, y: idealY }
|
|
177
|
+
p3New = { x: p3.x, y: idealY }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const collides =
|
|
181
|
+
segmentIntersectsAnyRect(p1, p2New, combinedObstacles) ||
|
|
182
|
+
segmentIntersectsAnyRect(p2New, p3New, combinedObstacles) ||
|
|
183
|
+
segmentIntersectsAnyRect(p3New, p4, combinedObstacles) ||
|
|
184
|
+
segmentIntersectsAnyRect(p1, p2New, labelBounds) ||
|
|
185
|
+
segmentIntersectsAnyRect(p2New, p3New, labelBounds) ||
|
|
186
|
+
segmentIntersectsAnyRect(p3New, p4, labelBounds)
|
|
187
|
+
|
|
188
|
+
if (!collides) {
|
|
189
|
+
newPath[i + 1] = p2New
|
|
190
|
+
newPath[i + 2] = p3New
|
|
191
|
+
i = 0
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const finalSimplifiedPath = simplifyPath(newPath)
|
|
196
|
+
return {
|
|
197
|
+
...targetTrace,
|
|
198
|
+
tracePath: finalSimplifiedPath,
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { InputProblem } from "lib/types/InputProblem"
|
|
2
|
+
import { minimizeTurns } from "./turnMinimization"
|
|
3
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
4
|
+
import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
|
|
5
|
+
import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
6
|
+
|
|
7
|
+
export const minimizeTurnsWithFilteredLabels = ({
|
|
8
|
+
targetMspConnectionPairId,
|
|
9
|
+
traces,
|
|
10
|
+
inputProblem,
|
|
11
|
+
allLabelPlacements,
|
|
12
|
+
mergedLabelNetIdMap,
|
|
13
|
+
paddingBuffer,
|
|
14
|
+
}: {
|
|
15
|
+
targetMspConnectionPairId: string
|
|
16
|
+
traces: SolvedTracePath[]
|
|
17
|
+
inputProblem: InputProblem
|
|
18
|
+
allLabelPlacements: NetLabelPlacement[]
|
|
19
|
+
mergedLabelNetIdMap: Record<string, Set<string>>
|
|
20
|
+
paddingBuffer: number
|
|
21
|
+
}): SolvedTracePath => {
|
|
22
|
+
const targetTrace = traces.find(
|
|
23
|
+
(t) => t.mspPairId === targetMspConnectionPairId,
|
|
24
|
+
)
|
|
25
|
+
if (!targetTrace) {
|
|
26
|
+
throw new Error(`Target trace ${targetMspConnectionPairId} not found`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const obstacleTraces = traces.filter(
|
|
30
|
+
(t) => t.mspPairId !== targetMspConnectionPairId,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
const TRACE_WIDTH = 0.01
|
|
34
|
+
const traceObstacles = obstacleTraces.flatMap((trace, i) =>
|
|
35
|
+
trace.tracePath.slice(0, -1).map((p1, pi) => {
|
|
36
|
+
const p2 = trace.tracePath[pi + 1]!
|
|
37
|
+
return {
|
|
38
|
+
chipId: `trace-obstacle-${i}-${pi}`,
|
|
39
|
+
minX: Math.min(p1.x, p2.x) - TRACE_WIDTH / 2,
|
|
40
|
+
minY: Math.min(p1.y, p2.y) - TRACE_WIDTH / 2,
|
|
41
|
+
maxX: Math.max(p1.x, p2.x) + TRACE_WIDTH / 2,
|
|
42
|
+
maxY: Math.max(p1.y, p2.y) + TRACE_WIDTH / 2,
|
|
43
|
+
}
|
|
44
|
+
}),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
const staticObstacles = getObstacleRects(inputProblem)
|
|
48
|
+
const combinedObstacles = [...staticObstacles, ...traceObstacles]
|
|
49
|
+
|
|
50
|
+
const originalPath = targetTrace.tracePath
|
|
51
|
+
const filteredLabels = allLabelPlacements.filter((label) => {
|
|
52
|
+
const originalNetIds = mergedLabelNetIdMap[label.globalConnNetId]
|
|
53
|
+
if (originalNetIds) {
|
|
54
|
+
return !originalNetIds.has(targetTrace.globalConnNetId)
|
|
55
|
+
}
|
|
56
|
+
return label.globalConnNetId !== targetTrace.globalConnNetId
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const labelBounds = filteredLabels.map((nl) => ({
|
|
60
|
+
minX: nl.center.x - nl.width / 2 - paddingBuffer,
|
|
61
|
+
maxX: nl.center.x + nl.width / 2 + paddingBuffer,
|
|
62
|
+
minY: nl.center.y - nl.height / 2 - paddingBuffer,
|
|
63
|
+
maxY: nl.center.y + nl.height / 2 + paddingBuffer,
|
|
64
|
+
}))
|
|
65
|
+
|
|
66
|
+
const newPath = minimizeTurns({
|
|
67
|
+
path: originalPath,
|
|
68
|
+
obstacles: combinedObstacles,
|
|
69
|
+
labelBounds,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
...targetTrace,
|
|
74
|
+
tracePath: newPath,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -5,7 +5,6 @@ import type { GraphicsObject } from "graphics-debug"
|
|
|
5
5
|
import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualizeInputProblem"
|
|
6
6
|
import type { InputProblem } from "../../types/InputProblem"
|
|
7
7
|
import { MergedNetLabelObstacleSolver } from "./sub-solvers/LabelMergingSolver/LabelMergingSolver"
|
|
8
|
-
import { TraceCleanupSolver } from "./sub-solvers/TraceCleanupSolver/TraceCleanupSolver"
|
|
9
8
|
import { getColorFromString } from "lib/utils/getColorFromString"
|
|
10
9
|
import { OverlapAvoidanceStepSolver } from "./sub-solvers/OverlapAvoidanceStepSolver/OverlapAvoidanceStepSolver"
|
|
11
10
|
|
|
@@ -27,7 +26,6 @@ export class TraceLabelOverlapAvoidanceSolver extends BaseSolver {
|
|
|
27
26
|
// sub-solver instances
|
|
28
27
|
labelMergingSolver?: MergedNetLabelObstacleSolver
|
|
29
28
|
overlapAvoidanceSolver?: OverlapAvoidanceStepSolver
|
|
30
|
-
traceCleanupSolver?: TraceCleanupSolver
|
|
31
29
|
pipelineStepIndex = 0
|
|
32
30
|
|
|
33
31
|
constructor(solverInput: TraceLabelOverlapAvoidanceSolverInput) {
|
|
@@ -75,24 +73,6 @@ export class TraceLabelOverlapAvoidanceSolver extends BaseSolver {
|
|
|
75
73
|
this.activeSubSolver = this.overlapAvoidanceSolver
|
|
76
74
|
break
|
|
77
75
|
|
|
78
|
-
case 2:
|
|
79
|
-
this.traceCleanupSolver = new TraceCleanupSolver({
|
|
80
|
-
inputProblem: this.inputProblem,
|
|
81
|
-
allTraces: this.overlapAvoidanceSolver!.getOutput().allTraces,
|
|
82
|
-
targetTraceIds: new Set(
|
|
83
|
-
this.overlapAvoidanceSolver!.getOutput().modifiedTraces.map(
|
|
84
|
-
(t) => t.mspPairId,
|
|
85
|
-
),
|
|
86
|
-
),
|
|
87
|
-
allLabelPlacements:
|
|
88
|
-
this.labelMergingSolver!.getOutput().netLabelPlacements,
|
|
89
|
-
mergedLabelNetIdMap:
|
|
90
|
-
this.labelMergingSolver!.getOutput().mergedLabelNetIdMap,
|
|
91
|
-
paddingBuffer: 0.01,
|
|
92
|
-
})
|
|
93
|
-
this.activeSubSolver = this.traceCleanupSolver
|
|
94
|
-
break
|
|
95
|
-
|
|
96
76
|
default:
|
|
97
77
|
this.solved = true
|
|
98
78
|
break
|
|
@@ -101,7 +81,7 @@ export class TraceLabelOverlapAvoidanceSolver extends BaseSolver {
|
|
|
101
81
|
|
|
102
82
|
getOutput() {
|
|
103
83
|
return {
|
|
104
|
-
traces: this.
|
|
84
|
+
traces: this.overlapAvoidanceSolver?.getOutput().allTraces ?? this.traces,
|
|
105
85
|
netLabelPlacements:
|
|
106
86
|
this.labelMergingSolver?.getOutput().netLabelPlacements ??
|
|
107
87
|
this.netLabelPlacements,
|
|
@@ -5,8 +5,8 @@ import { getRectBounds } from "../NetLabelPlacementSolver/SingleNetLabelPlacemen
|
|
|
5
5
|
import type { InputProblem } from "lib/types/InputProblem"
|
|
6
6
|
import { findTraceViolationZone } from "./violation"
|
|
7
7
|
import { generateSnipAndReconnectCandidates } from "./trySnipAndReconnect"
|
|
8
|
-
import { simplifyPath } from "./sub-solvers/TraceCleanupSolver/simplifyPath"
|
|
9
8
|
import { generateFourPointDetourCandidates } from "./tryFourPointDetour"
|
|
9
|
+
import { simplifyPath } from "../TraceCleanupSolver/simplifyPath"
|
|
10
10
|
|
|
11
11
|
export const generateRerouteCandidates = ({
|
|
12
12
|
trace,
|
|
@@ -8,7 +8,7 @@ import { isPathCollidingWithObstacles } from "lib/solvers/SchematicTraceLinesSol
|
|
|
8
8
|
import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
|
|
9
9
|
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
|
|
10
10
|
import { generateRerouteCandidates } from "../../rerouteCollidingTrace"
|
|
11
|
-
import { simplifyPath } from "
|
|
11
|
+
import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath"
|
|
12
12
|
|
|
13
13
|
interface SingleOverlapSolverInput {
|
|
14
14
|
trace: SolvedTracePath
|
|
@@ -42,6 +42,8 @@ export class TraceOverlapShiftSolver extends BaseSolver {
|
|
|
42
42
|
|
|
43
43
|
correctedTraceMap: Record<MspConnectionPairId, SolvedTracePath> = {}
|
|
44
44
|
|
|
45
|
+
cleanupPhase: "diagonals" | "done" | null = null
|
|
46
|
+
|
|
45
47
|
constructor(params: {
|
|
46
48
|
inputProblem: InputProblem
|
|
47
49
|
inputTracePaths: Array<SolvedTracePath>
|
|
@@ -211,6 +213,62 @@ export class TraceOverlapShiftSolver extends BaseSolver {
|
|
|
211
213
|
return null
|
|
212
214
|
}
|
|
213
215
|
|
|
216
|
+
private findNextDiagonalSegment() {
|
|
217
|
+
const EPS = 2e-3
|
|
218
|
+
for (const mspPairId in this.correctedTraceMap) {
|
|
219
|
+
const tracePath = this.correctedTraceMap[mspPairId]!.tracePath
|
|
220
|
+
for (let i = 0; i < tracePath.length - 1; i++) {
|
|
221
|
+
const p1 = tracePath[i]!
|
|
222
|
+
const p2 = tracePath[i + 1]!
|
|
223
|
+
|
|
224
|
+
const isHorizontal = Math.abs(p1.y - p2.y) < EPS
|
|
225
|
+
const isVertical = Math.abs(p1.x - p2.x) < EPS
|
|
226
|
+
|
|
227
|
+
if (!isHorizontal && !isVertical) {
|
|
228
|
+
return { mspPairId, tracePath, i, p1, p2 }
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return null
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private findAndFixNextDiagonalSegment(): boolean {
|
|
236
|
+
const diagonalInfo = this.findNextDiagonalSegment()
|
|
237
|
+
|
|
238
|
+
if (!diagonalInfo) {
|
|
239
|
+
return false // No diagonal segments found
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const { mspPairId, tracePath, i, p1, p2 } = diagonalInfo
|
|
243
|
+
const EPS = 2e-3
|
|
244
|
+
|
|
245
|
+
const p0 = i > 0 ? tracePath[i - 1] : null
|
|
246
|
+
const p3 = i + 2 < tracePath.length ? tracePath[i + 2] : null
|
|
247
|
+
|
|
248
|
+
const prevIsVertical = p0 ? Math.abs(p0.x - p1.x) < EPS : false
|
|
249
|
+
const prevIsHorizontal = p0 ? Math.abs(p0.y - p1.y) < EPS : false
|
|
250
|
+
|
|
251
|
+
const nextIsVertical = p3 ? Math.abs(p2.x - p3.x) < EPS : false
|
|
252
|
+
const nextIsHorizontal = p3 ? Math.abs(p2.y - p3.y) < EPS : false
|
|
253
|
+
|
|
254
|
+
const elbow1 = { x: p1.x, y: p2.y } // vertical from p1
|
|
255
|
+
const elbow2 = { x: p2.x, y: p1.y } // horizontal from p1
|
|
256
|
+
|
|
257
|
+
let score1 = 0
|
|
258
|
+
if (prevIsVertical) score1++
|
|
259
|
+
if (nextIsHorizontal) score1++
|
|
260
|
+
|
|
261
|
+
let score2 = 0
|
|
262
|
+
if (prevIsHorizontal) score2++
|
|
263
|
+
if (nextIsVertical) score2++
|
|
264
|
+
|
|
265
|
+
const elbowPoint = score1 < score2 ? elbow1 : elbow2
|
|
266
|
+
|
|
267
|
+
// Replace [p1, p2] with [p1, elbowPoint, p2]
|
|
268
|
+
tracePath.splice(i + 1, 0, elbowPoint)
|
|
269
|
+
return true // Fixed one diagonal, return true to re-evaluate in next step
|
|
270
|
+
}
|
|
271
|
+
|
|
214
272
|
override _step() {
|
|
215
273
|
if (this.activeSubSolver?.solved) {
|
|
216
274
|
for (const [mspPairId, newTrace] of Object.entries(
|
|
@@ -231,6 +289,19 @@ export class TraceOverlapShiftSolver extends BaseSolver {
|
|
|
231
289
|
const overlapIssue = this.findNextOverlapIssue()
|
|
232
290
|
|
|
233
291
|
if (overlapIssue === null) {
|
|
292
|
+
if (this.cleanupPhase === null) {
|
|
293
|
+
this.cleanupPhase = "diagonals"
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (this.cleanupPhase === "diagonals") {
|
|
297
|
+
const fixedDiagonal = this.findAndFixNextDiagonalSegment()
|
|
298
|
+
if (!fixedDiagonal) {
|
|
299
|
+
this.cleanupPhase = "done"
|
|
300
|
+
this.solved = true
|
|
301
|
+
}
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
// If cleanupPhase is "done", then the solver is truly solved.
|
|
234
305
|
this.solved = true
|
|
235
306
|
return
|
|
236
307
|
}
|
|
@@ -249,6 +320,7 @@ export class TraceOverlapShiftSolver extends BaseSolver {
|
|
|
249
320
|
}
|
|
250
321
|
|
|
251
322
|
const graphics = visualizeInputProblem(this.inputProblem)
|
|
323
|
+
graphics.circles = graphics.circles || []
|
|
252
324
|
|
|
253
325
|
// Draw current corrected traces
|
|
254
326
|
for (const trace of Object.values(this.correctedTraceMap)) {
|
|
@@ -258,6 +330,17 @@ export class TraceOverlapShiftSolver extends BaseSolver {
|
|
|
258
330
|
})
|
|
259
331
|
}
|
|
260
332
|
|
|
333
|
+
if (this.cleanupPhase === "diagonals") {
|
|
334
|
+
const diagonalInfo = this.findNextDiagonalSegment()
|
|
335
|
+
if (diagonalInfo) {
|
|
336
|
+
graphics.lines!.push({
|
|
337
|
+
points: [diagonalInfo.p1, diagonalInfo.p2],
|
|
338
|
+
strokeColor: "red",
|
|
339
|
+
strokeWidth: 0.05,
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
261
344
|
return graphics
|
|
262
345
|
}
|
|
263
346
|
}
|
package/package.json
CHANGED