@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 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
- * An inline label is only emitted when the connection actually got routed.
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 single-pin net may be drawn as a short outward trace
120
- * stub with its net name placed inline. Multi-pin net connections retain the
121
- * regular anchored-label behavior.
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 only for an eligible
1204
- * single-pin net connection; routed point-to-point traces omit it.
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 direct connections that opted in via `allowInlineNetLabel`.
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
- /** Direct connections that opted in, still waiting to be processed */
1242
- queuedDirectConnections: InputDirectConnection[];
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 a conventional port-only anchored placement into an inline label
1252
- * on a generated outward stub. The stub follows the pin's true facing
1253
- * direction; an anchored label may finish in another direction after an
1254
- * elbow, which is not the direction a terminal stub should leave the pin.
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 computePortOnlyInlinePlacement;
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
- /** Direct connections that opted in, still waiting to be processed */
12790
- queuedDirectConnections;
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.queuedDirectConnections = this.inputProblem.directConnections.filter(
12801
- (dc) => dc.allowInlineNetLabel && dc.netId
12802
- );
12803
- this.queuedPortOnlyNetConnections = this.inputProblem.netConnections.filter(
12804
- (nc) => nc.allowInlineNetLabel && nc.pinIds.length === 1 && nc.netId
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 directConnection = this.queuedDirectConnections.shift();
12828
- if (directConnection) {
12829
- const placement = this.computeInlinePlacement(directConnection);
12830
- if (placement) {
12831
- this.inlineNetLabelPlacements.push(placement);
12832
- }
12833
- return;
12834
- }
12835
- const portOnlyNetConnection = this.queuedPortOnlyNetConnections.shift();
12836
- if (portOnlyNetConnection) {
12837
- const placement = this.computePortOnlyInlinePlacement(
12838
- portOnlyNetConnection
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 a conventional port-only anchored placement into an inline label
12860
- * on a generated outward stub. The stub follows the pin's true facing
12861
- * direction; an anchored label may finish in another direction after an
12862
- * elbow, which is not the direction a terminal stub should leave the pin.
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
- computePortOnlyInlinePlacement(netConnection) {
12865
- const [pinId] = netConnection.pinIds;
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 === netConnection.netId && placement.pinIds.length === 1 && placement.pinIds[0] === pinId
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 = netConnection.inlineNetLabelHeight ?? DEFAULT_INLINE_NET_LABEL_HEIGHT;
12876
- const width = netConnection.inlineNetLabelWidth ?? netConnection.netLabelWidth ?? estimateInlineNetLabelWidth(netConnection.netId, height);
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: netConnection.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(directConnection) {
12921
- const traces = this.tracesByPinPairKey.get(getPinPairKey2(directConnection.pinIds)) ?? [];
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 = directConnection.inlineNetLabelHeight ?? DEFAULT_INLINE_NET_LABEL_HEIGHT;
12927
- const width = directConnection.inlineNetLabelWidth ?? directConnection.netLabelWidth ?? estimateInlineNetLabelWidth(directConnection.netId, height);
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: directConnection.netId,
12952
+ netId: connection.netId,
12956
12953
  mspPairId: trace.mspPairId,
12957
- pinIds: [...directConnection.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
- directConnection,
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
- directConnection,
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: directConnection.netId,
13042
+ netId: connection.netId,
13046
13043
  mspPairId: trace.mspPairId,
13047
- pinIds: [...directConnection.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 only for an eligible
56
- * single-pin net connection; routed point-to-point traces omit it.
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 direct connections that opted in via `allowInlineNetLabel`.
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
- /** Direct connections that opted in, still waiting to be processed */
106
- queuedDirectConnections: InputDirectConnection[]
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.queuedDirectConnections = this.inputProblem.directConnections.filter(
121
- (dc) => dc.allowInlineNetLabel && dc.netId,
122
- )
123
- this.queuedPortOnlyNetConnections = this.inputProblem.netConnections.filter(
124
- (nc) => nc.allowInlineNetLabel && nc.pinIds.length === 1 && nc.netId,
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 directConnection = this.queuedDirectConnections.shift()
151
- if (directConnection) {
152
- const placement = this.computeInlinePlacement(directConnection)
153
- if (placement) {
154
- this.inlineNetLabelPlacements.push(placement)
155
- }
156
- return
157
- }
158
-
159
- const portOnlyNetConnection = this.queuedPortOnlyNetConnections.shift()
160
- if (portOnlyNetConnection) {
161
- const placement = this.computePortOnlyInlinePlacement(
162
- portOnlyNetConnection,
163
- )
164
- if (placement) {
165
- this.inlineNetLabelPlacements.push(placement)
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 a conventional port-only anchored placement into an inline label
187
- * on a generated outward stub. The stub follows the pin's true facing
188
- * direction; an anchored label may finish in another direction after an
189
- * elbow, which is not the direction a terminal stub should leave the pin.
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 computePortOnlyInlinePlacement(
192
- netConnection: InputNetConnection,
204
+ private computeTerminalInlinePlacement(
205
+ connection: InlineEligibleConnection,
206
+ pinId: PinId,
193
207
  ): InlineNetLabelPlacement | null {
194
- const [pinId] = netConnection.pinIds
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 === netConnection.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
- netConnection.inlineNetLabelHeight ?? DEFAULT_INLINE_NET_LABEL_HEIGHT
224
+ connection.inlineNetLabelHeight ?? DEFAULT_INLINE_NET_LABEL_HEIGHT
212
225
  const width =
213
- netConnection.inlineNetLabelWidth ??
214
- netConnection.netLabelWidth ??
215
- estimateInlineNetLabelWidth(netConnection.netId, height)
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: netConnection.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
- directConnection: InputDirectConnection,
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(directConnection.pinIds)) ?? []
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
- directConnection.inlineNetLabelHeight ?? DEFAULT_INLINE_NET_LABEL_HEIGHT
315
+ connection.inlineNetLabelHeight ?? DEFAULT_INLINE_NET_LABEL_HEIGHT
303
316
  const width =
304
- directConnection.inlineNetLabelWidth ??
305
- directConnection.netLabelWidth ??
306
- estimateInlineNetLabelWidth(directConnection.netId!, height)
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: directConnection.netId,
376
+ netId: connection.netId,
364
377
  mspPairId: trace.mspPairId,
365
- pinIds: [...directConnection.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
- directConnection,
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
- directConnection,
416
+ connection,
404
417
  width,
405
418
  height,
406
419
  offset,
407
420
  }: {
408
421
  trace: SolvedTracePath
409
- directConnection: InputDirectConnection
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: directConnection.netId,
512
+ netId: connection.netId,
500
513
  mspPairId: trace.mspPairId,
501
- pinIds: [...directConnection.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
- * An inline label is only emitted when the connection actually got routed.
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 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.
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
@@ -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.143",
8
+ "version": "0.0.144",
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": -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
+ }
@@ -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
+ })