@tscircuit/schematic-trace-solver 0.0.44 → 0.0.46
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/.github/workflows/bun-formatcheck.yml +1 -1
- package/.github/workflows/bun-pver-release.yml +1 -1
- package/.github/workflows/bun-test.yml +1 -1
- package/.github/workflows/bun-typecheck.yml +1 -1
- package/dist/index.d.ts +43 -8
- package/dist/index.js +1109 -185
- package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +0 -1
- package/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts +83 -36
- package/lib/solvers/TraceCleanupSolver/hasCollisions.ts +16 -4
- package/lib/solvers/TraceCleanupSolver/is4PointRectangle.ts +17 -0
- package/lib/solvers/TraceCleanupSolver/isSegmentAnEndpointSegment.ts +36 -0
- package/lib/solvers/TraceCleanupSolver/mergeGraphicsObjects.ts +28 -0
- package/lib/solvers/TraceCleanupSolver/minimizeTurnsWithFilteredLabels.ts +18 -1
- package/lib/solvers/TraceCleanupSolver/recognizeStairStepPattern.ts +56 -0
- package/lib/solvers/TraceCleanupSolver/sub-solver/UntangleTraceSubsolver.ts +370 -0
- package/lib/solvers/TraceCleanupSolver/sub-solver/findAllLShapedTurns.ts +48 -0
- package/lib/solvers/TraceCleanupSolver/sub-solver/findIntersectionsWithObstacles.ts +36 -0
- package/lib/solvers/TraceCleanupSolver/sub-solver/generateLShapeRerouteCandidates.ts +107 -0
- package/lib/solvers/TraceCleanupSolver/sub-solver/generateRectangleCandidates.ts +55 -0
- package/lib/solvers/TraceCleanupSolver/sub-solver/getTraceObstacles.ts +25 -0
- package/lib/solvers/TraceCleanupSolver/sub-solver/isPathColliding.ts +58 -0
- package/lib/solvers/TraceCleanupSolver/sub-solver/visualizeCandidates.ts +33 -0
- package/lib/solvers/TraceCleanupSolver/sub-solver/visualizeCollision.ts +20 -0
- package/lib/solvers/TraceCleanupSolver/sub-solver/visualizeIntersectionPoints.ts +26 -0
- package/lib/solvers/TraceCleanupSolver/sub-solver/visualizeLSapes.ts +33 -0
- package/lib/solvers/TraceCleanupSolver/turnMinimization.ts +50 -56
- package/lib/solvers/TraceCleanupSolver/visualizeTightRectangle.ts +24 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver.ts +17 -3
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/detectTraceLabelOverlap.ts +26 -11
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/LabelMergingSolver.ts +95 -75
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/filterLabelsAtTraceEdges.ts +74 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/groupLabelsByChipAndOrientation.ts +45 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/mergeLabelGroup.ts +71 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/OverlapAvoidanceStepSolver.ts +150 -27
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/isPointInsideLabel.ts +23 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/visualizeDecomposition.ts +54 -0
- package/package.json +1 -1
- package/tests/examples/__snapshots__/example29.snap.svg +6 -6
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/__snapshots__/TraceLabelOverlapAvoidanceSolver.snap.svg +8 -8
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { BaseSolver } from "../../BaseSolver/BaseSolver"
|
|
2
|
+
import type { InputProblem } from "../../../types/InputProblem"
|
|
3
|
+
import type { SolvedTracePath } from "../../SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
4
|
+
import type { NetLabelPlacement } from "../../NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
5
|
+
|
|
6
|
+
import { findAllLShapedTurns, type LShape } from "./findAllLShapedTurns"
|
|
7
|
+
import { getTraceObstacles } from "./getTraceObstacles"
|
|
8
|
+
import { findIntersectionsWithObstacles } from "./findIntersectionsWithObstacles"
|
|
9
|
+
import { generateLShapeRerouteCandidates } from "./generateLShapeRerouteCandidates"
|
|
10
|
+
import { isPathColliding, type CollisionInfo } from "./isPathColliding"
|
|
11
|
+
import {
|
|
12
|
+
generateRectangleCandidates,
|
|
13
|
+
type Rectangle,
|
|
14
|
+
type RectangleCandidate,
|
|
15
|
+
} from "./generateRectangleCandidates"
|
|
16
|
+
|
|
17
|
+
import type { GraphicsObject } from "graphics-debug"
|
|
18
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
19
|
+
|
|
20
|
+
import { visualizeLSapes } from "./visualizeLSapes"
|
|
21
|
+
import { visualizeIntersectionPoints } from "./visualizeIntersectionPoints"
|
|
22
|
+
import { visualizeTightRectangle } from "../visualizeTightRectangle"
|
|
23
|
+
import { visualizeCandidates } from "./visualizeCandidates"
|
|
24
|
+
import { mergeGraphicsObjects } from "../mergeGraphicsObjects"
|
|
25
|
+
import { visualizeCollision } from "./visualizeCollision"
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Defines the input structure for the UntangleTraceSubsolver.
|
|
29
|
+
*/
|
|
30
|
+
export interface UntangleTraceSubsolverInput {
|
|
31
|
+
inputProblem: InputProblem
|
|
32
|
+
allTraces: SolvedTracePath[]
|
|
33
|
+
allLabelPlacements: NetLabelPlacement[]
|
|
34
|
+
mergedLabelNetIdMap: Record<string, Set<string>>
|
|
35
|
+
paddingBuffer: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Represents the different visualization modes for the UntangleTraceSubsolver.
|
|
40
|
+
*/
|
|
41
|
+
type VisualizationMode =
|
|
42
|
+
| "l_shapes"
|
|
43
|
+
| "intersection_points"
|
|
44
|
+
| "tight_rectangle"
|
|
45
|
+
| "candidates"
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The UntangleTraceSubsolver is designed to resolve complex overlaps and improve the routing of traces,
|
|
49
|
+
* particularly focusing on "L-shaped" turns that might be causing congestion or suboptimal paths.
|
|
50
|
+
* Its main workflow involves several steps:
|
|
51
|
+
* 1. **Identify L-Shapes**: It first identifies all L-shaped turns within the traces that need processing.
|
|
52
|
+
* 2. **Find Intersections**: For each L-shape, it determines intersection points with other traces and obstacles.
|
|
53
|
+
* 3. **Generate Rectangle Candidates**: Based on these intersection points, it generates potential rectangular regions for rerouting.
|
|
54
|
+
* 4. **Evaluate Candidates**: For each rectangular candidate, it generates alternative trace paths and evaluates them for collisions.
|
|
55
|
+
* 5. **Apply Best Route**: If a collision-free and improved route is found, it updates the trace path.
|
|
56
|
+
* This iterative process aims to untangle traces and create a cleaner, more efficient layout.
|
|
57
|
+
*/
|
|
58
|
+
export class UntangleTraceSubsolver extends BaseSolver {
|
|
59
|
+
private input: UntangleTraceSubsolverInput
|
|
60
|
+
private lShapesToProcess: LShape[] = []
|
|
61
|
+
private visualizationMode: VisualizationMode = "l_shapes"
|
|
62
|
+
|
|
63
|
+
private currentLShape: LShape | null = null
|
|
64
|
+
private intersectionPoints: Point[] = []
|
|
65
|
+
private tightRectangle: Rectangle | null = null
|
|
66
|
+
private candidates: Point[][] = []
|
|
67
|
+
private bestRoute: Point[] | null = null
|
|
68
|
+
private lastCollision: CollisionInfo | null = null
|
|
69
|
+
private collidingCandidate: Point[] | null = null
|
|
70
|
+
|
|
71
|
+
private rectangleCandidates: RectangleCandidate[] = []
|
|
72
|
+
private currentRectangleIndex = 0
|
|
73
|
+
|
|
74
|
+
private isInitialStep = true
|
|
75
|
+
private currentCandidateIndex = 0
|
|
76
|
+
private lShapeProcessingStep:
|
|
77
|
+
| "idle"
|
|
78
|
+
| "intersections"
|
|
79
|
+
| "rectangle_selection"
|
|
80
|
+
| "candidate_evaluation" = "idle"
|
|
81
|
+
private lShapeJustProcessed = false
|
|
82
|
+
private bestRouteFound: Point[] | null = null
|
|
83
|
+
|
|
84
|
+
constructor(solverInput: UntangleTraceSubsolverInput) {
|
|
85
|
+
super()
|
|
86
|
+
this.input = solverInput
|
|
87
|
+
this.visualizationMode = "l_shapes"
|
|
88
|
+
|
|
89
|
+
for (const trace of this.input.allTraces) {
|
|
90
|
+
const lShapes = findAllLShapedTurns(trace.tracePath)
|
|
91
|
+
this.lShapesToProcess.push(
|
|
92
|
+
...lShapes.map((l) => ({ ...l, traceId: trace.mspPairId as string })),
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
override _step(): void {
|
|
98
|
+
if (this.isInitialStep) {
|
|
99
|
+
this.isInitialStep = false
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (this.lShapeJustProcessed) {
|
|
104
|
+
this._resetAfterLShapProcessing()
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (this.lShapesToProcess.length === 0 && this.currentLShape === null) {
|
|
109
|
+
this.solved = true
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
switch (this.lShapeProcessingStep) {
|
|
114
|
+
case "idle":
|
|
115
|
+
this._handleIdleStep()
|
|
116
|
+
break
|
|
117
|
+
case "intersections":
|
|
118
|
+
this._handleIntersectionsStep()
|
|
119
|
+
break
|
|
120
|
+
case "rectangle_selection":
|
|
121
|
+
this._handleRectangleSelectionStep()
|
|
122
|
+
break
|
|
123
|
+
case "candidate_evaluation":
|
|
124
|
+
this._handleCandidateEvaluationStep()
|
|
125
|
+
break
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private _resetAfterLShapProcessing() {
|
|
130
|
+
this.lShapeProcessingStep = "idle"
|
|
131
|
+
this.currentLShape = null
|
|
132
|
+
this.currentCandidateIndex = 0
|
|
133
|
+
this.lShapeJustProcessed = false
|
|
134
|
+
this.visualizationMode = "l_shapes" // Reset visualization mode
|
|
135
|
+
this.intersectionPoints = [] // Clear temporary data
|
|
136
|
+
this.tightRectangle = null
|
|
137
|
+
this.candidates = []
|
|
138
|
+
this.bestRoute = null
|
|
139
|
+
this.lastCollision = null
|
|
140
|
+
this.collidingCandidate = null
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private _handleIdleStep() {
|
|
144
|
+
this.currentLShape = this.lShapesToProcess.shift()!
|
|
145
|
+
if (!this.currentLShape) {
|
|
146
|
+
this.solved = true
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
this.lShapeProcessingStep = "intersections"
|
|
150
|
+
this.visualizationMode = "l_shapes"
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private _handleIntersectionsStep() {
|
|
154
|
+
if (!this.currentLShape!.traceId) {
|
|
155
|
+
this.lShapeProcessingStep = "idle"
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
const allObstacles = getTraceObstacles(
|
|
159
|
+
this.input.allTraces,
|
|
160
|
+
this.currentLShape!.traceId,
|
|
161
|
+
)
|
|
162
|
+
const intersections1 = findIntersectionsWithObstacles(
|
|
163
|
+
this.currentLShape!.p1,
|
|
164
|
+
this.currentLShape!.p2,
|
|
165
|
+
allObstacles,
|
|
166
|
+
)
|
|
167
|
+
const intersections2 = findIntersectionsWithObstacles(
|
|
168
|
+
this.currentLShape!.p2,
|
|
169
|
+
this.currentLShape!.p3,
|
|
170
|
+
allObstacles,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
this.intersectionPoints = [...intersections1, ...intersections2]
|
|
174
|
+
|
|
175
|
+
if (intersections1.length === 0 || intersections2.length === 0) {
|
|
176
|
+
this.lShapeProcessingStep = "idle"
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
this.rectangleCandidates = generateRectangleCandidates(
|
|
181
|
+
intersections1,
|
|
182
|
+
intersections2,
|
|
183
|
+
)
|
|
184
|
+
this.currentRectangleIndex = 0
|
|
185
|
+
this.lShapeProcessingStep = "rectangle_selection"
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private _handleRectangleSelectionStep() {
|
|
189
|
+
if (this.currentRectangleIndex >= this.rectangleCandidates.length) {
|
|
190
|
+
this.lShapeProcessingStep = "idle"
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const { rect, i1, i2 } =
|
|
195
|
+
this.rectangleCandidates[this.currentRectangleIndex]
|
|
196
|
+
this.tightRectangle = rect
|
|
197
|
+
|
|
198
|
+
this.candidates = generateLShapeRerouteCandidates({
|
|
199
|
+
lShape: this.currentLShape!,
|
|
200
|
+
rectangle: this.tightRectangle!,
|
|
201
|
+
padding: 2 * this.input.paddingBuffer,
|
|
202
|
+
interactionPoint1: i1,
|
|
203
|
+
interactionPoint2: i2,
|
|
204
|
+
})
|
|
205
|
+
this.currentCandidateIndex = 0
|
|
206
|
+
this.lastCollision = null
|
|
207
|
+
this.collidingCandidate = null
|
|
208
|
+
|
|
209
|
+
this.visualizationMode = "candidates"
|
|
210
|
+
this.lShapeProcessingStep = "candidate_evaluation"
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private _handleCandidateEvaluationStep() {
|
|
214
|
+
this.visualizationMode = "candidates"
|
|
215
|
+
|
|
216
|
+
if (this.bestRouteFound) {
|
|
217
|
+
this._applyBestRoute(this.bestRouteFound)
|
|
218
|
+
this.bestRouteFound = null
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (this.currentCandidateIndex >= this.candidates.length) {
|
|
223
|
+
this.currentRectangleIndex++
|
|
224
|
+
this.lShapeProcessingStep = "rectangle_selection"
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const currentCandidate = this.candidates[this.currentCandidateIndex]
|
|
229
|
+
const collisionResult = isPathColliding(
|
|
230
|
+
currentCandidate,
|
|
231
|
+
this.input.allTraces,
|
|
232
|
+
this.currentLShape!.traceId,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
if (!collisionResult?.isColliding) {
|
|
236
|
+
this.bestRouteFound = currentCandidate
|
|
237
|
+
this.lastCollision = null
|
|
238
|
+
this.collidingCandidate = null
|
|
239
|
+
} else {
|
|
240
|
+
this.lastCollision = collisionResult
|
|
241
|
+
this.collidingCandidate = currentCandidate
|
|
242
|
+
this.currentCandidateIndex++
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private _applyBestRoute(bestRoute: Point[]) {
|
|
247
|
+
this.bestRoute = bestRoute
|
|
248
|
+
this.collidingCandidate = null
|
|
249
|
+
this.lastCollision = null
|
|
250
|
+
|
|
251
|
+
const traceIndex = this.input.allTraces.findIndex(
|
|
252
|
+
(trace) => trace.mspPairId === this.currentLShape!.traceId,
|
|
253
|
+
)
|
|
254
|
+
if (traceIndex !== -1) {
|
|
255
|
+
const originalTrace = this.input.allTraces[traceIndex]
|
|
256
|
+
const p2Index = originalTrace.tracePath.findIndex(
|
|
257
|
+
(p) =>
|
|
258
|
+
p.x === this.currentLShape!.p2.x && p.y === this.currentLShape!.p2.y,
|
|
259
|
+
)
|
|
260
|
+
if (p2Index !== -1) {
|
|
261
|
+
const newTracePath = [
|
|
262
|
+
...originalTrace.tracePath.slice(0, p2Index),
|
|
263
|
+
...bestRoute,
|
|
264
|
+
...originalTrace.tracePath.slice(p2Index + 1),
|
|
265
|
+
]
|
|
266
|
+
this.input.allTraces[traceIndex] = {
|
|
267
|
+
...originalTrace,
|
|
268
|
+
tracePath: newTracePath,
|
|
269
|
+
}
|
|
270
|
+
this.lShapesToProcess = this.lShapesToProcess.filter(
|
|
271
|
+
(l) => l.traceId !== this.currentLShape!.traceId,
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
this.lShapeJustProcessed = true
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
getOutput(): { traces: SolvedTracePath[] } {
|
|
279
|
+
return { traces: this.input.allTraces }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
override visualize(): GraphicsObject {
|
|
283
|
+
// console.log("VISUALIZE STATE:", {
|
|
284
|
+
// step: this.lShapeProcessingStep,
|
|
285
|
+
// vizMode: this.visualizationMode,
|
|
286
|
+
// lShape: this.currentLShape?.traceId,
|
|
287
|
+
// rectIdx: this.currentRectangleIndex,
|
|
288
|
+
// rectCount: this.rectangleCandidates.length,
|
|
289
|
+
// tightRect: this.tightRectangle,
|
|
290
|
+
// pathIdx: this.currentCandidateIndex,
|
|
291
|
+
// pathCount: this.candidates.length,
|
|
292
|
+
// lastCollision: this.lastCollision?.isColliding,
|
|
293
|
+
// })
|
|
294
|
+
|
|
295
|
+
switch (this.visualizationMode) {
|
|
296
|
+
case "l_shapes":
|
|
297
|
+
return visualizeLSapes(this.lShapesToProcess)
|
|
298
|
+
case "intersection_points":
|
|
299
|
+
return mergeGraphicsObjects([
|
|
300
|
+
this.currentLShape ? visualizeLSapes(this.currentLShape) : undefined,
|
|
301
|
+
visualizeIntersectionPoints(this.intersectionPoints),
|
|
302
|
+
])
|
|
303
|
+
case "tight_rectangle":
|
|
304
|
+
return mergeGraphicsObjects([
|
|
305
|
+
this.currentLShape ? visualizeLSapes(this.currentLShape) : undefined,
|
|
306
|
+
visualizeIntersectionPoints(this.intersectionPoints),
|
|
307
|
+
this.tightRectangle
|
|
308
|
+
? visualizeTightRectangle(this.tightRectangle)
|
|
309
|
+
: undefined,
|
|
310
|
+
])
|
|
311
|
+
case "candidates": {
|
|
312
|
+
if (this.lShapeJustProcessed) {
|
|
313
|
+
const allTracesGraphics: GraphicsObject = { lines: [] }
|
|
314
|
+
for (const trace of this.input.allTraces) {
|
|
315
|
+
const isUpdatedTrace =
|
|
316
|
+
trace.mspPairId === this.currentLShape?.traceId
|
|
317
|
+
for (let i = 0; i < trace.tracePath.length - 1; i++) {
|
|
318
|
+
allTracesGraphics.lines!.push({
|
|
319
|
+
points: [trace.tracePath[i], trace.tracePath[i + 1]],
|
|
320
|
+
strokeColor: isUpdatedTrace ? "green" : "#ccc",
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return allTracesGraphics
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const allTracesGraphics: GraphicsObject = { lines: [] }
|
|
328
|
+
for (const trace of this.input.allTraces) {
|
|
329
|
+
for (let i = 0; i < trace.tracePath.length - 1; i++) {
|
|
330
|
+
allTracesGraphics.lines!.push({
|
|
331
|
+
points: [trace.tracePath[i], trace.tracePath[i + 1]],
|
|
332
|
+
strokeColor: "#ccc", // Light gray for other traces
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let candidateToDraw: Point[] | undefined
|
|
338
|
+
if (this.bestRouteFound) {
|
|
339
|
+
candidateToDraw = this.bestRouteFound
|
|
340
|
+
} else if (this.lastCollision?.isColliding) {
|
|
341
|
+
candidateToDraw = this.collidingCandidate ?? undefined
|
|
342
|
+
} else {
|
|
343
|
+
if (this.currentCandidateIndex < this.candidates.length) {
|
|
344
|
+
candidateToDraw = this.candidates[this.currentCandidateIndex]
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return mergeGraphicsObjects([
|
|
349
|
+
allTracesGraphics,
|
|
350
|
+
this.currentLShape ? visualizeLSapes(this.currentLShape) : undefined,
|
|
351
|
+
this.tightRectangle
|
|
352
|
+
? visualizeTightRectangle(this.tightRectangle)
|
|
353
|
+
: undefined,
|
|
354
|
+
candidateToDraw
|
|
355
|
+
? visualizeCandidates(
|
|
356
|
+
[candidateToDraw],
|
|
357
|
+
this.bestRouteFound ? "green" : "blue",
|
|
358
|
+
this.intersectionPoints,
|
|
359
|
+
)
|
|
360
|
+
: undefined,
|
|
361
|
+
this.lastCollision
|
|
362
|
+
? visualizeCollision(this.lastCollision)
|
|
363
|
+
: undefined,
|
|
364
|
+
])
|
|
365
|
+
}
|
|
366
|
+
default:
|
|
367
|
+
return {}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Represents an L-shaped turn in a trace path, defined by three consecutive points.
|
|
5
|
+
* p1 and p3 are the endpoints of the L-shape, and p2 is the corner point.
|
|
6
|
+
*/
|
|
7
|
+
export interface LShape {
|
|
8
|
+
p1: Point
|
|
9
|
+
p2: Point // The corner
|
|
10
|
+
p3: Point
|
|
11
|
+
traceId?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Identifies and returns all L-shaped turns within a given trace path.
|
|
16
|
+
* An L-shaped turn is detected when two consecutive segments are orthogonal (one vertical, one horizontal)
|
|
17
|
+
* and both segments have a minimum length. This function iterates through the trace path,
|
|
18
|
+
* checking every sequence of three points to see if they form an L-shape.
|
|
19
|
+
*/
|
|
20
|
+
export const findAllLShapedTurns = (tracePath: Point[]): LShape[] => {
|
|
21
|
+
const lShapes: LShape[] = []
|
|
22
|
+
if (tracePath.length < 3) {
|
|
23
|
+
return lShapes
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for (let i = 0; i < tracePath.length - 2; i++) {
|
|
27
|
+
const p1 = tracePath[i]
|
|
28
|
+
const p2 = tracePath[i + 1]
|
|
29
|
+
const p3 = tracePath[i + 2]
|
|
30
|
+
|
|
31
|
+
const dx1 = p2.x - p1.x
|
|
32
|
+
const dy1 = p2.y - p1.y
|
|
33
|
+
const dx2 = p3.x - p2.x
|
|
34
|
+
const dy2 = p3.y - p2.y
|
|
35
|
+
|
|
36
|
+
// Check for a 90-degree turn (orthogonal segments)
|
|
37
|
+
if (
|
|
38
|
+
((dx1 === 0 && dy2 === 0 && dy1 !== 0 && dx2 !== 0) || // Vertical then Horizontal
|
|
39
|
+
(dy1 === 0 && dx2 === 0 && dx1 !== 0 && dy2 !== 0)) && // Horizontal then Vertical
|
|
40
|
+
dx1 * dx1 + dy1 * dy1 >= 0.25 && // p1-p2 arm length >= 0.5
|
|
41
|
+
dx2 * dx2 + dy2 * dy2 >= 0.25 // p2-p3 arm length >= 0.5
|
|
42
|
+
) {
|
|
43
|
+
lShapes.push({ p1, p2, p3 })
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return lShapes
|
|
48
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
import { getSegmentIntersection } from "@tscircuit/math-utils/line-intersections"
|
|
3
|
+
import type { TraceObstacle } from "./getTraceObstacles"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Finds all intersection points between a given line segment (p1-p2) and a list of trace obstacles.
|
|
7
|
+
* It iterates through each segment of every obstacle and checks for intersections with the input segment.
|
|
8
|
+
*/
|
|
9
|
+
export const findIntersectionsWithObstacles = (
|
|
10
|
+
p1: Point,
|
|
11
|
+
p2: Point,
|
|
12
|
+
obstacles: TraceObstacle[],
|
|
13
|
+
): Point[] => {
|
|
14
|
+
const intersections: Point[] = []
|
|
15
|
+
|
|
16
|
+
for (const obstacle of obstacles) {
|
|
17
|
+
const obstaclePath = obstacle.points
|
|
18
|
+
for (let i = 0; i < obstaclePath.length - 1; i++) {
|
|
19
|
+
const o1 = obstaclePath[i]
|
|
20
|
+
const o2 = obstaclePath[i + 1]
|
|
21
|
+
|
|
22
|
+
// Ensure both points are defined before proceeding
|
|
23
|
+
if (!o1 || !o2) {
|
|
24
|
+
// console.warn("Skipping obstacle segment due to undefined point:", { o1, o2, obstaclePath });
|
|
25
|
+
continue
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const intersection = getSegmentIntersection(p1, p2, o1, o2)
|
|
29
|
+
if (intersection) {
|
|
30
|
+
intersections.push(intersection)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return intersections
|
|
36
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
import type { LShape } from "./findAllLShapedTurns"
|
|
3
|
+
import type { Rectangle } from "./generateRectangleCandidates"
|
|
4
|
+
|
|
5
|
+
const EPS = 1e-6
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Checks if a segment defined by two points is vertical.
|
|
9
|
+
* It considers a segment vertical if the absolute difference between their x-coordinates is less than a small epsilon.
|
|
10
|
+
*/
|
|
11
|
+
const isVertical = (a: Point, b: Point, eps = EPS) => Math.abs(a.x - b.x) < eps
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generates candidate reroutes for an L-shaped turn within a given rectangular area.
|
|
15
|
+
* This function calculates a new path that attempts to smooth out the L-shape by routing around the corner
|
|
16
|
+
* through the provided rectangle, adding padding to avoid immediate collisions.
|
|
17
|
+
* It considers different orientations of the L-shape relative to the rectangle to determine the appropriate rerouting points.
|
|
18
|
+
*/
|
|
19
|
+
export const generateLShapeRerouteCandidates = ({
|
|
20
|
+
lShape,
|
|
21
|
+
rectangle,
|
|
22
|
+
padding = 0.5,
|
|
23
|
+
interactionPoint1,
|
|
24
|
+
interactionPoint2,
|
|
25
|
+
}: {
|
|
26
|
+
lShape: LShape
|
|
27
|
+
rectangle: Rectangle
|
|
28
|
+
padding: number
|
|
29
|
+
interactionPoint1: Point
|
|
30
|
+
interactionPoint2: Point
|
|
31
|
+
}): Point[][] => {
|
|
32
|
+
const { p1, p2, p3 } = lShape
|
|
33
|
+
const { x, y, width, height } = rectangle
|
|
34
|
+
|
|
35
|
+
let c2: Point
|
|
36
|
+
let i1_padded: Point = interactionPoint1
|
|
37
|
+
let i2_padded: Point = interactionPoint2
|
|
38
|
+
|
|
39
|
+
if (Math.abs(p2.x - x) < EPS && Math.abs(p2.y - (y + height)) < EPS) {
|
|
40
|
+
c2 = { x: x + width + padding, y: y - padding }
|
|
41
|
+
|
|
42
|
+
if (isVertical(p1, p2)) {
|
|
43
|
+
i1_padded = { x: interactionPoint1.x, y: interactionPoint1.y - padding }
|
|
44
|
+
} else {
|
|
45
|
+
// isHorizontal(p1, p2)
|
|
46
|
+
i1_padded = { x: interactionPoint1.x + padding, y: interactionPoint1.y }
|
|
47
|
+
}
|
|
48
|
+
if (isVertical(p2, p3)) {
|
|
49
|
+
i2_padded = { x: interactionPoint2.x, y: interactionPoint2.y - padding }
|
|
50
|
+
} else {
|
|
51
|
+
// isHorizontal(p2, p3)
|
|
52
|
+
i2_padded = { x: interactionPoint2.x + padding, y: interactionPoint2.y }
|
|
53
|
+
}
|
|
54
|
+
} else if (
|
|
55
|
+
Math.abs(p2.x - (x + width)) < EPS &&
|
|
56
|
+
Math.abs(p2.y - (y + height)) < EPS
|
|
57
|
+
) {
|
|
58
|
+
c2 = { x: x - padding, y: y - padding }
|
|
59
|
+
|
|
60
|
+
if (isVertical(p1, p2)) {
|
|
61
|
+
i1_padded = { x: interactionPoint1.x, y: interactionPoint1.y - padding }
|
|
62
|
+
} else {
|
|
63
|
+
// isHorizontal(p1, p2)
|
|
64
|
+
i1_padded = { x: interactionPoint1.x - padding, y: interactionPoint1.y }
|
|
65
|
+
}
|
|
66
|
+
if (isVertical(p2, p3)) {
|
|
67
|
+
i2_padded = { x: interactionPoint2.x, y: interactionPoint2.y - padding }
|
|
68
|
+
} else {
|
|
69
|
+
// isHorizontal(p2, p3)
|
|
70
|
+
i2_padded = { x: interactionPoint2.x - padding, y: interactionPoint2.y }
|
|
71
|
+
}
|
|
72
|
+
} else if (Math.abs(p2.x - x) < EPS && Math.abs(p2.y - y) < EPS) {
|
|
73
|
+
c2 = { x: x + width + padding, y: y + height + padding }
|
|
74
|
+
|
|
75
|
+
if (isVertical(p1, p2)) {
|
|
76
|
+
i1_padded = { x: interactionPoint1.x, y: interactionPoint1.y + padding }
|
|
77
|
+
} else {
|
|
78
|
+
// isHorizontal(p1, p2)
|
|
79
|
+
i1_padded = { x: interactionPoint1.x + padding, y: interactionPoint1.y }
|
|
80
|
+
}
|
|
81
|
+
if (isVertical(p2, p3)) {
|
|
82
|
+
i2_padded = { x: interactionPoint2.x, y: interactionPoint2.y + padding }
|
|
83
|
+
} else {
|
|
84
|
+
// isHorizontal(p2, p3)
|
|
85
|
+
i2_padded = { x: interactionPoint2.x + padding, y: interactionPoint2.y }
|
|
86
|
+
}
|
|
87
|
+
} else if (Math.abs(p2.x - (x + width)) < EPS && Math.abs(p2.y - y) < EPS) {
|
|
88
|
+
c2 = { x: x - padding, y: y + height + padding }
|
|
89
|
+
|
|
90
|
+
if (isVertical(p1, p2)) {
|
|
91
|
+
i1_padded = { x: interactionPoint1.x, y: interactionPoint1.y + padding }
|
|
92
|
+
} else {
|
|
93
|
+
// isHorizontal(p1, p2)
|
|
94
|
+
i1_padded = { x: interactionPoint1.x - padding, y: interactionPoint1.y }
|
|
95
|
+
}
|
|
96
|
+
if (isVertical(p2, p3)) {
|
|
97
|
+
i2_padded = { x: interactionPoint2.x, y: interactionPoint2.y + padding }
|
|
98
|
+
} else {
|
|
99
|
+
// isHorizontal(p2, p3)
|
|
100
|
+
i2_padded = { x: interactionPoint2.x - padding, y: interactionPoint2.y }
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
return []
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return [[i1_padded, c2, i2_padded]]
|
|
107
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
|
|
3
|
+
export interface Rectangle {
|
|
4
|
+
x: number
|
|
5
|
+
y: number
|
|
6
|
+
width: number
|
|
7
|
+
height: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface RectangleCandidate {
|
|
11
|
+
rect: Rectangle
|
|
12
|
+
i1: Point
|
|
13
|
+
i2: Point
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generates potential rectangular areas from two sets of intersection points.
|
|
18
|
+
* This function takes two arrays of points, typically representing intersections along two segments of an L-shape.
|
|
19
|
+
* It pairs up points from each array to form diagonals of potential rectangles.
|
|
20
|
+
* Only rectangles with a non-zero area are considered valid candidates.
|
|
21
|
+
*/
|
|
22
|
+
export const generateRectangleCandidates = (
|
|
23
|
+
intersections1: Point[],
|
|
24
|
+
intersections2: Point[],
|
|
25
|
+
): RectangleCandidate[] => {
|
|
26
|
+
const rectangleCandidates: RectangleCandidate[] = []
|
|
27
|
+
|
|
28
|
+
for (const p1 of intersections1) {
|
|
29
|
+
for (const p2 of intersections2) {
|
|
30
|
+
const minX = Math.min(p1.x, p2.x)
|
|
31
|
+
const minY = Math.min(p1.y, p2.y)
|
|
32
|
+
const maxX = Math.max(p1.x, p2.x)
|
|
33
|
+
const maxY = Math.max(p1.y, p2.y)
|
|
34
|
+
|
|
35
|
+
const width = maxX - minX
|
|
36
|
+
const height = maxY - minY
|
|
37
|
+
|
|
38
|
+
// Ensure the rectangle has a non-zero area
|
|
39
|
+
if (width > 1e-6 && height > 1e-6) {
|
|
40
|
+
rectangleCandidates.push({
|
|
41
|
+
rect: {
|
|
42
|
+
x: minX,
|
|
43
|
+
y: minY,
|
|
44
|
+
width,
|
|
45
|
+
height,
|
|
46
|
+
},
|
|
47
|
+
i1: p1,
|
|
48
|
+
i2: p2,
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return rectangleCandidates
|
|
55
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SolvedTracePath } from "../../SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
2
|
+
|
|
3
|
+
export interface TraceObstacle {
|
|
4
|
+
points: Array<{ x: number; y: number }>
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Extracts obstacles from a list of solved trace paths, excluding a specific trace.
|
|
9
|
+
* This function is used to treat other traces as obstacles when rerouting or cleaning up a particular trace.
|
|
10
|
+
* It returns an array of TraceObstacle objects, where each obstacle is represented by the points of a trace path.
|
|
11
|
+
*/
|
|
12
|
+
export const getTraceObstacles = (
|
|
13
|
+
allTraces: SolvedTracePath[],
|
|
14
|
+
excludeTraceId: string,
|
|
15
|
+
): TraceObstacle[] => {
|
|
16
|
+
const obstacles: TraceObstacle[] = []
|
|
17
|
+
|
|
18
|
+
for (const trace of allTraces) {
|
|
19
|
+
if (trace.mspPairId !== excludeTraceId) {
|
|
20
|
+
obstacles.push({ points: trace.tracePath })
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return obstacles
|
|
25
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
import type { SolvedTracePath } from "../../SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
3
|
+
import { getSegmentIntersection } from "@tscircuit/math-utils/line-intersections"
|
|
4
|
+
|
|
5
|
+
export type CollisionInfo = {
|
|
6
|
+
isColliding: boolean
|
|
7
|
+
collidingTraceId?: string
|
|
8
|
+
collisionPoint?: Point
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Checks if a given path collides with any other traces in a list of solved trace paths.
|
|
13
|
+
* It iterates through each segment of the input path and compares it against every segment
|
|
14
|
+
* of all other traces (excluding a specified trace to avoid self-collision checks).
|
|
15
|
+
* If an intersection is found between any segments, it indicates a collision.
|
|
16
|
+
*/
|
|
17
|
+
export const isPathColliding = (
|
|
18
|
+
path: Point[],
|
|
19
|
+
allTraces: SolvedTracePath[],
|
|
20
|
+
traceIdToExclude?: string,
|
|
21
|
+
): CollisionInfo => {
|
|
22
|
+
if (path.length < 2) {
|
|
23
|
+
return { isColliding: false }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
27
|
+
const pathSegP1 = path[i]
|
|
28
|
+
const pathSegQ1 = path[i + 1]
|
|
29
|
+
|
|
30
|
+
for (const existingTrace of allTraces) {
|
|
31
|
+
if (existingTrace.mspPairId === traceIdToExclude) {
|
|
32
|
+
continue // Skip self-collision check
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (let j = 0; j < existingTrace.tracePath.length - 1; j++) {
|
|
36
|
+
const existingSegP2 = existingTrace.tracePath[j]
|
|
37
|
+
const existingSegQ2 = existingTrace.tracePath[j + 1]
|
|
38
|
+
|
|
39
|
+
const intersectionPoint = getSegmentIntersection(
|
|
40
|
+
pathSegP1,
|
|
41
|
+
pathSegQ1,
|
|
42
|
+
existingSegP2,
|
|
43
|
+
existingSegQ2,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if (intersectionPoint) {
|
|
47
|
+
return {
|
|
48
|
+
isColliding: true,
|
|
49
|
+
collidingTraceId: existingTrace.mspPairId as string,
|
|
50
|
+
collisionPoint: intersectionPoint,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { isColliding: false } // No collision found
|
|
58
|
+
}
|