@tscircuit/schematic-trace-solver 0.0.141 → 0.0.143

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -635,13 +635,14 @@ export class AvailableNetOrientationSolver extends BaseSolver {
635
635
  labelIndex: number,
636
636
  ) {
637
637
  const direction = dir(orientation)
638
- const candidatePoints = this.getTraceAnchorCandidatePoints(label).sort(
639
- (a, b) => {
640
- const aAlongDirection = a.x * direction.x + a.y * direction.y
641
- const bAlongDirection = b.x * direction.x + b.y * direction.y
642
- return bAlongDirection - aAlongDirection
643
- },
644
- )
638
+ const candidatePoints = this.getTraceAnchorCandidatePoints(
639
+ label,
640
+ orientation,
641
+ ).sort((a, b) => {
642
+ const aAlongDirection = a.x * direction.x + a.y * direction.y
643
+ const bAlongDirection = b.x * direction.x + b.y * direction.y
644
+ return bAlongDirection - aAlongDirection
645
+ })
645
646
 
646
647
  for (const anchorPoint of candidatePoints) {
647
648
  const candidate = this.createCandidate(label, anchorPoint, orientation)
@@ -668,13 +669,14 @@ export class AvailableNetOrientationSolver extends BaseSolver {
668
669
  labelIndex: number,
669
670
  ) {
670
671
  const direction = dir(orientation)
671
- const candidatePoints = this.getTraceAnchorCandidatePoints(label).sort(
672
- (a, b) => {
673
- const aAlongDirection = a.x * direction.x + a.y * direction.y
674
- const bAlongDirection = b.x * direction.x + b.y * direction.y
675
- return bAlongDirection - aAlongDirection
676
- },
677
- )
672
+ const candidatePoints = this.getTraceAnchorCandidatePoints(
673
+ label,
674
+ orientation,
675
+ ).sort((a, b) => {
676
+ const aAlongDirection = a.x * direction.x + a.y * direction.y
677
+ const bAlongDirection = b.x * direction.x + b.y * direction.y
678
+ return bAlongDirection - aAlongDirection
679
+ })
678
680
 
679
681
  for (const connectorSource of candidatePoints) {
680
682
  const preservedColumnAnchor = this.getSearchStartAnchor(
@@ -742,11 +744,52 @@ export class AvailableNetOrientationSolver extends BaseSolver {
742
744
  return null
743
745
  }
744
746
 
745
- private getTraceAnchorCandidatePoints(label: NetLabelPlacement) {
747
+ private getTraceAnchorCandidatePoints(
748
+ label: NetLabelPlacement,
749
+ orientation: FacingDirection,
750
+ ) {
746
751
  const seen = new Set<string>()
747
752
  const points: Point[] = []
753
+ const connectedTraceIds = new Set(label.mspConnectionPairIds ?? [])
754
+
755
+ if (isYOrientation(orientation)) {
756
+ const connectedPinIds = new Set<string>()
757
+ for (const traceId of connectedTraceIds) {
758
+ for (const pinId of this.traceMap[traceId]?.pinIds ?? []) {
759
+ connectedPinIds.add(pinId)
760
+ }
761
+ }
762
+
763
+ // Multi-pin rails are routed as a chain of pairwise traces. Follow the
764
+ // pairs only when they share both a pin and an aligned vertical rail,
765
+ // which identifies one continuous rail. A shared pin alone can join
766
+ // unrelated branches whose rail offsets are far apart; walking those
767
+ // branches would extend the label connector across the schematic.
768
+ let foundConnectedTrace = true
769
+ while (foundConnectedTrace) {
770
+ foundConnectedTrace = false
771
+ for (const trace of Object.values(this.traceMap)) {
772
+ if (connectedTraceIds.has(trace.mspPairId)) continue
773
+ if (trace.mspPairId.startsWith("available-net-orientation-")) continue
774
+ if (trace.globalConnNetId !== label.globalConnNetId) continue
775
+ if (!trace.pinIds.some((pinId) => connectedPinIds.has(pinId)))
776
+ continue
777
+ const connectedTraces = [...connectedTraceIds].flatMap((traceId) => {
778
+ const connectedTrace = this.traceMap[traceId]
779
+ return connectedTrace ? [connectedTrace] : []
780
+ })
781
+ if (!this.sharesVerticalRailWithAny(trace, connectedTraces)) {
782
+ continue
783
+ }
784
+
785
+ connectedTraceIds.add(trace.mspPairId)
786
+ for (const pinId of trace.pinIds) connectedPinIds.add(pinId)
787
+ foundConnectedTrace = true
788
+ }
789
+ }
790
+ }
748
791
 
749
- for (const traceId of label.mspConnectionPairIds ?? []) {
792
+ for (const traceId of connectedTraceIds) {
750
793
  const trace = this.traceMap[traceId]
751
794
  if (!trace) continue
752
795
  for (const point of trace.tracePath) {
@@ -760,6 +803,39 @@ export class AvailableNetOrientationSolver extends BaseSolver {
760
803
  return points
761
804
  }
762
805
 
806
+ private sharesVerticalRailWithAny(
807
+ trace: SolvedTracePath,
808
+ otherTraces: SolvedTracePath[],
809
+ ) {
810
+ const verticalRailXs = new Set<number>()
811
+ for (const otherTrace of otherTraces) {
812
+ for (let i = 0; i < otherTrace.tracePath.length - 1; i++) {
813
+ const start = otherTrace.tracePath[i]!
814
+ const end = otherTrace.tracePath[i + 1]!
815
+ if (
816
+ Math.abs(start.x - end.x) <= EPS &&
817
+ Math.abs(start.y - end.y) > EPS
818
+ ) {
819
+ verticalRailXs.add(start.x)
820
+ }
821
+ }
822
+ }
823
+
824
+ for (let i = 0; i < trace.tracePath.length - 1; i++) {
825
+ const start = trace.tracePath[i]!
826
+ const end = trace.tracePath[i + 1]!
827
+ if (
828
+ Math.abs(start.x - end.x) <= EPS &&
829
+ Math.abs(start.y - end.y) > EPS &&
830
+ [...verticalRailXs].some((x) => Math.abs(x - start.x) <= EPS)
831
+ ) {
832
+ return true
833
+ }
834
+ }
835
+
836
+ return false
837
+ }
838
+
763
839
  private getAvailableOrientations(label: NetLabelPlacement) {
764
840
  const effectiveNetId = label.netId ?? label.globalConnNetId
765
841
  return this.inputProblem.availableNetLabelOrientations[effectiveNetId] ?? []
@@ -3,9 +3,11 @@ import type { GraphicsObject, Rect } from "graphics-debug"
3
3
  import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
4
4
  import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
5
5
  import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
6
+ import { getPinDirection } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/getPinDirection"
6
7
  import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
7
8
  import type {
8
9
  InputDirectConnection,
10
+ InputNetConnection,
9
11
  InputProblem,
10
12
  PinId,
11
13
  } from "lib/types/InputProblem"
@@ -15,6 +17,7 @@ import {
15
17
  type AxisAlignedSegment,
16
18
  getAxisAlignedSegments,
17
19
  } from "./getAxisAlignedSegments"
20
+ import { alignPortOnlyInlineNetLabelStubs } from "./alignPortOnlyInlineNetLabelStubs"
18
21
 
19
22
  export const DEFAULT_INLINE_NET_LABEL_HEIGHT = 0.18
20
23
 
@@ -45,9 +48,15 @@ export const INLINE_NET_LABEL_MAX_SPAN_JOG = 0.4
45
48
  export interface InlineNetLabelPlacement {
46
49
  globalConnNetId: string
47
50
  netId?: string
48
- mspPairId: string
51
+ mspPairId?: string
49
52
  pinIds: PinId[]
50
53
 
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.
57
+ */
58
+ stubTracePath?: [Point, Point]
59
+
51
60
  /** Axis the text runs along: "x" reads left-to-right, "y" reads bottom-to-top */
52
61
  axis: "x" | "y"
53
62
 
@@ -96,7 +105,11 @@ export class InlineNetLabelSolver extends BaseSolver {
96
105
  /** Direct connections that opted in, still waiting to be processed */
97
106
  queuedDirectConnections: InputDirectConnection[]
98
107
 
108
+ /** Single-pin net connections that opted in, still waiting to be processed */
109
+ queuedPortOnlyNetConnections: InputNetConnection[]
110
+
99
111
  private tracesByPinPairKey: Map<string, SolvedTracePath[]>
112
+ private hasAlignedPortOnlyStubs = false
100
113
 
101
114
  constructor(input: InlineNetLabelSolverInput) {
102
115
  super()
@@ -107,6 +120,9 @@ export class InlineNetLabelSolver extends BaseSolver {
107
120
  this.queuedDirectConnections = this.inputProblem.directConnections.filter(
108
121
  (dc) => dc.allowInlineNetLabel && dc.netId,
109
122
  )
123
+ this.queuedPortOnlyNetConnections = this.inputProblem.netConnections.filter(
124
+ (nc) => nc.allowInlineNetLabel && nc.pinIds.length === 1 && nc.netId,
125
+ )
110
126
 
111
127
  this.tracesByPinPairKey = new Map()
112
128
  for (const trace of this.traces) {
@@ -132,15 +148,140 @@ export class InlineNetLabelSolver extends BaseSolver {
132
148
 
133
149
  override _step() {
134
150
  const directConnection = this.queuedDirectConnections.shift()
135
- if (!directConnection) {
136
- this.solved = true
137
- this.stats.inlineNetLabelCount = this.inlineNetLabelPlacements.length
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)
166
+ }
167
+ return
168
+ }
169
+
170
+ if (!this.hasAlignedPortOnlyStubs) {
171
+ this.inlineNetLabelPlacements = alignPortOnlyInlineNetLabelStubs({
172
+ placements: this.inlineNetLabelPlacements,
173
+ inputProblem: this.inputProblem,
174
+ traces: this.traces,
175
+ netLabelPlacements: this.inputNetLabelPlacements,
176
+ })
177
+ this.hasAlignedPortOnlyStubs = true
138
178
  return
139
179
  }
140
180
 
141
- const placement = this.computeInlinePlacement(directConnection)
142
- if (placement) {
143
- this.inlineNetLabelPlacements.push(placement)
181
+ this.solved = true
182
+ this.stats.inlineNetLabelCount = this.inlineNetLabelPlacements.length
183
+ }
184
+
185
+ /**
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.
190
+ */
191
+ private computePortOnlyInlinePlacement(
192
+ netConnection: InputNetConnection,
193
+ ): InlineNetLabelPlacement | null {
194
+ const [pinId] = netConnection.pinIds
195
+ if (!pinId) return null
196
+
197
+ const anchoredPlacement = this.inputNetLabelPlacements.find(
198
+ (placement) =>
199
+ placement.netId === netConnection.netId &&
200
+ placement.pinIds.length === 1 &&
201
+ placement.pinIds[0] === pinId,
202
+ )
203
+ if (!anchoredPlacement) return null
204
+
205
+ const inputChip = this.inputProblem.chips.find((chip) =>
206
+ chip.pins.some((pin) => pin.pinId === pinId),
207
+ )
208
+ const inputPin = inputChip?.pins.find((pin) => pin.pinId === pinId)
209
+
210
+ const height =
211
+ netConnection.inlineNetLabelHeight ?? DEFAULT_INLINE_NET_LABEL_HEIGHT
212
+ const width =
213
+ netConnection.inlineNetLabelWidth ??
214
+ netConnection.netLabelWidth ??
215
+ estimateInlineNetLabelWidth(netConnection.netId, height)
216
+
217
+ // Leave a small wire tail at both ends of the text so it unmistakably
218
+ // reads as a label on a trace rather than free-standing text.
219
+ const stubLength = Math.max(width + 0.2, 0.6)
220
+ const start = inputPin
221
+ ? { x: inputPin.x, y: inputPin.y }
222
+ : anchoredPlacement.anchorPoint
223
+ const direction =
224
+ inputPin && inputChip
225
+ ? (inputPin._facingDirection ?? getPinDirection(inputPin, inputChip))
226
+ : anchoredPlacement.orientation
227
+ const end: Point =
228
+ direction === "x+"
229
+ ? { x: start.x + stubLength, y: start.y }
230
+ : direction === "x-"
231
+ ? { x: start.x - stubLength, y: start.y }
232
+ : direction === "y+"
233
+ ? { x: start.x, y: start.y + stubLength }
234
+ : { x: start.x, y: start.y - stubLength }
235
+
236
+ const axis: InlineNetLabelPlacement["axis"] =
237
+ direction === "x+" || direction === "x-" ? "x" : "y"
238
+ const side: InlineNetLabelPlacement["side"] = axis === "x" ? "y+" : "x-"
239
+ const anchorPoint = {
240
+ x: (start.x + end.x) / 2,
241
+ y: (start.y + end.y) / 2,
242
+ }
243
+ const offset = height / 2 + INLINE_NET_LABEL_TRACE_MARGIN
244
+ const center: Point =
245
+ side === "y+"
246
+ ? { x: anchorPoint.x, y: anchorPoint.y + offset }
247
+ : { x: anchorPoint.x - offset, y: anchorPoint.y }
248
+
249
+ const halfAlong = width / 2
250
+ const halfAcross = height / 2
251
+ const bounds: Bounds =
252
+ axis === "x"
253
+ ? {
254
+ minX: center.x - halfAlong,
255
+ maxX: center.x + halfAlong,
256
+ minY: center.y - halfAcross,
257
+ maxY: center.y + halfAcross,
258
+ }
259
+ : {
260
+ minX: center.x - halfAcross,
261
+ maxX: center.x + halfAcross,
262
+ minY: center.y - halfAlong,
263
+ maxY: center.y + halfAlong,
264
+ }
265
+
266
+ if (
267
+ this.isObstructed(bounds, {
268
+ ownGlobalConnNetId: anchoredPlacement.globalConnNetId,
269
+ })
270
+ ) {
271
+ return null
272
+ }
273
+
274
+ return {
275
+ globalConnNetId: anchoredPlacement.globalConnNetId,
276
+ netId: netConnection.netId,
277
+ pinIds: [pinId],
278
+ stubTracePath: [start, end],
279
+ axis,
280
+ anchorPoint,
281
+ center,
282
+ width,
283
+ height,
284
+ side,
144
285
  }
145
286
  }
146
287
 
@@ -209,7 +350,13 @@ export class InlineNetLabelSolver extends BaseSolver {
209
350
  maxY: center.y + halfAlong,
210
351
  }
211
352
 
212
- if (this.isObstructed(bounds, trace)) continue
353
+ if (
354
+ this.isObstructed(bounds, {
355
+ ownTrace: trace,
356
+ ownGlobalConnNetId: trace.globalConnNetId,
357
+ })
358
+ )
359
+ continue
213
360
 
214
361
  return {
215
362
  globalConnNetId: trace.globalConnNetId,
@@ -334,7 +481,13 @@ export class InlineNetLabelSolver extends BaseSolver {
334
481
  maxY: center.y + halfAlong,
335
482
  }
336
483
 
337
- if (this.isObstructed(bounds, trace)) continue
484
+ if (
485
+ this.isObstructed(bounds, {
486
+ ownTrace: trace,
487
+ ownGlobalConnNetId: trace.globalConnNetId,
488
+ })
489
+ )
490
+ continue
338
491
 
339
492
  const anchorPoint: Point =
340
493
  axis === "x"
@@ -363,7 +516,16 @@ export class InlineNetLabelSolver extends BaseSolver {
363
516
  * An inline label may not sit on top of a chip, a component's text, or a
364
517
  * trace belonging to another net.
365
518
  */
366
- private isObstructed(bounds: Bounds, ownTrace: SolvedTracePath): boolean {
519
+ private isObstructed(
520
+ bounds: Bounds,
521
+ {
522
+ ownTrace,
523
+ ownGlobalConnNetId,
524
+ }: {
525
+ ownTrace?: SolvedTracePath
526
+ ownGlobalConnNetId: string
527
+ },
528
+ ): boolean {
367
529
  for (const chip of this.inputProblem.chips) {
368
530
  const chipBounds: Bounds = {
369
531
  minX: chip.center.x - chip.width / 2,
@@ -379,8 +541,8 @@ export class InlineNetLabelSolver extends BaseSolver {
379
541
  }
380
542
 
381
543
  for (const trace of this.traces) {
382
- if (trace.mspPairId === ownTrace.mspPairId) continue
383
- if (trace.globalConnNetId === ownTrace.globalConnNetId) continue
544
+ if (ownTrace && trace.mspPairId === ownTrace.mspPairId) continue
545
+ if (trace.globalConnNetId === ownGlobalConnNetId) continue
384
546
  if (doesPathIntersectBounds(trace.tracePath, bounds)) return true
385
547
  }
386
548
 
@@ -399,10 +561,23 @@ export class InlineNetLabelSolver extends BaseSolver {
399
561
  return keys
400
562
  }
401
563
 
564
+ private getOutputTraces(supersededNetLabelKeys: Set<string>) {
565
+ return this.traces.filter(
566
+ (trace) =>
567
+ !(
568
+ supersededNetLabelKeys.has(trace.globalConnNetId) &&
569
+ trace.mspPairId.startsWith("available-net-orientation-")
570
+ ),
571
+ )
572
+ }
573
+
402
574
  getOutput() {
403
575
  const superseded = this.getSupersededNetLabelKeys()
404
576
  return {
405
- traces: this.traces,
577
+ // AvailableNetOrientationSolver may have routed an elbow from a port to
578
+ // the anchored label that this inline placement supersedes. Keep the
579
+ // actual net trace, but discard that now-orphaned label connector.
580
+ traces: this.getOutputTraces(superseded),
406
581
  netLabelPlacements: this.inputNetLabelPlacements.filter(
407
582
  (placement) => !superseded.has(placement.globalConnNetId),
408
583
  ),
@@ -420,7 +595,8 @@ export class InlineNetLabelSolver extends BaseSolver {
420
595
  graphics.points ??= []
421
596
  graphics.texts ??= []
422
597
 
423
- for (const trace of this.traces) {
598
+ const output = this.getOutput()
599
+ for (const trace of output.traces) {
424
600
  graphics.lines.push({
425
601
  points: trace.tracePath,
426
602
  strokeColor: "purple",
@@ -446,6 +622,12 @@ export class InlineNetLabelSolver extends BaseSolver {
446
622
  }
447
623
 
448
624
  for (const inlineLabel of this.inlineNetLabelPlacements) {
625
+ if (inlineLabel.stubTracePath) {
626
+ graphics.lines.push({
627
+ points: inlineLabel.stubTracePath,
628
+ strokeColor: "purple",
629
+ })
630
+ }
449
631
  const isHorizontal = inlineLabel.axis === "x"
450
632
  graphics.rects.push({
451
633
  center: inlineLabel.center,
@@ -460,12 +642,29 @@ export class InlineNetLabelSolver extends BaseSolver {
460
642
  ].join("\n"),
461
643
  } as Rect & { strokeColor: string })
462
644
  graphics.texts.push({
463
- x: inlineLabel.center.x,
464
- y: inlineLabel.center.y,
645
+ x:
646
+ inlineLabel.stubTracePath && inlineLabel.axis === "x"
647
+ ? inlineLabel.center.x +
648
+ (inlineLabel.stubTracePath[1].x > inlineLabel.stubTracePath[0].x
649
+ ? -inlineLabel.width / 2
650
+ : inlineLabel.width / 2)
651
+ : inlineLabel.center.x,
652
+ y:
653
+ inlineLabel.stubTracePath && inlineLabel.axis === "y"
654
+ ? inlineLabel.center.y +
655
+ (inlineLabel.stubTracePath[1].y > inlineLabel.stubTracePath[0].y
656
+ ? -inlineLabel.width / 2
657
+ : inlineLabel.width / 2)
658
+ : inlineLabel.center.y,
465
659
  text: inlineLabel.netId ?? "",
466
660
  color: "green",
467
661
  fontSize: inlineLabel.height,
468
- anchorSide: "center",
662
+ anchorSide: inlineLabel.stubTracePath
663
+ ? inlineLabel.stubTracePath[1][inlineLabel.axis] >
664
+ inlineLabel.stubTracePath[0][inlineLabel.axis]
665
+ ? "center_left"
666
+ : "center_right"
667
+ : "center",
469
668
  // Vertical labels read bottom-to-top, alongside the wire they name.
470
669
  rotation: inlineLabel.axis === "y" ? 90 : undefined,
471
670
  })