@tscircuit/schematic-trace-solver 0.0.77 → 0.0.78
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/ISSUE_TEMPLATE/json-bug-report.yml +24 -0
- package/.github/scripts/import-json-bug-report.ts +483 -0
- package/.github/workflows/json-bug-report.yml +154 -0
- package/dist/index.d.ts +32 -10
- package/dist/index.js +216 -74
- package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +4 -0
- package/lib/solvers/AvailableNetOrientationSolver/types.ts +1 -0
- package/lib/solvers/NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver.ts +6 -0
- package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/SingleNetLabelPlacementSolver.ts +21 -1
- package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/SingleNetLabelPlacementSolver_visualize.ts +9 -3
- package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/solvePortOnlyPin.ts +27 -2
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver.ts +1 -0
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +121 -30
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions.ts +10 -10
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/mid.ts +5 -8
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect.ts +31 -6
- package/lib/solvers/TraceAnchoredNetLabelOverlapSolver/TraceAnchoredNetLabelOverlapSolver.ts +3 -0
- package/lib/solvers/TraceAnchoredNetLabelOverlapSolver/types.ts +1 -0
- package/lib/solvers/VccNetLabelCornerPlacementSolver/VccNetLabelCornerPlacementSolver.ts +3 -0
- package/lib/solvers/VccNetLabelCornerPlacementSolver/types.ts +1 -0
- package/lib/utils/textBoxBounds.ts +40 -0
- package/package.json +1 -1
- package/tests/repros/__snapshots__/manufacturePartNumber-text-box.snap.svg +12 -12
|
@@ -19,6 +19,7 @@ import { chooseHostTraceForGroup } from "./host"
|
|
|
19
19
|
import { anchorsForSegment } from "./anchors"
|
|
20
20
|
import { solveNetLabelPlacementForPortOnlyPin } from "./solvePortOnlyPin"
|
|
21
21
|
import { visualizeSingleNetLabelPlacementSolver } from "./SingleNetLabelPlacementSolver_visualize"
|
|
22
|
+
import { rectIntersectsAnyTextBox } from "lib/utils/textBoxBounds"
|
|
22
23
|
|
|
23
24
|
export {
|
|
24
25
|
NET_LABEL_HORIZONTAL_WIDTH,
|
|
@@ -70,7 +71,12 @@ export class SingleNetLabelPlacementSolver extends BaseSolver {
|
|
|
70
71
|
bounds: { minX: number; minY: number; maxX: number; maxY: number }
|
|
71
72
|
anchor: { x: number; y: number }
|
|
72
73
|
orientation: FacingDirection
|
|
73
|
-
status:
|
|
74
|
+
status:
|
|
75
|
+
| "ok"
|
|
76
|
+
| "chip-collision"
|
|
77
|
+
| "trace-collision"
|
|
78
|
+
| "text-collision"
|
|
79
|
+
| "parallel-to-segment"
|
|
74
80
|
hostSegIndex: number
|
|
75
81
|
}> = []
|
|
76
82
|
|
|
@@ -276,6 +282,20 @@ export class SingleNetLabelPlacementSolver extends BaseSolver {
|
|
|
276
282
|
continue
|
|
277
283
|
}
|
|
278
284
|
|
|
285
|
+
if (rectIntersectsAnyTextBox(bounds, this.inputProblem)) {
|
|
286
|
+
this.testedCandidates.push({
|
|
287
|
+
center: testCenter,
|
|
288
|
+
width,
|
|
289
|
+
height,
|
|
290
|
+
bounds,
|
|
291
|
+
anchor,
|
|
292
|
+
orientation,
|
|
293
|
+
status: "text-collision",
|
|
294
|
+
hostSegIndex: si,
|
|
295
|
+
})
|
|
296
|
+
continue
|
|
297
|
+
}
|
|
298
|
+
|
|
279
299
|
// Trace collision check (ignore the host segment)
|
|
280
300
|
const traceIntersectionResult = rectIntersectsAnyTrace(
|
|
281
301
|
bounds,
|
|
@@ -38,7 +38,9 @@ export function visualizeSingleNetLabelPlacementSolver(
|
|
|
38
38
|
? "rgba(220, 0, 0, 0.25)"
|
|
39
39
|
: c.status === "trace-collision"
|
|
40
40
|
? "rgba(220, 140, 0, 0.25)"
|
|
41
|
-
:
|
|
41
|
+
: c.status === "text-collision"
|
|
42
|
+
? "rgba(160, 0, 220, 0.2)"
|
|
43
|
+
: "rgba(120, 120, 120, 0.15)"
|
|
42
44
|
const stroke =
|
|
43
45
|
c.status === "ok"
|
|
44
46
|
? "green"
|
|
@@ -46,7 +48,9 @@ export function visualizeSingleNetLabelPlacementSolver(
|
|
|
46
48
|
? "red"
|
|
47
49
|
: c.status === "trace-collision"
|
|
48
50
|
? "orange"
|
|
49
|
-
: "
|
|
51
|
+
: c.status === "text-collision"
|
|
52
|
+
? "purple"
|
|
53
|
+
: "gray"
|
|
50
54
|
const candidateLabel =
|
|
51
55
|
c.status === "ok"
|
|
52
56
|
? "status: ok(valid net label candidate)"
|
|
@@ -54,7 +58,9 @@ export function visualizeSingleNetLabelPlacementSolver(
|
|
|
54
58
|
? "status: chip-collision"
|
|
55
59
|
: c.status === "trace-collision"
|
|
56
60
|
? "status: trace-collision"
|
|
57
|
-
:
|
|
61
|
+
: c.status === "text-collision"
|
|
62
|
+
? "status: text-collision"
|
|
63
|
+
: "status: parallel-to-segment"
|
|
58
64
|
|
|
59
65
|
graphics.rects!.push({
|
|
60
66
|
center: {
|
package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/solvePortOnlyPin.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
getRectBounds,
|
|
15
15
|
} from "./geometry"
|
|
16
16
|
import { rectIntersectsAnyTrace } from "./collisions"
|
|
17
|
+
import { rectIntersectsAnyTextBox } from "lib/utils/textBoxBounds"
|
|
17
18
|
|
|
18
19
|
export function solveNetLabelPlacementForPortOnlyPin(params: {
|
|
19
20
|
inputProblem: InputProblem
|
|
@@ -32,7 +33,12 @@ export function solveNetLabelPlacementForPortOnlyPin(params: {
|
|
|
32
33
|
bounds: { minX: number; minY: number; maxX: number; maxY: number }
|
|
33
34
|
anchor: { x: number; y: number }
|
|
34
35
|
orientation: FacingDirection
|
|
35
|
-
status:
|
|
36
|
+
status:
|
|
37
|
+
| "ok"
|
|
38
|
+
| "chip-collision"
|
|
39
|
+
| "trace-collision"
|
|
40
|
+
| "text-collision"
|
|
41
|
+
| "parallel-to-segment"
|
|
36
42
|
hostSegIndex: number
|
|
37
43
|
}>
|
|
38
44
|
error?: string
|
|
@@ -97,7 +103,12 @@ export function solveNetLabelPlacementForPortOnlyPin(params: {
|
|
|
97
103
|
bounds: { minX: number; minY: number; maxX: number; maxY: number }
|
|
98
104
|
anchor: { x: number; y: number }
|
|
99
105
|
orientation: FacingDirection
|
|
100
|
-
status:
|
|
106
|
+
status:
|
|
107
|
+
| "ok"
|
|
108
|
+
| "chip-collision"
|
|
109
|
+
| "trace-collision"
|
|
110
|
+
| "text-collision"
|
|
111
|
+
| "parallel-to-segment"
|
|
101
112
|
hostSegIndex: number
|
|
102
113
|
}> = []
|
|
103
114
|
|
|
@@ -133,6 +144,20 @@ export function solveNetLabelPlacementForPortOnlyPin(params: {
|
|
|
133
144
|
continue
|
|
134
145
|
}
|
|
135
146
|
|
|
147
|
+
if (rectIntersectsAnyTextBox(bounds, inputProblem)) {
|
|
148
|
+
testedCandidates.push({
|
|
149
|
+
center,
|
|
150
|
+
width,
|
|
151
|
+
height,
|
|
152
|
+
bounds,
|
|
153
|
+
anchor,
|
|
154
|
+
orientation,
|
|
155
|
+
status: "text-collision",
|
|
156
|
+
hostSegIndex: -1,
|
|
157
|
+
})
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
|
|
136
161
|
// Trace collision check
|
|
137
162
|
const traceIntersectionResult = rectIntersectsAnyTrace(
|
|
138
163
|
bounds,
|
|
@@ -2,16 +2,11 @@ import type { GraphicsObject } from "graphics-debug"
|
|
|
2
2
|
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
|
|
3
3
|
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
4
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"
|
|
5
|
+
import type { InputChip, InputProblem, PinId } from "lib/types/InputProblem"
|
|
11
6
|
import type { Point } from "@tscircuit/math-utils"
|
|
12
7
|
import { calculateElbow } from "calculate-elbow"
|
|
13
8
|
import { getPinDirection } from "../SchematicTraceSingleLineSolver/getPinDirection"
|
|
14
|
-
import { getObstacleRects, type
|
|
9
|
+
import { getObstacleRects, type ObstacleRect } from "./rect"
|
|
15
10
|
import { findFirstCollision, isHorizontal, isVertical } from "./collisions"
|
|
16
11
|
import {
|
|
17
12
|
aabbFromPoints,
|
|
@@ -20,32 +15,39 @@ import {
|
|
|
20
15
|
type Axis,
|
|
21
16
|
} from "./mid"
|
|
22
17
|
import { pathKey, shiftSegmentOrth } from "./pathOps"
|
|
18
|
+
import type { FacingDirection } from "lib/utils/dir"
|
|
19
|
+
import { getDimsForOrientation } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
|
|
20
|
+
import type { RectPadding } from "lib/utils/textBoxBounds"
|
|
23
21
|
|
|
24
22
|
type PathKey = string
|
|
25
23
|
|
|
26
24
|
export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
27
25
|
pins: MspConnectionPair["pins"]
|
|
26
|
+
connectionPair?: MspConnectionPair
|
|
28
27
|
inputProblem: InputProblem
|
|
29
28
|
chipMap: Record<string, InputChip>
|
|
30
29
|
|
|
31
|
-
obstacles:
|
|
32
|
-
|
|
30
|
+
obstacles: ObstacleRect[]
|
|
31
|
+
textObstacles: Set<ObstacleRect>
|
|
33
32
|
aabb: { minX: number; maxX: number; minY: number; maxY: number }
|
|
34
33
|
|
|
35
34
|
baseElbow: Point[]
|
|
36
35
|
|
|
37
36
|
solvedTracePath: Point[] | null = null
|
|
38
37
|
|
|
39
|
-
private queue: Array<{ path: Point[];
|
|
38
|
+
private queue: Array<{ path: Point[]; collisionRects: Set<ObstacleRect> }> =
|
|
39
|
+
[]
|
|
40
40
|
private visited: Set<PathKey> = new Set()
|
|
41
41
|
|
|
42
42
|
constructor(params: {
|
|
43
43
|
pins: MspConnectionPair["pins"]
|
|
44
|
+
connectionPair?: MspConnectionPair
|
|
44
45
|
inputProblem: InputProblem
|
|
45
46
|
chipMap: Record<string, InputChip>
|
|
46
47
|
}) {
|
|
47
48
|
super()
|
|
48
49
|
this.pins = params.pins
|
|
50
|
+
this.connectionPair = params.connectionPair
|
|
49
51
|
this.inputProblem = params.inputProblem
|
|
50
52
|
this.chipMap = params.chipMap
|
|
51
53
|
|
|
@@ -57,9 +59,14 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
// Build obstacle rects from chips
|
|
61
|
-
this
|
|
62
|
-
this.
|
|
62
|
+
// Build obstacle rects from chips and schematic text boxes. Text boxes are
|
|
63
|
+
// padded by the label footprint for this net so labels have clearance too.
|
|
64
|
+
this.obstacles = getObstacleRects(this.inputProblem, {
|
|
65
|
+
textBoxPadding: this.getTextBoxPaddingForConnectionPair(),
|
|
66
|
+
})
|
|
67
|
+
this.textObstacles = new Set(
|
|
68
|
+
this.obstacles.filter((r) => r.kind === "text_box"),
|
|
69
|
+
)
|
|
63
70
|
|
|
64
71
|
// Build initial elbow path
|
|
65
72
|
const [pin1, pin2] = this.pins
|
|
@@ -84,7 +91,7 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
84
91
|
)
|
|
85
92
|
|
|
86
93
|
// Seed search
|
|
87
|
-
this.queue.push({ path: this.baseElbow,
|
|
94
|
+
this.queue.push({ path: this.baseElbow, collisionRects: new Set() })
|
|
88
95
|
this.visited.add(pathKey(this.baseElbow))
|
|
89
96
|
}
|
|
90
97
|
|
|
@@ -94,10 +101,92 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
94
101
|
return {
|
|
95
102
|
chipMap: this.chipMap,
|
|
96
103
|
pins: this.pins,
|
|
104
|
+
connectionPair: this.connectionPair,
|
|
97
105
|
inputProblem: this.inputProblem,
|
|
98
106
|
}
|
|
99
107
|
}
|
|
100
108
|
|
|
109
|
+
private getTextBoxPaddingForConnectionPair(): RectPadding {
|
|
110
|
+
if (!this.inputProblem.textBoxes?.length) return {}
|
|
111
|
+
|
|
112
|
+
const netId = this.connectionPair?.userNetId
|
|
113
|
+
if (!netId) return {}
|
|
114
|
+
|
|
115
|
+
const orientations =
|
|
116
|
+
this.inputProblem.availableNetLabelOrientations[netId] ??
|
|
117
|
+
(["x+", "x-", "y+", "y-"] as FacingDirection[])
|
|
118
|
+
const netLabelWidth = this.getNetLabelWidthForConnectionPair(netId)
|
|
119
|
+
const netLabelHeight = this.getNetLabelHeightForConnectionPair(netId)
|
|
120
|
+
const padding: Required<RectPadding> = {
|
|
121
|
+
minX: 0,
|
|
122
|
+
minY: 0,
|
|
123
|
+
maxX: 0,
|
|
124
|
+
maxY: 0,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const orientation of orientations) {
|
|
128
|
+
const { width, height } = getDimsForOrientation({
|
|
129
|
+
orientation,
|
|
130
|
+
netLabelWidth,
|
|
131
|
+
netLabelHeight,
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
if (orientation === "y+" || orientation === "y-") {
|
|
135
|
+
padding.minX = Math.max(padding.minX, width / 2)
|
|
136
|
+
padding.maxX = Math.max(padding.maxX, width / 2)
|
|
137
|
+
if (orientation === "y+") {
|
|
138
|
+
padding.minY = Math.max(padding.minY, height)
|
|
139
|
+
} else {
|
|
140
|
+
padding.maxY = Math.max(padding.maxY, height)
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
padding.minY = Math.max(padding.minY, height / 2)
|
|
144
|
+
padding.maxY = Math.max(padding.maxY, height / 2)
|
|
145
|
+
if (orientation === "x+") {
|
|
146
|
+
padding.minX = Math.max(padding.minX, width)
|
|
147
|
+
} else {
|
|
148
|
+
padding.maxX = Math.max(padding.maxX, width)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return padding
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private getNetLabelWidthForConnectionPair(netId: string) {
|
|
157
|
+
const ncWidth = this.inputProblem.netConnections.find(
|
|
158
|
+
(nc) => nc.netId === netId,
|
|
159
|
+
)?.netLabelWidth
|
|
160
|
+
if (ncWidth !== undefined) return ncWidth
|
|
161
|
+
|
|
162
|
+
const dcWidthByNetId = this.inputProblem.directConnections.find(
|
|
163
|
+
(dc) => dc.netId === netId,
|
|
164
|
+
)?.netLabelWidth
|
|
165
|
+
if (dcWidthByNetId !== undefined) return dcWidthByNetId
|
|
166
|
+
|
|
167
|
+
const pinIds = this.pins.map((p) => p.pinId)
|
|
168
|
+
const dcWidthByPinId = this.inputProblem.directConnections.find((dc) =>
|
|
169
|
+
dc.pinIds.some((pid) => pinIds.includes(pid)),
|
|
170
|
+
)?.netLabelWidth
|
|
171
|
+
if (dcWidthByPinId !== undefined) return dcWidthByPinId
|
|
172
|
+
|
|
173
|
+
return this.inputProblem.netConnections.find((nc) =>
|
|
174
|
+
nc.pinIds.some((pid) => pinIds.includes(pid)),
|
|
175
|
+
)?.netLabelWidth
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private getNetLabelHeightForConnectionPair(netId: string) {
|
|
179
|
+
const ncHeight = this.inputProblem.netConnections.find(
|
|
180
|
+
(nc) => nc.netId === netId,
|
|
181
|
+
)?.netLabelHeight
|
|
182
|
+
if (ncHeight !== undefined) return ncHeight
|
|
183
|
+
|
|
184
|
+
const pinIds = this.pins.map((p) => p.pinId)
|
|
185
|
+
return this.inputProblem.netConnections.find((nc) =>
|
|
186
|
+
nc.pinIds.some((pid) => pinIds.includes(pid)),
|
|
187
|
+
)?.netLabelHeight
|
|
188
|
+
}
|
|
189
|
+
|
|
101
190
|
private axisOfSegment(a: Point, b: Point): Axis | null {
|
|
102
191
|
if (isVertical(a, b)) return "x"
|
|
103
192
|
if (isHorizontal(a, b)) return "y"
|
|
@@ -127,10 +216,18 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
127
216
|
return
|
|
128
217
|
}
|
|
129
218
|
|
|
130
|
-
const { path,
|
|
219
|
+
const { path, collisionRects } = state
|
|
131
220
|
|
|
132
221
|
const [PA, PB] = this.pins
|
|
133
|
-
const collision = findFirstCollision(path, this.obstacles
|
|
222
|
+
const collision = findFirstCollision(path, this.obstacles, {
|
|
223
|
+
excludeRectsForSegment: (segIndex) => {
|
|
224
|
+
const lastSegIndex = path.length - 2
|
|
225
|
+
if (segIndex === 0 || segIndex === lastSegIndex) {
|
|
226
|
+
return this.textObstacles
|
|
227
|
+
}
|
|
228
|
+
return new Set<ObstacleRect>()
|
|
229
|
+
},
|
|
230
|
+
})
|
|
134
231
|
|
|
135
232
|
if (!collision) {
|
|
136
233
|
// Sanity check: ensure path still connects PA -> PB
|
|
@@ -182,7 +279,7 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
182
279
|
// Note: PA and PB are already defined above
|
|
183
280
|
const candidates: number[] = []
|
|
184
281
|
|
|
185
|
-
if (
|
|
282
|
+
if (collisionRects.size === 0) {
|
|
186
283
|
// First collision on this search branch: use mid(PA, C) and mid(PB, C)
|
|
187
284
|
const m1 = midBetweenPointAndRect(axis, { x: PA.x, y: PA.y }, rect)
|
|
188
285
|
const m2 = midBetweenPointAndRect(axis, { x: PB.x, y: PB.y }, rect)
|
|
@@ -193,20 +290,14 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
193
290
|
candidates.push(...uniqueCandidates)
|
|
194
291
|
} else {
|
|
195
292
|
// Subsequent collisions: mid between C and nearest rect/bounds from the set
|
|
196
|
-
const mids = candidateMidsFromSet(
|
|
197
|
-
axis,
|
|
198
|
-
rect,
|
|
199
|
-
this.rectById,
|
|
200
|
-
collisionChipIds,
|
|
201
|
-
this.aabb,
|
|
202
|
-
)
|
|
293
|
+
const mids = candidateMidsFromSet(axis, rect, collisionRects, this.aabb)
|
|
203
294
|
candidates.push(...mids)
|
|
204
295
|
}
|
|
205
296
|
|
|
206
297
|
// Generate new shifted paths, order by total path length (shorter first)
|
|
207
298
|
const newStates: Array<{
|
|
208
299
|
path: Point[]
|
|
209
|
-
|
|
300
|
+
collisionRects: Set<ObstacleRect>
|
|
210
301
|
len: number
|
|
211
302
|
}> = []
|
|
212
303
|
|
|
@@ -216,15 +307,15 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
216
307
|
const key = pathKey(newPath)
|
|
217
308
|
if (this.visited.has(key)) continue
|
|
218
309
|
this.visited.add(key)
|
|
219
|
-
const nextSet = new Set(
|
|
220
|
-
nextSet.add(rect
|
|
310
|
+
const nextSet = new Set(collisionRects)
|
|
311
|
+
nextSet.add(rect)
|
|
221
312
|
const len = this.pathLength(newPath)
|
|
222
|
-
newStates.push({ path: newPath,
|
|
313
|
+
newStates.push({ path: newPath, collisionRects: nextSet, len })
|
|
223
314
|
}
|
|
224
315
|
|
|
225
316
|
newStates.sort((a, b) => a.len - b.len)
|
|
226
317
|
for (const st of newStates) {
|
|
227
|
-
this.queue.push({ path: st.path,
|
|
318
|
+
this.queue.push({ path: st.path, collisionRects: st.collisionRects })
|
|
228
319
|
}
|
|
229
320
|
}
|
|
230
321
|
|
|
@@ -254,7 +345,7 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
254
345
|
})
|
|
255
346
|
|
|
256
347
|
// Draw all the new candidates
|
|
257
|
-
for (const { path
|
|
348
|
+
for (const { path } of this.queue) {
|
|
258
349
|
g.lines!.push({ points: path, strokeColor: "teal", strokeDash: "2 2" })
|
|
259
350
|
}
|
|
260
351
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
-
import type {
|
|
2
|
+
import type { RectBounds } from "./rect"
|
|
3
3
|
|
|
4
4
|
const EPS = 1e-9
|
|
5
5
|
|
|
@@ -8,10 +8,10 @@ export const isVertical = (a: Point, b: Point, eps = EPS) =>
|
|
|
8
8
|
export const isHorizontal = (a: Point, b: Point, eps = EPS) =>
|
|
9
9
|
Math.abs(a.y - b.y) < eps
|
|
10
10
|
|
|
11
|
-
export const segmentIntersectsRect = (
|
|
11
|
+
export const segmentIntersectsRect = <TRect extends RectBounds>(
|
|
12
12
|
a: Point,
|
|
13
13
|
b: Point,
|
|
14
|
-
r:
|
|
14
|
+
r: TRect,
|
|
15
15
|
eps = EPS,
|
|
16
16
|
): boolean => {
|
|
17
17
|
const vert = isVertical(a, b, eps)
|
|
@@ -35,19 +35,19 @@ export const segmentIntersectsRect = (
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export const findFirstCollision = (
|
|
38
|
+
export const findFirstCollision = <TRect extends RectBounds>(
|
|
39
39
|
pts: Point[],
|
|
40
|
-
rects:
|
|
40
|
+
rects: TRect[],
|
|
41
41
|
opts: {
|
|
42
|
-
|
|
42
|
+
excludeRectsForSegment?: (segIndex: number) => Set<TRect>
|
|
43
43
|
} = {},
|
|
44
|
-
): { segIndex: number; rect:
|
|
44
|
+
): { segIndex: number; rect: TRect } | null => {
|
|
45
45
|
for (let i = 0; i < pts.length - 1; i++) {
|
|
46
46
|
const a = pts[i]!
|
|
47
47
|
const b = pts[i + 1]!
|
|
48
|
-
const excluded = opts.
|
|
48
|
+
const excluded = opts.excludeRectsForSegment?.(i) ?? new Set<TRect>()
|
|
49
49
|
for (const r of rects) {
|
|
50
|
-
if (excluded.has(r
|
|
50
|
+
if (excluded.has(r)) continue
|
|
51
51
|
if (segmentIntersectsRect(a, b, r)) {
|
|
52
52
|
return { segIndex: i, rect: r }
|
|
53
53
|
}
|
|
@@ -61,7 +61,7 @@ export const findFirstCollision = (
|
|
|
61
61
|
*/
|
|
62
62
|
export const isPathCollidingWithObstacles = (
|
|
63
63
|
path: Point[],
|
|
64
|
-
obstacles:
|
|
64
|
+
obstacles: RectBounds[],
|
|
65
65
|
): boolean => {
|
|
66
66
|
for (let i = 0; i < path.length - 1; i++) {
|
|
67
67
|
for (const obstacle of obstacles) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
-
import type {
|
|
2
|
+
import type { ObstacleRect } from "./rect"
|
|
3
3
|
|
|
4
4
|
const EPS = 1e-9
|
|
5
5
|
|
|
@@ -15,7 +15,7 @@ export const aabbFromPoints = (a: Point, b: Point) => ({
|
|
|
15
15
|
export const midBetweenPointAndRect = (
|
|
16
16
|
axis: Axis,
|
|
17
17
|
p: Point,
|
|
18
|
-
r:
|
|
18
|
+
r: ObstacleRect,
|
|
19
19
|
eps = EPS,
|
|
20
20
|
): number[] => {
|
|
21
21
|
if (axis === "x") {
|
|
@@ -41,15 +41,12 @@ export const midBetweenPointAndRect = (
|
|
|
41
41
|
|
|
42
42
|
export const candidateMidsFromSet = (
|
|
43
43
|
axis: Axis,
|
|
44
|
-
colliding:
|
|
45
|
-
|
|
46
|
-
collisionRectIds: Set<string>,
|
|
44
|
+
colliding: ObstacleRect,
|
|
45
|
+
collisionRects: Set<ObstacleRect>,
|
|
47
46
|
aabb: { minX: number; maxX: number; minY: number; maxY: number },
|
|
48
47
|
eps = EPS,
|
|
49
48
|
): number[] => {
|
|
50
|
-
const setRects = [...
|
|
51
|
-
.map((id) => rectsById.get(id))
|
|
52
|
-
.filter((r): r is ChipWithBounds => !!r)
|
|
49
|
+
const setRects = [...collisionRects]
|
|
53
50
|
|
|
54
51
|
if (axis === "x") {
|
|
55
52
|
const leftBoundaries = [aabb.minX, ...setRects.map((r) => r.maxX)].filter(
|
|
@@ -1,19 +1,44 @@
|
|
|
1
1
|
import type { InputChip, InputProblem } from "lib/types/InputProblem"
|
|
2
2
|
import { getInputChipBounds } from "lib/solvers/GuidelinesSolver/getInputChipBounds"
|
|
3
|
+
import { getTextBoxBounds, type RectPadding } from "lib/utils/textBoxBounds"
|
|
3
4
|
|
|
4
|
-
export type
|
|
5
|
-
chipId: string
|
|
5
|
+
export type RectBounds = {
|
|
6
6
|
minX: number
|
|
7
7
|
minY: number
|
|
8
8
|
maxX: number
|
|
9
9
|
maxY: number
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export
|
|
12
|
+
export type ChipObstacleRect = RectBounds & {
|
|
13
|
+
kind: "chip"
|
|
14
|
+
chipId: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type TextBoxObstacleRect = RectBounds & {
|
|
18
|
+
kind: "text_box"
|
|
19
|
+
textBox: NonNullable<InputProblem["textBoxes"]>[number]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ObstacleRect = ChipObstacleRect | TextBoxObstacleRect
|
|
23
|
+
|
|
24
|
+
export const chipToRect = (chip: InputChip): ChipObstacleRect => {
|
|
13
25
|
const b = getInputChipBounds(chip)
|
|
14
|
-
return { chipId: chip.chipId, ...b }
|
|
26
|
+
return { kind: "chip", chipId: chip.chipId, ...b }
|
|
15
27
|
}
|
|
16
28
|
|
|
17
|
-
export const getObstacleRects = (
|
|
18
|
-
|
|
29
|
+
export const getObstacleRects = (
|
|
30
|
+
problem: InputProblem,
|
|
31
|
+
opts: { textBoxPadding?: RectPadding } = {},
|
|
32
|
+
): ObstacleRect[] => {
|
|
33
|
+
const chipRects = problem.chips.map(chipToRect)
|
|
34
|
+
const textBoxRects = (problem.textBoxes ?? []).map((textBox) => {
|
|
35
|
+
const b = getTextBoxBounds(textBox, opts.textBoxPadding)
|
|
36
|
+
return {
|
|
37
|
+
kind: "text_box" as const,
|
|
38
|
+
textBox,
|
|
39
|
+
...b,
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
return [...chipRects, ...textBoxRects]
|
|
19
44
|
}
|
package/lib/solvers/TraceAnchoredNetLabelOverlapSolver/TraceAnchoredNetLabelOverlapSolver.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
TraceAnchoredNetLabelOverlapSolverParams,
|
|
21
21
|
} from "./types"
|
|
22
22
|
import { visualizeTraceAnchoredNetLabelOverlapSolver } from "./visualize"
|
|
23
|
+
import { rectIntersectsAnyTextBox } from "lib/utils/textBoxBounds"
|
|
23
24
|
|
|
24
25
|
type ActiveOverlapSearch = {
|
|
25
26
|
overlap: LabelOverlap
|
|
@@ -227,6 +228,8 @@ export class TraceAnchoredNetLabelOverlapSolver extends BaseSolver {
|
|
|
227
228
|
): CandidateStatus {
|
|
228
229
|
const bounds = getLabelBounds(candidate)
|
|
229
230
|
if (this.intersectsAnyChip(bounds)) return "chip-collision"
|
|
231
|
+
if (rectIntersectsAnyTextBox(bounds, this.inputProblem))
|
|
232
|
+
return "text-collision"
|
|
230
233
|
if (traceCrossesBoundsInterior(bounds, this.traces))
|
|
231
234
|
return "trace-collision"
|
|
232
235
|
if (this.intersectsAnyOtherLabel(bounds, labelIndex)) {
|
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
VccNetLabelCornerPlacementSolverParams,
|
|
24
24
|
} from "./types"
|
|
25
25
|
import { visualizeVccNetLabelCornerPlacementSolver } from "./visualize"
|
|
26
|
+
import { rectIntersectsAnyTextBox } from "lib/utils/textBoxBounds"
|
|
26
27
|
|
|
27
28
|
export class VccNetLabelCornerPlacementSolver extends BaseSolver {
|
|
28
29
|
inputProblem: InputProblem
|
|
@@ -171,6 +172,8 @@ export class VccNetLabelCornerPlacementSolver extends BaseSolver {
|
|
|
171
172
|
labelIndex: number,
|
|
172
173
|
): CornerCandidateStatus {
|
|
173
174
|
if (this.intersectsAnyChip(bounds)) return "chip-collision"
|
|
175
|
+
if (rectIntersectsAnyTextBox(bounds, this.inputProblem))
|
|
176
|
+
return "text-collision"
|
|
174
177
|
if (traceCrossesBoundsInterior(bounds, this.traceMap)) {
|
|
175
178
|
return "trace-collision"
|
|
176
179
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Bounds } from "@tscircuit/math-utils"
|
|
2
|
+
import type { InputProblem, TextBoxes } from "lib/types/InputProblem"
|
|
3
|
+
|
|
4
|
+
export type RectPadding = {
|
|
5
|
+
minX?: number
|
|
6
|
+
minY?: number
|
|
7
|
+
maxX?: number
|
|
8
|
+
maxY?: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getTextBoxBounds(
|
|
12
|
+
textBox: TextBoxes,
|
|
13
|
+
padding: RectPadding = {},
|
|
14
|
+
): Bounds {
|
|
15
|
+
return {
|
|
16
|
+
minX: textBox.center.x - textBox.width / 2 - (padding.minX ?? 0),
|
|
17
|
+
minY: textBox.center.y - textBox.height / 2 - (padding.minY ?? 0),
|
|
18
|
+
maxX: textBox.center.x + textBox.width / 2 + (padding.maxX ?? 0),
|
|
19
|
+
maxY: textBox.center.y + textBox.height / 2 + (padding.maxY ?? 0),
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function boundsOverlap(a: Bounds, b: Bounds, eps = 1e-9): boolean {
|
|
24
|
+
return (
|
|
25
|
+
a.minX < b.maxX - eps &&
|
|
26
|
+
a.maxX > b.minX + eps &&
|
|
27
|
+
a.minY < b.maxY - eps &&
|
|
28
|
+
a.maxY > b.minY + eps
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function rectIntersectsAnyTextBox(
|
|
33
|
+
bounds: Bounds,
|
|
34
|
+
inputProblem: InputProblem,
|
|
35
|
+
): boolean {
|
|
36
|
+
for (const textBox of inputProblem.textBoxes ?? []) {
|
|
37
|
+
if (boundsOverlap(bounds, getTextBoxBounds(textBox))) return true
|
|
38
|
+
}
|
|
39
|
+
return false
|
|
40
|
+
}
|