@tscircuit/schematic-trace-solver 0.0.160 → 0.0.162

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.
Files changed (63) hide show
  1. package/dist/index.d.ts +5 -0
  2. package/dist/index.js +221 -29
  3. package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +210 -6
  4. package/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts +28 -21
  5. package/lib/solvers/MspConnectionPairSolver/getMspConnectionPairsFromPins.ts +3 -3
  6. package/lib/solvers/MspConnectionPairSolver/shouldSeparateGroundNetRows.ts +40 -0
  7. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getFixedLabelCoordinate.ts +44 -13
  8. package/lib/types/InputProblem.ts +3 -0
  9. package/package.json +1 -1
  10. package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +1 -1
  11. package/tests/bug-reports/bug-report-20260728T144234Z/__snapshots__/bug-report-20260728T144234Z.snap.svg +1 -1
  12. package/tests/bug-reports/bug-report-20260728T225606Z/__snapshots__/bug-report-20260728T225606Z.snap.svg +79 -79
  13. package/tests/bug-reports/bug-report-20260730T061837Z/__snapshots__/bug-report-20260730T061837Z.snap.svg +85 -85
  14. package/tests/bug-reports/bug-report-20260731T052229Z/__snapshots__/bug-report-20260731T052229Z.snap.svg +1 -1
  15. package/tests/bug-reports/bug-report-20260804T095800Z/__snapshots__/bug-report-20260804T095800Z.snap.svg +163 -163
  16. package/tests/bug-reports/bug-report-20260804T171919Z/__snapshots__/bug-report-20260804T171919Z.snap.svg +547 -547
  17. package/tests/bug-reports/bug-report-20260804T225912Z/__snapshots__/bug-report-20260804T225912Z.snap.svg +13 -13
  18. package/tests/bug-reports/bug-report-20260805T061316Z/__snapshots__/bug-report-20260805T061316Z.snap.svg +267 -267
  19. package/tests/bug-reports/bug-report-20260806T061548Z/__snapshots__/bug-report-20260806T061548Z.snap.svg +267 -267
  20. package/tests/bug-reports/bug-report-20260806T093501Z/__snapshots__/bug-report-20260806T093501Z.snap.svg +109 -109
  21. package/tests/bug-reports/bug-report-20260811T075142Z/__snapshots__/bug-report-20260811T075142Z.snap.svg +1 -1
  22. package/tests/bug-reports/bug-report-20260815T073240Z/__snapshots__/bug-report-20260815T073240Z.snap.svg +49 -49
  23. package/tests/bug-reports/bug-report-20260819T091818Z/__snapshots__/bug-report-20260819T091818Z.snap.svg +1 -1
  24. package/tests/bug-reports/bug-report-20260825T045913Z/__snapshots__/bug-report-20260825T045913Z.snap.svg +103 -103
  25. package/tests/bug-reports/bug-report-20260825T103621Z/__snapshots__/bug-report-20260825T103621Z.snap.svg +103 -103
  26. package/tests/bug-reports/bug-report-20260826T072956Z/__snapshots__/bug-report-20260826T072956Z.snap.svg +163 -163
  27. package/tests/examples/__snapshots__/example51.snap.svg +568 -568
  28. package/tests/fixtures/convertSolverOutputToCircuitJson.test.ts +16 -1
  29. package/tests/fixtures/convertSolverOutputToCircuitJson.ts +23 -12
  30. package/tests/repros/__snapshots__/board-1273-trace-overlap-cycle.snap.svg +736 -736
  31. package/tests/repros/__snapshots__/board-15984-same-net-junction.snap.svg +79 -79
  32. package/tests/repros/__snapshots__/board1096-usb-label-overlap-iteration.snap.svg +127 -127
  33. package/tests/repros/__snapshots__/repro-bluetooth-controller-ground-decoupling-groups.snap.svg +115 -0
  34. package/tests/repros/__snapshots__/repro-board-589-regulator-section.snap.svg +31 -31
  35. package/tests/repros/__snapshots__/repro-board-648-esp12f-section.snap.svg +121 -121
  36. package/tests/repros/__snapshots__/repro-bq25895-cross-net-trace-overlap.snap.svg +43 -43
  37. package/tests/repros/__snapshots__/repro-core-555-power-cross-net-overlap.snap.svg +19 -19
  38. package/tests/repros/__snapshots__/repro-core-ground-inline-label-fallback.snap.svg +277 -0
  39. package/tests/repros/__snapshots__/repro-core-subcircuit-missing-ground.snap.svg +19 -19
  40. package/tests/repros/__snapshots__/repro-core-tb67s579ftg-inline-label-routing.snap.svg +7 -7
  41. package/tests/repros/__snapshots__/repro-dc-power-isolated-routing.snap.svg +37 -37
  42. package/tests/repros/__snapshots__/repro-hdc2080-ground-label-alignment.snap.svg +137 -0
  43. package/tests/repros/__snapshots__/repro-nrf52810-clock-routing.snap.svg +61 -61
  44. package/tests/repros/__snapshots__/repro-pga300-redundant-ground-traces.snap.svg +115 -115
  45. package/tests/repros/__snapshots__/repro-pmp11282-isolated-dcdc.snap.svg +743 -743
  46. package/tests/repros/__snapshots__/repro-rp2040-usb-cc2-ground-overlap.snap.svg +43 -43
  47. package/tests/repros/__snapshots__/repro-ti-power-output-section.snap.svg +1 -1
  48. package/tests/repros/__snapshots__/repro-tida010076-j4-ground-bus.snap.svg +17 -17
  49. package/tests/repros/__snapshots__/repro-tida010076-page02.snap.svg +1131 -1131
  50. package/tests/repros/__snapshots__/repro-type-c-16pin-self-connection.snap.svg +7 -7
  51. package/tests/repros/__snapshots__/repro-type-c-16pin-to-resistor.snap.svg +7 -7
  52. package/tests/repros/__snapshots__/repro-usb-power-vbus-label-detour.snap.svg +666 -666
  53. package/tests/repros/__snapshots__/repro161-power-section-autolayout.snap.svg +73 -73
  54. package/tests/repros/__snapshots__/repro48-555-timer-vcc-rail-label.snap.svg +73 -73
  55. package/tests/repros/assets/repro-bluetooth-controller-ground-decoupling-groups.input.json +214 -0
  56. package/tests/repros/assets/repro-core-ground-inline-label-fallback.input.json +863 -0
  57. package/tests/repros/repro-bluetooth-controller-ground-decoupling-groups.test.ts +18 -0
  58. package/tests/repros/repro-core-ground-inline-label-fallback.test.ts +34 -0
  59. package/tests/repros/repro-hdc2080-ground-label-alignment.test.ts +239 -0
  60. package/tests/repros/repro-tida010076-j4-ground-bus.test.ts +14 -10
  61. package/tests/repros/repro-tida010076-page02.test.ts +2 -2
  62. package/tests/solvers/InlineNetLabelSolver/__snapshots__/inline-net-label02.snap.svg +1 -1
  63. package/tests/solvers/MspConnectionPairSolver/msp-connection-pair-solver-ground-net-rows.test.ts +57 -0
