@tscircuit/schematic-trace-solver 0.0.143 → 0.0.144
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 +20 -16
- package/dist/index.js +43 -46
- package/lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts +70 -57
- package/lib/types/InputProblem.ts +7 -4
- package/package.json +1 -1
- package/tests/assets/inline-net-label-two-pin-net.json +67 -0
- package/tests/solvers/InlineNetLabelSolver/__snapshots__/inline-net-label-two-pin-net.snap.svg +57 -0
- package/tests/solvers/InlineNetLabelSolver/inline-net-label-two-pin-net.test.ts +38 -0
- package/tests/solvers/InlineNetLabelSolver/two-pin-terminal-stubs-atomic.test.ts +78 -0
package/dist/index.d.ts
CHANGED
|
@@ -96,7 +96,9 @@ interface InputDirectConnection {
|
|
|
96
96
|
*
|
|
97
97
|
* Only set this for connections whose net name is worth showing on the wire -
|
|
98
98
|
* the solver trusts the caller (e.g. @tscircuit/core) to make that decision.
|
|
99
|
-
*
|
|
99
|
+
* When the connection is routed, the label is placed along the trace. If
|
|
100
|
+
* routing is intentionally skipped, both endpoint labels may instead become
|
|
101
|
+
* outward inline stubs.
|
|
100
102
|
*/
|
|
101
103
|
allowInlineNetLabel?: boolean;
|
|
102
104
|
/**
|
|
@@ -116,9 +118,10 @@ interface InputNetConnection {
|
|
|
116
118
|
netLabelWidth?: number;
|
|
117
119
|
netLabelHeight?: number;
|
|
118
120
|
/**
|
|
119
|
-
* When true, a named
|
|
120
|
-
*
|
|
121
|
-
*
|
|
121
|
+
* When true, a named one- or two-pin net may use inline labels. A single-pin
|
|
122
|
+
* net gets an outward stub. A routed two-pin net gets one label along its
|
|
123
|
+
* trace; when that route is intentionally skipped, both endpoints get
|
|
124
|
+
* outward stubs. Nets with more than two pins retain anchored labels.
|
|
122
125
|
*/
|
|
123
126
|
allowInlineNetLabel?: boolean;
|
|
124
127
|
/** Extent of the inline text along the generated trace stub. */
|
|
@@ -1200,8 +1203,9 @@ interface InlineNetLabelPlacement {
|
|
|
1200
1203
|
mspPairId?: string;
|
|
1201
1204
|
pinIds: PinId[];
|
|
1202
1205
|
/**
|
|
1203
|
-
* A generated single-ended trace stub. Present
|
|
1204
|
-
*
|
|
1206
|
+
* A generated single-ended trace stub. Present for a single-pin net or for
|
|
1207
|
+
* one endpoint of an unrouted two-pin net; routed point-to-point traces omit
|
|
1208
|
+
* it.
|
|
1205
1209
|
*/
|
|
1206
1210
|
stubTracePath?: [Point, Point];
|
|
1207
1211
|
/** Axis the text runs along: "x" reads left-to-right, "y" reads bottom-to-top */
|
|
@@ -1226,9 +1230,11 @@ interface InlineNetLabelSolverInput {
|
|
|
1226
1230
|
traces: SolvedTracePath[];
|
|
1227
1231
|
netLabelPlacements: NetLabelPlacement[];
|
|
1228
1232
|
}
|
|
1233
|
+
type InlineEligibleConnection = InputDirectConnection | InputNetConnection;
|
|
1229
1234
|
/**
|
|
1230
1235
|
* Places "inline net labels" - net names drawn alongside the trace they belong
|
|
1231
|
-
* to - for
|
|
1236
|
+
* to - for one- or two-pin connections that opted in via
|
|
1237
|
+
* `allowInlineNetLabel`.
|
|
1232
1238
|
*
|
|
1233
1239
|
* Any regular (anchored) net label placement for the same net is dropped, so a
|
|
1234
1240
|
* net is never labeled twice.
|
|
@@ -1238,22 +1244,20 @@ declare class InlineNetLabelSolver extends BaseSolver {
|
|
|
1238
1244
|
traces: SolvedTracePath[];
|
|
1239
1245
|
inputNetLabelPlacements: NetLabelPlacement[];
|
|
1240
1246
|
inlineNetLabelPlacements: InlineNetLabelPlacement[];
|
|
1241
|
-
/**
|
|
1242
|
-
|
|
1243
|
-
/** Single-pin net connections that opted in, still waiting to be processed */
|
|
1244
|
-
queuedPortOnlyNetConnections: InputNetConnection[];
|
|
1247
|
+
/** Eligible one- or two-pin connections still waiting to be processed. */
|
|
1248
|
+
queuedConnections: InlineEligibleConnection[];
|
|
1245
1249
|
private tracesByPinPairKey;
|
|
1246
1250
|
private hasAlignedPortOnlyStubs;
|
|
1247
1251
|
constructor(input: InlineNetLabelSolverInput);
|
|
1248
1252
|
getConstructorParams(): [InlineNetLabelSolverInput];
|
|
1249
1253
|
_step(): void;
|
|
1250
1254
|
/**
|
|
1251
|
-
* Converts
|
|
1252
|
-
*
|
|
1253
|
-
*
|
|
1254
|
-
*
|
|
1255
|
+
* Converts one conventional endpoint placement into an inline label on a
|
|
1256
|
+
* generated outward stub. The stub follows the pin's true facing direction;
|
|
1257
|
+
* an anchored label may finish in another direction after an elbow, which is
|
|
1258
|
+
* not the direction a terminal stub should leave the pin.
|
|
1255
1259
|
*/
|
|
1256
|
-
private
|
|
1260
|
+
private computeTerminalInlinePlacement;
|
|
1257
1261
|
private computeInlinePlacement;
|
|
1258
1262
|
/**
|
|
1259
1263
|
* Places the label over the whole route when the route is straight except
|
package/dist/index.js
CHANGED
|
@@ -12786,10 +12786,8 @@ var InlineNetLabelSolver = class extends BaseSolver {
|
|
|
12786
12786
|
traces;
|
|
12787
12787
|
inputNetLabelPlacements;
|
|
12788
12788
|
inlineNetLabelPlacements = [];
|
|
12789
|
-
/**
|
|
12790
|
-
|
|
12791
|
-
/** Single-pin net connections that opted in, still waiting to be processed */
|
|
12792
|
-
queuedPortOnlyNetConnections;
|
|
12789
|
+
/** Eligible one- or two-pin connections still waiting to be processed. */
|
|
12790
|
+
queuedConnections;
|
|
12793
12791
|
tracesByPinPairKey;
|
|
12794
12792
|
hasAlignedPortOnlyStubs = false;
|
|
12795
12793
|
constructor(input) {
|
|
@@ -12797,12 +12795,14 @@ var InlineNetLabelSolver = class extends BaseSolver {
|
|
|
12797
12795
|
this.inputProblem = input.inputProblem;
|
|
12798
12796
|
this.traces = input.traces;
|
|
12799
12797
|
this.inputNetLabelPlacements = input.netLabelPlacements;
|
|
12800
|
-
this.
|
|
12801
|
-
|
|
12802
|
-
|
|
12803
|
-
|
|
12804
|
-
|
|
12805
|
-
|
|
12798
|
+
this.queuedConnections = [
|
|
12799
|
+
...this.inputProblem.directConnections.filter(
|
|
12800
|
+
(connection) => connection.allowInlineNetLabel && connection.netId
|
|
12801
|
+
),
|
|
12802
|
+
...this.inputProblem.netConnections.filter(
|
|
12803
|
+
(connection) => connection.allowInlineNetLabel && connection.pinIds.length >= 1 && connection.pinIds.length <= 2 && connection.netId
|
|
12804
|
+
)
|
|
12805
|
+
];
|
|
12806
12806
|
this.tracesByPinPairKey = /* @__PURE__ */ new Map();
|
|
12807
12807
|
for (const trace of this.traces) {
|
|
12808
12808
|
const key = getPinPairKey2(trace.pins.map((p) => p.pinId));
|
|
@@ -12824,21 +12824,19 @@ var InlineNetLabelSolver = class extends BaseSolver {
|
|
|
12824
12824
|
];
|
|
12825
12825
|
}
|
|
12826
12826
|
_step() {
|
|
12827
|
-
const
|
|
12828
|
-
if (
|
|
12829
|
-
const
|
|
12830
|
-
if (
|
|
12831
|
-
this.
|
|
12832
|
-
|
|
12833
|
-
|
|
12834
|
-
|
|
12835
|
-
|
|
12836
|
-
|
|
12837
|
-
|
|
12838
|
-
|
|
12839
|
-
|
|
12840
|
-
if (placement) {
|
|
12841
|
-
this.inlineNetLabelPlacements.push(placement);
|
|
12827
|
+
const connection = this.queuedConnections.shift();
|
|
12828
|
+
if (connection) {
|
|
12829
|
+
const routedTraces = connection.pinIds.length === 2 ? this.tracesByPinPairKey.get(getPinPairKey2(connection.pinIds)) ?? [] : [];
|
|
12830
|
+
if (routedTraces.length > 0) {
|
|
12831
|
+
const placement = this.computeInlinePlacement(connection);
|
|
12832
|
+
if (placement) this.inlineNetLabelPlacements.push(placement);
|
|
12833
|
+
} else {
|
|
12834
|
+
const terminalPlacements = connection.pinIds.map(
|
|
12835
|
+
(pinId) => this.computeTerminalInlinePlacement(connection, pinId)
|
|
12836
|
+
);
|
|
12837
|
+
if (terminalPlacements.every((placement) => placement !== null)) {
|
|
12838
|
+
this.inlineNetLabelPlacements.push(...terminalPlacements);
|
|
12839
|
+
}
|
|
12842
12840
|
}
|
|
12843
12841
|
return;
|
|
12844
12842
|
}
|
|
@@ -12856,24 +12854,23 @@ var InlineNetLabelSolver = class extends BaseSolver {
|
|
|
12856
12854
|
this.stats.inlineNetLabelCount = this.inlineNetLabelPlacements.length;
|
|
12857
12855
|
}
|
|
12858
12856
|
/**
|
|
12859
|
-
* Converts
|
|
12860
|
-
*
|
|
12861
|
-
*
|
|
12862
|
-
*
|
|
12857
|
+
* Converts one conventional endpoint placement into an inline label on a
|
|
12858
|
+
* generated outward stub. The stub follows the pin's true facing direction;
|
|
12859
|
+
* an anchored label may finish in another direction after an elbow, which is
|
|
12860
|
+
* not the direction a terminal stub should leave the pin.
|
|
12863
12861
|
*/
|
|
12864
|
-
|
|
12865
|
-
|
|
12866
|
-
if (!pinId) return null;
|
|
12862
|
+
computeTerminalInlinePlacement(connection, pinId) {
|
|
12863
|
+
if (!connection.netId) return null;
|
|
12867
12864
|
const anchoredPlacement = this.inputNetLabelPlacements.find(
|
|
12868
|
-
(placement) => placement.netId ===
|
|
12865
|
+
(placement) => placement.netId === connection.netId && placement.pinIds.length === 1 && placement.pinIds[0] === pinId
|
|
12869
12866
|
);
|
|
12870
12867
|
if (!anchoredPlacement) return null;
|
|
12871
12868
|
const inputChip = this.inputProblem.chips.find(
|
|
12872
12869
|
(chip) => chip.pins.some((pin) => pin.pinId === pinId)
|
|
12873
12870
|
);
|
|
12874
12871
|
const inputPin = inputChip?.pins.find((pin) => pin.pinId === pinId);
|
|
12875
|
-
const height =
|
|
12876
|
-
const width =
|
|
12872
|
+
const height = connection.inlineNetLabelHeight ?? DEFAULT_INLINE_NET_LABEL_HEIGHT;
|
|
12873
|
+
const width = connection.inlineNetLabelWidth ?? connection.netLabelWidth ?? estimateInlineNetLabelWidth(connection.netId, height);
|
|
12877
12874
|
const stubLength = Math.max(width + 0.2, 0.6);
|
|
12878
12875
|
const start = inputPin ? { x: inputPin.x, y: inputPin.y } : anchoredPlacement.anchorPoint;
|
|
12879
12876
|
const direction = inputPin && inputChip ? inputPin._facingDirection ?? getPinDirection(inputPin, inputChip) : anchoredPlacement.orientation;
|
|
@@ -12906,7 +12903,7 @@ var InlineNetLabelSolver = class extends BaseSolver {
|
|
|
12906
12903
|
}
|
|
12907
12904
|
return {
|
|
12908
12905
|
globalConnNetId: anchoredPlacement.globalConnNetId,
|
|
12909
|
-
netId:
|
|
12906
|
+
netId: connection.netId,
|
|
12910
12907
|
pinIds: [pinId],
|
|
12911
12908
|
stubTracePath: [start, end],
|
|
12912
12909
|
axis,
|
|
@@ -12917,14 +12914,14 @@ var InlineNetLabelSolver = class extends BaseSolver {
|
|
|
12917
12914
|
side
|
|
12918
12915
|
};
|
|
12919
12916
|
}
|
|
12920
|
-
computeInlinePlacement(
|
|
12921
|
-
const traces = this.tracesByPinPairKey.get(getPinPairKey2(
|
|
12917
|
+
computeInlinePlacement(connection) {
|
|
12918
|
+
const traces = this.tracesByPinPairKey.get(getPinPairKey2(connection.pinIds)) ?? [];
|
|
12922
12919
|
if (traces.length === 0) return null;
|
|
12923
12920
|
const trace = traces[0];
|
|
12924
12921
|
const segments = getAxisAlignedSegments(trace.tracePath);
|
|
12925
12922
|
if (segments.length === 0) return null;
|
|
12926
|
-
const height =
|
|
12927
|
-
const width =
|
|
12923
|
+
const height = connection.inlineNetLabelHeight ?? DEFAULT_INLINE_NET_LABEL_HEIGHT;
|
|
12924
|
+
const width = connection.inlineNetLabelWidth ?? connection.netLabelWidth ?? estimateInlineNetLabelWidth(connection.netId, height);
|
|
12928
12925
|
const offset = height / 2 + INLINE_NET_LABEL_TRACE_MARGIN;
|
|
12929
12926
|
const usableSegments = segments.filter((segment) => segment.length >= width).sort((a, b) => b.length - a.length);
|
|
12930
12927
|
for (const segment of usableSegments) {
|
|
@@ -12952,9 +12949,9 @@ var InlineNetLabelSolver = class extends BaseSolver {
|
|
|
12952
12949
|
continue;
|
|
12953
12950
|
return {
|
|
12954
12951
|
globalConnNetId: trace.globalConnNetId,
|
|
12955
|
-
netId:
|
|
12952
|
+
netId: connection.netId,
|
|
12956
12953
|
mspPairId: trace.mspPairId,
|
|
12957
|
-
pinIds: [...
|
|
12954
|
+
pinIds: [...connection.pinIds],
|
|
12958
12955
|
axis: segment.axis,
|
|
12959
12956
|
anchorPoint,
|
|
12960
12957
|
center,
|
|
@@ -12967,7 +12964,7 @@ var InlineNetLabelSolver = class extends BaseSolver {
|
|
|
12967
12964
|
}
|
|
12968
12965
|
const spanPlacement = this.computeSpanPlacement({
|
|
12969
12966
|
trace,
|
|
12970
|
-
|
|
12967
|
+
connection,
|
|
12971
12968
|
width,
|
|
12972
12969
|
height,
|
|
12973
12970
|
offset
|
|
@@ -12983,7 +12980,7 @@ var InlineNetLabelSolver = class extends BaseSolver {
|
|
|
12983
12980
|
*/
|
|
12984
12981
|
computeSpanPlacement({
|
|
12985
12982
|
trace,
|
|
12986
|
-
|
|
12983
|
+
connection,
|
|
12987
12984
|
width,
|
|
12988
12985
|
height,
|
|
12989
12986
|
offset
|
|
@@ -13042,9 +13039,9 @@ var InlineNetLabelSolver = class extends BaseSolver {
|
|
|
13042
13039
|
const anchorPoint = axis === "x" ? { x: along, y: side === "y+" ? maxY : minY } : { x: side === "x-" ? minX : maxX, y: along };
|
|
13043
13040
|
return {
|
|
13044
13041
|
globalConnNetId: trace.globalConnNetId,
|
|
13045
|
-
netId:
|
|
13042
|
+
netId: connection.netId,
|
|
13046
13043
|
mspPairId: trace.mspPairId,
|
|
13047
|
-
pinIds: [...
|
|
13044
|
+
pinIds: [...connection.pinIds],
|
|
13048
13045
|
axis,
|
|
13049
13046
|
anchorPoint,
|
|
13050
13047
|
center,
|
|
@@ -52,8 +52,9 @@ export interface InlineNetLabelPlacement {
|
|
|
52
52
|
pinIds: PinId[]
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
* A generated single-ended trace stub. Present
|
|
56
|
-
*
|
|
55
|
+
* A generated single-ended trace stub. Present for a single-pin net or for
|
|
56
|
+
* one endpoint of an unrouted two-pin net; routed point-to-point traces omit
|
|
57
|
+
* it.
|
|
57
58
|
*/
|
|
58
59
|
stubTracePath?: [Point, Point]
|
|
59
60
|
|
|
@@ -85,12 +86,15 @@ interface InlineNetLabelSolverInput {
|
|
|
85
86
|
netLabelPlacements: NetLabelPlacement[]
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
type InlineEligibleConnection = InputDirectConnection | InputNetConnection
|
|
90
|
+
|
|
88
91
|
const getPinPairKey = (pinIds: readonly string[]) =>
|
|
89
92
|
[...pinIds].sort().join("::")
|
|
90
93
|
|
|
91
94
|
/**
|
|
92
95
|
* Places "inline net labels" - net names drawn alongside the trace they belong
|
|
93
|
-
* to - for
|
|
96
|
+
* to - for one- or two-pin connections that opted in via
|
|
97
|
+
* `allowInlineNetLabel`.
|
|
94
98
|
*
|
|
95
99
|
* Any regular (anchored) net label placement for the same net is dropped, so a
|
|
96
100
|
* net is never labeled twice.
|
|
@@ -102,11 +106,8 @@ export class InlineNetLabelSolver extends BaseSolver {
|
|
|
102
106
|
|
|
103
107
|
inlineNetLabelPlacements: InlineNetLabelPlacement[] = []
|
|
104
108
|
|
|
105
|
-
/**
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
/** Single-pin net connections that opted in, still waiting to be processed */
|
|
109
|
-
queuedPortOnlyNetConnections: InputNetConnection[]
|
|
109
|
+
/** Eligible one- or two-pin connections still waiting to be processed. */
|
|
110
|
+
queuedConnections: InlineEligibleConnection[]
|
|
110
111
|
|
|
111
112
|
private tracesByPinPairKey: Map<string, SolvedTracePath[]>
|
|
112
113
|
private hasAlignedPortOnlyStubs = false
|
|
@@ -117,12 +118,18 @@ export class InlineNetLabelSolver extends BaseSolver {
|
|
|
117
118
|
this.traces = input.traces
|
|
118
119
|
this.inputNetLabelPlacements = input.netLabelPlacements
|
|
119
120
|
|
|
120
|
-
this.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
121
|
+
this.queuedConnections = [
|
|
122
|
+
...this.inputProblem.directConnections.filter(
|
|
123
|
+
(connection) => connection.allowInlineNetLabel && connection.netId,
|
|
124
|
+
),
|
|
125
|
+
...this.inputProblem.netConnections.filter(
|
|
126
|
+
(connection) =>
|
|
127
|
+
connection.allowInlineNetLabel &&
|
|
128
|
+
connection.pinIds.length >= 1 &&
|
|
129
|
+
connection.pinIds.length <= 2 &&
|
|
130
|
+
connection.netId,
|
|
131
|
+
),
|
|
132
|
+
]
|
|
126
133
|
|
|
127
134
|
this.tracesByPinPairKey = new Map()
|
|
128
135
|
for (const trace of this.traces) {
|
|
@@ -147,22 +154,28 @@ export class InlineNetLabelSolver extends BaseSolver {
|
|
|
147
154
|
}
|
|
148
155
|
|
|
149
156
|
override _step() {
|
|
150
|
-
const
|
|
151
|
-
if (
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
157
|
+
const connection = this.queuedConnections.shift()
|
|
158
|
+
if (connection) {
|
|
159
|
+
const routedTraces =
|
|
160
|
+
connection.pinIds.length === 2
|
|
161
|
+
? (this.tracesByPinPairKey.get(getPinPairKey(connection.pinIds)) ??
|
|
162
|
+
[])
|
|
163
|
+
: []
|
|
164
|
+
|
|
165
|
+
if (routedTraces.length > 0) {
|
|
166
|
+
const placement = this.computeInlinePlacement(connection)
|
|
167
|
+
if (placement) this.inlineNetLabelPlacements.push(placement)
|
|
168
|
+
} else {
|
|
169
|
+
// A one-pin net has one conventional endpoint label, while a skipped
|
|
170
|
+
// two-pin route has one at each endpoint. Convert a two-pin pair
|
|
171
|
+
// atomically so a collision at one endpoint cannot leave mixed inline
|
|
172
|
+
// and anchored representations for the same net.
|
|
173
|
+
const terminalPlacements = connection.pinIds.map((pinId) =>
|
|
174
|
+
this.computeTerminalInlinePlacement(connection, pinId),
|
|
175
|
+
)
|
|
176
|
+
if (terminalPlacements.every((placement) => placement !== null)) {
|
|
177
|
+
this.inlineNetLabelPlacements.push(...terminalPlacements)
|
|
178
|
+
}
|
|
166
179
|
}
|
|
167
180
|
return
|
|
168
181
|
}
|
|
@@ -183,20 +196,20 @@ export class InlineNetLabelSolver extends BaseSolver {
|
|
|
183
196
|
}
|
|
184
197
|
|
|
185
198
|
/**
|
|
186
|
-
* Converts
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
199
|
+
* Converts one conventional endpoint placement into an inline label on a
|
|
200
|
+
* generated outward stub. The stub follows the pin's true facing direction;
|
|
201
|
+
* an anchored label may finish in another direction after an elbow, which is
|
|
202
|
+
* not the direction a terminal stub should leave the pin.
|
|
190
203
|
*/
|
|
191
|
-
private
|
|
192
|
-
|
|
204
|
+
private computeTerminalInlinePlacement(
|
|
205
|
+
connection: InlineEligibleConnection,
|
|
206
|
+
pinId: PinId,
|
|
193
207
|
): InlineNetLabelPlacement | null {
|
|
194
|
-
|
|
195
|
-
if (!pinId) return null
|
|
208
|
+
if (!connection.netId) return null
|
|
196
209
|
|
|
197
210
|
const anchoredPlacement = this.inputNetLabelPlacements.find(
|
|
198
211
|
(placement) =>
|
|
199
|
-
placement.netId ===
|
|
212
|
+
placement.netId === connection.netId &&
|
|
200
213
|
placement.pinIds.length === 1 &&
|
|
201
214
|
placement.pinIds[0] === pinId,
|
|
202
215
|
)
|
|
@@ -208,11 +221,11 @@ export class InlineNetLabelSolver extends BaseSolver {
|
|
|
208
221
|
const inputPin = inputChip?.pins.find((pin) => pin.pinId === pinId)
|
|
209
222
|
|
|
210
223
|
const height =
|
|
211
|
-
|
|
224
|
+
connection.inlineNetLabelHeight ?? DEFAULT_INLINE_NET_LABEL_HEIGHT
|
|
212
225
|
const width =
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
estimateInlineNetLabelWidth(
|
|
226
|
+
connection.inlineNetLabelWidth ??
|
|
227
|
+
connection.netLabelWidth ??
|
|
228
|
+
estimateInlineNetLabelWidth(connection.netId, height)
|
|
216
229
|
|
|
217
230
|
// Leave a small wire tail at both ends of the text so it unmistakably
|
|
218
231
|
// reads as a label on a trace rather than free-standing text.
|
|
@@ -273,7 +286,7 @@ export class InlineNetLabelSolver extends BaseSolver {
|
|
|
273
286
|
|
|
274
287
|
return {
|
|
275
288
|
globalConnNetId: anchoredPlacement.globalConnNetId,
|
|
276
|
-
netId:
|
|
289
|
+
netId: connection.netId,
|
|
277
290
|
pinIds: [pinId],
|
|
278
291
|
stubTracePath: [start, end],
|
|
279
292
|
axis,
|
|
@@ -286,12 +299,12 @@ export class InlineNetLabelSolver extends BaseSolver {
|
|
|
286
299
|
}
|
|
287
300
|
|
|
288
301
|
private computeInlinePlacement(
|
|
289
|
-
|
|
302
|
+
connection: InlineEligibleConnection,
|
|
290
303
|
): InlineNetLabelPlacement | null {
|
|
291
304
|
// Only connections the router actually drew a trace for can carry an inline
|
|
292
305
|
// label - there's nothing to run parallel to otherwise.
|
|
293
306
|
const traces =
|
|
294
|
-
this.tracesByPinPairKey.get(getPinPairKey(
|
|
307
|
+
this.tracesByPinPairKey.get(getPinPairKey(connection.pinIds)) ?? []
|
|
295
308
|
if (traces.length === 0) return null
|
|
296
309
|
|
|
297
310
|
const trace = traces[0]!
|
|
@@ -299,11 +312,11 @@ export class InlineNetLabelSolver extends BaseSolver {
|
|
|
299
312
|
if (segments.length === 0) return null
|
|
300
313
|
|
|
301
314
|
const height =
|
|
302
|
-
|
|
315
|
+
connection.inlineNetLabelHeight ?? DEFAULT_INLINE_NET_LABEL_HEIGHT
|
|
303
316
|
const width =
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
estimateInlineNetLabelWidth(
|
|
317
|
+
connection.inlineNetLabelWidth ??
|
|
318
|
+
connection.netLabelWidth ??
|
|
319
|
+
estimateInlineNetLabelWidth(connection.netId!, height)
|
|
307
320
|
|
|
308
321
|
const offset = height / 2 + INLINE_NET_LABEL_TRACE_MARGIN
|
|
309
322
|
|
|
@@ -360,9 +373,9 @@ export class InlineNetLabelSolver extends BaseSolver {
|
|
|
360
373
|
|
|
361
374
|
return {
|
|
362
375
|
globalConnNetId: trace.globalConnNetId,
|
|
363
|
-
netId:
|
|
376
|
+
netId: connection.netId,
|
|
364
377
|
mspPairId: trace.mspPairId,
|
|
365
|
-
pinIds: [...
|
|
378
|
+
pinIds: [...connection.pinIds],
|
|
366
379
|
axis: segment.axis,
|
|
367
380
|
anchorPoint,
|
|
368
381
|
center,
|
|
@@ -380,7 +393,7 @@ export class InlineNetLabelSolver extends BaseSolver {
|
|
|
380
393
|
// endpoints.
|
|
381
394
|
const spanPlacement = this.computeSpanPlacement({
|
|
382
395
|
trace,
|
|
383
|
-
|
|
396
|
+
connection,
|
|
384
397
|
width,
|
|
385
398
|
height,
|
|
386
399
|
offset,
|
|
@@ -400,13 +413,13 @@ export class InlineNetLabelSolver extends BaseSolver {
|
|
|
400
413
|
*/
|
|
401
414
|
private computeSpanPlacement({
|
|
402
415
|
trace,
|
|
403
|
-
|
|
416
|
+
connection,
|
|
404
417
|
width,
|
|
405
418
|
height,
|
|
406
419
|
offset,
|
|
407
420
|
}: {
|
|
408
421
|
trace: SolvedTracePath
|
|
409
|
-
|
|
422
|
+
connection: InlineEligibleConnection
|
|
410
423
|
width: number
|
|
411
424
|
height: number
|
|
412
425
|
offset: number
|
|
@@ -496,9 +509,9 @@ export class InlineNetLabelSolver extends BaseSolver {
|
|
|
496
509
|
|
|
497
510
|
return {
|
|
498
511
|
globalConnNetId: trace.globalConnNetId,
|
|
499
|
-
netId:
|
|
512
|
+
netId: connection.netId,
|
|
500
513
|
mspPairId: trace.mspPairId,
|
|
501
|
-
pinIds: [...
|
|
514
|
+
pinIds: [...connection.pinIds],
|
|
502
515
|
axis,
|
|
503
516
|
anchorPoint,
|
|
504
517
|
center,
|
|
@@ -43,7 +43,9 @@ export interface InputDirectConnection {
|
|
|
43
43
|
*
|
|
44
44
|
* Only set this for connections whose net name is worth showing on the wire -
|
|
45
45
|
* the solver trusts the caller (e.g. @tscircuit/core) to make that decision.
|
|
46
|
-
*
|
|
46
|
+
* When the connection is routed, the label is placed along the trace. If
|
|
47
|
+
* routing is intentionally skipped, both endpoint labels may instead become
|
|
48
|
+
* outward inline stubs.
|
|
47
49
|
*/
|
|
48
50
|
allowInlineNetLabel?: boolean
|
|
49
51
|
|
|
@@ -67,9 +69,10 @@ export interface InputNetConnection {
|
|
|
67
69
|
netLabelHeight?: number
|
|
68
70
|
|
|
69
71
|
/**
|
|
70
|
-
* When true, a named
|
|
71
|
-
*
|
|
72
|
-
*
|
|
72
|
+
* When true, a named one- or two-pin net may use inline labels. A single-pin
|
|
73
|
+
* net gets an outward stub. A routed two-pin net gets one label along its
|
|
74
|
+
* trace; when that route is intentionally skipped, both endpoints get
|
|
75
|
+
* outward stubs. Nets with more than two pins retain anchored labels.
|
|
73
76
|
*/
|
|
74
77
|
allowInlineNetLabel?: boolean
|
|
75
78
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"chips": [
|
|
3
|
+
{
|
|
4
|
+
"chipId": "U1",
|
|
5
|
+
"center": { "x": -1, "y": 1 },
|
|
6
|
+
"width": 1,
|
|
7
|
+
"height": 1,
|
|
8
|
+
"sectionId": "routed",
|
|
9
|
+
"pins": [
|
|
10
|
+
{ "pinId": "U1.1", "x": -0.5, "y": 1, "_facingDirection": "x+" }
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"chipId": "U2",
|
|
15
|
+
"center": { "x": 1, "y": 1 },
|
|
16
|
+
"width": 1,
|
|
17
|
+
"height": 1,
|
|
18
|
+
"sectionId": "routed",
|
|
19
|
+
"pins": [
|
|
20
|
+
{ "pinId": "U2.1", "x": 0.5, "y": 1, "_facingDirection": "x-" }
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"chipId": "U3",
|
|
25
|
+
"center": { "x": -2, "y": -1 },
|
|
26
|
+
"width": 1,
|
|
27
|
+
"height": 1,
|
|
28
|
+
"sectionId": "left",
|
|
29
|
+
"pins": [
|
|
30
|
+
{ "pinId": "U3.1", "x": -1.5, "y": -1, "_facingDirection": "x+" }
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"chipId": "U4",
|
|
35
|
+
"center": { "x": 2, "y": -1 },
|
|
36
|
+
"width": 1,
|
|
37
|
+
"height": 1,
|
|
38
|
+
"sectionId": "right",
|
|
39
|
+
"pins": [
|
|
40
|
+
{ "pinId": "U4.1", "x": 1.5, "y": -1, "_facingDirection": "x-" }
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"directConnections": [],
|
|
45
|
+
"netConnections": [
|
|
46
|
+
{
|
|
47
|
+
"netId": "NET_ROUTED",
|
|
48
|
+
"pinIds": ["U1.1", "U2.1"],
|
|
49
|
+
"netLabelWidth": 0.9,
|
|
50
|
+
"allowInlineNetLabel": true,
|
|
51
|
+
"inlineNetLabelWidth": 0.9,
|
|
52
|
+
"inlineNetLabelHeight": 0.12
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"netId": "NET_SECTIONED",
|
|
56
|
+
"pinIds": ["U3.1", "U4.1"],
|
|
57
|
+
"netLabelWidth": 1.1,
|
|
58
|
+
"allowInlineNetLabel": true,
|
|
59
|
+
"inlineNetLabelWidth": 1.1,
|
|
60
|
+
"inlineNetLabelHeight": 0.12
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
"availableNetLabelOrientations": {
|
|
64
|
+
"NET_ROUTED": ["x-", "x+"],
|
|
65
|
+
"NET_SECTIONED": ["x-", "x+"]
|
|
66
|
+
}
|
|
67
|
+
}
|
package/tests/solvers/InlineNetLabelSolver/__snapshots__/inline-net-label-two-pin-net.snap.svg
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<svg width="640" height="640" viewBox="0 0 640 640" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="white"/><g><polyline data-points="-0.5,1 0.5,1" data-type="line" data-label="" points="264,208 376,208" fill="none" stroke="hsl(253, 100%, 50%, 0.8)" stroke-width="1px" stroke-dasharray="4 2"/></g><g><polyline data-points="-0.5,1 0.5,1" data-type="line" data-label="" points="264,208 376,208" fill="none" stroke="purple" stroke-width="1px"/></g><g><polyline data-points="-1.5,-1 -0.19999999999999996,-1" data-type="line" data-label="" points="152,432 297.6,432" fill="none" stroke="purple" stroke-width="1px"/></g><g><polyline data-points="1.5,-1 0.19999999999999996,-1" data-type="line" data-label="" points="488,432 342.4,432" fill="none" stroke="purple" stroke-width="1px"/></g><g><rect data-type="rect" data-label="U1" data-x="-1" data-y="1" x="152" y="152" width="112" height="112" fill="hsl(164, 100%, 50%, 0.8)" stroke="black" stroke-width="0.008928571428571428"/></g><g><rect data-type="rect" data-label="U2" data-x="1" data-y="1" x="376" y="152" width="112" height="112" fill="hsl(165, 100%, 50%, 0.8)" stroke="black" stroke-width="0.008928571428571428"/></g><g><rect data-type="rect" data-label="U3" data-x="-2" data-y="-1" x="40" y="376" width="112" height="112" fill="hsl(166, 100%, 50%, 0.8)" stroke="black" stroke-width="0.008928571428571428"/></g><g><rect data-type="rect" data-label="U4" data-x="2" data-y="-1" x="488" y="376" width="112" height="112" fill="hsl(167, 100%, 50%, 0.8)" stroke="black" stroke-width="0.008928571428571428"/></g><g><rect data-type="rect" data-label="INLINE netId: NET_ROUTED
|
|
2
|
+
axis: x
|
|
3
|
+
side: y+" data-x="0" data-y="1.11" x="269.6" y="188.96" width="100.79999999999995" height="13.439999999999998" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.008928571428571428"/></g><g><rect data-type="rect" data-label="INLINE netId: NET_SECTIONED
|
|
4
|
+
axis: x
|
|
5
|
+
side: y+" data-x="-0.85" data-y="-0.89" x="163.20000000000005" y="412.96000000000004" width="123.19999999999996" height="13.43999999999994" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.008928571428571428"/></g><g><rect data-type="rect" data-label="INLINE netId: NET_SECTIONED
|
|
6
|
+
axis: x
|
|
7
|
+
side: y+" data-x="0.85" data-y="-0.89" x="353.6" y="412.96000000000004" width="123.19999999999993" height="13.43999999999994" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.008928571428571428"/></g><text data-type="text" data-label="NET_ROUTED" data-x="0" data-y="1.11" x="320" y="195.68" fill="green" font-size="13.44" font-family="sans-serif" text-anchor="middle" dominant-baseline="central">NET_ROUTED</text><text data-type="text" data-label="NET_SECTIONED" data-x="-1.4" data-y="-0.89" x="163.20000000000002" y="419.68" fill="green" font-size="13.44" font-family="sans-serif" text-anchor="start" dominant-baseline="central">NET_SECTIONED</text><text data-type="text" data-label="NET_SECTIONED" data-x="1.4" data-y="-0.89" x="476.79999999999995" y="419.68" fill="green" font-size="13.44" font-family="sans-serif" text-anchor="end" dominant-baseline="central">NET_SECTIONED</text><g><circle data-type="point" data-label="U1.1
|
|
8
|
+
x+" data-x="-0.5" data-y="1" cx="264" cy="208" r="3" fill="hsl(319, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="U2.1
|
|
9
|
+
x-" data-x="0.5" data-y="1" cx="376" cy="208" r="3" fill="hsl(200, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="U3.1
|
|
10
|
+
x+" data-x="-1.5" data-y="-1" cx="152" cy="432" r="3" fill="hsl(81, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="U4.1
|
|
11
|
+
x-" data-x="1.5" data-y="-1" cx="488" cy="432" r="3" fill="hsl(322, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="inline anchor
|
|
12
|
+
NET_ROUTED" data-x="0" data-y="1" cx="320" cy="208" r="3" fill="green"/></g><g><circle data-type="point" data-label="inline anchor
|
|
13
|
+
NET_SECTIONED" data-x="-0.85" data-y="-1" cx="224.8" cy="432" r="3" fill="green"/></g><g><circle data-type="point" data-label="inline anchor
|
|
14
|
+
NET_SECTIONED" data-x="0.85" data-y="-1" cx="415.2" cy="432" r="3" fill="green"/></g><g id="crosshair" style="display: none"><line id="crosshair-h" y1="0" y2="640" stroke="#666" stroke-width="0.5"/><line id="crosshair-v" x1="0" x2="640" stroke="#666" stroke-width="0.5"/><text id="coordinates" font-family="monospace" font-size="12" fill="#666"></text></g><script><![CDATA[
|
|
15
|
+
document.currentScript.parentElement.addEventListener('mousemove', (e) => {
|
|
16
|
+
const svg = e.currentTarget;
|
|
17
|
+
const rect = svg.getBoundingClientRect();
|
|
18
|
+
const x = e.clientX - rect.left;
|
|
19
|
+
const y = e.clientY - rect.top;
|
|
20
|
+
const crosshair = svg.getElementById('crosshair');
|
|
21
|
+
const h = svg.getElementById('crosshair-h');
|
|
22
|
+
const v = svg.getElementById('crosshair-v');
|
|
23
|
+
const coords = svg.getElementById('coordinates');
|
|
24
|
+
|
|
25
|
+
crosshair.style.display = 'block';
|
|
26
|
+
h.setAttribute('x1', '0');
|
|
27
|
+
h.setAttribute('x2', '640');
|
|
28
|
+
h.setAttribute('y1', y);
|
|
29
|
+
h.setAttribute('y2', y);
|
|
30
|
+
v.setAttribute('x1', x);
|
|
31
|
+
v.setAttribute('x2', x);
|
|
32
|
+
v.setAttribute('y1', '0');
|
|
33
|
+
v.setAttribute('y2', '640');
|
|
34
|
+
|
|
35
|
+
// Calculate real coordinates using inverse transformation
|
|
36
|
+
const matrix = {"a":112,"c":0,"e":320,"b":0,"d":-112,"f":320};
|
|
37
|
+
// Manually invert and apply the affine transform
|
|
38
|
+
// Since we only use translate and scale, we can directly compute:
|
|
39
|
+
// x' = (x - tx) / sx
|
|
40
|
+
// y' = (y - ty) / sy
|
|
41
|
+
const sx = matrix.a;
|
|
42
|
+
const sy = matrix.d;
|
|
43
|
+
const tx = matrix.e;
|
|
44
|
+
const ty = matrix.f;
|
|
45
|
+
const realPoint = {
|
|
46
|
+
x: (x - tx) / sx,
|
|
47
|
+
y: (y - ty) / sy // Flip y back since we used negative scale
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
coords.textContent = `(${realPoint.x.toFixed(2)}, ${realPoint.y.toFixed(2)})`;
|
|
51
|
+
coords.setAttribute('x', (x + 5).toString());
|
|
52
|
+
coords.setAttribute('y', (y - 5).toString());
|
|
53
|
+
});
|
|
54
|
+
document.currentScript.parentElement.addEventListener('mouseleave', () => {
|
|
55
|
+
document.currentScript.parentElement.getElementById('crosshair').style.display = 'none';
|
|
56
|
+
});
|
|
57
|
+
]]></script></svg>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
|
|
3
|
+
import inputProblem from "../../assets/inline-net-label-two-pin-net.json"
|
|
4
|
+
import "tests/fixtures/matcher"
|
|
5
|
+
|
|
6
|
+
test("two-pin named nets use a routed inline label or two terminal stubs", () => {
|
|
7
|
+
const solver = new SchematicTracePipelineSolver(inputProblem as any)
|
|
8
|
+
|
|
9
|
+
solver.solve()
|
|
10
|
+
|
|
11
|
+
const output = solver.inlineNetLabelSolver!.getOutput()
|
|
12
|
+
const routedPlacements = output.inlineNetLabelPlacements.filter(
|
|
13
|
+
(placement) => placement.netId === "NET_ROUTED",
|
|
14
|
+
)
|
|
15
|
+
const sectionedPlacements = output.inlineNetLabelPlacements.filter(
|
|
16
|
+
(placement) => placement.netId === "NET_SECTIONED",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
expect(routedPlacements).toHaveLength(1)
|
|
20
|
+
expect(routedPlacements[0]!.pinIds).toHaveLength(2)
|
|
21
|
+
expect(routedPlacements[0]!.stubTracePath).toBeUndefined()
|
|
22
|
+
|
|
23
|
+
expect(sectionedPlacements).toHaveLength(2)
|
|
24
|
+
expect(
|
|
25
|
+
sectionedPlacements.every(
|
|
26
|
+
(placement) =>
|
|
27
|
+
placement.pinIds.length === 1 && placement.stubTracePath?.length === 2,
|
|
28
|
+
),
|
|
29
|
+
).toBe(true)
|
|
30
|
+
expect(output.netLabelPlacements).toHaveLength(0)
|
|
31
|
+
expect(
|
|
32
|
+
output.traces.filter((trace) =>
|
|
33
|
+
trace.mspPairId.startsWith("available-net-orientation-"),
|
|
34
|
+
),
|
|
35
|
+
).toHaveLength(0)
|
|
36
|
+
|
|
37
|
+
expect(solver).toMatchSolverSnapshot(import.meta.path)
|
|
38
|
+
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { InlineNetLabelSolver } from "lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver"
|
|
3
|
+
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
4
|
+
import type { InputProblem } from "lib/types/InputProblem"
|
|
5
|
+
|
|
6
|
+
test("two-pin terminal stubs fall back atomically when one endpoint is obstructed", () => {
|
|
7
|
+
const inputProblem: InputProblem = {
|
|
8
|
+
chips: [
|
|
9
|
+
{
|
|
10
|
+
chipId: "U1",
|
|
11
|
+
center: { x: -2, y: 0 },
|
|
12
|
+
width: 1,
|
|
13
|
+
height: 1,
|
|
14
|
+
pins: [{ pinId: "U1.1", x: -1.5, y: 0, _facingDirection: "x+" }],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
chipId: "U2",
|
|
18
|
+
center: { x: 2, y: 0 },
|
|
19
|
+
width: 1,
|
|
20
|
+
height: 1,
|
|
21
|
+
pins: [{ pinId: "U2.1", x: 1.5, y: 0, _facingDirection: "x-" }],
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
directConnections: [],
|
|
25
|
+
netConnections: [
|
|
26
|
+
{
|
|
27
|
+
netId: "NET_SIGNAL",
|
|
28
|
+
pinIds: ["U1.1", "U2.1"],
|
|
29
|
+
allowInlineNetLabel: true,
|
|
30
|
+
inlineNetLabelWidth: 1.1,
|
|
31
|
+
inlineNetLabelHeight: 0.12,
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
textBoxes: [
|
|
35
|
+
{
|
|
36
|
+
center: { x: -0.85, y: 0.11 },
|
|
37
|
+
width: 1.1,
|
|
38
|
+
height: 0.12,
|
|
39
|
+
text: "obstacle",
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
availableNetLabelOrientations: { NET_SIGNAL: ["x-", "x+"] },
|
|
43
|
+
}
|
|
44
|
+
const netLabelPlacements: NetLabelPlacement[] = [
|
|
45
|
+
{
|
|
46
|
+
globalConnNetId: "NET_SIGNAL",
|
|
47
|
+
netId: "NET_SIGNAL",
|
|
48
|
+
mspConnectionPairIds: [],
|
|
49
|
+
pinIds: ["U1.1"],
|
|
50
|
+
orientation: "x+",
|
|
51
|
+
anchorPoint: { x: -1.5, y: 0 },
|
|
52
|
+
center: { x: -0.9, y: 0 },
|
|
53
|
+
width: 1.1,
|
|
54
|
+
height: 0.12,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
globalConnNetId: "NET_SIGNAL",
|
|
58
|
+
netId: "NET_SIGNAL",
|
|
59
|
+
mspConnectionPairIds: [],
|
|
60
|
+
pinIds: ["U2.1"],
|
|
61
|
+
orientation: "x-",
|
|
62
|
+
anchorPoint: { x: 1.5, y: 0 },
|
|
63
|
+
center: { x: 0.9, y: 0 },
|
|
64
|
+
width: 1.1,
|
|
65
|
+
height: 0.12,
|
|
66
|
+
},
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
const solver = new InlineNetLabelSolver({
|
|
70
|
+
inputProblem,
|
|
71
|
+
traces: [],
|
|
72
|
+
netLabelPlacements,
|
|
73
|
+
})
|
|
74
|
+
solver.solve()
|
|
75
|
+
|
|
76
|
+
expect(solver.inlineNetLabelPlacements).toHaveLength(0)
|
|
77
|
+
expect(solver.getOutput().netLabelPlacements).toHaveLength(2)
|
|
78
|
+
})
|