@tscircuit/schematic-trace-solver 0.0.44 → 0.0.45

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 (25) hide show
  1. package/dist/index.d.ts +14 -3
  2. package/dist/index.js +754 -95
  3. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +0 -1
  4. package/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts +83 -36
  5. package/lib/solvers/TraceCleanupSolver/hasCollisions.ts +16 -4
  6. package/lib/solvers/TraceCleanupSolver/is4PointRectangle.ts +17 -0
  7. package/lib/solvers/TraceCleanupSolver/isSegmentAnEndpointSegment.ts +36 -0
  8. package/lib/solvers/TraceCleanupSolver/mergeGraphicsObjects.ts +28 -0
  9. package/lib/solvers/TraceCleanupSolver/minimizeTurnsWithFilteredLabels.ts +18 -1
  10. package/lib/solvers/TraceCleanupSolver/recognizeStairStepPattern.ts +56 -0
  11. package/lib/solvers/TraceCleanupSolver/sub-solver/UntangleTraceSubsolver.ts +370 -0
  12. package/lib/solvers/TraceCleanupSolver/sub-solver/findAllLShapedTurns.ts +48 -0
  13. package/lib/solvers/TraceCleanupSolver/sub-solver/findIntersectionsWithObstacles.ts +36 -0
  14. package/lib/solvers/TraceCleanupSolver/sub-solver/generateLShapeRerouteCandidates.ts +107 -0
  15. package/lib/solvers/TraceCleanupSolver/sub-solver/generateRectangleCandidates.ts +55 -0
  16. package/lib/solvers/TraceCleanupSolver/sub-solver/getTraceObstacles.ts +25 -0
  17. package/lib/solvers/TraceCleanupSolver/sub-solver/isPathColliding.ts +58 -0
  18. package/lib/solvers/TraceCleanupSolver/sub-solver/visualizeCandidates.ts +33 -0
  19. package/lib/solvers/TraceCleanupSolver/sub-solver/visualizeCollision.ts +20 -0
  20. package/lib/solvers/TraceCleanupSolver/sub-solver/visualizeIntersectionPoints.ts +26 -0
  21. package/lib/solvers/TraceCleanupSolver/sub-solver/visualizeLSapes.ts +33 -0
  22. package/lib/solvers/TraceCleanupSolver/turnMinimization.ts +50 -56
  23. package/lib/solvers/TraceCleanupSolver/visualizeTightRectangle.ts +24 -0
  24. package/package.json +1 -1
  25. package/tests/examples/__snapshots__/example29.snap.svg +6 -6
@@ -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
+ }