@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.
@@ -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
@@ -5,7 +5,7 @@
5
5
  "url": "https://github.com/tscircuit/schematic-trace-solver.git"
6
6
  },
7
7
  "main": "dist/index.js",
8
- "version": "0.0.141",
8
+ "version": "0.0.143",
9
9
  "type": "module",
10
10
  "scripts": {
11
11
  "start": "cosmos",
@@ -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
+ }