@tscircuit/schematic-trace-solver 0.0.160 → 0.0.161

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 (58) hide show
  1. package/dist/index.d.ts +3 -0
  2. package/dist/index.js +171 -8
  3. package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +210 -6
  4. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getFixedLabelCoordinate.ts +44 -13
  5. package/package.json +1 -1
  6. package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +1 -1
  7. package/tests/bug-reports/bug-report-20260728T144234Z/__snapshots__/bug-report-20260728T144234Z.snap.svg +1 -1
  8. package/tests/bug-reports/bug-report-20260728T225606Z/__snapshots__/bug-report-20260728T225606Z.snap.svg +79 -79
  9. package/tests/bug-reports/bug-report-20260730T061837Z/__snapshots__/bug-report-20260730T061837Z.snap.svg +85 -85
  10. package/tests/bug-reports/bug-report-20260731T052229Z/__snapshots__/bug-report-20260731T052229Z.snap.svg +1 -1
  11. package/tests/bug-reports/bug-report-20260804T095800Z/__snapshots__/bug-report-20260804T095800Z.snap.svg +163 -163
  12. package/tests/bug-reports/bug-report-20260804T171919Z/__snapshots__/bug-report-20260804T171919Z.snap.svg +547 -547
  13. package/tests/bug-reports/bug-report-20260804T225912Z/__snapshots__/bug-report-20260804T225912Z.snap.svg +13 -13
  14. package/tests/bug-reports/bug-report-20260805T061316Z/__snapshots__/bug-report-20260805T061316Z.snap.svg +267 -267
  15. package/tests/bug-reports/bug-report-20260806T061548Z/__snapshots__/bug-report-20260806T061548Z.snap.svg +267 -267
  16. package/tests/bug-reports/bug-report-20260806T093501Z/__snapshots__/bug-report-20260806T093501Z.snap.svg +109 -109
  17. package/tests/bug-reports/bug-report-20260811T075142Z/__snapshots__/bug-report-20260811T075142Z.snap.svg +1 -1
  18. package/tests/bug-reports/bug-report-20260815T073240Z/__snapshots__/bug-report-20260815T073240Z.snap.svg +49 -49
  19. package/tests/bug-reports/bug-report-20260819T091818Z/__snapshots__/bug-report-20260819T091818Z.snap.svg +1 -1
  20. package/tests/bug-reports/bug-report-20260825T045913Z/__snapshots__/bug-report-20260825T045913Z.snap.svg +103 -103
  21. package/tests/bug-reports/bug-report-20260825T103621Z/__snapshots__/bug-report-20260825T103621Z.snap.svg +103 -103
  22. package/tests/bug-reports/bug-report-20260826T072956Z/__snapshots__/bug-report-20260826T072956Z.snap.svg +163 -163
  23. package/tests/examples/__snapshots__/example51.snap.svg +568 -568
  24. package/tests/fixtures/convertSolverOutputToCircuitJson.test.ts +16 -1
  25. package/tests/fixtures/convertSolverOutputToCircuitJson.ts +23 -12
  26. package/tests/repros/__snapshots__/board-1273-trace-overlap-cycle.snap.svg +736 -736
  27. package/tests/repros/__snapshots__/board-15984-same-net-junction.snap.svg +79 -79
  28. package/tests/repros/__snapshots__/board1096-usb-label-overlap-iteration.snap.svg +127 -127
  29. package/tests/repros/__snapshots__/repro-bluetooth-controller-ground-decoupling-groups.snap.svg +106 -0
  30. package/tests/repros/__snapshots__/repro-board-589-regulator-section.snap.svg +31 -31
  31. package/tests/repros/__snapshots__/repro-board-648-esp12f-section.snap.svg +121 -121
  32. package/tests/repros/__snapshots__/repro-bq25895-cross-net-trace-overlap.snap.svg +43 -43
  33. package/tests/repros/__snapshots__/repro-core-555-power-cross-net-overlap.snap.svg +19 -19
  34. package/tests/repros/__snapshots__/repro-core-ground-inline-label-fallback.snap.svg +277 -0
  35. package/tests/repros/__snapshots__/repro-core-subcircuit-missing-ground.snap.svg +19 -19
  36. package/tests/repros/__snapshots__/repro-core-tb67s579ftg-inline-label-routing.snap.svg +7 -7
  37. package/tests/repros/__snapshots__/repro-dc-power-isolated-routing.snap.svg +37 -37
  38. package/tests/repros/__snapshots__/repro-hdc2080-ground-label-alignment.snap.svg +137 -0
  39. package/tests/repros/__snapshots__/repro-nrf52810-clock-routing.snap.svg +61 -61
  40. package/tests/repros/__snapshots__/repro-pga300-redundant-ground-traces.snap.svg +115 -115
  41. package/tests/repros/__snapshots__/repro-pmp11282-isolated-dcdc.snap.svg +743 -743
  42. package/tests/repros/__snapshots__/repro-rp2040-usb-cc2-ground-overlap.snap.svg +43 -43
  43. package/tests/repros/__snapshots__/repro-ti-power-output-section.snap.svg +1 -1
  44. package/tests/repros/__snapshots__/repro-tida010076-j4-ground-bus.snap.svg +17 -17
  45. package/tests/repros/__snapshots__/repro-tida010076-page02.snap.svg +1131 -1131
  46. package/tests/repros/__snapshots__/repro-type-c-16pin-self-connection.snap.svg +7 -7
  47. package/tests/repros/__snapshots__/repro-type-c-16pin-to-resistor.snap.svg +7 -7
  48. package/tests/repros/__snapshots__/repro-usb-power-vbus-label-detour.snap.svg +666 -666
  49. package/tests/repros/__snapshots__/repro161-power-section-autolayout.snap.svg +73 -73
  50. package/tests/repros/__snapshots__/repro48-555-timer-vcc-rail-label.snap.svg +73 -73
  51. package/tests/repros/assets/repro-bluetooth-controller-ground-decoupling-groups.input.json +214 -0
  52. package/tests/repros/assets/repro-core-ground-inline-label-fallback.input.json +863 -0
  53. package/tests/repros/repro-bluetooth-controller-ground-decoupling-groups.test.ts +18 -0
  54. package/tests/repros/repro-core-ground-inline-label-fallback.test.ts +34 -0
  55. package/tests/repros/repro-hdc2080-ground-label-alignment.test.ts +239 -0
  56. package/tests/repros/repro-tida010076-j4-ground-bus.test.ts +14 -10
  57. package/tests/repros/repro-tida010076-page02.test.ts +2 -2
  58. package/tests/solvers/InlineNetLabelSolver/__snapshots__/inline-net-label02.snap.svg +1 -1
