@tscircuit/schematic-trace-solver 0.0.161 → 0.0.163

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -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
@@ -823,7 +825,7 @@ type CandidateLabel = {
823
825
  height: number;
824
826
  };
825
827
  type CandidateStatus$2 = "valid" | "chip-collision" | "text-collision" | "trace-collision" | "trace-clearance-violation" | "netlabel-collision";
826
- type CandidatePhase = "rotate" | "trace-anchor" | "outward-trace-anchor" | "shift" | "lateral-shift";
828
+ type CandidatePhase = "rotate" | "trace-anchor" | "connected-rail-shift" | "outward-trace-anchor" | "shift" | "lateral-shift";
827
829
  type EvaluatedCandidate = CandidateLabel & {
828
830
  status: CandidateStatus$2;
829
831
  selected: boolean;
@@ -900,6 +902,7 @@ declare class AvailableNetOrientationSolver extends BaseSolver {
900
902
  private getNetLabelHeight;
901
903
  private getCandidateStatus;
902
904
  private isAcceptableTraceAnchorChipCollision;
905
+ private staysOutsideConnectedPinRow;
903
906
  private getBoundsStatus;
904
907
  private isTraceTooCloseToLabel;
905
908
  private getLabelTraceClearanceBounds;
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
  }
@@ -8802,6 +8831,7 @@ function getPaddedBounds(bounds, padding) {
8802
8831
 
8803
8832
  // lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts
8804
8833
  var LABEL_TRACE_CLEARANCE2 = 0.1;
8834
+ var CONNECTED_PIN_ROW_OVERLAP_TOLERANCE = 0.1;
8805
8835
  var AvailableNetOrientationSolver = class extends BaseSolver {
8806
8836
  inputProblem;
8807
8837
  traces;
@@ -9823,7 +9853,7 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9823
9853
  label
9824
9854
  });
9825
9855
  if (boundsStatus !== "valid") {
9826
- if (phase !== "trace-anchor" || boundsStatus !== "chip-collision" || !this.isAcceptableTraceAnchorChipCollision(candidate, label, bounds)) {
9856
+ if (phase !== "trace-anchor" && phase !== "connected-rail-shift" || boundsStatus !== "chip-collision" || !this.isAcceptableTraceAnchorChipCollision(candidate, label, bounds) || phase === "connected-rail-shift" && !this.staysOutsideConnectedPinRow({ candidate, label, bounds })) {
9827
9857
  return boundsStatus;
9828
9858
  }
9829
9859
  const nonChipBoundsStatus = this.getBoundsStatus({
@@ -9877,6 +9907,15 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9877
9907
  return anchorPoint.y < chipBounds.minY - EPS12 || anchorPoint.y > chipBounds.maxY + EPS12;
9878
9908
  });
9879
9909
  }
9910
+ staysOutsideConnectedPinRow(params) {
9911
+ const { candidate, label, bounds } = params;
9912
+ const pin = this.pinMap[label.pinIds[0]];
9913
+ if (!pin || !isYOrientation(candidate.orientation)) return false;
9914
+ if (candidate.anchorPoint.x < pin.x) {
9915
+ return bounds.maxX <= pin.x + CONNECTED_PIN_ROW_OVERLAP_TOLERANCE + EPS12;
9916
+ }
9917
+ return bounds.minX >= pin.x - CONNECTED_PIN_ROW_OVERLAP_TOLERANCE - EPS12;
9918
+ }
9880
9919
  getBoundsStatus(candidateBoundsCheck) {
9881
9920
  const { bounds, labelIndex, label, ignoreChipCollisions } = candidateBoundsCheck;
9882
9921
  if (!ignoreChipCollisions) {
@@ -10031,8 +10070,9 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
10031
10070
  orientation,
10032
10071
  direction,
10033
10072
  baseAnchor: connectorSource,
10034
- maxSearchDistance: this.maxSearchDistance,
10073
+ maxSearchDistance: this.getSearchDistanceLimit(label, orientation),
10035
10074
  outwardDistance: 0,
10075
+ phase: "connected-rail-shift",
10036
10076
  stopOnTraceCollision: false,
10037
10077
  connectorSource,
10038
10078
  startDistance: 0
@@ -51,6 +51,7 @@ import { visualizeAvailableNetOrientationSolver } from "./visualize"
51
51
  import { AvailableNetOrientationObstacleIndex } from "./AvailableNetOrientationObstacleIndex"
52
52
 
53
53
  const LABEL_TRACE_CLEARANCE = 0.1
54
+ const CONNECTED_PIN_ROW_OVERLAP_TOLERANCE = 0.1
54
55
 
55
56
  export class AvailableNetOrientationSolver extends BaseSolver {
56
57
  inputProblem: InputProblem
@@ -1443,9 +1444,11 @@ export class AvailableNetOrientationSolver extends BaseSolver {
1443
1444
  })
1444
1445
  if (boundsStatus !== "valid") {
1445
1446
  if (
1446
- phase !== "trace-anchor" ||
1447
+ (phase !== "trace-anchor" && phase !== "connected-rail-shift") ||
1447
1448
  boundsStatus !== "chip-collision" ||
1448
- !this.isAcceptableTraceAnchorChipCollision(candidate, label, bounds)
1449
+ !this.isAcceptableTraceAnchorChipCollision(candidate, label, bounds) ||
1450
+ (phase === "connected-rail-shift" &&
1451
+ !this.staysOutsideConnectedPinRow({ candidate, label, bounds }))
1449
1452
  ) {
1450
1453
  return boundsStatus
1451
1454
  }
@@ -1525,6 +1528,20 @@ export class AvailableNetOrientationSolver extends BaseSolver {
1525
1528
  })
1526
1529
  }
1527
1530
 
1531
+ private staysOutsideConnectedPinRow(params: {
1532
+ candidate: CandidateLabel
1533
+ label: NetLabelPlacement
1534
+ bounds: Bounds
1535
+ }) {
1536
+ const { candidate, label, bounds } = params
1537
+ const pin = this.pinMap[label.pinIds[0]!]
1538
+ if (!pin || !isYOrientation(candidate.orientation)) return false
1539
+ if (candidate.anchorPoint.x < pin.x) {
1540
+ return bounds.maxX <= pin.x + CONNECTED_PIN_ROW_OVERLAP_TOLERANCE + EPS
1541
+ }
1542
+ return bounds.minX >= pin.x - CONNECTED_PIN_ROW_OVERLAP_TOLERANCE - EPS
1543
+ }
1544
+
1528
1545
  private getBoundsStatus(candidateBoundsCheck: {
1529
1546
  bounds: Bounds
1530
1547
  labelIndex: number
@@ -1756,8 +1773,9 @@ export class AvailableNetOrientationSolver extends BaseSolver {
1756
1773
  orientation,
1757
1774
  direction,
1758
1775
  baseAnchor: connectorSource,
1759
- maxSearchDistance: this.maxSearchDistance,
1776
+ maxSearchDistance: this.getSearchDistanceLimit(label, orientation),
1760
1777
  outwardDistance: 0,
1778
+ phase: "connected-rail-shift",
1761
1779
  stopOnTraceCollision: false,
1762
1780
  connectorSource,
1763
1781
  startDistance: 0,
@@ -39,6 +39,7 @@ export type CandidateStatus =
39
39
  export type CandidatePhase =
40
40
  | "rotate"
41
41
  | "trace-anchor"
42
+ | "connected-rail-shift"
42
43
  | "outward-trace-anchor"
43
44
  | "shift"
44
45
  | "lateral-shift"
@@ -16,6 +16,7 @@ import { getConnectivityMapsFromInputProblem } from "./getConnectivityMapFromInp
16
16
  import { getGroundConnectionPolicy } from "./getGroundConnectionPolicy"
17
17
  import { getOrthogonalMinimumSpanningTree } from "./getMspConnectionPairsFromPins"
18
18
  import { getLabeledConnectionRouteReason } from "./isLabeledPeripheralConnection"
19
+ import { shouldSeparateGroundNetRows } from "./shouldSeparateGroundNetRows"
19
20
 
20
21
  export type MspConnectionPairId = string
21
22
  export const DEFAULT_MAX_MSP_PAIR_DISTANCE = 1
@@ -194,26 +195,32 @@ export class MspConnectionPairSolver extends BaseSolver {
194
195
  PinId,
195
196
  InputPin & { chipId: string }
196
197
  >
197
- const msp = getOrthogonalMinimumSpanningTree(
198
- directlyConnectedPins.map((p) => this.pinMap[p]!).filter(Boolean),
199
- {
200
- maxDistance: this.maxMspPairDistance,
201
- forbidEdge: (a, b) =>
202
- !this.canRouteGroundPair(a.pinId, b.pinId) ||
203
- arePinsInDifferentSchematicSections(
204
- this.inputProblem,
205
- a as InputPin & { chipId: string },
206
- b as InputPin & { chipId: string },
207
- ) ||
208
- doesPairCrossRestrictedCenterLines({
209
- inputProblem: this.inputProblem,
210
- chipMap: this.chipMap,
211
- pinIdMap,
212
- p1: a as InputPin & { chipId: string },
213
- p2: b as InputPin & { chipId: string },
214
- }),
215
- },
198
+ const userNetId = this.userNetIdByPinId[directlyConnectedPins[0]!]
199
+ const netConnection = this.inputProblem.netConnections.find(
200
+ (connection) => connection.netId === userNetId,
216
201
  )
202
+ const directlyConnectedPinObjects = directlyConnectedPins.map(
203
+ (pinId) => this.pinMap[pinId]!,
204
+ )
205
+ const msp = getOrthogonalMinimumSpanningTree(directlyConnectedPinObjects, {
206
+ maxDistance: this.maxMspPairDistance,
207
+ forbidEdge: (a, b) =>
208
+ !this.canRouteGroundPair(a.pinId, b.pinId) ||
209
+ shouldSeparateGroundNetRows({
210
+ netConnection,
211
+ netPins: directlyConnectedPinObjects,
212
+ pin1: a,
213
+ pin2: b,
214
+ }) ||
215
+ arePinsInDifferentSchematicSections(this.inputProblem, a, b) ||
216
+ doesPairCrossRestrictedCenterLines({
217
+ inputProblem: this.inputProblem,
218
+ chipMap: this.chipMap,
219
+ pinIdMap,
220
+ p1: a,
221
+ p2: b,
222
+ }),
223
+ })
217
224
 
218
225
  for (const [pin1, pin2] of msp) {
219
226
  const p1Obj = this.pinMap[pin1!]!
@@ -237,13 +244,13 @@ export class MspConnectionPairSolver extends BaseSolver {
237
244
  }
238
245
 
239
246
  const globalConnNetId = this.globalConnMap.getNetConnectedToId(pin1!)!
240
- const userNetId =
247
+ const pairUserNetId =
241
248
  this.userNetIdByPinId[pin1!] ?? this.userNetIdByPinId[pin2!]
242
249
  this.addMspConnectionPair({
243
250
  mspPairId: `${pin1}-${pin2}`,
244
251
  dcConnNetId: dcNetId,
245
252
  globalConnNetId,
246
- userNetId,
253
+ userNetId: pairUserNetId,
247
254
  pins: [p1Obj, p2Obj],
248
255
  })
249
256
  }
@@ -16,11 +16,11 @@ import type { InputPin, PinId } from "lib/types/InputProblem"
16
16
  * - When multiple candidates have the same distance, we pick the one
17
17
  * whose pinId is lexicographically smallest (for stable output).
18
18
  */
19
- export function getOrthogonalMinimumSpanningTree(
20
- pins: InputPin[],
19
+ export function getOrthogonalMinimumSpanningTree<Pin extends InputPin>(
20
+ pins: Pin[],
21
21
  opts: {
22
22
  maxDistance?: number
23
- forbidEdge?: (a: InputPin, b: InputPin) => boolean
23
+ forbidEdge?: (a: Pin, b: Pin) => boolean
24
24
  } = {},
25
25
  ): Array<[PinId, PinId]> {
26
26
  const n = pins.length
@@ -0,0 +1,40 @@
1
+ import type { InputNetConnection, InputPin } from "lib/types/InputProblem"
2
+
3
+ const SAME_RAIL_Y_TOLERANCE = 1e-6
4
+ const MIN_GROUPED_RAIL_PIN_COUNT = 3
5
+
6
+ function isGroupedHorizontalRail({
7
+ pin,
8
+ netPins,
9
+ }: {
10
+ pin: InputPin & { chipId: string }
11
+ netPins: Array<InputPin & { chipId: string }>
12
+ }) {
13
+ // Multiple pins from one symbol do not establish a grouped component rail.
14
+ const sameRailPins = netPins.filter(
15
+ (otherPin) => Math.abs(otherPin.y - pin.y) <= SAME_RAIL_Y_TOLERANCE,
16
+ )
17
+ const railChipIds = new Set(sameRailPins.map((railPin) => railPin.chipId))
18
+ return railChipIds.size >= MIN_GROUPED_RAIL_PIN_COUNT
19
+ }
20
+
21
+ export function shouldSeparateGroundNetRows({
22
+ netConnection,
23
+ netPins,
24
+ pin1,
25
+ pin2,
26
+ }: {
27
+ netConnection?: InputNetConnection
28
+ netPins: Array<InputPin & { chipId: string }>
29
+ pin1: InputPin & { chipId: string }
30
+ pin2: InputPin & { chipId: string }
31
+ }) {
32
+ if (!netConnection?.isGround || pin1.chipId === pin2.chipId) return false
33
+ if (Math.abs(pin1.y - pin2.y) <= SAME_RAIL_Y_TOLERANCE) return false
34
+
35
+ // Net labels preserve ground connectivity without a cross-row MSP edge.
36
+ return (
37
+ isGroupedHorizontalRail({ pin: pin1, netPins }) &&
38
+ isGroupedHorizontalRail({ pin: pin2, netPins })
39
+ )
40
+ }
@@ -98,6 +98,9 @@ export interface InputNetConnection {
98
98
  netId: string
99
99
  pinIds: Array<PinId>
100
100
 
101
+ /** True when the authoritative source net is electrical ground. */
102
+ isGround?: boolean
103
+
101
104
  /**
102
105
  * User-facing text to render for this net label. `netId` remains the stable
103
106
  * connectivity identifier and may be an internal id. When this is omitted
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.161",
8
+ "version": "0.0.163",
9
9
  "type": "module",
10
10
  "scripts": {
11
11
  "start": "cosmos",