@tscircuit/schematic-trace-solver 0.0.141 → 0.0.143
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 +28 -1
- package/dist/index.js +348 -34
- package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +92 -16
- package/lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts +216 -17
- package/lib/solvers/InlineNetLabelSolver/alignPortOnlyInlineNetLabelStubs.ts +233 -0
- package/lib/types/InputProblem.ts +13 -0
- package/package.json +1 -1
- package/tests/assets/inline-net-label-port-stub.json +67 -0
- package/tests/bug-reports/bug-report-20260721T221026Z/__snapshots__/bug-report-20260721T221026Z.snap.svg +3 -3
- package/tests/repros/__snapshots__/repro-tida010076-j4-ground-bus.snap.svg +52 -0
- package/tests/repros/__snapshots__/repro-tida010076-page02.snap.svg +624 -0
- package/tests/repros/assets/repro-tida010076-page02.input.json +2964 -0
- package/tests/repros/repro-tida010076-j4-ground-bus.test.ts +78 -0
- package/tests/repros/repro-tida010076-page02.test.ts +46 -0
- package/tests/repros/repro-usb-power-vbus-label-detour.test.ts +23 -1
- package/tests/solvers/InlineNetLabelSolver/__snapshots__/inline-net-label-port-stub.snap.svg +64 -0
- package/tests/solvers/InlineNetLabelSolver/align-port-only-inline-net-label-stubs-conflict.test.ts +75 -0
- package/tests/solvers/InlineNetLabelSolver/inline-net-label-port-stub.test.ts +82 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import type { Bounds, Point } from "@tscircuit/math-utils"
|
|
2
|
+
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
3
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
4
|
+
import type { InputProblem } from "lib/types/InputProblem"
|
|
5
|
+
import { boundsOverlap, getTextBoxBounds } from "lib/utils/textBoxBounds"
|
|
6
|
+
import type { InlineNetLabelPlacement } from "./InlineNetLabelSolver"
|
|
7
|
+
|
|
8
|
+
type StubDirection = "x+" | "x-" | "y+" | "y-"
|
|
9
|
+
|
|
10
|
+
const getStubDirection = (path: [Point, Point]): StubDirection | undefined => {
|
|
11
|
+
const [start, end] = path
|
|
12
|
+
if (Math.abs(end.x - start.x) >= Math.abs(end.y - start.y)) {
|
|
13
|
+
return end.x >= start.x ? "x+" : "x-"
|
|
14
|
+
}
|
|
15
|
+
return end.y >= start.y ? "y+" : "y-"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const getStubLength = (path: [Point, Point]) => {
|
|
19
|
+
const [start, end] = path
|
|
20
|
+
return Math.abs(end.x - start.x) + Math.abs(end.y - start.y)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const getLabelBounds = (placement: {
|
|
24
|
+
center: Point
|
|
25
|
+
width: number
|
|
26
|
+
height: number
|
|
27
|
+
axis?: "x" | "y"
|
|
28
|
+
}): Bounds => {
|
|
29
|
+
const isVertical = placement.axis === "y"
|
|
30
|
+
const width = isVertical ? placement.height : placement.width
|
|
31
|
+
const height = isVertical ? placement.width : placement.height
|
|
32
|
+
return {
|
|
33
|
+
minX: placement.center.x - width / 2,
|
|
34
|
+
maxX: placement.center.x + width / 2,
|
|
35
|
+
minY: placement.center.y - height / 2,
|
|
36
|
+
maxY: placement.center.y + height / 2,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const getPathBounds = (path: Point[]): Bounds => ({
|
|
41
|
+
minX: Math.min(...path.map((point) => point.x)),
|
|
42
|
+
maxX: Math.max(...path.map((point) => point.x)),
|
|
43
|
+
minY: Math.min(...path.map((point) => point.y)),
|
|
44
|
+
maxY: Math.max(...path.map((point) => point.y)),
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const doesPathIntersectBounds = (path: Point[], bounds: Bounds): boolean => {
|
|
48
|
+
for (let index = 0; index < path.length - 1; index++) {
|
|
49
|
+
const segmentBounds = getPathBounds([path[index]!, path[index + 1]!])
|
|
50
|
+
if (boundsOverlap(segmentBounds, bounds)) return true
|
|
51
|
+
}
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const resizeStub = (
|
|
56
|
+
placement: InlineNetLabelPlacement,
|
|
57
|
+
direction: StubDirection,
|
|
58
|
+
targetLength: number,
|
|
59
|
+
): InlineNetLabelPlacement => {
|
|
60
|
+
const [start] = placement.stubTracePath!
|
|
61
|
+
const end: Point =
|
|
62
|
+
direction === "x+"
|
|
63
|
+
? { x: start.x + targetLength, y: start.y }
|
|
64
|
+
: direction === "x-"
|
|
65
|
+
? { x: start.x - targetLength, y: start.y }
|
|
66
|
+
: direction === "y+"
|
|
67
|
+
? { x: start.x, y: start.y + targetLength }
|
|
68
|
+
: { x: start.x, y: start.y - targetLength }
|
|
69
|
+
const anchorPoint = {
|
|
70
|
+
x: (start.x + end.x) / 2,
|
|
71
|
+
y: (start.y + end.y) / 2,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
...placement,
|
|
76
|
+
stubTracePath: [start, end],
|
|
77
|
+
anchorPoint,
|
|
78
|
+
// Extending a short row only adds wire at its free end. Keep the label
|
|
79
|
+
// itself next to the pin so terminal labels can be start/end aligned and
|
|
80
|
+
// collision checks continue to describe the rendered text box.
|
|
81
|
+
center: placement.center,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Aligns the free ends of port-only inline-label stubs on each component side.
|
|
87
|
+
*
|
|
88
|
+
* A group is changed atomically: if any longer stub or retained label would
|
|
89
|
+
* collide with a chip, component text, routed trace, retained anchored label,
|
|
90
|
+
* or another inline label/stub, every member keeps its original length.
|
|
91
|
+
*/
|
|
92
|
+
export const alignPortOnlyInlineNetLabelStubs = ({
|
|
93
|
+
placements,
|
|
94
|
+
inputProblem,
|
|
95
|
+
traces,
|
|
96
|
+
netLabelPlacements,
|
|
97
|
+
}: {
|
|
98
|
+
placements: InlineNetLabelPlacement[]
|
|
99
|
+
inputProblem: InputProblem
|
|
100
|
+
traces: SolvedTracePath[]
|
|
101
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
102
|
+
}): InlineNetLabelPlacement[] => {
|
|
103
|
+
const chipIdByPinId = new Map<string, string>()
|
|
104
|
+
for (const chip of inputProblem.chips) {
|
|
105
|
+
for (const pin of chip.pins) chipIdByPinId.set(pin.pinId, chip.chipId)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const groups = new Map<
|
|
109
|
+
string,
|
|
110
|
+
Array<{
|
|
111
|
+
placementIndex: number
|
|
112
|
+
placement: InlineNetLabelPlacement
|
|
113
|
+
direction: StubDirection
|
|
114
|
+
ownerChipId?: string
|
|
115
|
+
}>
|
|
116
|
+
>()
|
|
117
|
+
for (const [placementIndex, placement] of placements.entries()) {
|
|
118
|
+
if (!placement.stubTracePath || placement.pinIds.length !== 1) continue
|
|
119
|
+
const direction = getStubDirection(placement.stubTracePath)
|
|
120
|
+
if (!direction) continue
|
|
121
|
+
const ownerChipId = chipIdByPinId.get(placement.pinIds[0]!)
|
|
122
|
+
const groupKey = `${ownerChipId ?? placement.pinIds[0]}::${direction}`
|
|
123
|
+
const group = groups.get(groupKey) ?? []
|
|
124
|
+
group.push({ placementIndex, placement, direction, ownerChipId })
|
|
125
|
+
groups.set(groupKey, group)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const alignedPlacements = [...placements]
|
|
129
|
+
const supersededGlobalNetIds = new Set(
|
|
130
|
+
placements.map((placement) => placement.globalConnNetId),
|
|
131
|
+
)
|
|
132
|
+
const retainedAnchoredLabelBounds = netLabelPlacements
|
|
133
|
+
.filter(
|
|
134
|
+
(placement) => !supersededGlobalNetIds.has(placement.globalConnNetId),
|
|
135
|
+
)
|
|
136
|
+
.map(getLabelBounds)
|
|
137
|
+
|
|
138
|
+
for (const group of groups.values()) {
|
|
139
|
+
if (group.length < 2) continue
|
|
140
|
+
const targetLength = Math.max(
|
|
141
|
+
...group.map(({ placement }) => getStubLength(placement.stubTracePath!)),
|
|
142
|
+
)
|
|
143
|
+
const proposals = group.map(({ placement, direction }) =>
|
|
144
|
+
resizeStub(placement, direction, targetLength),
|
|
145
|
+
)
|
|
146
|
+
const groupPlacementIndices = new Set(
|
|
147
|
+
group.map(({ placementIndex }) => placementIndex),
|
|
148
|
+
)
|
|
149
|
+
const fixedInlinePlacements = placements.filter(
|
|
150
|
+
(_, placementIndex) => !groupPlacementIndices.has(placementIndex),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
const hasConflict = proposals.some((proposal, proposalIndex) => {
|
|
154
|
+
const labelBounds = getLabelBounds(proposal)
|
|
155
|
+
const stubPath = proposal.stubTracePath!
|
|
156
|
+
const stubBounds = getPathBounds(stubPath)
|
|
157
|
+
const ownerChipId = group[proposalIndex]!.ownerChipId
|
|
158
|
+
|
|
159
|
+
for (const chip of inputProblem.chips) {
|
|
160
|
+
const chipBounds: Bounds = {
|
|
161
|
+
minX: chip.center.x - chip.width / 2,
|
|
162
|
+
maxX: chip.center.x + chip.width / 2,
|
|
163
|
+
minY: chip.center.y - chip.height / 2,
|
|
164
|
+
maxY: chip.center.y + chip.height / 2,
|
|
165
|
+
}
|
|
166
|
+
if (boundsOverlap(labelBounds, chipBounds)) return true
|
|
167
|
+
if (
|
|
168
|
+
chip.chipId !== ownerChipId &&
|
|
169
|
+
boundsOverlap(stubBounds, chipBounds)
|
|
170
|
+
)
|
|
171
|
+
return true
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (const textBox of inputProblem.textBoxes ?? []) {
|
|
175
|
+
const textBounds = getTextBoxBounds(textBox)
|
|
176
|
+
if (
|
|
177
|
+
boundsOverlap(labelBounds, textBounds) ||
|
|
178
|
+
boundsOverlap(stubBounds, textBounds)
|
|
179
|
+
)
|
|
180
|
+
return true
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const trace of traces) {
|
|
184
|
+
if (trace.globalConnNetId === proposal.globalConnNetId) continue
|
|
185
|
+
if (
|
|
186
|
+
doesPathIntersectBounds(trace.tracePath, labelBounds) ||
|
|
187
|
+
doesPathIntersectBounds(trace.tracePath, stubBounds)
|
|
188
|
+
)
|
|
189
|
+
return true
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (
|
|
193
|
+
retainedAnchoredLabelBounds.some(
|
|
194
|
+
(bounds) =>
|
|
195
|
+
boundsOverlap(labelBounds, bounds) ||
|
|
196
|
+
boundsOverlap(stubBounds, bounds),
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
return true
|
|
200
|
+
|
|
201
|
+
for (const fixedPlacement of fixedInlinePlacements) {
|
|
202
|
+
const fixedLabelBounds = getLabelBounds(fixedPlacement)
|
|
203
|
+
if (
|
|
204
|
+
boundsOverlap(labelBounds, fixedLabelBounds) ||
|
|
205
|
+
boundsOverlap(stubBounds, fixedLabelBounds)
|
|
206
|
+
)
|
|
207
|
+
return true
|
|
208
|
+
if (
|
|
209
|
+
fixedPlacement.stubTracePath &&
|
|
210
|
+
(doesPathIntersectBounds(fixedPlacement.stubTracePath, labelBounds) ||
|
|
211
|
+
doesPathIntersectBounds(stubPath, fixedLabelBounds))
|
|
212
|
+
)
|
|
213
|
+
return true
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
for (const [otherIndex, otherProposal] of proposals.entries()) {
|
|
217
|
+
if (otherIndex === proposalIndex) continue
|
|
218
|
+
const otherLabelBounds = getLabelBounds(otherProposal)
|
|
219
|
+
if (boundsOverlap(labelBounds, otherLabelBounds)) return true
|
|
220
|
+
if (doesPathIntersectBounds(stubPath, otherLabelBounds)) return true
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return false
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
if (hasConflict) continue
|
|
227
|
+
for (const [groupIndex, { placementIndex }] of group.entries()) {
|
|
228
|
+
alignedPlacements[placementIndex] = proposals[groupIndex]!
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return alignedPlacements
|
|
233
|
+
}
|
|
@@ -65,6 +65,19 @@ export interface InputNetConnection {
|
|
|
65
65
|
pinIds: Array<PinId>
|
|
66
66
|
netLabelWidth?: number
|
|
67
67
|
netLabelHeight?: number
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* When true, a named single-pin net may be drawn as a short outward trace
|
|
71
|
+
* stub with its net name placed inline. Multi-pin net connections retain the
|
|
72
|
+
* regular anchored-label behavior.
|
|
73
|
+
*/
|
|
74
|
+
allowInlineNetLabel?: boolean
|
|
75
|
+
|
|
76
|
+
/** Extent of the inline text along the generated trace stub. */
|
|
77
|
+
inlineNetLabelWidth?: number
|
|
78
|
+
|
|
79
|
+
/** Height of the inline text perpendicular to the generated trace stub. */
|
|
80
|
+
inlineNetLabelHeight?: number
|
|
68
81
|
}
|
|
69
82
|
|
|
70
83
|
export interface InputProblem {
|
package/package.json
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"chips": [
|
|
3
|
+
{
|
|
4
|
+
"chipId": "U1",
|
|
5
|
+
"center": { "x": 0, "y": 0 },
|
|
6
|
+
"width": 1,
|
|
7
|
+
"height": 1,
|
|
8
|
+
"pins": [
|
|
9
|
+
{ "pinId": "U1.1", "x": -0.5, "y": 0.3, "_facingDirection": "x-" },
|
|
10
|
+
{ "pinId": "U1.2", "x": 0.5, "y": -0.2, "_facingDirection": "x+" },
|
|
11
|
+
{ "pinId": "U1.3", "x": -0.5, "y": -0.1, "_facingDirection": "x-" },
|
|
12
|
+
{ "pinId": "U1.4", "x": -0.2, "y": 0.5, "_facingDirection": "y+" },
|
|
13
|
+
{ "pinId": "U1.5", "x": 0.2, "y": -0.5, "_facingDirection": "y-" }
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"directConnections": [],
|
|
18
|
+
"netConnections": [
|
|
19
|
+
{
|
|
20
|
+
"netId": "NET_PRU0_MII_TX_CLK1",
|
|
21
|
+
"pinIds": ["U1.1"],
|
|
22
|
+
"netLabelWidth": 1.65,
|
|
23
|
+
"allowInlineNetLabel": true,
|
|
24
|
+
"inlineNetLabelWidth": 1.65,
|
|
25
|
+
"inlineNetLabelHeight": 0.12
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"netId": "NET_PRU0_MII_RXLINK1",
|
|
29
|
+
"pinIds": ["U1.2"],
|
|
30
|
+
"netLabelWidth": 1.6,
|
|
31
|
+
"allowInlineNetLabel": true,
|
|
32
|
+
"inlineNetLabelWidth": 1.6,
|
|
33
|
+
"inlineNetLabelHeight": 0.12
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"netId": "NET_MDC",
|
|
37
|
+
"pinIds": ["U1.3"],
|
|
38
|
+
"netLabelWidth": 0.65,
|
|
39
|
+
"allowInlineNetLabel": true,
|
|
40
|
+
"inlineNetLabelWidth": 0.65,
|
|
41
|
+
"inlineNetLabelHeight": 0.12
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"netId": "NET_TOP",
|
|
45
|
+
"pinIds": ["U1.4"],
|
|
46
|
+
"netLabelWidth": 0.75,
|
|
47
|
+
"allowInlineNetLabel": true,
|
|
48
|
+
"inlineNetLabelWidth": 0.75,
|
|
49
|
+
"inlineNetLabelHeight": 0.12
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"netId": "NET_BOTTOM",
|
|
53
|
+
"pinIds": ["U1.5"],
|
|
54
|
+
"netLabelWidth": 0.95,
|
|
55
|
+
"allowInlineNetLabel": true,
|
|
56
|
+
"inlineNetLabelWidth": 0.95,
|
|
57
|
+
"inlineNetLabelHeight": 0.12
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
"availableNetLabelOrientations": {
|
|
61
|
+
"NET_PRU0_MII_TX_CLK1": ["x-", "x+"],
|
|
62
|
+
"NET_PRU0_MII_RXLINK1": ["x-", "x+"],
|
|
63
|
+
"NET_MDC": ["x-", "x+"],
|
|
64
|
+
"NET_TOP": ["x-", "x+"],
|
|
65
|
+
"NET_BOTTOM": ["x-", "x+"]
|
|
66
|
+
}
|
|
67
|
+
}
|