@tscircuit/schematic-trace-solver 0.0.153 → 0.0.155

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 (22) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +354 -118
  3. package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +60 -16
  4. package/lib/solvers/SameNetJunctionAlignmentSolver/alignSameNetJunctions.ts +81 -8
  5. package/lib/solvers/TraceCleanupSolver/alignSameNetRails.ts +17 -0
  6. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/evaluateRailGroup.ts +32 -4
  7. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getComponentSideRailSegments.ts +28 -5
  8. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getFixedLabelCoordinate.ts +114 -0
  9. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getRailGroups.ts +98 -32
  10. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/moveRailSegments.ts +2 -0
  11. package/package.json +1 -1
  12. package/site/bug-reports/bug-report-20260825T103621Z.page.tsx +4 -0
  13. package/tests/bug-reports/bug-report-20260707T134549Z/__snapshots__/bug-report-20260707T134549Z.snap.svg +2 -2
  14. package/tests/bug-reports/bug-report-20260730T061837Z/__snapshots__/bug-report-20260730T061837Z.snap.svg +2 -2
  15. package/tests/bug-reports/bug-report-20260825T103621Z/__snapshots__/bug-report-20260825T103621Z.snap.svg +452 -0
  16. package/tests/bug-reports/bug-report-20260825T103621Z/bug-report-20260825T103621Z.json +1696 -0
  17. package/tests/bug-reports/bug-report-20260825T103621Z/bug-report-20260825T103621Z.test.ts +12 -0
  18. package/tests/examples/__snapshots__/example51.snap.svg +2 -2
  19. package/tests/repros/__snapshots__/repro-pga300-c6-trace-alignment.snap.svg +88 -0
  20. package/tests/repros/__snapshots__/repro-ti-power-output-section.snap.svg +45 -45
  21. package/tests/repros/assets/repro-pga300-c6-trace-alignment.input.json +97 -0
  22. package/tests/repros/repro-pga300-c6-trace-alignment.test.ts +36 -0
