@tscircuit/schematic-trace-solver 0.0.142 → 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. */
@@ -825,6 +828,7 @@ declare class AvailableNetOrientationSolver extends BaseSolver {
825
828
  private findValidTraceAnchorCandidate;
826
829
  private findValidOutwardTraceAnchorCandidate;
827
830
  private getTraceAnchorCandidatePoints;
831
+ private sharesVerticalRailWithAny;
828
832
  private getAvailableOrientations;
829
833
  private findValidShiftedCandidate;
830
834
  /**
@@ -1199,8 +1203,9 @@ interface InlineNetLabelPlacement {
1199
1203
  mspPairId?: string;
1200
1204
  pinIds: PinId[];
1201
1205
  /**
1202
- * A generated single-ended trace stub. Present only for an eligible
1203
- * 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.
1204
1209
  */
1205
1210
  stubTracePath?: [Point, Point];
1206
1211
  /** Axis the text runs along: "x" reads left-to-right, "y" reads bottom-to-top */
@@ -1225,9 +1230,11 @@ interface InlineNetLabelSolverInput {
1225
1230
  traces: SolvedTracePath[];
1226
1231
  netLabelPlacements: NetLabelPlacement[];
1227
1232
  }
1233
+ type InlineEligibleConnection = InputDirectConnection | InputNetConnection;
1228
1234
  /**
1229
1235
  * Places "inline net labels" - net names drawn alongside the trace they belong
1230
- * to - for direct connections that opted in via `allowInlineNetLabel`.
1236
+ * to - for one- or two-pin connections that opted in via
1237
+ * `allowInlineNetLabel`.
1231
1238
  *
1232
1239
  * Any regular (anchored) net label placement for the same net is dropped, so a
1233
1240
  * net is never labeled twice.
@@ -1237,22 +1244,20 @@ declare class InlineNetLabelSolver extends BaseSolver {
1237
1244
  traces: SolvedTracePath[];
1238
1245
  inputNetLabelPlacements: NetLabelPlacement[];
1239
1246
  inlineNetLabelPlacements: InlineNetLabelPlacement[];
1240
- /** Direct connections that opted in, still waiting to be processed */
1241
- queuedDirectConnections: InputDirectConnection[];
1242
- /** Single-pin net connections that opted in, still waiting to be processed */
1243
- queuedPortOnlyNetConnections: InputNetConnection[];
1247
+ /** Eligible one- or two-pin connections still waiting to be processed. */
1248
+ queuedConnections: InlineEligibleConnection[];
1244
1249
  private tracesByPinPairKey;
1245
1250
  private hasAlignedPortOnlyStubs;
1246
1251
  constructor(input: InlineNetLabelSolverInput);
1247
1252
  getConstructorParams(): [InlineNetLabelSolverInput];
1248
1253
  _step(): void;
1249
1254
  /**
1250
- * Converts a conventional port-only anchored placement into an inline label
1251
- * on a generated outward stub. The stub follows the pin's true facing
1252
- * direction; an anchored label may finish in another direction after an
1253
- * 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.
1254
1259
  */
1255
- private computePortOnlyInlinePlacement;
1260
+ private computeTerminalInlinePlacement;
1256
1261
  private computeInlinePlacement;
1257
1262
  /**
1258
1263
  * Places the label over the whole route when the route is straight except
package/dist/index.js CHANGED
@@ -8842,13 +8842,14 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
8842
8842
  }
8843
8843
  findValidTraceAnchorCandidate(label, orientation, labelIndex) {
8844
8844
  const direction = dir(orientation);
8845
- const candidatePoints = this.getTraceAnchorCandidatePoints(label).sort(
8846
- (a, b) => {
8847
- const aAlongDirection = a.x * direction.x + a.y * direction.y;
8848
- const bAlongDirection = b.x * direction.x + b.y * direction.y;
8849
- return bAlongDirection - aAlongDirection;
8850
- }
8851
- );
8845
+ const candidatePoints = this.getTraceAnchorCandidatePoints(
8846
+ label,
8847
+ orientation
8848
+ ).sort((a, b) => {
8849
+ const aAlongDirection = a.x * direction.x + a.y * direction.y;
8850
+ const bAlongDirection = b.x * direction.x + b.y * direction.y;
8851
+ return bAlongDirection - aAlongDirection;
8852
+ });
8852
8853
  for (const anchorPoint of candidatePoints) {
8853
8854
  const candidate = this.createCandidate(label, anchorPoint, orientation);
8854
8855
  const result = this.evaluateCandidate(
@@ -8867,13 +8868,14 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
8867
8868
  }
8868
8869
  findValidOutwardTraceAnchorCandidate(label, orientation, labelIndex) {
8869
8870
  const direction = dir(orientation);
8870
- const candidatePoints = this.getTraceAnchorCandidatePoints(label).sort(
8871
- (a, b) => {
8872
- const aAlongDirection = a.x * direction.x + a.y * direction.y;
8873
- const bAlongDirection = b.x * direction.x + b.y * direction.y;
8874
- return bAlongDirection - aAlongDirection;
8875
- }
8876
- );
8871
+ const candidatePoints = this.getTraceAnchorCandidatePoints(
8872
+ label,
8873
+ orientation
8874
+ ).sort((a, b) => {
8875
+ const aAlongDirection = a.x * direction.x + a.y * direction.y;
8876
+ const bAlongDirection = b.x * direction.x + b.y * direction.y;
8877
+ return bAlongDirection - aAlongDirection;
8878
+ });
8877
8879
  for (const connectorSource of candidatePoints) {
8878
8880
  const preservedColumnAnchor = this.getSearchStartAnchor(
8879
8881
  label,
@@ -8931,10 +8933,40 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
8931
8933
  }
8932
8934
  return null;
8933
8935
  }
8934
- getTraceAnchorCandidatePoints(label) {
8936
+ getTraceAnchorCandidatePoints(label, orientation) {
8935
8937
  const seen = /* @__PURE__ */ new Set();
8936
8938
  const points = [];
8937
- for (const traceId of label.mspConnectionPairIds ?? []) {
8939
+ const connectedTraceIds = new Set(label.mspConnectionPairIds ?? []);
8940
+ if (isYOrientation(orientation)) {
8941
+ const connectedPinIds = /* @__PURE__ */ new Set();
8942
+ for (const traceId of connectedTraceIds) {
8943
+ for (const pinId of this.traceMap[traceId]?.pinIds ?? []) {
8944
+ connectedPinIds.add(pinId);
8945
+ }
8946
+ }
8947
+ let foundConnectedTrace = true;
8948
+ while (foundConnectedTrace) {
8949
+ foundConnectedTrace = false;
8950
+ for (const trace of Object.values(this.traceMap)) {
8951
+ if (connectedTraceIds.has(trace.mspPairId)) continue;
8952
+ if (trace.mspPairId.startsWith("available-net-orientation-")) continue;
8953
+ if (trace.globalConnNetId !== label.globalConnNetId) continue;
8954
+ if (!trace.pinIds.some((pinId) => connectedPinIds.has(pinId)))
8955
+ continue;
8956
+ const connectedTraces = [...connectedTraceIds].flatMap((traceId) => {
8957
+ const connectedTrace = this.traceMap[traceId];
8958
+ return connectedTrace ? [connectedTrace] : [];
8959
+ });
8960
+ if (!this.sharesVerticalRailWithAny(trace, connectedTraces)) {
8961
+ continue;
8962
+ }
8963
+ connectedTraceIds.add(trace.mspPairId);
8964
+ for (const pinId of trace.pinIds) connectedPinIds.add(pinId);
8965
+ foundConnectedTrace = true;
8966
+ }
8967
+ }
8968
+ }
8969
+ for (const traceId of connectedTraceIds) {
8938
8970
  const trace = this.traceMap[traceId];
8939
8971
  if (!trace) continue;
8940
8972
  for (const point of trace.tracePath) {
@@ -8946,6 +8978,26 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
8946
8978
  }
8947
8979
  return points;
8948
8980
  }
8981
+ sharesVerticalRailWithAny(trace, otherTraces) {
8982
+ const verticalRailXs = /* @__PURE__ */ new Set();
8983
+ for (const otherTrace of otherTraces) {
8984
+ for (let i = 0; i < otherTrace.tracePath.length - 1; i++) {
8985
+ const start = otherTrace.tracePath[i];
8986
+ const end = otherTrace.tracePath[i + 1];
8987
+ if (Math.abs(start.x - end.x) <= EPS12 && Math.abs(start.y - end.y) > EPS12) {
8988
+ verticalRailXs.add(start.x);
8989
+ }
8990
+ }
8991
+ }
8992
+ for (let i = 0; i < trace.tracePath.length - 1; i++) {
8993
+ const start = trace.tracePath[i];
8994
+ const end = trace.tracePath[i + 1];
8995
+ if (Math.abs(start.x - end.x) <= EPS12 && Math.abs(start.y - end.y) > EPS12 && [...verticalRailXs].some((x) => Math.abs(x - start.x) <= EPS12)) {
8996
+ return true;
8997
+ }
8998
+ }
8999
+ return false;
9000
+ }
8949
9001
  getAvailableOrientations(label) {
8950
9002
  const effectiveNetId = label.netId ?? label.globalConnNetId;
8951
9003
  return this.inputProblem.availableNetLabelOrientations[effectiveNetId] ?? [];
@@ -12734,10 +12786,8 @@ var InlineNetLabelSolver = class extends BaseSolver {
12734
12786
  traces;
12735
12787
  inputNetLabelPlacements;
12736
12788
  inlineNetLabelPlacements = [];
12737
- /** Direct connections that opted in, still waiting to be processed */
12738
- queuedDirectConnections;
12739
- /** Single-pin net connections that opted in, still waiting to be processed */
12740
- queuedPortOnlyNetConnections;
12789
+ /** Eligible one- or two-pin connections still waiting to be processed. */
12790
+ queuedConnections;
12741
12791
  tracesByPinPairKey;
12742
12792
  hasAlignedPortOnlyStubs = false;
12743
12793
  constructor(input) {
@@ -12745,12 +12795,14 @@ var InlineNetLabelSolver = class extends BaseSolver {
12745
12795
  this.inputProblem = input.inputProblem;
12746
12796
  this.traces = input.traces;
12747
12797
  this.inputNetLabelPlacements = input.netLabelPlacements;
12748
- this.queuedDirectConnections = this.inputProblem.directConnections.filter(
12749
- (dc) => dc.allowInlineNetLabel && dc.netId
12750
- );
12751
- this.queuedPortOnlyNetConnections = this.inputProblem.netConnections.filter(
12752
- (nc) => nc.allowInlineNetLabel && nc.pinIds.length === 1 && nc.netId
12753
- );
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
+ ];
12754
12806
  this.tracesByPinPairKey = /* @__PURE__ */ new Map();
12755
12807
  for (const trace of this.traces) {
12756
12808
  const key = getPinPairKey2(trace.pins.map((p) => p.pinId));
@@ -12772,21 +12824,19 @@ var InlineNetLabelSolver = class extends BaseSolver {
12772
12824
  ];
12773
12825
  }
12774
12826
  _step() {
12775
- const directConnection = this.queuedDirectConnections.shift();
12776
- if (directConnection) {
12777
- const placement = this.computeInlinePlacement(directConnection);
12778
- if (placement) {
12779
- this.inlineNetLabelPlacements.push(placement);
12780
- }
12781
- return;
12782
- }
12783
- const portOnlyNetConnection = this.queuedPortOnlyNetConnections.shift();
12784
- if (portOnlyNetConnection) {
12785
- const placement = this.computePortOnlyInlinePlacement(
12786
- portOnlyNetConnection
12787
- );
12788
- if (placement) {
12789
- 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
+ }
12790
12840
  }
12791
12841
  return;
12792
12842
  }
@@ -12804,24 +12854,23 @@ var InlineNetLabelSolver = class extends BaseSolver {
12804
12854
  this.stats.inlineNetLabelCount = this.inlineNetLabelPlacements.length;
12805
12855
  }
12806
12856
  /**
12807
- * Converts a conventional port-only anchored placement into an inline label
12808
- * on a generated outward stub. The stub follows the pin's true facing
12809
- * direction; an anchored label may finish in another direction after an
12810
- * 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.
12811
12861
  */
12812
- computePortOnlyInlinePlacement(netConnection) {
12813
- const [pinId] = netConnection.pinIds;
12814
- if (!pinId) return null;
12862
+ computeTerminalInlinePlacement(connection, pinId) {
12863
+ if (!connection.netId) return null;
12815
12864
  const anchoredPlacement = this.inputNetLabelPlacements.find(
12816
- (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
12817
12866
  );
12818
12867
  if (!anchoredPlacement) return null;
12819
12868
  const inputChip = this.inputProblem.chips.find(
12820
12869
  (chip) => chip.pins.some((pin) => pin.pinId === pinId)
12821
12870
  );
12822
12871
  const inputPin = inputChip?.pins.find((pin) => pin.pinId === pinId);
12823
- const height = netConnection.inlineNetLabelHeight ?? DEFAULT_INLINE_NET_LABEL_HEIGHT;
12824
- 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);
12825
12874
  const stubLength = Math.max(width + 0.2, 0.6);
12826
12875
  const start = inputPin ? { x: inputPin.x, y: inputPin.y } : anchoredPlacement.anchorPoint;
12827
12876
  const direction = inputPin && inputChip ? inputPin._facingDirection ?? getPinDirection(inputPin, inputChip) : anchoredPlacement.orientation;
@@ -12854,7 +12903,7 @@ var InlineNetLabelSolver = class extends BaseSolver {
12854
12903
  }
12855
12904
  return {
12856
12905
  globalConnNetId: anchoredPlacement.globalConnNetId,
12857
- netId: netConnection.netId,
12906
+ netId: connection.netId,
12858
12907
  pinIds: [pinId],
12859
12908
  stubTracePath: [start, end],
12860
12909
  axis,
@@ -12865,14 +12914,14 @@ var InlineNetLabelSolver = class extends BaseSolver {
12865
12914
  side
12866
12915
  };
12867
12916
  }
12868
- computeInlinePlacement(directConnection) {
12869
- const traces = this.tracesByPinPairKey.get(getPinPairKey2(directConnection.pinIds)) ?? [];
12917
+ computeInlinePlacement(connection) {
12918
+ const traces = this.tracesByPinPairKey.get(getPinPairKey2(connection.pinIds)) ?? [];
12870
12919
  if (traces.length === 0) return null;
12871
12920
  const trace = traces[0];
12872
12921
  const segments = getAxisAlignedSegments(trace.tracePath);
12873
12922
  if (segments.length === 0) return null;
12874
- const height = directConnection.inlineNetLabelHeight ?? DEFAULT_INLINE_NET_LABEL_HEIGHT;
12875
- 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);
12876
12925
  const offset = height / 2 + INLINE_NET_LABEL_TRACE_MARGIN;
12877
12926
  const usableSegments = segments.filter((segment) => segment.length >= width).sort((a, b) => b.length - a.length);
12878
12927
  for (const segment of usableSegments) {
@@ -12900,9 +12949,9 @@ var InlineNetLabelSolver = class extends BaseSolver {
12900
12949
  continue;
12901
12950
  return {
12902
12951
  globalConnNetId: trace.globalConnNetId,
12903
- netId: directConnection.netId,
12952
+ netId: connection.netId,
12904
12953
  mspPairId: trace.mspPairId,
12905
- pinIds: [...directConnection.pinIds],
12954
+ pinIds: [...connection.pinIds],
12906
12955
  axis: segment.axis,
12907
12956
  anchorPoint,
12908
12957
  center,
@@ -12915,7 +12964,7 @@ var InlineNetLabelSolver = class extends BaseSolver {
12915
12964
  }
12916
12965
  const spanPlacement = this.computeSpanPlacement({
12917
12966
  trace,
12918
- directConnection,
12967
+ connection,
12919
12968
  width,
12920
12969
  height,
12921
12970
  offset
@@ -12931,7 +12980,7 @@ var InlineNetLabelSolver = class extends BaseSolver {
12931
12980
  */
12932
12981
  computeSpanPlacement({
12933
12982
  trace,
12934
- directConnection,
12983
+ connection,
12935
12984
  width,
12936
12985
  height,
12937
12986
  offset
@@ -12990,9 +13039,9 @@ var InlineNetLabelSolver = class extends BaseSolver {
12990
13039
  const anchorPoint = axis === "x" ? { x: along, y: side === "y+" ? maxY : minY } : { x: side === "x-" ? minX : maxX, y: along };
12991
13040
  return {
12992
13041
  globalConnNetId: trace.globalConnNetId,
12993
- netId: directConnection.netId,
13042
+ netId: connection.netId,
12994
13043
  mspPairId: trace.mspPairId,
12995
- pinIds: [...directConnection.pinIds],
13044
+ pinIds: [...connection.pinIds],
12996
13045
  axis,
12997
13046
  anchorPoint,
12998
13047
  center,
@@ -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] ?? []