package/dist/index.d.ts CHANGED
@@ -141,6 +141,8 @@ interface InputDirectConnection {
141
141
  interface InputNetConnection {
142
142
  netId: string;
143
143
  pinIds: Array<PinId>;
144
+ /** True when the authoritative source net is electrical ground. */
145
+ isGround?: boolean;
144
146
  /**
145
147
  * User-facing text to render for this net label. `netId` remains the stable
146
148
  * connectivity identifier and may be an internal id. When this is omitted
@@ -906,6 +908,9 @@ declare class AvailableNetOrientationSolver extends BaseSolver {
906
908
  private shouldCheckTraceClearanceForLabel;
907
909
  private hasRoutedLabelOnSameNet;
908
910
  private isOutwardHorizontalFallback;
911
+ private canPlaceVerticalLabelOnHostRail;
912
+ private getTextBlockedMultiPinRailTraces;
913
+ private findValidNearbyRailCandidate;
909
914
  private hasPortOnlyLabelOnSameNet;
910
915
  private hasOppositeVerticalRailOnSameChipSide;
911
916
  private getSingleChipSideForNetConnection;
package/dist/index.js CHANGED
@@ -539,6 +539,30 @@ var getLabeledConnectionRouteReason = ({
539
539
  };
540
540
  var isLabeledPeripheralConnection = (params) => getLabeledConnectionRouteReason(params) !== null;
541
541
 
542
+ // lib/solvers/MspConnectionPairSolver/shouldSeparateGroundNetRows.ts
543
+ var SAME_RAIL_Y_TOLERANCE = 1e-6;
544
+ var MIN_GROUPED_RAIL_PIN_COUNT = 3;
545
+ function isGroupedHorizontalRail({
546
+ pin,
547
+ netPins
548
+ }) {
549
+ const sameRailPins = netPins.filter(
550
+ (otherPin) => Math.abs(otherPin.y - pin.y) <= SAME_RAIL_Y_TOLERANCE
551
+ );
552
+ const railChipIds = new Set(sameRailPins.map((railPin) => railPin.chipId));
553
+ return railChipIds.size >= MIN_GROUPED_RAIL_PIN_COUNT;
554
+ }
555
+ function shouldSeparateGroundNetRows({
556
+ netConnection,
557
+ netPins,
558
+ pin1,
559
+ pin2
560
+ }) {
561
+ if (!netConnection?.isGround || pin1.chipId === pin2.chipId) return false;
562
+ if (Math.abs(pin1.y - pin2.y) <= SAME_RAIL_Y_TOLERANCE) return false;
563
+ return isGroupedHorizontalRail({ pin: pin1, netPins }) && isGroupedHorizontalRail({ pin: pin2, netPins });
564
+ }
565
+
542
566
  // lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts
543
567
  var DEFAULT_MAX_MSP_PAIR_DISTANCE = 1;
544
568
  var getPinPairKey = (pinIds) => [...pinIds].sort().join("::");
@@ -643,36 +667,41 @@ var MspConnectionPairSolver = class extends BaseSolver {
643
667
  return;
644
668
  }
645
669
  const globalConnNetId = this.globalConnMap.getNetConnectedToId(pin1);
646
- const userNetId = this.userNetIdByPinId[pin1] ?? this.userNetIdByPinId[pin2];
670
+ const userNetId2 = this.userNetIdByPinId[pin1] ?? this.userNetIdByPinId[pin2];
647
671
  const suppressNetLabel = pairDistance > this.maxMspPairDistance && labeledConnectionRouteReason === "overlapping-fallback-labels";
648
672
  this.addMspConnectionPair({
649
673
  mspPairId: `${pin1}-${pin2}`,
650
674
  dcConnNetId: dcNetId,
651
675
  globalConnNetId,
652
- userNetId,
676
+ userNetId: userNetId2,
653
677
  suppressNetLabel,
654
678
  pins: [p1, p2]
655
679
  });
656
680
  return;
657
681
  }
658
682
  const pinIdMap = new Map(Object.entries(this.pinMap));
659
- const msp = getOrthogonalMinimumSpanningTree(
660
- directlyConnectedPins.map((p) => this.pinMap[p]).filter(Boolean),
661
- {
662
- maxDistance: this.maxMspPairDistance,
663
- forbidEdge: (a, b) => !this.canRouteGroundPair(a.pinId, b.pinId) || arePinsInDifferentSchematicSections(
664
- this.inputProblem,
665
- a,
666
- b
667
- ) || doesPairCrossRestrictedCenterLines({
668
- inputProblem: this.inputProblem,
669
- chipMap: this.chipMap,
670
- pinIdMap,
671
- p1: a,
672
- p2: b
673
- })
674
- }
675
- );
683
+ const userNetId = this.userNetIdByPinId[directlyConnectedPins[0]];
684
+ const netConnection = this.inputProblem.netConnections.find(
685
+ (connection) => connection.netId === userNetId
686
+ );
687
+ const directlyConnectedPinObjects = directlyConnectedPins.map(
688
+ (pinId) => this.pinMap[pinId]
689
+ );
690
+ const msp = getOrthogonalMinimumSpanningTree(directlyConnectedPinObjects, {
691
+ maxDistance: this.maxMspPairDistance,
692
+ forbidEdge: (a, b) => !this.canRouteGroundPair(a.pinId, b.pinId) || shouldSeparateGroundNetRows({
693
+ netConnection,
694
+ netPins: directlyConnectedPinObjects,
695
+ pin1: a,
696
+ pin2: b
697
+ }) || arePinsInDifferentSchematicSections(this.inputProblem, a, b) || doesPairCrossRestrictedCenterLines({
698
+ inputProblem: this.inputProblem,
699
+ chipMap: this.chipMap,
700
+ pinIdMap,
701
+ p1: a,
702
+ p2: b
703
+ })
704
+ });
676
705
  for (const [pin1, pin2] of msp) {
677
706
  const p1Obj = this.pinMap[pin1];
678
707
  const p2Obj = this.pinMap[pin2];
@@ -689,12 +718,12 @@ var MspConnectionPairSolver = class extends BaseSolver {
689
718
  continue;
690
719
  }
691
720
  const globalConnNetId = this.globalConnMap.getNetConnectedToId(pin1);
692
- const userNetId = this.userNetIdByPinId[pin1] ?? this.userNetIdByPinId[pin2];
721
+ const pairUserNetId = this.userNetIdByPinId[pin1] ?? this.userNetIdByPinId[pin2];
693
722
  this.addMspConnectionPair({
694
723
  mspPairId: `${pin1}-${pin2}`,
695
724
  dcConnNetId: dcNetId,
696
725
  globalConnNetId,
697
- userNetId,
726
+ userNetId: pairUserNetId,
698
727
  pins: [p1Obj, p2Obj]
699
728
  });
700
729
  }
@@ -5864,6 +5893,9 @@ var getLabelsLinkedToRailGroup = (group, netLabelPlacements, traces) => {
5864
5893
  var getFixedLabelCoordinate = (group, netLabelPlacements, traces) => {
5865
5894
  const traceMap = new Map(traces.map((trace) => [trace.mspPairId, trace]));
5866
5895
  const groupTraceIds = new Set(group.map((segment) => segment.traceId));
5896
+ const groupIsWithinSingleComponent = [...groupTraceIds].every(
5897
+ (traceId) => traceMap.get(traceId)?.pins.every((pin) => pin.chipId === group[0].componentId) === true
5898
+ );
5867
5899
  const groupHasOnlySingleRailTraces = [...groupTraceIds].every(
5868
5900
  (traceId) => traceMap.get(traceId)?.tracePath.length === 4
5869
5901
  );
@@ -5874,12 +5906,20 @@ var getFixedLabelCoordinate = (group, netLabelPlacements, traces) => {
5874
5906
  netLabelPlacements,
5875
5907
  traces
5876
5908
  );
5877
- const anchoringLabels = linkedLabels.filter(
5878
- (label) => label.orientation === group[0].componentFacingDirection && label.mspConnectionPairIds.some((traceId) => {
5909
+ const labelsOrientedAlongRail = new Set(
5910
+ linkedLabels.filter(
5911
+ (label) => orientation === "vertical" && (label.orientation === "y+" || label.orientation === "y-") || orientation === "horizontal" && (label.orientation === "x+" || label.orientation === "x-")
5912
+ )
5913
+ );
5914
+ const anchoringLabels = linkedLabels.filter((label) => {
5915
+ if (label.orientation !== group[0].componentFacingDirection && (!groupIsWithinSingleComponent || !labelsOrientedAlongRail.has(label))) {
5916
+ return false;
5917
+ }
5918
+ return label.mspConnectionPairIds.some((traceId) => {
5879
5919
  const trace = traceMap.get(traceId);
5880
5920
  return trace && tracePathContainsPoint(trace.tracePath, label.anchorPoint);
5881
- })
5882
- );
5921
+ });
5922
+ });
5883
5923
  const coordinates = getDistinctCoordinates(
5884
5924
  anchoringLabels.map(
5885
5925
  (label) => orientation === "vertical" ? label.anchorPoint.x : label.anchorPoint.y
@@ -5896,12 +5936,21 @@ var getFixedLabelCoordinate = (group, netLabelPlacements, traces) => {
5896
5936
  )
5897
5937
  );
5898
5938
  if (labelIsAnchoredToRoutedBackbone) return coordinate;
5939
+ if (anchoringLabels.some((label) => labelsOrientedAlongRail.has(label))) {
5940
+ const railGroupSpan = Math.max(...group.map((segment) => segment.maxAlong)) - Math.min(...group.map((segment) => segment.minAlong));
5941
+ const coordinateIsLocalToRailGroup = group.every(
5942
+ (segment) => Math.abs(segment.coordinate - coordinate) <= railGroupSpan + RAIL_ALIGNMENT_EPSILON
5943
+ );
5944
+ if (coordinateIsLocalToRailGroup) return coordinate;
5945
+ return null;
5946
+ }
5899
5947
  const coordinateIsLocalToEveryTrace = group.every((segment) => {
5900
5948
  const trace = traceMap.get(segment.traceId);
5901
5949
  if (!trace) return false;
5902
5950
  return Math.abs(segment.coordinate - coordinate) <= distance4(trace.pins[0], trace.pins[1]) + RAIL_ALIGNMENT_EPSILON;
5903
5951
  });
5904
- return coordinateIsLocalToEveryTrace ? coordinate : null;
5952
+ if (coordinateIsLocalToEveryTrace) return coordinate;
5953
+ return null;
5905
5954
  };
5906
5955
 
5907
5956
  // lib/solvers/TraceCleanupSolver/sameNetRailAlignment/moveRailSegments.ts
@@ -8939,6 +8988,9 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
8939
8988
  const orientations = this.getAvailableOrientations(label);
8940
8989
  if (orientations.length === 0) return false;
8941
8990
  if (!orientations.includes(label.orientation)) return true;
8991
+ if (this.getTextBlockedMultiPinRailTraces(label, orientations[0])) {
8992
+ return true;
8993
+ }
8942
8994
  if (!this.crowdedPortOnlyLabelIndices.has(labelIndex)) return false;
8943
8995
  const bounds = getRectBounds(label.center, label.width, label.height);
8944
8996
  return this.outputNetLabelPlacements.some((otherLabel, otherIndex) => {
@@ -9060,8 +9112,34 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9060
9112
  );
9061
9113
  const isTwoPinNet = netConnection?.pinIds.length === 2;
9062
9114
  const isDistanceSplitVerticalRail = (netConnection?.pinIds.length ?? 0) > 2 && isYOrientation(requiredOrientation) && this.hasPortOnlyLabelOnSameNet(label);
9115
+ const textBlockedMultiPinRailTraces = this.getTextBlockedMultiPinRailTraces(
9116
+ label,
9117
+ requiredOrientation
9118
+ );
9063
9119
  const isPairedSameSidePowerRail = isYOrientation(requiredOrientation) && this.hasOppositeVerticalRailOnSameChipSide(label, requiredOrientation);
9120
+ if (textBlockedMultiPinRailTraces) {
9121
+ const nearbyCandidate = this.findValidNearbyRailCandidate(
9122
+ label,
9123
+ requiredOrientation,
9124
+ labelIndex,
9125
+ textBlockedMultiPinRailTraces
9126
+ );
9127
+ if (nearbyCandidate) return nearbyCandidate;
9128
+ }
9064
9129
  if (orientations.length === 1 && isYOrientation(requiredOrientation) && this.isOutwardHorizontalFallback(label) && this.hasTraceContinuingInOrientation(label, requiredOrientation) && (isDistanceSplitVerticalRail || isPairedSameSidePowerRail)) {
9130
+ const hostRailCandidate = this.findValidTraceAnchorCandidate(
9131
+ label,
9132
+ requiredOrientation,
9133
+ labelIndex
9134
+ );
9135
+ if (hostRailCandidate) {
9136
+ const canUseHostRail = this.canPlaceVerticalLabelOnHostRail(
9137
+ hostRailCandidate,
9138
+ label
9139
+ );
9140
+ if (canUseHostRail) return hostRailCandidate;
9141
+ hostRailCandidate.selected = false;
9142
+ }
9065
9143
  const traceAnchorCandidate2 = this.findValidOutwardTraceAnchorCandidate(
9066
9144
  label,
9067
9145
  requiredOrientation,
@@ -9495,14 +9573,21 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9495
9573
  maxSearchDistance,
9496
9574
  outwardDistance,
9497
9575
  phase = "shift",
9498
- stopOnTraceCollision = true
9576
+ stopOnTraceCollision = true,
9577
+ connectorSource,
9578
+ startDistance = LABEL_SEARCH_STEP
9499
9579
  } = params;
9500
- for (let distance7 = LABEL_SEARCH_STEP; distance7 <= maxSearchDistance + EPS12; distance7 += LABEL_SEARCH_STEP) {
9580
+ for (let distance7 = startDistance; distance7 <= maxSearchDistance + EPS12; distance7 += LABEL_SEARCH_STEP) {
9501
9581
  const anchorPoint = {
9502
9582
  x: baseAnchor.x + direction.x * distance7,
9503
9583
  y: baseAnchor.y + direction.y * distance7
9504
9584
  };
9505
- const candidate = this.createCandidate(label, anchorPoint, orientation);
9585
+ const candidate = this.createCandidate(
9586
+ label,
9587
+ anchorPoint,
9588
+ orientation,
9589
+ connectorSource
9590
+ );
9506
9591
  const result = this.evaluateCandidate(
9507
9592
  candidate,
9508
9593
  label,
@@ -9878,6 +9963,113 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9878
9963
  const chipSide = this.getChipSideForPoint(label.anchorPoint);
9879
9964
  return chipSide === "left" && label.orientation === "x-" || chipSide === "right" && label.orientation === "x+";
9880
9965
  }
9966
+ canPlaceVerticalLabelOnHostRail(candidate, label) {
9967
+ const isOnHostRail = label.mspConnectionPairIds.some((traceId) => {
9968
+ const trace = this.traceMap[traceId];
9969
+ return trace && tracePathContainsPoint(trace.tracePath, candidate.anchorPoint);
9970
+ });
9971
+ if (!isOnHostRail) return false;
9972
+ const hostPin = this.pinMap[label.pinIds[0]];
9973
+ if (!hostPin) return false;
9974
+ if (label.pinIds.some(
9975
+ (pinId) => this.pinMap[pinId]?.chipId !== hostPin.chipId
9976
+ )) {
9977
+ return false;
9978
+ }
9979
+ const chip = this.chipObstacleSpatialIndex.chips.find(
9980
+ (chip2) => chip2.chipId === hostPin.chipId
9981
+ );
9982
+ if (!chip) return false;
9983
+ const labelBounds = getRectBounds(
9984
+ candidate.center,
9985
+ candidate.width,
9986
+ candidate.height
9987
+ );
9988
+ if (labelBounds.minY < chip.bounds.minY - EPS12 || labelBounds.maxY > chip.bounds.maxY + EPS12) {
9989
+ return false;
9990
+ }
9991
+ if (candidate.anchorPoint.x >= chip.bounds.minX - EPS12 && candidate.anchorPoint.x <= chip.bounds.maxX + EPS12) {
9992
+ return false;
9993
+ }
9994
+ return chip.pins.every(
9995
+ (pin) => label.pinIds.includes(pin.pinId) || pin.y <= labelBounds.minY + EPS12 || pin.y >= labelBounds.maxY - EPS12
9996
+ );
9997
+ }
9998
+ getTextBlockedMultiPinRailTraces(label, orientation) {
9999
+ if (!isYOrientation(orientation)) return null;
10000
+ const componentId = this.pinMap[label.pinIds[0]]?.chipId;
10001
+ const localTraces = Object.values(this.traceMap).filter(
10002
+ (trace) => trace.globalConnNetId === label.globalConnNetId && trace.pins.every((pin) => pin.chipId === componentId)
10003
+ );
10004
+ const connectedPinIds = new Set(label.pinIds);
10005
+ const connectedTraces = /* @__PURE__ */ new Set();
10006
+ for (let changed = true; changed; ) {
10007
+ changed = false;
10008
+ for (const trace of localTraces) {
10009
+ if (connectedTraces.has(trace)) continue;
10010
+ if (!trace.pinIds.some((pinId) => connectedPinIds.has(pinId))) continue;
10011
+ connectedTraces.add(trace);
10012
+ for (const pinId of trace.pinIds) connectedPinIds.add(pinId);
10013
+ changed = true;
10014
+ }
10015
+ }
10016
+ if (connectedPinIds.size <= 3) return null;
10017
+ const pins = [...connectedPinIds].map((pinId) => this.pinMap[pinId]);
10018
+ const direction = dir(orientation);
10019
+ const pinX = pins[0].x;
10020
+ if (pins.some((pin) => Math.abs(pin.x - pinX) > EPS12)) return null;
10021
+ const sidePins = this.inputProblem.chips.find((chip) => chip.chipId === componentId).pins.filter((pin) => Math.abs(pin.x - pinX) <= EPS12);
10022
+ const minY = Math.min(...pins.map((pin) => pin.y));
10023
+ const maxY = Math.max(...pins.map((pin) => pin.y));
10024
+ if (sidePins.some(
10025
+ (pin) => pin.y >= minY - EPS12 && pin.y <= maxY + EPS12 && !connectedPinIds.has(pin.pinId)
10026
+ ) || orientation === "y-" && minY > Math.min(...sidePins.map((pin) => pin.y)) + EPS12 || orientation === "y+" && maxY < Math.max(...sidePins.map((pin) => pin.y)) - EPS12) {
10027
+ return null;
10028
+ }
10029
+ const outerPinPosition = Math.max(
10030
+ ...pins.map((pin) => pin.x * direction.x + pin.y * direction.y)
10031
+ );
10032
+ const blockedByText = (this.inputProblem.textBoxes ?? []).some(
10033
+ (textBox) => textBox.chipId === componentId && textBox.center.x * direction.x + textBox.center.y * direction.y > outerPinPosition + EPS12
10034
+ );
10035
+ return blockedByText ? [...connectedTraces] : null;
10036
+ }
10037
+ findValidNearbyRailCandidate(label, orientation, labelIndex, localTraces) {
10038
+ if (!isYOrientation(orientation)) return null;
10039
+ const direction = dir(orientation);
10040
+ const connectorY = [
10041
+ ...new Set(localTraces.flatMap((trace) => trace.pinIds))
10042
+ ].map((pinId) => this.pinMap[pinId]).sort((a, b) => b.y * direction.y - a.y * direction.y)[0]?.y;
10043
+ if (connectorY === void 0) return null;
10044
+ const pinX = this.pinMap[label.pinIds[0]].x;
10045
+ const railXs = [
10046
+ ...new Set(
10047
+ localTraces.flatMap((trace) => trace.tracePath.map((point) => point.x))
10048
+ )
10049
+ ].filter((x) => Math.abs(x - pinX) > EPS12).sort((a, b) => Math.abs(a - pinX) - Math.abs(b - pinX));
10050
+ for (const x of railXs) {
10051
+ const connectorSource = { x, y: connectorY };
10052
+ if (!Object.values(this.traceMap).some(
10053
+ (trace) => trace.globalConnNetId === label.globalConnNetId && tracePathContainsPoint(trace.tracePath, connectorSource)
10054
+ )) {
10055
+ continue;
10056
+ }
10057
+ const candidate = this.findValidCandidateInShiftColumn({
10058
+ label,
10059
+ labelIndex,
10060
+ orientation,
10061
+ direction,
10062
+ baseAnchor: connectorSource,
10063
+ maxSearchDistance: this.maxSearchDistance,
10064
+ outwardDistance: 0,
10065
+ stopOnTraceCollision: false,
10066
+ connectorSource,
10067
+ startDistance: 0
10068
+ });
10069
+ if (candidate) return candidate;
10070
+ }
10071
+ return null;
10072
+ }
9881
10073
  hasPortOnlyLabelOnSameNet(label) {
9882
10074
  return this.outputNetLabelPlacements.some(
9883
10075
  (otherLabel) => otherLabel !== label && otherLabel.globalConnNetId === label.globalConnNetId && this.isPortOnlyLabel(otherLabel)
@@ -8,6 +8,7 @@ import {
8
8
  getDimsForOrientation,
9
9
  getRectBounds,
10
10
  } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
11
+ import { tracePathContainsPoint } from "lib/solvers/RailNetLabelCornerPlacementSolver/geometry"
11
12
  import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
12
13
  import type {
13
14
  ChipId,
@@ -250,6 +251,9 @@ export class AvailableNetOrientationSolver extends BaseSolver {
250
251
  const orientations = this.getAvailableOrientations(label)
251
252
  if (orientations.length === 0) return false
252
253
  if (!orientations.includes(label.orientation)) return true
254
+ if (this.getTextBlockedMultiPinRailTraces(label, orientations[0]!)) {
255
+ return true
256
+ }
253
257
  if (!this.crowdedPortOnlyLabelIndices.has(labelIndex)) return false
254
258
 
255
259
  const bounds = getRectBounds(label.center, label.width, label.height)
@@ -407,9 +411,22 @@ export class AvailableNetOrientationSolver extends BaseSolver {
407
411
  (netConnection?.pinIds.length ?? 0) > 2 &&
408
412
  isYOrientation(requiredOrientation) &&
409
413
  this.hasPortOnlyLabelOnSameNet(label)
414
+ const textBlockedMultiPinRailTraces = this.getTextBlockedMultiPinRailTraces(
415
+ label,
416
+ requiredOrientation,
417
+ )
410
418
  const isPairedSameSidePowerRail =
411
419
  isYOrientation(requiredOrientation) &&
412
420
  this.hasOppositeVerticalRailOnSameChipSide(label, requiredOrientation)
421
+ if (textBlockedMultiPinRailTraces) {
422
+ const nearbyCandidate = this.findValidNearbyRailCandidate(
423
+ label,
424
+ requiredOrientation,
425
+ labelIndex,
426
+ textBlockedMultiPinRailTraces,
427
+ )
428
+ if (nearbyCandidate) return nearbyCandidate
429
+ }
413
430
  if (
414
431
  orientations.length === 1 &&
415
432
  isYOrientation(requiredOrientation) &&
@@ -417,9 +434,22 @@ export class AvailableNetOrientationSolver extends BaseSolver {
417
434
  this.hasTraceContinuingInOrientation(label, requiredOrientation) &&
418
435
  (isDistanceSplitVerticalRail || isPairedSameSidePowerRail)
419
436
  ) {
420
- // Keep the established outward column, but attach at the furthest trace
421
- // point in the required vertical direction. This places y+ labels above
422
- // a rail and y- labels below it while using a short horizontal connector.
437
+ const hostRailCandidate = this.findValidTraceAnchorCandidate(
438
+ label,
439
+ requiredOrientation,
440
+ labelIndex,
441
+ )
442
+ if (hostRailCandidate) {
443
+ const canUseHostRail = this.canPlaceVerticalLabelOnHostRail(
444
+ hostRailCandidate,
445
+ label,
446
+ )
447
+ if (canUseHostRail) return hostRailCandidate
448
+ hostRailCandidate.selected = false
449
+ }
450
+
451
+ // Keep the established outward column when the direct rail end is not a
452
+ // clear segment of the connected component side.
423
453
  const traceAnchorCandidate = this.findValidOutwardTraceAnchorCandidate(
424
454
  label,
425
455
  requiredOrientation,
@@ -785,7 +815,6 @@ export class AvailableNetOrientationSolver extends BaseSolver {
785
815
  connectedPinIds.add(pinId)
786
816
  }
787
817
  }
788
-
789
818
  // Multi-pin rails are routed as a chain of pairwise traces. Follow the
790
819
  // pairs only when they share both a pin and an aligned vertical rail,
791
820
  // which identifies one continuous rail. A shared pin alone can join
@@ -1012,6 +1041,8 @@ export class AvailableNetOrientationSolver extends BaseSolver {
1012
1041
  outwardDistance: number
1013
1042
  phase?: CandidatePhase
1014
1043
  stopOnTraceCollision?: boolean
1044
+ connectorSource?: Point
1045
+ startDistance?: number
1015
1046
  }) {
1016
1047
  const {
1017
1048
  label,
@@ -1023,10 +1054,12 @@ export class AvailableNetOrientationSolver extends BaseSolver {
1023
1054
  outwardDistance,
1024
1055
  phase = "shift",
1025
1056
  stopOnTraceCollision = true,
1057
+ connectorSource,
1058
+ startDistance = LABEL_SEARCH_STEP,
1026
1059
  } = params
1027
1060
 
1028
1061
  for (
1029
- let distance = LABEL_SEARCH_STEP;
1062
+ let distance = startDistance;
1030
1063
  distance <= maxSearchDistance + EPS;
1031
1064
  distance += LABEL_SEARCH_STEP
1032
1065
  ) {
@@ -1034,7 +1067,12 @@ export class AvailableNetOrientationSolver extends BaseSolver {
1034
1067
  x: baseAnchor.x + direction.x * distance,
1035
1068
  y: baseAnchor.y + direction.y * distance,
1036
1069
  }
1037
- const candidate = this.createCandidate(label, anchorPoint, orientation)
1070
+ const candidate = this.createCandidate(
1071
+ label,
1072
+ anchorPoint,
1073
+ orientation,
1074
+ connectorSource,
1075
+ )
1038
1076
  const result = this.evaluateCandidate(
1039
1077
  candidate,
1040
1078
  label,
@@ -1563,6 +1601,172 @@ export class AvailableNetOrientationSolver extends BaseSolver {
1563
1601
  )
1564
1602
  }
1565
1603
 
1604
+ private canPlaceVerticalLabelOnHostRail(
1605
+ candidate: CandidateLabel,
1606
+ label: NetLabelPlacement,
1607
+ ) {
1608
+ const isOnHostRail = label.mspConnectionPairIds.some((traceId) => {
1609
+ const trace = this.traceMap[traceId]
1610
+ return (
1611
+ trace && tracePathContainsPoint(trace.tracePath, candidate.anchorPoint)
1612
+ )
1613
+ })
1614
+ if (!isOnHostRail) return false
1615
+
1616
+ const hostPin = this.pinMap[label.pinIds[0]!]
1617
+ if (!hostPin) return false
1618
+ if (
1619
+ label.pinIds.some(
1620
+ (pinId) => this.pinMap[pinId]?.chipId !== hostPin.chipId,
1621
+ )
1622
+ ) {
1623
+ return false
1624
+ }
1625
+
1626
+ const chip = this.chipObstacleSpatialIndex.chips.find(
1627
+ (chip) => chip.chipId === hostPin.chipId,
1628
+ )
1629
+ if (!chip) return false
1630
+
1631
+ const labelBounds = getRectBounds(
1632
+ candidate.center,
1633
+ candidate.width,
1634
+ candidate.height,
1635
+ )
1636
+ if (
1637
+ labelBounds.minY < chip.bounds.minY - EPS ||
1638
+ labelBounds.maxY > chip.bounds.maxY + EPS
1639
+ ) {
1640
+ return false
1641
+ }
1642
+
1643
+ if (
1644
+ candidate.anchorPoint.x >= chip.bounds.minX - EPS &&
1645
+ candidate.anchorPoint.x <= chip.bounds.maxX + EPS
1646
+ ) {
1647
+ return false
1648
+ }
1649
+
1650
+ return chip.pins.every(
1651
+ (pin) =>
1652
+ label.pinIds.includes(pin.pinId) ||
1653
+ pin.y <= labelBounds.minY + EPS ||
1654
+ pin.y >= labelBounds.maxY - EPS,
1655
+ )
1656
+ }
1657
+
1658
+ private getTextBlockedMultiPinRailTraces(
1659
+ label: NetLabelPlacement,
1660
+ orientation: FacingDirection,
1661
+ ) {
1662
+ if (!isYOrientation(orientation)) return null
1663
+ const componentId = this.pinMap[label.pinIds[0]!]?.chipId
1664
+ const localTraces = Object.values(this.traceMap).filter(
1665
+ (trace) =>
1666
+ trace.globalConnNetId === label.globalConnNetId &&
1667
+ trace.pins.every((pin) => pin.chipId === componentId),
1668
+ )
1669
+ const connectedPinIds = new Set(label.pinIds)
1670
+ const connectedTraces = new Set<SolvedTracePath>()
1671
+ for (let changed = true; changed; ) {
1672
+ changed = false
1673
+ for (const trace of localTraces) {
1674
+ if (connectedTraces.has(trace)) continue
1675
+ if (!trace.pinIds.some((pinId) => connectedPinIds.has(pinId))) continue
1676
+ connectedTraces.add(trace)
1677
+ for (const pinId of trace.pinIds) connectedPinIds.add(pinId)
1678
+ changed = true
1679
+ }
1680
+ }
1681
+ if (connectedPinIds.size <= 3) return null
1682
+
1683
+ const pins = [...connectedPinIds].map((pinId) => this.pinMap[pinId]!)
1684
+ const direction = dir(orientation)
1685
+ const pinX = pins[0]!.x
1686
+ if (pins.some((pin) => Math.abs(pin.x - pinX) > EPS)) return null
1687
+ const sidePins = this.inputProblem.chips
1688
+ .find((chip) => chip.chipId === componentId)!
1689
+ .pins.filter((pin) => Math.abs(pin.x - pinX) <= EPS)
1690
+ const minY = Math.min(...pins.map((pin) => pin.y))
1691
+ const maxY = Math.max(...pins.map((pin) => pin.y))
1692
+ if (
1693
+ sidePins.some(
1694
+ (pin) =>
1695
+ pin.y >= minY - EPS &&
1696
+ pin.y <= maxY + EPS &&
1697
+ !connectedPinIds.has(pin.pinId),
1698
+ ) ||
1699
+ (orientation === "y-" &&
1700
+ minY > Math.min(...sidePins.map((pin) => pin.y)) + EPS) ||
1701
+ (orientation === "y+" &&
1702
+ maxY < Math.max(...sidePins.map((pin) => pin.y)) - EPS)
1703
+ ) {
1704
+ return null
1705
+ }
1706
+ const outerPinPosition = Math.max(
1707
+ ...pins.map((pin) => pin.x * direction.x + pin.y * direction.y),
1708
+ )
1709
+ const blockedByText = (this.inputProblem.textBoxes ?? []).some(
1710
+ (textBox) =>
1711
+ textBox.chipId === componentId &&
1712
+ textBox.center.x * direction.x + textBox.center.y * direction.y >
1713
+ outerPinPosition + EPS,
1714
+ )
1715
+ return blockedByText ? [...connectedTraces] : null
1716
+ }
1717
+
1718
+ private findValidNearbyRailCandidate(
1719
+ label: NetLabelPlacement,
1720
+ orientation: FacingDirection,
1721
+ labelIndex: number,
1722
+ localTraces: SolvedTracePath[],
1723
+ ) {
1724
+ if (!isYOrientation(orientation)) return null
1725
+ const direction = dir(orientation)
1726
+ const connectorY = [
1727
+ ...new Set(localTraces.flatMap((trace) => trace.pinIds)),
1728
+ ]
1729
+ .map((pinId) => this.pinMap[pinId]!)
1730
+ .sort((a, b) => b.y * direction.y - a.y * direction.y)[0]?.y
1731
+ if (connectorY === undefined) return null
1732
+
1733
+ const pinX = this.pinMap[label.pinIds[0]!]!.x
1734
+ const railXs = [
1735
+ ...new Set(
1736
+ localTraces.flatMap((trace) => trace.tracePath.map((point) => point.x)),
1737
+ ),
1738
+ ]
1739
+ .filter((x) => Math.abs(x - pinX) > EPS)
1740
+ .sort((a, b) => Math.abs(a - pinX) - Math.abs(b - pinX))
1741
+
1742
+ for (const x of railXs) {
1743
+ const connectorSource = { x, y: connectorY }
1744
+ if (
1745
+ !Object.values(this.traceMap).some(
1746
+ (trace) =>
1747
+ trace.globalConnNetId === label.globalConnNetId &&
1748
+ tracePathContainsPoint(trace.tracePath, connectorSource),
1749
+ )
1750
+ ) {
1751
+ continue
1752
+ }
1753
+ const candidate = this.findValidCandidateInShiftColumn({
1754
+ label,
1755
+ labelIndex,
1756
+ orientation,
1757
+ direction,
1758
+ baseAnchor: connectorSource,
1759
+ maxSearchDistance: this.maxSearchDistance,
1760
+ outwardDistance: 0,
1761
+ stopOnTraceCollision: false,
1762
+ connectorSource,
1763
+ startDistance: 0,
1764
+ })
1765
+ if (candidate) return candidate
1766
+ }
1767
+ return null
1768
+ }
1769
+
1566
1770
  private hasPortOnlyLabelOnSameNet(label: NetLabelPlacement) {
1567
1771
  return this.outputNetLabelPlacements.some(
1568
1772
  (otherLabel) =>