package/dist/index.js CHANGED
@@ -5690,22 +5690,8 @@ var getRailAlignmentFallbackCoordinates = ({
5690
5690
  return getDistinctCoordinates(coordinates);
5691
5691
  };
5692
5692
 
5693
- // lib/solvers/TraceCleanupSolver/sameNetRailAlignment/moveRailSegments.ts
5694
- var moveRailSegments = (trace, segments, coordinate) => {
5695
- const pointsToMove = /* @__PURE__ */ new Set();
5696
- for (const segment of segments) {
5697
- pointsToMove.add(segment.segmentIndex);
5698
- pointsToMove.add(segment.segmentIndex + 1);
5699
- }
5700
- const orientation = segments[0].orientation;
5701
- const tracePath = simplifyPath(
5702
- trace.tracePath.map((point, index) => {
5703
- if (!pointsToMove.has(index)) return point;
5704
- return orientation === "vertical" ? { ...point, x: coordinate } : { ...point, y: coordinate };
5705
- })
5706
- );
5707
- return { ...trace, tracePath };
5708
- };
5693
+ // lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getFixedLabelCoordinate.ts
5694
+ import { distance as distance4 } from "@tscircuit/math-utils";
5709
5695
 
5710
5696
  // lib/solvers/RailNetLabelCornerPlacementSolver/geometry.ts
5711
5697
  var EPS7 = 1e-6;
@@ -5740,6 +5726,95 @@ var isPointOnSegment = (point, start, end) => {
5740
5726
  };
5741
5727
  var getSegmentOrientation = (a, b) => Math.abs(a.y - b.y) <= EPS7 ? "horizontal" : "vertical";
5742
5728
 
5729
+ // lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getFixedLabelCoordinate.ts
5730
+ var getTransitivelyConnectedTraceIds = (group, traces) => {
5731
+ const groupNetId = group[0].globalConnNetId;
5732
+ const sameNetTraces = traces.filter(
5733
+ (trace) => trace.globalConnNetId === groupNetId
5734
+ );
5735
+ const connectedTraceIds = new Set(group.map((segment) => segment.traceId));
5736
+ const connectedPinIds = new Set(
5737
+ sameNetTraces.filter((trace) => connectedTraceIds.has(trace.mspPairId)).flatMap((trace) => trace.pinIds)
5738
+ );
5739
+ for (let changed = true; changed; ) {
5740
+ changed = false;
5741
+ for (const trace of sameNetTraces) {
5742
+ if (connectedTraceIds.has(trace.mspPairId)) continue;
5743
+ if (!trace.pinIds.some((pinId) => connectedPinIds.has(pinId))) continue;
5744
+ connectedTraceIds.add(trace.mspPairId);
5745
+ for (const pinId of trace.pinIds) connectedPinIds.add(pinId);
5746
+ changed = true;
5747
+ }
5748
+ }
5749
+ return connectedTraceIds;
5750
+ };
5751
+ var getLabelsLinkedToRailGroup = (group, netLabelPlacements, traces) => {
5752
+ const traceIds = getTransitivelyConnectedTraceIds(group, traces);
5753
+ return netLabelPlacements.filter(
5754
+ (label) => label.mspConnectionPairIds.some((traceId) => traceIds.has(traceId))
5755
+ );
5756
+ };
5757
+ var getFixedLabelCoordinate = (group, netLabelPlacements, traces) => {
5758
+ const traceMap = new Map(traces.map((trace) => [trace.mspPairId, trace]));
5759
+ const groupTraceIds = new Set(group.map((segment) => segment.traceId));
5760
+ const groupHasOnlySingleRailTraces = [...groupTraceIds].every(
5761
+ (traceId) => traceMap.get(traceId)?.tracePath.length === 4
5762
+ );
5763
+ if (!groupHasOnlySingleRailTraces) return null;
5764
+ const orientation = group[0].orientation;
5765
+ const linkedLabels = getLabelsLinkedToRailGroup(
5766
+ group,
5767
+ netLabelPlacements,
5768
+ traces
5769
+ );
5770
+ const anchoringLabels = linkedLabels.filter(
5771
+ (label) => label.orientation === group[0].componentFacingDirection && label.mspConnectionPairIds.some((traceId) => {
5772
+ const trace = traceMap.get(traceId);
5773
+ return trace && tracePathContainsPoint(trace.tracePath, label.anchorPoint);
5774
+ })
5775
+ );
5776
+ const coordinates = getDistinctCoordinates(
5777
+ anchoringLabels.map(
5778
+ (label) => orientation === "vertical" ? label.anchorPoint.x : label.anchorPoint.y
5779
+ )
5780
+ );
5781
+ if (coordinates.length !== 1) return null;
5782
+ const labelCoordinate = coordinates[0];
5783
+ const coordinate = group.find(
5784
+ (segment) => Math.abs(segment.coordinate - labelCoordinate) <= RAIL_ALIGNMENT_EPSILON
5785
+ )?.coordinate ?? labelCoordinate;
5786
+ const labelIsAnchoredToRoutedBackbone = anchoringLabels.some(
5787
+ (label) => label.mspConnectionPairIds.some(
5788
+ (traceId) => (traceMap.get(traceId)?.tracePath.length ?? 0) > 4
5789
+ )
5790
+ );
5791
+ if (labelIsAnchoredToRoutedBackbone) return coordinate;
5792
+ const coordinateIsLocalToEveryTrace = group.every((segment) => {
5793
+ const trace = traceMap.get(segment.traceId);
5794
+ if (!trace) return false;
5795
+ return Math.abs(segment.coordinate - coordinate) <= distance4(trace.pins[0], trace.pins[1]) + RAIL_ALIGNMENT_EPSILON;
5796
+ });
5797
+ return coordinateIsLocalToEveryTrace ? coordinate : null;
5798
+ };
5799
+
5800
+ // lib/solvers/TraceCleanupSolver/sameNetRailAlignment/moveRailSegments.ts
5801
+ var moveRailSegments = (trace, segments, coordinate) => {
5802
+ const pointsToMove = /* @__PURE__ */ new Set();
5803
+ for (const segment of segments) {
5804
+ if (nearlyEqual(segment.coordinate, coordinate)) continue;
5805
+ pointsToMove.add(segment.segmentIndex);
5806
+ pointsToMove.add(segment.segmentIndex + 1);
5807
+ }
5808
+ const orientation = segments[0].orientation;
5809
+ const tracePath = simplifyPath(
5810
+ trace.tracePath.map((point, index) => {
5811
+ if (!pointsToMove.has(index)) return point;
5812
+ return orientation === "vertical" ? { ...point, x: coordinate } : { ...point, y: coordinate };
5813
+ })
5814
+ );
5815
+ return { ...trace, tracePath };
5816
+ };
5817
+
5743
5818
  // lib/solvers/TraceCleanupSolver/sameNetRailAlignment/preservesLabelAnchors.ts
5744
5819
  var getAnchoredTraceIds = (label, traces) => new Set(
5745
5820
  traces.filter(
@@ -5855,10 +5930,10 @@ var projectPointToPath = (point, path) => {
5855
5930
  let bestDistance = Number.POSITIVE_INFINITY;
5856
5931
  for (let i = 0; i < path.length - 1; i++) {
5857
5932
  const projectedPoint = projectPointToSegment(point, path[i], path[i + 1]);
5858
- const distance5 = getDistance2(point, projectedPoint);
5859
- if (distance5 < bestDistance) {
5933
+ const distance7 = getDistance2(point, projectedPoint);
5934
+ if (distance7 < bestDistance) {
5860
5935
  bestPoint = projectedPoint;
5861
- bestDistance = distance5;
5936
+ bestDistance = distance7;
5862
5937
  }
5863
5938
  }
5864
5939
  return bestPoint;
@@ -5964,13 +6039,18 @@ var evaluateRailGroup = ({
5964
6039
  const originalCoordinates = getDistinctCoordinates(
5965
6040
  group.map((segment) => segment.coordinate)
5966
6041
  );
6042
+ const fixedLabelCoordinate = getFixedLabelCoordinate(
6043
+ group,
6044
+ netLabelPlacements,
6045
+ traces
6046
+ );
5967
6047
  const otherNetTraces = traces.filter(
5968
6048
  (trace) => trace.globalConnNetId !== group[0].globalConnNetId
5969
6049
  );
5970
6050
  const immutableSameNetTraces = traces.filter(
5971
6051
  (trace) => trace.globalConnNetId === group[0].globalConnNetId && !eligibleTraceIds.has(trace.mspPairId)
5972
6052
  );
5973
- const evaluateCoordinates = (coordinates) => {
6053
+ const evaluateCoordinates = (coordinates, options) => {
5974
6054
  let best = null;
5975
6055
  for (const coordinate of coordinates) {
5976
6056
  const candidateMap = /* @__PURE__ */ new Map();
@@ -6006,7 +6086,9 @@ var evaluateRailGroup = ({
6006
6086
  allCandidateTraces
6007
6087
  );
6008
6088
  if (metrics.otherNetCrossings > baseline.otherNetCrossings) continue;
6009
- if (!isReadabilityImprovement(metrics, baseline)) continue;
6089
+ if (options?.coordinateIsFixedByLabel ? metrics.turnCount > baseline.turnCount : !isReadabilityImprovement(metrics, baseline)) {
6090
+ continue;
6091
+ }
6010
6092
  const score = {
6011
6093
  ...metrics,
6012
6094
  displacement: group.reduce(
@@ -6022,13 +6104,21 @@ var evaluateRailGroup = ({
6022
6104
  return tracePathChanged(original, candidate2);
6023
6105
  }).map((trace) => trace.mspPairId);
6024
6106
  if (changedTraceIds.length === 0) continue;
6025
- const candidate = { traces: allCandidateTraces, changedTraceIds, score };
6107
+ const candidate = {
6108
+ traces: allCandidateTraces,
6109
+ changedTraceIds,
6110
+ score
6111
+ };
6026
6112
  if (!best || scoreIsBetter(candidate.score, best.score)) best = candidate;
6027
6113
  }
6028
6114
  return best;
6029
6115
  };
6030
- const originalCandidate = evaluateCoordinates(originalCoordinates);
6116
+ const originalCandidate = evaluateCoordinates(
6117
+ fixedLabelCoordinate === null ? originalCoordinates : [fixedLabelCoordinate],
6118
+ { coordinateIsFixedByLabel: fixedLabelCoordinate !== null }
6119
+ );
6031
6120
  if (originalCandidate) return originalCandidate;
6121
+ if (fixedLabelCoordinate !== null) return null;
6032
6122
  return evaluateCoordinates(
6033
6123
  getRailAlignmentFallbackCoordinates({
6034
6124
  group,
@@ -6039,6 +6129,7 @@ var evaluateRailGroup = ({
6039
6129
  };
6040
6130
 
6041
6131
  // lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getComponentSideRailSegments.ts
6132
+ import { distance as distance5 } from "@tscircuit/math-utils";
6042
6133
  var getMovableRailSegments = (trace) => {
6043
6134
  const segments = [];
6044
6135
  const path = trace.tracePath;
@@ -6083,8 +6174,9 @@ var railIsOutsideComponent = (segment, chip, facingDirection) => {
6083
6174
  return segment.orientation === "horizontal" && segment.coordinate <= minY + RAIL_ALIGNMENT_EPSILON;
6084
6175
  }
6085
6176
  };
6086
- var getComponentSideRailSegments = (trace, chipMap) => {
6177
+ var getComponentSideRailSegments = (trace, chipMap, options) => {
6087
6178
  const segments = [];
6179
+ const isLocalConnection = distance5(trace.pins[0], trace.pins[1]) <= (options?.maxMspPairDistance ?? DEFAULT_MAX_MSP_PAIR_DISTANCE);
6088
6180
  for (const segment of getMovableRailSegments(trace)) {
6089
6181
  const associations = trace.pins.flatMap((pin, pinIndex) => {
6090
6182
  const chip = chipMap.get(pin.chipId);
@@ -6102,9 +6194,18 @@ var getComponentSideRailSegments = (trace, chipMap) => {
6102
6194
  ];
6103
6195
  });
6104
6196
  associations.sort((a, b) => a.distanceFromEndpoint - b.distanceFromEndpoint);
6105
- const association = associations[0];
6106
- if (!association) continue;
6107
- segments.push({ ...segment, ...association });
6197
+ const minimumDistance = associations[0]?.distanceFromEndpoint;
6198
+ const nearestAssociationKeys = /* @__PURE__ */ new Set();
6199
+ for (const association of associations) {
6200
+ if (nearestAssociationKeys.size > 0 && (!options?.includeTiedEndpointAssociations || !isLocalConnection)) {
6201
+ break;
6202
+ }
6203
+ if (association.distanceFromEndpoint !== minimumDistance) continue;
6204
+ const associationKey = `${association.componentId}:${association.componentFacingDirection}`;
6205
+ if (nearestAssociationKeys.has(associationKey)) continue;
6206
+ nearestAssociationKeys.add(associationKey);
6207
+ segments.push({ ...segment, ...association });
6208
+ }
6108
6209
  }
6109
6210
  return segments;
6110
6211
  };
@@ -6135,41 +6236,90 @@ var tracesSharePin = (a, b, traceMap) => {
6135
6236
  return traceMap.get(b.traceId).pins.some((pin) => aPinIds.has(pin.pinId));
6136
6237
  };
6137
6238
  var canJoinRailGroup = (start, current, candidate, traceMap, obstacles) => candidate.globalConnNetId === start.globalConnNetId && candidate.orientation === start.orientation && candidate.componentId === start.componentId && candidate.componentFacingDirection === start.componentFacingDirection && (rangesTouchOrOverlap(current, candidate) || tracesSharePin(current, candidate, traceMap)) && corridorIsClear(current, candidate, obstacles);
6138
- var getRailGroups = (traces, eligibleTraceIds, inputProblem, obstacles) => {
6239
+ var getRailGroups = (traces, eligibleTraceIds, inputProblem, obstacles, netLabelPlacements) => {
6139
6240
  const chipMap = new Map(inputProblem.chips.map((chip) => [chip.chipId, chip]));
6140
- const segments = traces.filter((trace) => eligibleTraceIds.has(trace.mspPairId)).flatMap((trace) => getComponentSideRailSegments(trace, chipMap));
6141
6241
  const traceMap = new Map(traces.map((trace) => [trace.mspPairId, trace]));
6142
- const visited = /* @__PURE__ */ new Set();
6143
- const groups = [];
6144
- for (let startIndex = 0; startIndex < segments.length; startIndex++) {
6145
- if (visited.has(startIndex)) continue;
6146
- const start = segments[startIndex];
6147
- const queue = [startIndex];
6148
- const group = [];
6149
- visited.add(startIndex);
6150
- for (let queueIndex = 0; queueIndex < queue.length; queueIndex++) {
6151
- const current = segments[queue[queueIndex]];
6152
- group.push(current);
6153
- for (let candidateIndex = 0; candidateIndex < segments.length; candidateIndex++) {
6154
- if (visited.has(candidateIndex)) continue;
6155
- const candidate = segments[candidateIndex];
6156
- if (!canJoinRailGroup(start, current, candidate, traceMap, obstacles)) {
6157
- continue;
6242
+ const eligibleTraces = traces.filter(
6243
+ (trace) => eligibleTraceIds.has(trace.mspPairId)
6244
+ );
6245
+ const collectConnectedGroups = (segments) => {
6246
+ const visited = /* @__PURE__ */ new Set();
6247
+ const connectedGroups = [];
6248
+ for (let startIndex = 0; startIndex < segments.length; startIndex++) {
6249
+ if (visited.has(startIndex)) continue;
6250
+ const start = segments[startIndex];
6251
+ const queue = [startIndex];
6252
+ const group = [];
6253
+ visited.add(startIndex);
6254
+ for (let queueIndex = 0; queueIndex < queue.length; queueIndex++) {
6255
+ const current = segments[queue[queueIndex]];
6256
+ group.push(current);
6257
+ for (let candidateIndex = 0; candidateIndex < segments.length; candidateIndex++) {
6258
+ if (visited.has(candidateIndex)) continue;
6259
+ const candidate = segments[candidateIndex];
6260
+ if (!canJoinRailGroup(start, current, candidate, traceMap, obstacles)) {
6261
+ continue;
6262
+ }
6263
+ visited.add(candidateIndex);
6264
+ queue.push(candidateIndex);
6158
6265
  }
6159
- visited.add(candidateIndex);
6160
- queue.push(candidateIndex);
6161
6266
  }
6267
+ connectedGroups.push(group);
6162
6268
  }
6269
+ return connectedGroups;
6270
+ };
6271
+ const groupKey = (group) => [
6272
+ group[0].componentId,
6273
+ group[0].componentFacingDirection,
6274
+ group[0].orientation,
6275
+ ...group.map((segment) => `${segment.traceId}:${segment.segmentIndex}`).sort()
6276
+ ].join("|");
6277
+ const selectedGroups = [];
6278
+ const selectedGroupKeys = /* @__PURE__ */ new Set();
6279
+ const addEligibleGroup = (group, options) => {
6163
6280
  const traceCount = new Set(group.map((segment) => segment.traceId)).size;
6281
+ if (traceCount < 2) return;
6282
+ const fixedLabelCoordinate = getFixedLabelCoordinate(
6283
+ group,
6284
+ netLabelPlacements,
6285
+ traces
6286
+ );
6287
+ if (options?.requireFixedLabel && fixedLabelCoordinate === null) return;
6164
6288
  const hasDifferentCoordinates = group.some(
6165
6289
  (segment) => !nearlyEqual(segment.coordinate, group[0].coordinate)
6166
6290
  );
6167
- if (traceCount >= 2 && hasDifferentCoordinates) groups.push(group);
6291
+ const hasDifferentFixedLabelCoordinate = fixedLabelCoordinate !== null && !nearlyEqual(fixedLabelCoordinate, group[0].coordinate);
6292
+ if (!hasDifferentCoordinates && !hasDifferentFixedLabelCoordinate) return;
6293
+ const key = groupKey(group);
6294
+ if (selectedGroupKeys.has(key)) return;
6295
+ selectedGroupKeys.add(key);
6296
+ selectedGroups.push(group);
6297
+ };
6298
+ const primarySegments = eligibleTraces.flatMap(
6299
+ (trace) => getComponentSideRailSegments(trace, chipMap)
6300
+ );
6301
+ for (const group of collectConnectedGroups(primarySegments)) {
6302
+ addEligibleGroup(group);
6303
+ }
6304
+ const tiedEndpointSegments = eligibleTraces.flatMap(
6305
+ (trace) => getComponentSideRailSegments(trace, chipMap, {
6306
+ includeTiedEndpointAssociations: true,
6307
+ maxMspPairDistance: inputProblem.maxMspPairDistance
6308
+ })
6309
+ );
6310
+ for (const group of collectConnectedGroups(tiedEndpointSegments)) {
6311
+ addEligibleGroup(group, { requireFixedLabel: true });
6168
6312
  }
6169
- return groups;
6313
+ return selectedGroups;
6170
6314
  };
6171
6315
 
6172
6316
  // lib/solvers/TraceCleanupSolver/alignSameNetRails.ts
6317
+ var getTraceStateKey = (traces) => JSON.stringify(
6318
+ traces.map((trace) => ({
6319
+ mspPairId: trace.mspPairId,
6320
+ tracePath: trace.tracePath
6321
+ }))
6322
+ );
6173
6323
  var alignSameNetRails = ({
6174
6324
  inputProblem,
6175
6325
  traces,
@@ -6177,6 +6327,7 @@ var alignSameNetRails = ({
6177
6327
  eligibleTraceIds
6178
6328
  }) => {
6179
6329
  let outputTraces = [...traces];
6330
+ const seenTraceStates = /* @__PURE__ */ new Set([getTraceStateKey(outputTraces)]);
6180
6331
  const obstacles = getObstacleRects(inputProblem);
6181
6332
  const alignedTraceIds = /* @__PURE__ */ new Set();
6182
6333
  let alignedRailGroupCount = 0;
@@ -6189,7 +6340,8 @@ var alignSameNetRails = ({
6189
6340
  outputTraces,
6190
6341
  eligibleTraceIds,
6191
6342
  inputProblem,
6192
- obstacles
6343
+ obstacles,
6344
+ netLabelPlacements
6193
6345
  );
6194
6346
  let applied = null;
6195
6347
  for (const group of groups) {
@@ -6203,9 +6355,13 @@ var alignSameNetRails = ({
6203
6355
  if (applied) break;
6204
6356
  }
6205
6357
  if (!applied) break;
6358
+ const candidateStateKey = getTraceStateKey(applied.traces);
6359
+ const repeatsSeenState = seenTraceStates.has(candidateStateKey);
6360
+ if (!repeatsSeenState) seenTraceStates.add(candidateStateKey);
6206
6361
  outputTraces = applied.traces;
6207
6362
  alignedRailGroupCount++;
6208
6363
  for (const traceId of applied.changedTraceIds) alignedTraceIds.add(traceId);
6364
+ if (repeatsSeenState) break;
6209
6365
  }
6210
6366
  return {
6211
6367
  traces: outputTraces,
@@ -7801,11 +7957,11 @@ var getLabelHugDistance = (tracePath, obstacleLabel) => {
7801
7957
  obstacleLabel.width,
7802
7958
  obstacleLabel.height
7803
7959
  );
7804
- let distance5 = 0;
7960
+ let distance7 = 0;
7805
7961
  for (const point of tracePath) {
7806
- distance5 += getPointDistanceFromRect(point, bounds);
7962
+ distance7 += getPointDistanceFromRect(point, bounds);
7807
7963
  }
7808
- return distance5;
7964
+ return distance7;
7809
7965
  };
7810
7966
  var getPointDistanceFromRect = (point, rect) => {
7811
7967
  const dx = Math.max(rect.minX - point.x, 0, point.x - rect.maxX);
@@ -8082,16 +8238,16 @@ var Example28Solver = class extends BaseSolver {
8082
8238
  const outward = dir(label.orientation);
8083
8239
  if (outward.x === 0 && outward.y === 0) return null;
8084
8240
  for (let step = 1; step <= LABEL_MAX_OUTWARD_STEPS; step++) {
8085
- const distance5 = step * LABEL_OUTWARD_STEP;
8241
+ const distance7 = step * LABEL_OUTWARD_STEP;
8086
8242
  const candidate = {
8087
8243
  ...label,
8088
8244
  anchorPoint: {
8089
- x: label.anchorPoint.x + outward.x * distance5,
8090
- y: label.anchorPoint.y + outward.y * distance5
8245
+ x: label.anchorPoint.x + outward.x * distance7,
8246
+ y: label.anchorPoint.y + outward.y * distance7
8091
8247
  },
8092
8248
  center: {
8093
- x: label.center.x + outward.x * distance5,
8094
- y: label.center.y + outward.y * distance5
8249
+ x: label.center.x + outward.x * distance7,
8250
+ y: label.center.y + outward.y * distance7
8095
8251
  }
8096
8252
  };
8097
8253
  const candidateWithClearance = {
@@ -8960,10 +9116,8 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
8960
9116
  }
8961
9117
  findValidTraceAnchorCandidate(label, orientation, labelIndex) {
8962
9118
  const direction = dir(orientation);
8963
- const candidatePoints = this.getTraceAnchorCandidatePoints(
8964
- label,
8965
- orientation
8966
- ).sort((a, b) => {
9119
+ const { points } = this.getTraceAnchorCandidates(label, orientation);
9120
+ const candidatePoints = points.sort((a, b) => {
8967
9121
  const aAlongDirection = a.x * direction.x + a.y * direction.y;
8968
9122
  const bAlongDirection = b.x * direction.x + b.y * direction.y;
8969
9123
  return bAlongDirection - aAlongDirection;
@@ -8986,19 +9140,19 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
8986
9140
  }
8987
9141
  findValidOutwardTraceAnchorCandidate(label, orientation, labelIndex) {
8988
9142
  const direction = dir(orientation);
8989
- const candidatePoints = this.getTraceAnchorCandidatePoints(
8990
- label,
8991
- orientation
8992
- ).sort((a, b) => {
9143
+ const preservedColumnAnchor = this.getSearchStartAnchor(label, orientation);
9144
+ const { points, preferNearestBendOnOutwardRow } = this.getTraceAnchorCandidates(label, orientation);
9145
+ const candidatePoints = points.sort((a, b) => {
8993
9146
  const aAlongDirection = a.x * direction.x + a.y * direction.y;
8994
9147
  const bAlongDirection = b.x * direction.x + b.y * direction.y;
8995
- return bAlongDirection - aAlongDirection;
9148
+ if (!preferNearestBendOnOutwardRow) {
9149
+ return bAlongDirection - aAlongDirection;
9150
+ }
9151
+ const aPerpendicularDistance = isYOrientation(orientation) ? Math.abs(a.x - preservedColumnAnchor.x) : Math.abs(a.y - preservedColumnAnchor.y);
9152
+ const bPerpendicularDistance = isYOrientation(orientation) ? Math.abs(b.x - preservedColumnAnchor.x) : Math.abs(b.y - preservedColumnAnchor.y);
9153
+ return bAlongDirection - aAlongDirection || aPerpendicularDistance - bPerpendicularDistance;
8996
9154
  });
8997
9155
  for (const connectorSource of candidatePoints) {
8998
- const preservedColumnAnchor = this.getSearchStartAnchor(
8999
- label,
9000
- orientation
9001
- );
9002
9156
  const preservedColumnCandidate = this.createCandidate(
9003
9157
  label,
9004
9158
  {
@@ -9024,10 +9178,10 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9024
9178
  orientation
9025
9179
  );
9026
9180
  if (outwardDirection.x === 0 && outwardDirection.y === 0) continue;
9027
- for (let distance5 = LABEL_SEARCH_STEP; distance5 <= this.maxSearchDistance + EPS12; distance5 += LABEL_SEARCH_STEP) {
9181
+ for (let distance7 = LABEL_SEARCH_STEP; distance7 <= this.maxSearchDistance + EPS12; distance7 += LABEL_SEARCH_STEP) {
9028
9182
  const anchorPoint = {
9029
- x: connectorSource.x + outwardDirection.x * distance5,
9030
- y: connectorSource.y + outwardDirection.y * distance5
9183
+ x: connectorSource.x + outwardDirection.x * distance7,
9184
+ y: connectorSource.y + outwardDirection.y * distance7
9031
9185
  };
9032
9186
  const candidate = this.createCandidate(
9033
9187
  label,
@@ -9040,7 +9194,7 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9040
9194
  label,
9041
9195
  labelIndex,
9042
9196
  "outward-trace-anchor",
9043
- distance5
9197
+ distance7
9044
9198
  );
9045
9199
  this.recordCandidateResult(result);
9046
9200
  if (result.status === "valid") {
@@ -9051,10 +9205,11 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9051
9205
  }
9052
9206
  return null;
9053
9207
  }
9054
- getTraceAnchorCandidatePoints(label, orientation) {
9208
+ getTraceAnchorCandidates(label, orientation) {
9055
9209
  const seen = /* @__PURE__ */ new Set();
9056
9210
  const points = [];
9057
- const connectedTraceIds = new Set(label.mspConnectionPairIds ?? []);
9211
+ const directHostTraceIds = new Set(label.mspConnectionPairIds ?? []);
9212
+ const connectedTraceIds = new Set(directHostTraceIds);
9058
9213
  if (isYOrientation(orientation)) {
9059
9214
  const connectedPinIds = /* @__PURE__ */ new Set();
9060
9215
  for (const traceId of connectedTraceIds) {
@@ -9094,7 +9249,39 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9094
9249
  points.push(point);
9095
9250
  }
9096
9251
  }
9097
- return points;
9252
+ const direction = dir(orientation);
9253
+ const furthestAlong = Math.max(
9254
+ ...points.map((point) => point.x * direction.x + point.y * direction.y)
9255
+ );
9256
+ const furthestRowPoints = points.filter(
9257
+ (point) => Math.abs(
9258
+ point.x * direction.x + point.y * direction.y - furthestAlong
9259
+ ) <= EPS12
9260
+ );
9261
+ const directHostPinIds = new Set(
9262
+ [...directHostTraceIds].flatMap(
9263
+ (traceId) => this.traceMap[traceId]?.pinIds ?? []
9264
+ )
9265
+ );
9266
+ const adjacentTraces = [...connectedTraceIds].flatMap((traceId) => {
9267
+ if (directHostTraceIds.has(traceId)) return [];
9268
+ const trace = this.traceMap[traceId];
9269
+ if (!trace?.pinIds.some((pinId) => directHostPinIds.has(pinId))) return [];
9270
+ return [trace];
9271
+ });
9272
+ return {
9273
+ points,
9274
+ // Prefer the nearest bend when the outward row is on a trace directly
9275
+ // adjacent to the label's host. Deeper transitive chains keep their
9276
+ // stable trace ordering.
9277
+ preferNearestBendOnOutwardRow: adjacentTraces.some(
9278
+ (trace) => furthestRowPoints.some(
9279
+ (point) => trace.tracePath.some(
9280
+ (tracePoint) => Math.abs(tracePoint.x - point.x) <= EPS12 && Math.abs(tracePoint.y - point.y) <= EPS12
9281
+ )
9282
+ )
9283
+ )
9284
+ };
9098
9285
  }
9099
9286
  sharesVerticalRailWithAny(trace, otherTraces) {
9100
9287
  const verticalRailXs = /* @__PURE__ */ new Set();
@@ -9203,10 +9390,10 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9203
9390
  phase = "shift",
9204
9391
  stopOnTraceCollision = true
9205
9392
  } = params;
9206
- for (let distance5 = LABEL_SEARCH_STEP; distance5 <= maxSearchDistance + EPS12; distance5 += LABEL_SEARCH_STEP) {
9393
+ for (let distance7 = LABEL_SEARCH_STEP; distance7 <= maxSearchDistance + EPS12; distance7 += LABEL_SEARCH_STEP) {
9207
9394
  const anchorPoint = {
9208
- x: baseAnchor.x + direction.x * distance5,
9209
- y: baseAnchor.y + direction.y * distance5
9395
+ x: baseAnchor.x + direction.x * distance7,
9396
+ y: baseAnchor.y + direction.y * distance7
9210
9397
  };
9211
9398
  const candidate = this.createCandidate(label, anchorPoint, orientation);
9212
9399
  const result = this.evaluateCandidate(
@@ -9214,7 +9401,7 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9214
9401
  label,
9215
9402
  labelIndex,
9216
9403
  phase,
9217
- distance5,
9404
+ distance7,
9218
9405
  outwardDistance
9219
9406
  );
9220
9407
  this.recordCandidateResult(result);
@@ -9226,11 +9413,11 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9226
9413
  }
9227
9414
  return null;
9228
9415
  }
9229
- evaluateCandidate(candidate, label, labelIndex, phase, distance5, outwardDistance) {
9416
+ evaluateCandidate(candidate, label, labelIndex, phase, distance7, outwardDistance) {
9230
9417
  return {
9231
9418
  ...candidate,
9232
9419
  phase,
9233
- distance: distance5,
9420
+ distance: distance7,
9234
9421
  outwardDistance,
9235
9422
  selected: false,
9236
9423
  status: this.getCandidateStatus({
@@ -9736,10 +9923,10 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9736
9923
  if (point.x < bounds.minX - EPS12 || point.x > bounds.maxX + EPS12 || point.y < bounds.minY - EPS12 || point.y > bounds.maxY + EPS12) {
9737
9924
  continue;
9738
9925
  }
9739
- for (const [side, distance5] of getSideDistances(point, bounds)) {
9740
- if (distance5 < nearestDistance) {
9926
+ for (const [side, distance7] of getSideDistances(point, bounds)) {
9927
+ if (distance7 < nearestDistance) {
9741
9928
  nearestSide = side;
9742
- nearestDistance = distance5;
9929
+ nearestDistance = distance7;
9743
9930
  }
9744
9931
  }
9745
9932
  }
@@ -9768,10 +9955,10 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9768
9955
  let nearestSide = null;
9769
9956
  let nearestDistance = Number.POSITIVE_INFINITY;
9770
9957
  for (const chip of this.chipObstacleSpatialIndex.chips) {
9771
- for (const [side, distance5] of getSideDistances(point, chip.bounds)) {
9772
- if (distance5 < nearestDistance) {
9958
+ for (const [side, distance7] of getSideDistances(point, chip.bounds)) {
9959
+ if (distance7 < nearestDistance) {
9773
9960
  nearestSide = side;
9774
- nearestDistance = distance5;
9961
+ nearestDistance = distance7;
9775
9962
  }
9776
9963
  }
9777
9964
  }
@@ -10354,17 +10541,17 @@ var getTraceLength = (trace) => {
10354
10541
  }
10355
10542
  return length;
10356
10543
  };
10357
- var getPointAtTraceDistance = (trace, distance5) => {
10544
+ var getPointAtTraceDistance = (trace, distance7) => {
10358
10545
  let pathDistance = 0;
10359
10546
  for (let i = 0; i < trace.tracePath.length - 1; i++) {
10360
10547
  const start = trace.tracePath[i];
10361
10548
  const end = trace.tracePath[i + 1];
10362
10549
  const segmentLength = getManhattanDistance(start, end);
10363
10550
  const nextDistance = pathDistance + segmentLength;
10364
- if (distance5 <= nextDistance + EPS13) {
10551
+ if (distance7 <= nextDistance + EPS13) {
10365
10552
  const offset = Math.max(
10366
10553
  0,
10367
- Math.min(segmentLength, distance5 - pathDistance)
10554
+ Math.min(segmentLength, distance7 - pathDistance)
10368
10555
  );
10369
10556
  const direction = getSegmentDirection(start, end);
10370
10557
  return {
@@ -10489,13 +10676,13 @@ var getCandidateDistances = (traceLength, vertexDistances) => {
10489
10676
  const distances = /* @__PURE__ */ new Set();
10490
10677
  const maxSteps = Math.ceil(traceLength / CANDIDATE_STEP);
10491
10678
  for (let i = 0; i <= maxSteps; i++) {
10492
- const distance5 = Math.min(traceLength, i * CANDIDATE_STEP);
10493
- distances.add(roundDistance(distance5));
10679
+ const distance7 = Math.min(traceLength, i * CANDIDATE_STEP);
10680
+ distances.add(roundDistance(distance7));
10494
10681
  }
10495
- for (const distance5 of vertexDistances) {
10496
- distances.add(roundDistance(distance5));
10682
+ for (const distance7 of vertexDistances) {
10683
+ distances.add(roundDistance(distance7));
10497
10684
  }
10498
- return [...distances].filter((distance5) => distance5 >= -EPS13 && distance5 <= traceLength + EPS13).sort((a, b) => a - b);
10685
+ return [...distances].filter((distance7) => distance7 >= -EPS13 && distance7 <= traceLength + EPS13).sort((a, b) => a - b);
10499
10686
  };
10500
10687
  var getOrientationsForPoint = (params) => {
10501
10688
  const { inputProblem, label, point, orientationConstraint } = params;
@@ -10668,7 +10855,7 @@ var getNetLabelHeight = (inputProblem, label) => {
10668
10855
  (nc) => nc.pinIds.some((pid) => label.pinIds.includes(pid))
10669
10856
  )?.netLabelHeight;
10670
10857
  };
10671
- var roundDistance = (distance5) => Number(distance5.toFixed(6));
10858
+ var roundDistance = (distance7) => Number(distance7.toFixed(6));
10672
10859
  var isSamePlacement = (label, point, orientation) => Math.abs(point.x - label.anchorPoint.x) <= EPS13 && Math.abs(point.y - label.anchorPoint.y) <= EPS13 && orientation === label.orientation;
10673
10860
 
10674
10861
  // lib/solvers/TraceAnchoredNetLabelOverlapSolver/visualize.ts
@@ -11649,7 +11836,7 @@ ${c.status}`
11649
11836
  };
11650
11837
 
11651
11838
  // lib/solvers/UnroutedTraceRecoverySolver/UnroutedTraceRecoverySolver.ts
11652
- import { distance as distance4, doSegmentsIntersect as doSegmentsIntersect3 } from "@tscircuit/math-utils";
11839
+ import { distance as distance6, doSegmentsIntersect as doSegmentsIntersect3 } from "@tscircuit/math-utils";
11653
11840
  var ROUTE_CLEARANCE = 0.2;
11654
11841
  var COORDINATE_TOLERANCE = 1e-9;
11655
11842
  var GROUND_NET_ID = "GND";
@@ -11811,7 +11998,7 @@ var getJunctionCandidates = ({
11811
11998
  facingDirection: pin._facingDirection
11812
11999
  });
11813
12000
  for (const junctionPoint of junctionPoints) {
11814
- if (distance4(pin, junctionPoint) > maxConnectionDistance) {
12001
+ if (distance6(pin, junctionPoint) > maxConnectionDistance) {
11815
12002
  continue;
11816
12003
  }
11817
12004
  candidates.push(
@@ -12027,7 +12214,7 @@ var UnroutedTraceRecoverySolver = class extends BaseSolver {
12027
12214
  if (connectionPair.globalConnNetId === this.groundGlobalConnNetId) {
12028
12215
  return;
12029
12216
  }
12030
- if (distance4(connectionPair.pins[0], connectionPair.pins[1]) > this.maxConnectionDistance) {
12217
+ if (distance6(connectionPair.pins[0], connectionPair.pins[1]) > this.maxConnectionDistance) {
12031
12218
  return;
12032
12219
  }
12033
12220
  const obstacles = getObstacleRects(this.inputProblem);
@@ -12139,6 +12326,7 @@ var pathEntersAnyNetLabel = ({
12139
12326
  // lib/solvers/SameNetJunctionAlignmentSolver/alignSameNetJunctions.ts
12140
12327
  var MAX_ALIGNED_LOAD_PIN_OFFSET = 0.2;
12141
12328
  var MAX_SAME_NET_LABEL_BOUNDARY_RAIL_OFFSET = 0.2;
12329
+ var MAX_SHARED_PIN_RAIL_OFFSET = 0.05;
12142
12330
  var getSharedPin = ({
12143
12331
  donorTrace,
12144
12332
  branchTrace
@@ -12178,6 +12366,28 @@ var railIsOnFacingSide = ({
12178
12366
  if (pin._facingDirection === "y+") return railY > pin.y;
12179
12367
  return false;
12180
12368
  };
12369
+ var railIsOnSharedPinFacingSide = ({
12370
+ railY,
12371
+ pin
12372
+ }) => {
12373
+ if (pin._facingDirection === "y+") return railY > pin.y;
12374
+ if (pin._facingDirection === "y-") return railY < pin.y;
12375
+ return false;
12376
+ };
12377
+ var getHorizontalPinApproachPoint = ({
12378
+ branchTrace,
12379
+ sharedPin,
12380
+ otherPin
12381
+ }) => {
12382
+ if (otherPin._facingDirection !== "x-" && otherPin._facingDirection !== "x+") {
12383
+ return null;
12384
+ }
12385
+ const sharedToOtherPath = branchTrace.pins[0].pinId === sharedPin.pinId ? branchTrace.tracePath : [...branchTrace.tracePath].reverse();
12386
+ const approachPoint = sharedToOtherPath.at(-2);
12387
+ if (!approachPoint || !nearlyEqual(approachPoint.y, otherPin.y)) return null;
12388
+ const approachesFromFacingSide = otherPin._facingDirection === "x-" ? approachPoint.x < otherPin.x : approachPoint.x > otherPin.x;
12389
+ return approachesFromFacingSide ? approachPoint : null;
12390
+ };
12181
12391
  var getAttachedLabelIndexes = (trace, netLabelPlacements) => netLabelPlacements.flatMap((label, index) => {
12182
12392
  const labelOwnsTrace = label.mspConnectionPairIds.includes(trace.mspPairId) || label.mspConnectionPairIds.length === 0 && label.globalConnNetId === trace.globalConnNetId;
12183
12393
  return labelOwnsTrace && tracePathContainsPoint(trace.tracePath, label.anchorPoint) ? [index] : [];
@@ -12226,19 +12436,45 @@ var getAlignedBranchPath = ({
12226
12436
  if (branchRail && nearlyEqual(branchRail.start.y, donorRail.start.y)) {
12227
12437
  return null;
12228
12438
  }
12229
- if (!railIsOnFacingSide({ railY: donorRail.start.y, pin: otherPin })) {
12439
+ const railFacesOtherPin = railIsOnFacingSide({
12440
+ railY: donorRail.start.y,
12441
+ pin: otherPin
12442
+ });
12443
+ const railFacesSharedPin = railIsOnSharedPinFacingSide({
12444
+ railY: donorRail.start.y,
12445
+ pin: sharedPin
12446
+ }) && Math.abs(donorRail.start.y - sharedPin.y) <= MAX_SHARED_PIN_RAIL_OFFSET;
12447
+ if (!railFacesOtherPin && !railFacesSharedPin) {
12230
12448
  return null;
12231
12449
  }
12232
12450
  const junction = getJunctionPoint({ segment: donorRail, sharedPin });
12233
12451
  const extendsDonorRail = donorOtherPin.x < junction.x && otherPin.x > junction.x || donorOtherPin.x > junction.x && otherPin.x < junction.x;
12234
12452
  if (!extendsDonorRail) return null;
12235
- const sharedToOther = simplifyPath([
12236
- { x: sharedPin.x, y: sharedPin.y },
12237
- { x: junction.x, y: sharedPin.y },
12238
- { x: junction.x, y: junction.y },
12239
- { x: otherPin.x, y: junction.y },
12240
- { x: otherPin.x, y: otherPin.y }
12241
- ]);
12453
+ let sharedToOther;
12454
+ if (!railFacesOtherPin && railFacesSharedPin) {
12455
+ const approachPoint = getHorizontalPinApproachPoint({
12456
+ branchTrace,
12457
+ sharedPin,
12458
+ otherPin
12459
+ });
12460
+ if (!approachPoint) return null;
12461
+ sharedToOther = simplifyPath([
12462
+ { x: sharedPin.x, y: sharedPin.y },
12463
+ { x: junction.x, y: sharedPin.y },
12464
+ { x: junction.x, y: junction.y },
12465
+ { x: approachPoint.x, y: junction.y },
12466
+ { x: approachPoint.x, y: otherPin.y },
12467
+ { x: otherPin.x, y: otherPin.y }
12468
+ ]);
12469
+ } else {
12470
+ sharedToOther = simplifyPath([
12471
+ { x: sharedPin.x, y: sharedPin.y },
12472
+ { x: junction.x, y: sharedPin.y },
12473
+ { x: junction.x, y: junction.y },
12474
+ { x: otherPin.x, y: junction.y },
12475
+ { x: otherPin.x, y: otherPin.y }
12476
+ ]);
12477
+ }
12242
12478
  if (branchTrace.pins[0].pinId === sharedPin.pinId) return sharedToOther;
12243
12479
  return [...sharedToOther].reverse();
12244
12480
  };
@@ -12961,17 +13197,17 @@ var getRequiredOutwardDistance = (label, inlineBounds) => {
12961
13197
  const targetMinY = Math.max(...nearby.map((bounds) => bounds.maxY));
12962
13198
  return Math.max(0, targetMinY - labelBounds.minY + LABEL_CLEARANCE2);
12963
13199
  };
12964
- var moveLabel = (label, orientation, distance5) => {
13200
+ var moveLabel = (label, orientation, distance7) => {
12965
13201
  const direction = dir(orientation);
12966
13202
  return {
12967
13203
  ...label,
12968
13204
  anchorPoint: {
12969
- x: label.anchorPoint.x + direction.x * distance5,
12970
- y: label.anchorPoint.y + direction.y * distance5
13205
+ x: label.anchorPoint.x + direction.x * distance7,
13206
+ y: label.anchorPoint.y + direction.y * distance7
12971
13207
  },
12972
13208
  center: {
12973
- x: label.center.x + direction.x * distance5,
12974
- y: label.center.y + direction.y * distance5
13209
+ x: label.center.x + direction.x * distance7,
13210
+ y: label.center.y + direction.y * distance7
12975
13211
  }
12976
13212
  };
12977
13213
  };
@@ -13096,15 +13332,15 @@ var pushAnchoredNetLabelsAwayFromInlineLabels = ({
13096
13332
  const movedLabelIndices = /* @__PURE__ */ new Set();
13097
13333
  for (let triggerIndex = 0; triggerIndex < outputLabels.length; triggerIndex++) {
13098
13334
  const trigger = outputLabels[triggerIndex];
13099
- const distance5 = getRequiredOutwardDistance(trigger, inlineBounds);
13100
- if (distance5 <= POINT_EPSILON || distance5 > MAX_OUTWARD_DISTANCE) continue;
13335
+ const distance7 = getRequiredOutwardDistance(trigger, inlineBounds);
13336
+ if (distance7 <= POINT_EPSILON || distance7 > MAX_OUTWARD_DISTANCE) continue;
13101
13337
  const { group, ownerChipIds } = getContiguousLabelGroup({
13102
13338
  triggerIndex,
13103
13339
  labels: outputLabels,
13104
13340
  chipIdByPinId
13105
13341
  });
13106
13342
  const distances = new Map(
13107
- [...group].map((labelIndex) => [labelIndex, distance5])
13343
+ [...group].map((labelIndex) => [labelIndex, distance7])
13108
13344
  );
13109
13345
  let failed = false;
13110
13346
  for (let iteration = 0; iteration < outputLabels.length; iteration++) {