package/dist/index.d.ts CHANGED
@@ -906,6 +906,9 @@ declare class AvailableNetOrientationSolver extends BaseSolver {
906
906
  private shouldCheckTraceClearanceForLabel;
907
907
  private hasRoutedLabelOnSameNet;
908
908
  private isOutwardHorizontalFallback;
909
+ private canPlaceVerticalLabelOnHostRail;
910
+ private getTextBlockedMultiPinRailTraces;
911
+ private findValidNearbyRailCandidate;
909
912
  private hasPortOnlyLabelOnSameNet;
910
913
  private hasOppositeVerticalRailOnSameChipSide;
911
914
  private getSingleChipSideForNetConnection;
package/dist/index.js CHANGED
@@ -5864,6 +5864,9 @@ var getLabelsLinkedToRailGroup = (group, netLabelPlacements, traces) => {
5864
5864
  var getFixedLabelCoordinate = (group, netLabelPlacements, traces) => {
5865
5865
  const traceMap = new Map(traces.map((trace) => [trace.mspPairId, trace]));
5866
5866
  const groupTraceIds = new Set(group.map((segment) => segment.traceId));
5867
+ const groupIsWithinSingleComponent = [...groupTraceIds].every(
5868
+ (traceId) => traceMap.get(traceId)?.pins.every((pin) => pin.chipId === group[0].componentId) === true
5869
+ );
5867
5870
  const groupHasOnlySingleRailTraces = [...groupTraceIds].every(
5868
5871
  (traceId) => traceMap.get(traceId)?.tracePath.length === 4
5869
5872
  );
@@ -5874,12 +5877,20 @@ var getFixedLabelCoordinate = (group, netLabelPlacements, traces) => {
5874
5877
  netLabelPlacements,
5875
5878
  traces
5876
5879
  );
5877
- const anchoringLabels = linkedLabels.filter(
5878
- (label) => label.orientation === group[0].componentFacingDirection && label.mspConnectionPairIds.some((traceId) => {
5880
+ const labelsOrientedAlongRail = new Set(
5881
+ linkedLabels.filter(
5882
+ (label) => orientation === "vertical" && (label.orientation === "y+" || label.orientation === "y-") || orientation === "horizontal" && (label.orientation === "x+" || label.orientation === "x-")
5883
+ )
5884
+ );
5885
+ const anchoringLabels = linkedLabels.filter((label) => {
5886
+ if (label.orientation !== group[0].componentFacingDirection && (!groupIsWithinSingleComponent || !labelsOrientedAlongRail.has(label))) {
5887
+ return false;
5888
+ }
5889
+ return label.mspConnectionPairIds.some((traceId) => {
5879
5890
  const trace = traceMap.get(traceId);
5880
5891
  return trace && tracePathContainsPoint(trace.tracePath, label.anchorPoint);
5881
- })
5882
- );
5892
+ });
5893
+ });
5883
5894
  const coordinates = getDistinctCoordinates(
5884
5895
  anchoringLabels.map(
5885
5896
  (label) => orientation === "vertical" ? label.anchorPoint.x : label.anchorPoint.y
@@ -5896,12 +5907,21 @@ var getFixedLabelCoordinate = (group, netLabelPlacements, traces) => {
5896
5907
  )
5897
5908
  );
5898
5909
  if (labelIsAnchoredToRoutedBackbone) return coordinate;
5910
+ if (anchoringLabels.some((label) => labelsOrientedAlongRail.has(label))) {
5911
+ const railGroupSpan = Math.max(...group.map((segment) => segment.maxAlong)) - Math.min(...group.map((segment) => segment.minAlong));
5912
+ const coordinateIsLocalToRailGroup = group.every(
5913
+ (segment) => Math.abs(segment.coordinate - coordinate) <= railGroupSpan + RAIL_ALIGNMENT_EPSILON
5914
+ );
5915
+ if (coordinateIsLocalToRailGroup) return coordinate;
5916
+ return null;
5917
+ }
5899
5918
  const coordinateIsLocalToEveryTrace = group.every((segment) => {
5900
5919
  const trace = traceMap.get(segment.traceId);
5901
5920
  if (!trace) return false;
5902
5921
  return Math.abs(segment.coordinate - coordinate) <= distance4(trace.pins[0], trace.pins[1]) + RAIL_ALIGNMENT_EPSILON;
5903
5922
  });
5904
- return coordinateIsLocalToEveryTrace ? coordinate : null;
5923
+ if (coordinateIsLocalToEveryTrace) return coordinate;
5924
+ return null;
5905
5925
  };
5906
5926
 
5907
5927
  // lib/solvers/TraceCleanupSolver/sameNetRailAlignment/moveRailSegments.ts
@@ -8939,6 +8959,9 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
8939
8959
  const orientations = this.getAvailableOrientations(label);
8940
8960
  if (orientations.length === 0) return false;
8941
8961
  if (!orientations.includes(label.orientation)) return true;
8962
+ if (this.getTextBlockedMultiPinRailTraces(label, orientations[0])) {
8963
+ return true;
8964
+ }
8942
8965
  if (!this.crowdedPortOnlyLabelIndices.has(labelIndex)) return false;
8943
8966
  const bounds = getRectBounds(label.center, label.width, label.height);
8944
8967
  return this.outputNetLabelPlacements.some((otherLabel, otherIndex) => {
@@ -9060,8 +9083,34 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9060
9083
  );
9061
9084
  const isTwoPinNet = netConnection?.pinIds.length === 2;
9062
9085
  const isDistanceSplitVerticalRail = (netConnection?.pinIds.length ?? 0) > 2 && isYOrientation(requiredOrientation) && this.hasPortOnlyLabelOnSameNet(label);
9086
+ const textBlockedMultiPinRailTraces = this.getTextBlockedMultiPinRailTraces(
9087
+ label,
9088
+ requiredOrientation
9089
+ );
9063
9090
  const isPairedSameSidePowerRail = isYOrientation(requiredOrientation) && this.hasOppositeVerticalRailOnSameChipSide(label, requiredOrientation);
9091
+ if (textBlockedMultiPinRailTraces) {
9092
+ const nearbyCandidate = this.findValidNearbyRailCandidate(
9093
+ label,
9094
+ requiredOrientation,
9095
+ labelIndex,
9096
+ textBlockedMultiPinRailTraces
9097
+ );
9098
+ if (nearbyCandidate) return nearbyCandidate;
9099
+ }
9064
9100
  if (orientations.length === 1 && isYOrientation(requiredOrientation) && this.isOutwardHorizontalFallback(label) && this.hasTraceContinuingInOrientation(label, requiredOrientation) && (isDistanceSplitVerticalRail || isPairedSameSidePowerRail)) {
9101
+ const hostRailCandidate = this.findValidTraceAnchorCandidate(
9102
+ label,
9103
+ requiredOrientation,
9104
+ labelIndex
9105
+ );
9106
+ if (hostRailCandidate) {
9107
+ const canUseHostRail = this.canPlaceVerticalLabelOnHostRail(
9108
+ hostRailCandidate,
9109
+ label
9110
+ );
9111
+ if (canUseHostRail) return hostRailCandidate;
9112
+ hostRailCandidate.selected = false;
9113
+ }
9065
9114
  const traceAnchorCandidate2 = this.findValidOutwardTraceAnchorCandidate(
9066
9115
  label,
9067
9116
  requiredOrientation,
@@ -9495,14 +9544,21 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9495
9544
  maxSearchDistance,
9496
9545
  outwardDistance,
9497
9546
  phase = "shift",
9498
- stopOnTraceCollision = true
9547
+ stopOnTraceCollision = true,
9548
+ connectorSource,
9549
+ startDistance = LABEL_SEARCH_STEP
9499
9550
  } = params;
9500
- for (let distance7 = LABEL_SEARCH_STEP; distance7 <= maxSearchDistance + EPS12; distance7 += LABEL_SEARCH_STEP) {
9551
+ for (let distance7 = startDistance; distance7 <= maxSearchDistance + EPS12; distance7 += LABEL_SEARCH_STEP) {
9501
9552
  const anchorPoint = {
9502
9553
  x: baseAnchor.x + direction.x * distance7,
9503
9554
  y: baseAnchor.y + direction.y * distance7
9504
9555
  };
9505
- const candidate = this.createCandidate(label, anchorPoint, orientation);
9556
+ const candidate = this.createCandidate(
9557
+ label,
9558
+ anchorPoint,
9559
+ orientation,
9560
+ connectorSource
9561
+ );
9506
9562
  const result = this.evaluateCandidate(
9507
9563
  candidate,
9508
9564
  label,
@@ -9878,6 +9934,113 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9878
9934
  const chipSide = this.getChipSideForPoint(label.anchorPoint);
9879
9935
  return chipSide === "left" && label.orientation === "x-" || chipSide === "right" && label.orientation === "x+";
9880
9936
  }
9937
+ canPlaceVerticalLabelOnHostRail(candidate, label) {
9938
+ const isOnHostRail = label.mspConnectionPairIds.some((traceId) => {
9939
+ const trace = this.traceMap[traceId];
9940
+ return trace && tracePathContainsPoint(trace.tracePath, candidate.anchorPoint);
9941
+ });
9942
+ if (!isOnHostRail) return false;
9943
+ const hostPin = this.pinMap[label.pinIds[0]];
9944
+ if (!hostPin) return false;
9945
+ if (label.pinIds.some(
9946
+ (pinId) => this.pinMap[pinId]?.chipId !== hostPin.chipId
9947
+ )) {
9948
+ return false;
9949
+ }
9950
+ const chip = this.chipObstacleSpatialIndex.chips.find(
9951
+ (chip2) => chip2.chipId === hostPin.chipId
9952
+ );
9953
+ if (!chip) return false;
9954
+ const labelBounds = getRectBounds(
9955
+ candidate.center,
9956
+ candidate.width,
9957
+ candidate.height
9958
+ );
9959
+ if (labelBounds.minY < chip.bounds.minY - EPS12 || labelBounds.maxY > chip.bounds.maxY + EPS12) {
9960
+ return false;
9961
+ }
9962
+ if (candidate.anchorPoint.x >= chip.bounds.minX - EPS12 && candidate.anchorPoint.x <= chip.bounds.maxX + EPS12) {
9963
+ return false;
9964
+ }
9965
+ return chip.pins.every(
9966
+ (pin) => label.pinIds.includes(pin.pinId) || pin.y <= labelBounds.minY + EPS12 || pin.y >= labelBounds.maxY - EPS12
9967
+ );
9968
+ }
9969
+ getTextBlockedMultiPinRailTraces(label, orientation) {
9970
+ if (!isYOrientation(orientation)) return null;
9971
+ const componentId = this.pinMap[label.pinIds[0]]?.chipId;
9972
+ const localTraces = Object.values(this.traceMap).filter(
9973
+ (trace) => trace.globalConnNetId === label.globalConnNetId && trace.pins.every((pin) => pin.chipId === componentId)
9974
+ );
9975
+ const connectedPinIds = new Set(label.pinIds);
9976
+ const connectedTraces = /* @__PURE__ */ new Set();
9977
+ for (let changed = true; changed; ) {
9978
+ changed = false;
9979
+ for (const trace of localTraces) {
9980
+ if (connectedTraces.has(trace)) continue;
9981
+ if (!trace.pinIds.some((pinId) => connectedPinIds.has(pinId))) continue;
9982
+ connectedTraces.add(trace);
9983
+ for (const pinId of trace.pinIds) connectedPinIds.add(pinId);
9984
+ changed = true;
9985
+ }
9986
+ }
9987
+ if (connectedPinIds.size <= 3) return null;
9988
+ const pins = [...connectedPinIds].map((pinId) => this.pinMap[pinId]);
9989
+ const direction = dir(orientation);
9990
+ const pinX = pins[0].x;
9991
+ if (pins.some((pin) => Math.abs(pin.x - pinX) > EPS12)) return null;
9992
+ const sidePins = this.inputProblem.chips.find((chip) => chip.chipId === componentId).pins.filter((pin) => Math.abs(pin.x - pinX) <= EPS12);
9993
+ const minY = Math.min(...pins.map((pin) => pin.y));
9994
+ const maxY = Math.max(...pins.map((pin) => pin.y));
9995
+ if (sidePins.some(
9996
+ (pin) => pin.y >= minY - EPS12 && pin.y <= maxY + EPS12 && !connectedPinIds.has(pin.pinId)
9997
+ ) || orientation === "y-" && minY > Math.min(...sidePins.map((pin) => pin.y)) + EPS12 || orientation === "y+" && maxY < Math.max(...sidePins.map((pin) => pin.y)) - EPS12) {
9998
+ return null;
9999
+ }
10000
+ const outerPinPosition = Math.max(
10001
+ ...pins.map((pin) => pin.x * direction.x + pin.y * direction.y)
10002
+ );
10003
+ const blockedByText = (this.inputProblem.textBoxes ?? []).some(
10004
+ (textBox) => textBox.chipId === componentId && textBox.center.x * direction.x + textBox.center.y * direction.y > outerPinPosition + EPS12
10005
+ );
10006
+ return blockedByText ? [...connectedTraces] : null;
10007
+ }
10008
+ findValidNearbyRailCandidate(label, orientation, labelIndex, localTraces) {
10009
+ if (!isYOrientation(orientation)) return null;
10010
+ const direction = dir(orientation);
10011
+ const connectorY = [
10012
+ ...new Set(localTraces.flatMap((trace) => trace.pinIds))
10013
+ ].map((pinId) => this.pinMap[pinId]).sort((a, b) => b.y * direction.y - a.y * direction.y)[0]?.y;
10014
+ if (connectorY === void 0) return null;
10015
+ const pinX = this.pinMap[label.pinIds[0]].x;
10016
+ const railXs = [
10017
+ ...new Set(
10018
+ localTraces.flatMap((trace) => trace.tracePath.map((point) => point.x))
10019
+ )
10020
+ ].filter((x) => Math.abs(x - pinX) > EPS12).sort((a, b) => Math.abs(a - pinX) - Math.abs(b - pinX));
10021
+ for (const x of railXs) {
10022
+ const connectorSource = { x, y: connectorY };
10023
+ if (!Object.values(this.traceMap).some(
10024
+ (trace) => trace.globalConnNetId === label.globalConnNetId && tracePathContainsPoint(trace.tracePath, connectorSource)
10025
+ )) {
10026
+ continue;
10027
+ }
10028
+ const candidate = this.findValidCandidateInShiftColumn({
10029
+ label,
10030
+ labelIndex,
10031
+ orientation,
10032
+ direction,
10033
+ baseAnchor: connectorSource,
10034
+ maxSearchDistance: this.maxSearchDistance,
10035
+ outwardDistance: 0,
10036
+ stopOnTraceCollision: false,
10037
+ connectorSource,
10038
+ startDistance: 0
10039
+ });
10040
+ if (candidate) return candidate;
10041
+ }
10042
+ return null;
10043
+ }
9881
10044
  hasPortOnlyLabelOnSameNet(label) {
9882
10045
  return this.outputNetLabelPlacements.some(
9883
10046
  (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) =>
@@ -53,6 +53,12 @@ export const getFixedLabelCoordinate = (
53
53
  ) => {
54
54
  const traceMap = new Map(traces.map((trace) => [trace.mspPairId, trace]))
55
55
  const groupTraceIds = new Set(group.map((segment) => segment.traceId))
56
+ const groupIsWithinSingleComponent = [...groupTraceIds].every(
57
+ (traceId) =>
58
+ traceMap
59
+ .get(traceId)
60
+ ?.pins.every((pin) => pin.chipId === group[0]!.componentId) === true,
61
+ )
56
62
  const groupHasOnlySingleRailTraces = [...groupTraceIds].every(
57
63
  (traceId) => traceMap.get(traceId)?.tracePath.length === 4,
58
64
  )
@@ -66,16 +72,27 @@ export const getFixedLabelCoordinate = (
66
72
  netLabelPlacements,
67
73
  traces,
68
74
  )
69
- const anchoringLabels = linkedLabels.filter(
70
- (label) =>
71
- label.orientation === group[0]!.componentFacingDirection &&
72
- label.mspConnectionPairIds.some((traceId) => {
73
- const trace = traceMap.get(traceId)
74
- return (
75
- trace && tracePathContainsPoint(trace.tracePath, label.anchorPoint)
76
- )
77
- }),
75
+ const labelsOrientedAlongRail = new Set(
76
+ linkedLabels.filter(
77
+ (label) =>
78
+ (orientation === "vertical" &&
79
+ (label.orientation === "y+" || label.orientation === "y-")) ||
80
+ (orientation === "horizontal" &&
81
+ (label.orientation === "x+" || label.orientation === "x-")),
82
+ ),
78
83
  )
84
+ const anchoringLabels = linkedLabels.filter((label) => {
85
+ if (
86
+ label.orientation !== group[0]!.componentFacingDirection &&
87
+ (!groupIsWithinSingleComponent || !labelsOrientedAlongRail.has(label))
88
+ ) {
89
+ return false
90
+ }
91
+ return label.mspConnectionPairIds.some((traceId) => {
92
+ const trace = traceMap.get(traceId)
93
+ return trace && tracePathContainsPoint(trace.tracePath, label.anchorPoint)
94
+ })
95
+ })
79
96
  const coordinates = getDistinctCoordinates(
80
97
  anchoringLabels.map((label) =>
81
98
  orientation === "vertical" ? label.anchorPoint.x : label.anchorPoint.y,
@@ -95,11 +112,24 @@ export const getFixedLabelCoordinate = (
95
112
  (traceId) => (traceMap.get(traceId)?.tracePath.length ?? 0) > 4,
96
113
  ),
97
114
  )
98
- // A multi-turn label trace is an intentional routed backbone, so its rail
99
- // coordinate may propagate through the local component chain. Labels riding
100
- // a single movable rail remain subject to the per-connection locality gate.
115
+ // A multi-turn label trace is an intentional routed backbone, so its
116
+ // coordinate may propagate through the connected rail group.
101
117
  if (labelIsAnchoredToRoutedBackbone) return coordinate
102
118
 
119
+ // A label at a same-component rail end may set the shared bus coordinate.
120
+ if (anchoringLabels.some((label) => labelsOrientedAlongRail.has(label))) {
121
+ const railGroupSpan =
122
+ Math.max(...group.map((segment) => segment.maxAlong)) -
123
+ Math.min(...group.map((segment) => segment.minAlong))
124
+ const coordinateIsLocalToRailGroup = group.every(
125
+ (segment) =>
126
+ Math.abs(segment.coordinate - coordinate) <=
127
+ railGroupSpan + RAIL_ALIGNMENT_EPSILON,
128
+ )
129
+ if (coordinateIsLocalToRailGroup) return coordinate
130
+ return null
131
+ }
132
+
103
133
  // Rail cleanup is local: a label cannot pull a connection farther than the
104
134
  // span of the pins that connection already joins.
105
135
  const coordinateIsLocalToEveryTrace = group.every((segment) => {
@@ -110,5 +140,6 @@ export const getFixedLabelCoordinate = (
110
140
  distance(trace.pins[0]!, trace.pins[1]!) + RAIL_ALIGNMENT_EPSILON
111
141
  )
112
142
  })
113
- return coordinateIsLocalToEveryTrace ? coordinate : null
143
+ if (coordinateIsLocalToEveryTrace) return coordinate
144
+ return null
114
145
  }
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "url": "https://github.com/tscircuit/schematic-trace-solver.git"
6
6
  },
7
7
  "main": "dist/index.js",
8
- "version": "0.0.160",
8
+ "version": "0.0.161",
9
9
  "type": "module",
10
10
  "scripts": {
11
11
  "start": "cosmos",