@tscircuit/schematic-trace-solver 0.0.143 → 0.0.145

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.
@@ -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,
@@ -14,7 +14,7 @@ import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualize
14
14
  import { doesPairCrossRestrictedCenterLines } from "./doesPairCrossRestrictedCenterLines"
15
15
  import { getConnectivityMapsFromInputProblem } from "./getConnectivityMapFromInputProblem"
16
16
  import { getOrthogonalMinimumSpanningTree } from "./getMspConnectionPairsFromPins"
17
- import { isLabeledPeripheralConnection } from "./isLabeledPeripheralConnection"
17
+ import { getLabeledConnectionRouteReason } from "./isLabeledPeripheralConnection"
18
18
 
19
19
  export type MspConnectionPairId = string
20
20
  export const DEFAULT_MAX_MSP_PAIR_DISTANCE = 1
@@ -26,6 +26,8 @@ export type MspConnectionPair = {
26
26
  dcConnNetId: string
27
27
  globalConnNetId: string
28
28
  userNetId?: string
29
+ /** The trace replaces fallback labels that could not fit between its pins. */
30
+ suppressNetLabel?: boolean
29
31
  pins: [InputPin & { chipId: string }, InputPin & { chipId: string }]
30
32
  }
31
33
 
@@ -121,14 +123,17 @@ export class MspConnectionPairSolver extends BaseSolver {
121
123
  if (this.directConnectionPinPairKeys.has(pinPairKey)) {
122
124
  pairDistance = distance(p1, p2)
123
125
  }
124
- // Labeled one-pin peripherals need a real trace even when they are far
125
- // apart; skipping the MSP pair would leave only the fallback path.
126
- const isLabeledPeripheral = isLabeledPeripheralConnection({
126
+ // Labeled one-pin peripherals and opposed pins whose fallback labels
127
+ // cannot fit need a real trace even when they are far apart.
128
+ const labeledConnectionRouteReason = getLabeledConnectionRouteReason({
127
129
  inputProblem: this.inputProblem,
128
130
  chipMap: this.chipMap,
129
131
  pins: [p1, p2],
130
132
  })
131
- if (pairDistance > this.maxMspPairDistance && !isLabeledPeripheral) {
133
+ if (
134
+ pairDistance > this.maxMspPairDistance &&
135
+ !labeledConnectionRouteReason
136
+ ) {
132
137
  // Too far apart; skip creating an MSP pair for this net
133
138
  return
134
139
  }
@@ -155,12 +160,16 @@ export class MspConnectionPairSolver extends BaseSolver {
155
160
  const globalConnNetId = this.globalConnMap.getNetConnectedToId(pin1!)!
156
161
  const userNetId =
157
162
  this.userNetIdByPinId[pin1!] ?? this.userNetIdByPinId[pin2!]
163
+ const suppressNetLabel =
164
+ pairDistance > this.maxMspPairDistance &&
165
+ labeledConnectionRouteReason === "overlapping-fallback-labels"
158
166
 
159
167
  this.mspConnectionPairs.push({
160
168
  mspPairId: `${pin1}-${pin2}`,
161
169
  dcConnNetId: dcNetId,
162
170
  globalConnNetId,
163
171
  userNetId,
172
+ suppressNetLabel,
164
173
  pins: [p1, p2],
165
174
  })
166
175
 
@@ -1,9 +1,14 @@
1
1
  import type { InputChip, InputPin, InputProblem } from "lib/types/InputProblem"
2
+ import { NET_LABEL_HORIZONTAL_HEIGHT } from "../NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
2
3
  import { getPinDirection } from "../SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/getPinDirection"
3
4
 
4
5
  type PinWithChipId = InputPin & { chipId: string }
5
6
 
6
- export const isLabeledPeripheralConnection = ({
7
+ export type LabeledConnectionRouteReason =
8
+ | "single-pin-peripheral"
9
+ | "overlapping-fallback-labels"
10
+
11
+ export const getLabeledConnectionRouteReason = ({
7
12
  inputProblem,
8
13
  chipMap,
9
14
  pins,
@@ -11,22 +16,18 @@ export const isLabeledPeripheralConnection = ({
11
16
  inputProblem: InputProblem
12
17
  chipMap: Record<string, InputChip>
13
18
  pins: [PinWithChipId, PinWithChipId]
14
- }): boolean => {
19
+ }): LabeledConnectionRouteReason | null => {
15
20
  const [firstPin, secondPin] = pins
16
21
  const directConnection = inputProblem.directConnections.find(
17
22
  (connection) =>
18
23
  connection.pinIds.includes(firstPin.pinId) &&
19
24
  connection.pinIds.includes(secondPin.pinId),
20
25
  )
21
- if (directConnection?.netLabelWidth === undefined) return false
26
+ if (directConnection?.netLabelWidth === undefined) return null
22
27
 
23
28
  const firstChip = chipMap[firstPin.chipId]
24
29
  const secondChip = chipMap[secondPin.chipId]
25
- if (!firstChip || !secondChip) return false
26
-
27
- const hasSinglePinPeripheral =
28
- firstChip.pins.length === 1 || secondChip.pins.length === 1
29
- if (!hasSinglePinPeripheral) return false
30
+ if (!firstChip || !secondChip) return null
30
31
 
31
32
  let firstFacingDirection = firstPin._facingDirection
32
33
  if (!firstFacingDirection) {
@@ -37,8 +38,43 @@ export const isLabeledPeripheralConnection = ({
37
38
  secondFacingDirection = getPinDirection(secondPin, secondChip)
38
39
  }
39
40
 
40
- return (
41
+ const hasOpposingHorizontalDirections =
41
42
  (firstFacingDirection === "x-" && secondFacingDirection === "x+") ||
42
43
  (firstFacingDirection === "x+" && secondFacingDirection === "x-")
43
- )
44
+ if (!hasOpposingHorizontalDirections) return null
45
+
46
+ const hasSinglePinPeripheral =
47
+ firstChip.pins.length === 1 || secondChip.pins.length === 1
48
+ if (hasSinglePinPeripheral) return "single-pin-peripheral"
49
+
50
+ // The overlap fallback is intended for explicitly grouped schematic
51
+ // sections, where hierarchy boxes and their nearby connectors are laid out
52
+ // as one visual unit. Applying it to ordinary sheets can reroute unrelated
53
+ // component-to-component labels elsewhere in the design.
54
+ const sharedSectionId =
55
+ firstChip.sectionId && firstChip.sectionId === secondChip.sectionId
56
+ ? firstChip.sectionId
57
+ : null
58
+ if (!sharedSectionId) return null
59
+
60
+ // When two multi-pin components face each other across a short gap, the
61
+ // fallback label at each pin can be wider than the available space. Route
62
+ // the explicit connection instead of rendering two copies of the long net
63
+ // name on top of one another.
64
+ const firstIsLeft = firstPin.x <= secondPin.x
65
+ const directionsPointTowardEachOther = firstIsLeft
66
+ ? firstFacingDirection === "x+" && secondFacingDirection === "x-"
67
+ : firstFacingDirection === "x-" && secondFacingDirection === "x+"
68
+ const labelsOverlapAlongX =
69
+ Math.abs(firstPin.x - secondPin.x) < directConnection.netLabelWidth * 2
70
+ const labelsOverlapAlongY =
71
+ Math.abs(firstPin.y - secondPin.y) < NET_LABEL_HORIZONTAL_HEIGHT
72
+
73
+ const fallbackLabelsOverlap =
74
+ directionsPointTowardEachOther && labelsOverlapAlongX && labelsOverlapAlongY
75
+ return fallbackLabelsOverlap ? "overlapping-fallback-labels" : null
44
76
  }
77
+
78
+ export const isLabeledPeripheralConnection = (
79
+ params: Parameters<typeof getLabeledConnectionRouteReason>[0],
80
+ ) => getLabeledConnectionRouteReason(params) !== null
@@ -188,6 +188,13 @@ export class NetLabelPlacementSolver extends BaseSolver {
188
188
  )
189
189
 
190
190
  if (compTraces.length > 0) {
191
+ // This routed trace exists specifically because two endpoint labels
192
+ // could not fit in the available gap. Do not replace that pair with
193
+ // one redundant long label on the newly routed wire. If routing had
194
+ // failed there would be no trace here, so the port-only fallback
195
+ // branch below would still label both endpoints.
196
+ if (compTraces.some((trace) => trace.suppressNetLabel)) continue
197
+
191
198
  // Choose a representative trace (longest by L1 length)
192
199
  const lengthOf = (path: SolvedTracePath) => {
193
200
  let sum = 0
@@ -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.145",
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
+ }