@tscircuit/schematic-trace-solver 0.0.32 → 0.0.33
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 +24 -53
- package/dist/index.js +492 -831
- package/lib/index.ts +1 -0
- package/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts +54 -3
- package/lib/solvers/MspConnectionPairSolver/doesPairCrossRestrictedCenterLines.ts +62 -0
- package/lib/solvers/MspConnectionPairSolver/getMspConnectionPairsFromPins.ts +7 -2
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver.ts +3 -10
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +239 -0
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions.ts +57 -0
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/mid.ts +97 -0
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/pathOps.ts +65 -0
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect.ts +19 -0
- package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +14 -14
- package/package.json +1 -1
- package/site/examples/example09.page.tsx +1 -1
- package/tests/examples/__snapshots__/example01.snap.svg +29 -29
- package/tests/examples/__snapshots__/example02.snap.svg +13 -10
- package/tests/examples/__snapshots__/example04.snap.svg +12 -12
- package/tests/examples/__snapshots__/example05.snap.svg +38 -38
- package/tests/examples/__snapshots__/example06.snap.svg +14 -14
- package/tests/examples/__snapshots__/example08.snap.svg +29 -23
- package/tests/examples/__snapshots__/example09.snap.svg +119 -149
- package/tests/examples/__snapshots__/example11.snap.svg +39 -33
- package/tests/examples/__snapshots__/example12.snap.svg +32 -29
- package/tests/examples/__snapshots__/example13.snap.svg +87 -84
- package/tests/examples/__snapshots__/example15.snap.svg +57 -57
- package/tests/examples/__snapshots__/example16.snap.svg +3 -3
- package/tests/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver_repro03.test.ts +1 -0
package/lib/index.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
InputChip,
|
|
4
|
+
InputPin,
|
|
5
|
+
InputProblem,
|
|
6
|
+
PinId,
|
|
7
|
+
} from "lib/types/InputProblem"
|
|
3
8
|
import { ConnectivityMap } from "connectivity-map"
|
|
4
9
|
import { getConnectivityMapsFromInputProblem } from "./getConnectivityMapFromInputProblem"
|
|
5
10
|
import { getOrthogonalMinimumSpanningTree } from "./getMspConnectionPairsFromPins"
|
|
11
|
+
import { doesPairCrossRestrictedCenterLines } from "./doesPairCrossRestrictedCenterLines"
|
|
6
12
|
import type { GraphicsObject } from "graphics-debug"
|
|
7
13
|
import { getColorFromString } from "lib/utils/getColorFromString"
|
|
8
14
|
import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualizeInputProblem"
|
|
@@ -104,6 +110,22 @@ export class MspConnectionPairSolver extends BaseSolver {
|
|
|
104
110
|
// Too far apart; skip creating an MSP pair for this net
|
|
105
111
|
return
|
|
106
112
|
}
|
|
113
|
+
// Avoid pairs that would cross restricted center lines
|
|
114
|
+
const pinIdMap = new Map(Object.entries(this.pinMap)) as Map<
|
|
115
|
+
PinId,
|
|
116
|
+
InputPin & { chipId: string }
|
|
117
|
+
>
|
|
118
|
+
if (
|
|
119
|
+
doesPairCrossRestrictedCenterLines({
|
|
120
|
+
inputProblem: this.inputProblem,
|
|
121
|
+
chipMap: this.chipMap,
|
|
122
|
+
pinIdMap,
|
|
123
|
+
p1,
|
|
124
|
+
p2,
|
|
125
|
+
})
|
|
126
|
+
) {
|
|
127
|
+
return
|
|
128
|
+
}
|
|
107
129
|
|
|
108
130
|
const globalConnNetId = this.globalConnMap.getNetConnectedToId(pin1!)!
|
|
109
131
|
const userNetId =
|
|
@@ -122,12 +144,41 @@ export class MspConnectionPairSolver extends BaseSolver {
|
|
|
122
144
|
|
|
123
145
|
// There are more than 3 pins, so we need to run MSP to find the best pairs
|
|
124
146
|
|
|
147
|
+
const pinIdMap = new Map(Object.entries(this.pinMap)) as Map<
|
|
148
|
+
PinId,
|
|
149
|
+
InputPin & { chipId: string }
|
|
150
|
+
>
|
|
125
151
|
const msp = getOrthogonalMinimumSpanningTree(
|
|
126
152
|
directlyConnectedPins.map((p) => this.pinMap[p]!).filter(Boolean),
|
|
127
|
-
{
|
|
153
|
+
{
|
|
154
|
+
maxDistance: this.maxMspPairDistance,
|
|
155
|
+
forbidEdge: (a, b) =>
|
|
156
|
+
doesPairCrossRestrictedCenterLines({
|
|
157
|
+
inputProblem: this.inputProblem,
|
|
158
|
+
chipMap: this.chipMap,
|
|
159
|
+
pinIdMap,
|
|
160
|
+
p1: a as InputPin & { chipId: string },
|
|
161
|
+
p2: b as InputPin & { chipId: string },
|
|
162
|
+
}),
|
|
163
|
+
},
|
|
128
164
|
)
|
|
129
165
|
|
|
130
166
|
for (const [pin1, pin2] of msp) {
|
|
167
|
+
const p1Obj = this.pinMap[pin1!]!
|
|
168
|
+
const p2Obj = this.pinMap[pin2!]!
|
|
169
|
+
// Skip any edge that would cross a restricted center line (safety filter)
|
|
170
|
+
if (
|
|
171
|
+
doesPairCrossRestrictedCenterLines({
|
|
172
|
+
inputProblem: this.inputProblem,
|
|
173
|
+
chipMap: this.chipMap,
|
|
174
|
+
pinIdMap,
|
|
175
|
+
p1: p1Obj,
|
|
176
|
+
p2: p2Obj,
|
|
177
|
+
})
|
|
178
|
+
) {
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
|
|
131
182
|
const globalConnNetId = this.globalConnMap.getNetConnectedToId(pin1!)!
|
|
132
183
|
const userNetId =
|
|
133
184
|
this.userNetIdByPinId[pin1!] ?? this.userNetIdByPinId[pin2!]
|
|
@@ -136,7 +187,7 @@ export class MspConnectionPairSolver extends BaseSolver {
|
|
|
136
187
|
dcConnNetId: dcNetId,
|
|
137
188
|
globalConnNetId,
|
|
138
189
|
userNetId,
|
|
139
|
-
pins: [
|
|
190
|
+
pins: [p1Obj, p2Obj],
|
|
140
191
|
})
|
|
141
192
|
}
|
|
142
193
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
InputChip,
|
|
3
|
+
InputPin,
|
|
4
|
+
InputProblem,
|
|
5
|
+
PinId,
|
|
6
|
+
} from "lib/types/InputProblem"
|
|
7
|
+
import { getRestrictedCenterLines } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/getRestrictedCenterLines"
|
|
8
|
+
|
|
9
|
+
export const doesPairCrossRestrictedCenterLines = (params: {
|
|
10
|
+
inputProblem: InputProblem
|
|
11
|
+
chipMap: Record<string, InputChip>
|
|
12
|
+
pinIdMap: Map<PinId, InputPin & { chipId: string }>
|
|
13
|
+
p1: InputPin & { chipId: string }
|
|
14
|
+
p2: InputPin & { chipId: string }
|
|
15
|
+
}): boolean => {
|
|
16
|
+
const { inputProblem, chipMap, pinIdMap, p1, p2 } = params
|
|
17
|
+
|
|
18
|
+
const restrictedCenterLines = getRestrictedCenterLines({
|
|
19
|
+
pins: [p1, p2],
|
|
20
|
+
inputProblem,
|
|
21
|
+
pinIdMap,
|
|
22
|
+
chipMap,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
if (restrictedCenterLines.size === 0) return false
|
|
26
|
+
|
|
27
|
+
const EPS = 1e-9
|
|
28
|
+
|
|
29
|
+
const crossesSegment = (
|
|
30
|
+
a: { x: number; y: number },
|
|
31
|
+
b: { x: number; y: number },
|
|
32
|
+
): boolean => {
|
|
33
|
+
for (const [, rcl] of restrictedCenterLines) {
|
|
34
|
+
if (rcl.axes.has("x") && typeof rcl.x === "number") {
|
|
35
|
+
if ((a.x - rcl.x) * (b.x - rcl.x) < -EPS) return true
|
|
36
|
+
}
|
|
37
|
+
if (rcl.axes.has("y") && typeof rcl.y === "number") {
|
|
38
|
+
if ((a.y - rcl.y) * (b.y - rcl.y) < -EPS) return true
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// If already aligned on one axis, just check that single segment
|
|
45
|
+
if (Math.abs(p1.x - p2.x) < EPS || Math.abs(p1.y - p2.y) < EPS) {
|
|
46
|
+
return crossesSegment({ x: p1.x, y: p1.y }, { x: p2.x, y: p2.y })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Two L-shape possibilities: horizontal-then-vertical or vertical-then-horizontal
|
|
50
|
+
const elbowHV = { x: p2.x, y: p1.y }
|
|
51
|
+
const elbowVH = { x: p1.x, y: p2.y }
|
|
52
|
+
|
|
53
|
+
const hvCrosses =
|
|
54
|
+
crossesSegment({ x: p1.x, y: p1.y }, elbowHV) ||
|
|
55
|
+
crossesSegment(elbowHV, { x: p2.x, y: p2.y })
|
|
56
|
+
const vhCrosses =
|
|
57
|
+
crossesSegment({ x: p1.x, y: p1.y }, elbowVH) ||
|
|
58
|
+
crossesSegment(elbowVH, { x: p2.x, y: p2.y })
|
|
59
|
+
|
|
60
|
+
// Forbid the pair only if both L-shape routes would cross a restricted center line
|
|
61
|
+
return hvCrosses && vhCrosses
|
|
62
|
+
}
|
|
@@ -18,7 +18,10 @@ import type { InputPin, PinId } from "lib/types/InputProblem"
|
|
|
18
18
|
*/
|
|
19
19
|
export function getOrthogonalMinimumSpanningTree(
|
|
20
20
|
pins: InputPin[],
|
|
21
|
-
opts: {
|
|
21
|
+
opts: {
|
|
22
|
+
maxDistance?: number
|
|
23
|
+
forbidEdge?: (a: InputPin, b: InputPin) => boolean
|
|
24
|
+
} = {},
|
|
22
25
|
): Array<[PinId, PinId]> {
|
|
23
26
|
const n = pins.length
|
|
24
27
|
const maxDistance = opts?.maxDistance ?? Number.POSITIVE_INFINITY
|
|
@@ -82,7 +85,9 @@ export function getOrthogonalMinimumSpanningTree(
|
|
|
82
85
|
for (let v = 0; v < n; v++) {
|
|
83
86
|
if (!inTree[v]) {
|
|
84
87
|
const d0 = manhattan(pins[u], pins[v])
|
|
85
|
-
const
|
|
88
|
+
const isForbidden = opts?.forbidEdge?.(pins[u], pins[v]) ?? false
|
|
89
|
+
const d =
|
|
90
|
+
d0 > maxDistance || isForbidden ? Number.POSITIVE_INFINITY : d0
|
|
86
91
|
if (
|
|
87
92
|
d < bestDist[v] ||
|
|
88
93
|
(d === bestDist[v] && pins[u].pinId < pins[parent[v]]?.pinId)
|
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
MspConnectionPairId,
|
|
8
8
|
} from "../MspConnectionPairSolver/MspConnectionPairSolver"
|
|
9
9
|
import type { ConnectivityMap } from "connectivity-map"
|
|
10
|
-
import {
|
|
10
|
+
import { SchematicTraceSingleLineSolver2 } from "./SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2"
|
|
11
11
|
import type { Guideline } from "../GuidelinesSolver/GuidelinesSolver"
|
|
12
12
|
import { visualizeGuidelines } from "../GuidelinesSolver/visualizeGuidelines"
|
|
13
13
|
import type { Point } from "@tscircuit/math-utils"
|
|
@@ -20,7 +20,6 @@ export interface SolvedTracePath extends MspConnectionPair {
|
|
|
20
20
|
|
|
21
21
|
export class SchematicTraceLinesSolver extends BaseSolver {
|
|
22
22
|
inputProblem: InputProblem
|
|
23
|
-
guidelines: Guideline[]
|
|
24
23
|
mspConnectionPairs: MspConnectionPair[]
|
|
25
24
|
|
|
26
25
|
dcConnMap: ConnectivityMap
|
|
@@ -34,7 +33,7 @@ export class SchematicTraceLinesSolver extends BaseSolver {
|
|
|
34
33
|
solvedTracePaths: Array<SolvedTracePath> = []
|
|
35
34
|
failedConnectionPairs: Array<MspConnectionPair & { error?: string }> = []
|
|
36
35
|
|
|
37
|
-
declare activeSubSolver:
|
|
36
|
+
declare activeSubSolver: SchematicTraceSingleLineSolver2 | null
|
|
38
37
|
|
|
39
38
|
constructor(params: {
|
|
40
39
|
mspConnectionPairs: MspConnectionPair[]
|
|
@@ -42,14 +41,12 @@ export class SchematicTraceLinesSolver extends BaseSolver {
|
|
|
42
41
|
dcConnMap: ConnectivityMap
|
|
43
42
|
globalConnMap: ConnectivityMap
|
|
44
43
|
inputProblem: InputProblem
|
|
45
|
-
guidelines: Guideline[]
|
|
46
44
|
}) {
|
|
47
45
|
super()
|
|
48
46
|
this.inputProblem = params.inputProblem
|
|
49
47
|
this.mspConnectionPairs = params.mspConnectionPairs
|
|
50
48
|
this.dcConnMap = params.dcConnMap
|
|
51
49
|
this.globalConnMap = params.globalConnMap
|
|
52
|
-
this.guidelines = params.guidelines
|
|
53
50
|
this.chipMap = params.chipMap
|
|
54
51
|
|
|
55
52
|
this.queuedConnectionPairs = [...this.mspConnectionPairs]
|
|
@@ -64,7 +61,6 @@ export class SchematicTraceLinesSolver extends BaseSolver {
|
|
|
64
61
|
mspConnectionPairs: this.mspConnectionPairs,
|
|
65
62
|
dcConnMap: this.dcConnMap,
|
|
66
63
|
globalConnMap: this.globalConnMap,
|
|
67
|
-
guidelines: this.guidelines,
|
|
68
64
|
}
|
|
69
65
|
}
|
|
70
66
|
|
|
@@ -111,11 +107,10 @@ export class SchematicTraceLinesSolver extends BaseSolver {
|
|
|
111
107
|
|
|
112
108
|
const { pins } = connectionPair
|
|
113
109
|
|
|
114
|
-
this.activeSubSolver = new
|
|
110
|
+
this.activeSubSolver = new SchematicTraceSingleLineSolver2({
|
|
115
111
|
inputProblem: this.inputProblem,
|
|
116
112
|
pins,
|
|
117
113
|
chipMap: this.chipMap,
|
|
118
|
-
guidelines: this.guidelines,
|
|
119
114
|
})
|
|
120
115
|
}
|
|
121
116
|
|
|
@@ -128,8 +123,6 @@ export class SchematicTraceLinesSolver extends BaseSolver {
|
|
|
128
123
|
connectionAlpha: 0.1,
|
|
129
124
|
})
|
|
130
125
|
|
|
131
|
-
visualizeGuidelines({ guidelines: this.guidelines, graphics })
|
|
132
|
-
|
|
133
126
|
for (const { mspPairId, tracePath } of this.solvedTracePaths) {
|
|
134
127
|
graphics.lines!.push({
|
|
135
128
|
points: tracePath,
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import type { GraphicsObject } from "graphics-debug"
|
|
2
|
+
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
|
|
3
|
+
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
4
|
+
import type { MspConnectionPair } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver"
|
|
5
|
+
import type {
|
|
6
|
+
ChipId,
|
|
7
|
+
InputChip,
|
|
8
|
+
InputProblem,
|
|
9
|
+
PinId,
|
|
10
|
+
} from "lib/types/InputProblem"
|
|
11
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
12
|
+
import { calculateElbow } from "calculate-elbow"
|
|
13
|
+
import { getPinDirection } from "../SchematicTraceSingleLineSolver/getPinDirection"
|
|
14
|
+
import { getObstacleRects, type ChipWithBounds } from "./rect"
|
|
15
|
+
import { findFirstCollision, isHorizontal, isVertical } from "./collisions"
|
|
16
|
+
import {
|
|
17
|
+
aabbFromPoints,
|
|
18
|
+
candidateMidsFromSet,
|
|
19
|
+
midBetweenPointAndRect,
|
|
20
|
+
type Axis,
|
|
21
|
+
} from "./mid"
|
|
22
|
+
import { pathKey, shiftSegmentOrth } from "./pathOps"
|
|
23
|
+
|
|
24
|
+
type PathKey = string
|
|
25
|
+
|
|
26
|
+
export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
27
|
+
pins: MspConnectionPair["pins"]
|
|
28
|
+
inputProblem: InputProblem
|
|
29
|
+
chipMap: Record<string, InputChip>
|
|
30
|
+
|
|
31
|
+
obstacles: ChipWithBounds[]
|
|
32
|
+
rectById: Map<string, ChipWithBounds>
|
|
33
|
+
aabb: { minX: number; maxX: number; minY: number; maxY: number }
|
|
34
|
+
|
|
35
|
+
baseElbow: Point[]
|
|
36
|
+
|
|
37
|
+
solvedTracePath: Point[] | null = null
|
|
38
|
+
|
|
39
|
+
private queue: Array<{ path: Point[]; collisionChipIds: Set<ChipId> }> = []
|
|
40
|
+
private visited: Set<PathKey> = new Set()
|
|
41
|
+
|
|
42
|
+
constructor(params: {
|
|
43
|
+
pins: MspConnectionPair["pins"]
|
|
44
|
+
inputProblem: InputProblem
|
|
45
|
+
chipMap: Record<string, InputChip>
|
|
46
|
+
}) {
|
|
47
|
+
super()
|
|
48
|
+
this.pins = params.pins
|
|
49
|
+
this.inputProblem = params.inputProblem
|
|
50
|
+
this.chipMap = params.chipMap
|
|
51
|
+
|
|
52
|
+
// Ensure facing directions are present
|
|
53
|
+
for (const pin of this.pins) {
|
|
54
|
+
if (!pin._facingDirection) {
|
|
55
|
+
const chip = this.chipMap[pin.chipId]
|
|
56
|
+
pin._facingDirection = getPinDirection(pin, chip)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Build obstacle rects from chips
|
|
61
|
+
this.obstacles = getObstacleRects(this.inputProblem)
|
|
62
|
+
this.rectById = new Map(this.obstacles.map((r) => [r.chipId, r]))
|
|
63
|
+
|
|
64
|
+
// Build initial elbow path
|
|
65
|
+
const [pin1, pin2] = this.pins
|
|
66
|
+
this.baseElbow = calculateElbow(
|
|
67
|
+
{
|
|
68
|
+
x: pin1.x,
|
|
69
|
+
y: pin1.y,
|
|
70
|
+
facingDirection: pin1._facingDirection!,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
x: pin2.x,
|
|
74
|
+
y: pin2.y,
|
|
75
|
+
facingDirection: pin2._facingDirection!,
|
|
76
|
+
},
|
|
77
|
+
{ overshoot: 0.2 },
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
// Bounds defined by PA and PB
|
|
81
|
+
this.aabb = aabbFromPoints(
|
|
82
|
+
{ x: pin1.x, y: pin1.y },
|
|
83
|
+
{ x: pin2.x, y: pin2.y },
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
// Seed search
|
|
87
|
+
this.queue.push({ path: this.baseElbow, collisionChipIds: new Set() })
|
|
88
|
+
this.visited.add(pathKey(this.baseElbow))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
override getConstructorParams(): ConstructorParameters<
|
|
92
|
+
typeof SchematicTraceSingleLineSolver2
|
|
93
|
+
>[0] {
|
|
94
|
+
return {
|
|
95
|
+
chipMap: this.chipMap,
|
|
96
|
+
pins: this.pins,
|
|
97
|
+
inputProblem: this.inputProblem,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private axisOfSegment(a: Point, b: Point): Axis | null {
|
|
102
|
+
if (isVertical(a, b)) return "x"
|
|
103
|
+
if (isHorizontal(a, b)) return "y"
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private pathLength(pts: Point[]): number {
|
|
108
|
+
let sum = 0
|
|
109
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
110
|
+
sum +=
|
|
111
|
+
Math.abs(pts[i + 1]!.x - pts[i]!.x) +
|
|
112
|
+
Math.abs(pts[i + 1]!.y - pts[i]!.y)
|
|
113
|
+
}
|
|
114
|
+
return sum
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
override _step() {
|
|
118
|
+
if (this.solvedTracePath) {
|
|
119
|
+
this.solved = true
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const state = this.queue.shift()
|
|
124
|
+
if (!state) {
|
|
125
|
+
this.failed = true
|
|
126
|
+
this.error = "No collision-free path found"
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const { path, collisionChipIds } = state
|
|
131
|
+
|
|
132
|
+
const collision = findFirstCollision(path, this.obstacles)
|
|
133
|
+
|
|
134
|
+
if (!collision) {
|
|
135
|
+
this.solvedTracePath = path
|
|
136
|
+
this.solved = true
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let { segIndex, rect } = collision
|
|
141
|
+
|
|
142
|
+
// Never move the first or last segments - move adjacent segment instead
|
|
143
|
+
const isFirstSegment = segIndex === 0
|
|
144
|
+
const isLastSegment = segIndex === path.length - 2
|
|
145
|
+
|
|
146
|
+
if (isFirstSegment) {
|
|
147
|
+
// If first segment collides, move the second segment instead
|
|
148
|
+
if (path.length < 3) {
|
|
149
|
+
// Path too short to have an adjacent segment to move
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
segIndex = 1
|
|
153
|
+
} else if (isLastSegment) {
|
|
154
|
+
// If last segment collides, move the second-to-last segment instead
|
|
155
|
+
if (path.length < 3) {
|
|
156
|
+
// Path too short to have an adjacent segment to move
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
segIndex = path.length - 3
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const a = path[segIndex]!
|
|
163
|
+
const b = path[segIndex + 1]!
|
|
164
|
+
const axis = this.axisOfSegment(a, b)
|
|
165
|
+
if (!axis) {
|
|
166
|
+
// Should not happen for elbow paths
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const [PA, PB] = this.pins
|
|
171
|
+
const candidates: number[] = []
|
|
172
|
+
|
|
173
|
+
if (collisionChipIds.size === 0) {
|
|
174
|
+
// First collision on this search branch: use mid(PA, C) and mid(PB, C)
|
|
175
|
+
const m1 = midBetweenPointAndRect(axis, { x: PA.x, y: PA.y }, rect)
|
|
176
|
+
const m2 = midBetweenPointAndRect(axis, { x: PB.x, y: PB.y }, rect)
|
|
177
|
+
|
|
178
|
+
// Combine and deduplicate candidates
|
|
179
|
+
const allCandidates = [...m1, ...m2]
|
|
180
|
+
const uniqueCandidates = [...new Set(allCandidates)]
|
|
181
|
+
candidates.push(...uniqueCandidates)
|
|
182
|
+
} else {
|
|
183
|
+
// Subsequent collisions: mid between C and nearest rect/bounds from the set
|
|
184
|
+
const mids = candidateMidsFromSet(
|
|
185
|
+
axis,
|
|
186
|
+
rect,
|
|
187
|
+
this.rectById,
|
|
188
|
+
collisionChipIds,
|
|
189
|
+
this.aabb,
|
|
190
|
+
)
|
|
191
|
+
candidates.push(...mids)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Generate new shifted paths, order by total path length (shorter first)
|
|
195
|
+
const newStates: Array<{
|
|
196
|
+
path: Point[]
|
|
197
|
+
collisionRectIds: Set<string>
|
|
198
|
+
len: number
|
|
199
|
+
}> = []
|
|
200
|
+
|
|
201
|
+
for (const coord of candidates) {
|
|
202
|
+
const newPath = shiftSegmentOrth(path, segIndex, axis, coord)
|
|
203
|
+
if (!newPath) continue
|
|
204
|
+
const key = pathKey(newPath)
|
|
205
|
+
if (this.visited.has(key)) continue
|
|
206
|
+
this.visited.add(key)
|
|
207
|
+
const nextSet = new Set(collisionChipIds)
|
|
208
|
+
nextSet.add(rect.chipId)
|
|
209
|
+
const len = this.pathLength(newPath)
|
|
210
|
+
newStates.push({ path: newPath, collisionRectIds: nextSet, len })
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
newStates.sort((a, b) => a.len - b.len)
|
|
214
|
+
for (const st of newStates) {
|
|
215
|
+
this.queue.push({ path: st.path, collisionChipIds: st.collisionRectIds })
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
override visualize(): GraphicsObject {
|
|
220
|
+
const g = visualizeInputProblem(this.inputProblem, {
|
|
221
|
+
chipAlpha: 0.1,
|
|
222
|
+
connectionAlpha: 0.1,
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
// Draw all the new candidates
|
|
226
|
+
for (const { path, collisionChipIds: collisionRectIds } of this.queue) {
|
|
227
|
+
g.lines!.push({ points: path, strokeColor: "teal", strokeDash: "2 2" })
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (this.solvedTracePath) {
|
|
231
|
+
g.lines!.push({ points: this.solvedTracePath, strokeColor: "green" })
|
|
232
|
+
} else if (this.queue.length > 0) {
|
|
233
|
+
// Show next candidate to be explored
|
|
234
|
+
g.lines!.push({ points: this.queue[0]!.path, strokeColor: "orange" })
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return g
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
import type { ChipWithBounds } from "./rect"
|
|
3
|
+
|
|
4
|
+
const EPS = 1e-9
|
|
5
|
+
|
|
6
|
+
export const isVertical = (a: Point, b: Point, eps = EPS) =>
|
|
7
|
+
Math.abs(a.x - b.x) < eps
|
|
8
|
+
export const isHorizontal = (a: Point, b: Point, eps = EPS) =>
|
|
9
|
+
Math.abs(a.y - b.y) < eps
|
|
10
|
+
|
|
11
|
+
export const segmentIntersectsRect = (
|
|
12
|
+
a: Point,
|
|
13
|
+
b: Point,
|
|
14
|
+
r: ChipWithBounds,
|
|
15
|
+
eps = EPS,
|
|
16
|
+
): boolean => {
|
|
17
|
+
const vert = isVertical(a, b, eps)
|
|
18
|
+
const horz = isHorizontal(a, b, eps)
|
|
19
|
+
if (!vert && !horz) return false
|
|
20
|
+
|
|
21
|
+
if (vert) {
|
|
22
|
+
const x = a.x
|
|
23
|
+
if (x < r.minX - eps || x > r.maxX + eps) return false
|
|
24
|
+
const segMinY = Math.min(a.y, b.y)
|
|
25
|
+
const segMaxY = Math.max(a.y, b.y)
|
|
26
|
+
const overlap = Math.min(segMaxY, r.maxY) - Math.max(segMinY, r.minY)
|
|
27
|
+
return overlap > eps
|
|
28
|
+
} else {
|
|
29
|
+
const y = a.y
|
|
30
|
+
if (y < r.minY - eps || y > r.maxY + eps) return false
|
|
31
|
+
const segMinX = Math.min(a.x, b.x)
|
|
32
|
+
const segMaxX = Math.max(a.x, b.x)
|
|
33
|
+
const overlap = Math.min(segMaxX, r.maxX) - Math.max(segMinX, r.minX)
|
|
34
|
+
return overlap > eps
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const findFirstCollision = (
|
|
39
|
+
pts: Point[],
|
|
40
|
+
rects: ChipWithBounds[],
|
|
41
|
+
opts: {
|
|
42
|
+
excludeRectIdsForSegment?: (segIndex: number) => Set<string>
|
|
43
|
+
} = {},
|
|
44
|
+
): { segIndex: number; rect: ChipWithBounds } | null => {
|
|
45
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
46
|
+
const a = pts[i]!
|
|
47
|
+
const b = pts[i + 1]!
|
|
48
|
+
const excluded = opts.excludeRectIdsForSegment?.(i) ?? new Set<string>()
|
|
49
|
+
for (const r of rects) {
|
|
50
|
+
if (excluded.has(r.chipId)) continue
|
|
51
|
+
if (segmentIntersectsRect(a, b, r)) {
|
|
52
|
+
return { segIndex: i, rect: r }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
import type { ChipWithBounds } from "./rect"
|
|
3
|
+
|
|
4
|
+
const EPS = 1e-9
|
|
5
|
+
|
|
6
|
+
export type Axis = "x" | "y"
|
|
7
|
+
|
|
8
|
+
export const aabbFromPoints = (a: Point, b: Point) => ({
|
|
9
|
+
minX: Math.min(a.x, b.x),
|
|
10
|
+
maxX: Math.max(a.x, b.x),
|
|
11
|
+
minY: Math.min(a.y, b.y),
|
|
12
|
+
maxY: Math.max(a.y, b.y),
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export const midBetweenPointAndRect = (
|
|
16
|
+
axis: Axis,
|
|
17
|
+
p: Point,
|
|
18
|
+
r: ChipWithBounds,
|
|
19
|
+
eps = EPS,
|
|
20
|
+
): number[] => {
|
|
21
|
+
if (axis === "x") {
|
|
22
|
+
if (p.x < r.minX - eps) {
|
|
23
|
+
return [(p.x + r.minX) / 2]
|
|
24
|
+
}
|
|
25
|
+
if (p.x > r.maxX + eps) {
|
|
26
|
+
return [(p.x + r.maxX) / 2]
|
|
27
|
+
}
|
|
28
|
+
// Point is within rect bounds on this axis - generate candidates on both sides
|
|
29
|
+
return [r.minX - 0.2, r.maxX + 0.2]
|
|
30
|
+
} else {
|
|
31
|
+
if (p.y < r.minY - eps) {
|
|
32
|
+
return [(p.y + r.minY) / 2]
|
|
33
|
+
}
|
|
34
|
+
if (p.y > r.maxY + eps) {
|
|
35
|
+
return [(p.y + r.maxY) / 2]
|
|
36
|
+
}
|
|
37
|
+
// Point is within rect bounds on this axis - generate candidates on both sides
|
|
38
|
+
return [r.minY - 0.2, r.maxY + 0.2]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const candidateMidsFromSet = (
|
|
43
|
+
axis: Axis,
|
|
44
|
+
colliding: ChipWithBounds,
|
|
45
|
+
rectsById: Map<string, ChipWithBounds>,
|
|
46
|
+
collisionRectIds: Set<string>,
|
|
47
|
+
aabb: { minX: number; maxX: number; minY: number; maxY: number },
|
|
48
|
+
eps = EPS,
|
|
49
|
+
): number[] => {
|
|
50
|
+
const setRects = [...collisionRectIds]
|
|
51
|
+
.map((id) => rectsById.get(id))
|
|
52
|
+
.filter((r): r is ChipWithBounds => !!r)
|
|
53
|
+
|
|
54
|
+
if (axis === "x") {
|
|
55
|
+
const leftBoundaries = [aabb.minX, ...setRects.map((r) => r.maxX)].filter(
|
|
56
|
+
(v) => v < colliding.minX - eps,
|
|
57
|
+
)
|
|
58
|
+
const rightBoundaries = [aabb.maxX, ...setRects.map((r) => r.minX)].filter(
|
|
59
|
+
(v) => v > colliding.maxX + eps,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const leftNeighbor =
|
|
63
|
+
leftBoundaries.length > 0 ? Math.max(...leftBoundaries) : undefined
|
|
64
|
+
const rightNeighbor =
|
|
65
|
+
rightBoundaries.length > 0 ? Math.min(...rightBoundaries) : undefined
|
|
66
|
+
|
|
67
|
+
const out: number[] = []
|
|
68
|
+
if (leftNeighbor !== undefined) {
|
|
69
|
+
out.push((leftNeighbor + colliding.minX) / 2)
|
|
70
|
+
}
|
|
71
|
+
if (rightNeighbor !== undefined) {
|
|
72
|
+
out.push((colliding.maxX + rightNeighbor) / 2)
|
|
73
|
+
}
|
|
74
|
+
return out
|
|
75
|
+
} else {
|
|
76
|
+
const bottomBoundaries = [aabb.minY, ...setRects.map((r) => r.maxY)].filter(
|
|
77
|
+
(v) => v < colliding.minY - eps,
|
|
78
|
+
)
|
|
79
|
+
const topBoundaries = [aabb.maxY, ...setRects.map((r) => r.minY)].filter(
|
|
80
|
+
(v) => v > colliding.maxY + eps,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const bottomNeighbor =
|
|
84
|
+
bottomBoundaries.length > 0 ? Math.max(...bottomBoundaries) : undefined
|
|
85
|
+
const topNeighbor =
|
|
86
|
+
topBoundaries.length > 0 ? Math.min(...topBoundaries) : undefined
|
|
87
|
+
|
|
88
|
+
const out: number[] = []
|
|
89
|
+
if (bottomNeighbor !== undefined) {
|
|
90
|
+
out.push((bottomNeighbor + colliding.minY) / 2)
|
|
91
|
+
}
|
|
92
|
+
if (topNeighbor !== undefined) {
|
|
93
|
+
out.push((colliding.maxY + topNeighbor) / 2)
|
|
94
|
+
}
|
|
95
|
+
return out
|
|
96
|
+
}
|
|
97
|
+
}
|