@tscircuit/schematic-trace-solver 0.0.21 → 0.0.23

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.
@@ -4,6 +4,9 @@ on:
4
4
  push:
5
5
  branches:
6
6
  - main
7
+ paths:
8
+ - 'lib/**'
9
+ workflow_dispatch:
7
10
  jobs:
8
11
  publish:
9
12
  runs-on: ubuntu-latest
package/dist/index.d.ts CHANGED
@@ -161,6 +161,7 @@ declare class SchematicTraceSingleLineSolver extends BaseSolver {
161
161
  chipMap: Record<string, InputChip>;
162
162
  movableSegments: Array<MovableSegment>;
163
163
  baseElbow: Point[];
164
+ allCandidatePaths: Array<Point[]>;
164
165
  queuedCandidatePaths: Array<Point[]>;
165
166
  chipObstacleSpatialIndex: ChipObstacleSpatialIndex;
166
167
  solvedTracePath: {
package/dist/index.js CHANGED
@@ -5503,6 +5503,12 @@ var MspConnectionPairSolver = class extends BaseSolver {
5503
5503
  }
5504
5504
  if (directlyConnectedPins.length === 2) {
5505
5505
  const [pin1, pin2] = directlyConnectedPins;
5506
+ const p1 = this.pinMap[pin1];
5507
+ const p2 = this.pinMap[pin2];
5508
+ const manhattanDist = Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y);
5509
+ if (manhattanDist > this.maxMspPairDistance) {
5510
+ return;
5511
+ }
5506
5512
  const globalConnNetId = this.globalConnMap.getNetConnectedToId(pin1);
5507
5513
  const userNetId = this.userNetIdByPinId[pin1] ?? this.userNetIdByPinId[pin2];
5508
5514
  this.mspConnectionPairs.push({
@@ -5510,7 +5516,7 @@ var MspConnectionPairSolver = class extends BaseSolver {
5510
5516
  dcConnNetId: dcNetId,
5511
5517
  globalConnNetId,
5512
5518
  userNetId,
5513
- pins: [this.pinMap[pin1], this.pinMap[pin2]]
5519
+ pins: [p1, p2]
5514
5520
  });
5515
5521
  return;
5516
5522
  }
@@ -6520,81 +6526,167 @@ var dir = (facingDirection) => {
6520
6526
  };
6521
6527
 
6522
6528
  // lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/generateElbowVariants.ts
6523
- var generateElbowVariants = ({
6524
- baseElbow,
6525
- guidelines
6526
- }) => {
6527
- const movableSegments = [];
6528
- for (let i = 1; i < baseElbow.length - 2; i++) {
6529
- const prev = baseElbow[i - 1];
6530
- const start = baseElbow[i];
6531
- const end = baseElbow[i + 1];
6532
- const isHorz = Math.abs(start.y - end.y) < 1e-6;
6533
- let freedom;
6534
- if (isHorz) {
6535
- freedom = prev.y <= start.y ? "y+" : "y-";
6536
- } else {
6537
- freedom = prev.x <= start.x ? "x+" : "x-";
6529
+ var EPS = 1e-6;
6530
+ var MIN_LEN = 1e-6;
6531
+ var isAxisAligned = (a, b) => Math.abs(a.x - b.x) < EPS || Math.abs(a.y - b.y) < EPS;
6532
+ var orientationOf = (a, b) => {
6533
+ const dx = Math.abs(a.x - b.x);
6534
+ const dy = Math.abs(a.y - b.y);
6535
+ if (dx < EPS && dy >= EPS) return "vertical";
6536
+ if (dy < EPS && dx >= EPS) return "horizontal";
6537
+ throw new Error("Non-orthogonal or degenerate segment detected.");
6538
+ };
6539
+ var assertOrthogonalPolyline = (pts) => {
6540
+ if (pts.length < 2) {
6541
+ throw new Error("Polyline must have at least two points.");
6542
+ }
6543
+ for (let i = 0; i < pts.length - 1; i++) {
6544
+ const a = pts[i];
6545
+ const b = pts[i + 1];
6546
+ if (!isAxisAligned(a, b)) {
6547
+ throw new Error("Polyline contains a non-orthogonal segment.");
6548
+ }
6549
+ const manhattan = Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
6550
+ if (manhattan < MIN_LEN) {
6551
+ throw new Error("Polyline contains a near-zero length segment.");
6538
6552
  }
6539
- movableSegments.push({ start, end, freedom, dir: dir(freedom) });
6540
6553
  }
6541
- const segmentGuidelineOptions = [];
6542
- for (const segment of movableSegments) {
6543
- const relevantPositions = [];
6544
- if (segment.freedom === "x+" || segment.freedom === "x-") {
6545
- for (const guideline of guidelines) {
6546
- if (guideline.orientation === "vertical" && guideline.x !== void 0) {
6547
- relevantPositions.push(guideline.x);
6548
- }
6554
+ };
6555
+ var uniqSortedWithTol = (vals, tol = EPS) => {
6556
+ const sorted = vals.slice().sort((a, b) => a - b);
6557
+ const out = [];
6558
+ for (const v of sorted) {
6559
+ if (out.length === 0 || Math.abs(v - out[out.length - 1]) > tol) out.push(v);
6560
+ }
6561
+ return out;
6562
+ };
6563
+ var keyForPolyline = (pts, decimals = 9) => pts.map((p) => `${p.x.toFixed(decimals)},${p.y.toFixed(decimals)}`).join("|");
6564
+ var preprocessElbow = (pts, tol = MIN_LEN) => {
6565
+ if (pts.length === 0) return [];
6566
+ const out = [{ ...pts[0] }];
6567
+ for (let i = 1; i < pts.length; i++) {
6568
+ const p = pts[i];
6569
+ const last = out[out.length - 1];
6570
+ const manhattan = Math.abs(p.x - last.x) + Math.abs(p.y - last.y);
6571
+ if (manhattan > tol) out.push({ ...p });
6572
+ }
6573
+ return out;
6574
+ };
6575
+ var collectAxisCandidates = (axis, guidelines, currentCoord) => {
6576
+ const positions = [];
6577
+ for (const g of guidelines) {
6578
+ if (axis === "x") {
6579
+ if (g.orientation === "vertical" && typeof g.x === "number") {
6580
+ positions.push(g.x);
6549
6581
  }
6550
- relevantPositions.push(segment.start.x);
6551
6582
  } else {
6552
- for (const guideline of guidelines) {
6553
- if (guideline.orientation === "horizontal" && guideline.y !== void 0) {
6554
- relevantPositions.push(guideline.y);
6555
- }
6583
+ if (g.orientation === "horizontal" && typeof g.y === "number") {
6584
+ positions.push(g.y);
6556
6585
  }
6557
- relevantPositions.push(segment.start.y);
6558
6586
  }
6559
- segmentGuidelineOptions.push(
6560
- [...new Set(relevantPositions)].sort((a, b) => a - b)
6561
- );
6562
6587
  }
6563
- const generateCombinations = (options) => {
6564
- if (options.length === 0) return [[]];
6565
- if (options.length === 1) return options[0].map((pos) => [pos]);
6566
- const combinations = [];
6567
- const firstOptions = options[0];
6568
- const restCombinations = generateCombinations(options.slice(1));
6569
- for (const firstOption of firstOptions) {
6570
- for (const restCombination of restCombinations) {
6571
- combinations.push([firstOption, ...restCombination]);
6588
+ positions.push(currentCoord);
6589
+ return uniqSortedWithTol(positions);
6590
+ };
6591
+ var computeFreedom = (prev, start, end) => {
6592
+ const orient = orientationOf(start, end);
6593
+ if (orient === "horizontal") {
6594
+ const facing = prev.y <= start.y ? "y+" : "y-";
6595
+ return { axis: "y", facing };
6596
+ } else {
6597
+ const facing = prev.x <= start.x ? "x+" : "x-";
6598
+ return { axis: "x", facing };
6599
+ }
6600
+ };
6601
+ var cartesian = (arrays) => arrays.length === 0 ? [[]] : arrays.reduce(
6602
+ (acc, curr) => acc.flatMap((a) => curr.map((c) => [...a, c])),
6603
+ [[]]
6604
+ );
6605
+ var generateElbowVariants = ({
6606
+ baseElbow,
6607
+ guidelines
6608
+ }) => {
6609
+ const elbow = preprocessElbow(baseElbow);
6610
+ assertOrthogonalPolyline(elbow);
6611
+ const nPts = elbow.length;
6612
+ const nSegs = nPts - 1;
6613
+ if (nSegs < 5) {
6614
+ return {
6615
+ elbowVariants: [elbow.map((p) => ({ ...p }))],
6616
+ movableSegments: []
6617
+ };
6618
+ }
6619
+ const firstMovableIndex = 2;
6620
+ const lastMovableIndex = nPts - 4;
6621
+ const movableSegments = [];
6622
+ const movableIdx = [];
6623
+ const axes = [];
6624
+ const optionsPerSegment = [];
6625
+ for (let i = firstMovableIndex; i <= lastMovableIndex; i++) {
6626
+ const prev = elbow[i - 1];
6627
+ const start = elbow[i];
6628
+ const end = elbow[i + 1];
6629
+ const next2 = elbow[i + 2];
6630
+ const { axis, facing } = computeFreedom(prev, start, end);
6631
+ movableSegments.push({
6632
+ start: { ...start },
6633
+ end: { ...end },
6634
+ freedom: facing,
6635
+ dir: dir(facing)
6636
+ });
6637
+ movableIdx.push(i);
6638
+ axes.push(axis);
6639
+ const currentCoord = axis === "x" ? start.x : start.y;
6640
+ const rawCandidates = collectAxisCandidates(axis, guidelines, currentCoord);
6641
+ const filtered = rawCandidates.filter((pos) => {
6642
+ if (axis === "y") {
6643
+ const lenPrev = Math.abs(prev.y - pos);
6644
+ const lenNext = Math.abs(next2.y - pos);
6645
+ return lenPrev > MIN_LEN && lenNext > MIN_LEN;
6646
+ } else {
6647
+ const lenPrev = Math.abs(prev.x - pos);
6648
+ const lenNext = Math.abs(next2.x - pos);
6649
+ return lenPrev > MIN_LEN && lenNext > MIN_LEN;
6572
6650
  }
6573
- }
6574
- return combinations;
6575
- };
6576
- const positionCombinations = generateCombinations(segmentGuidelineOptions);
6651
+ });
6652
+ optionsPerSegment.push(filtered);
6653
+ }
6654
+ const combos = cartesian(optionsPerSegment);
6655
+ const seen = /* @__PURE__ */ new Set();
6577
6656
  const elbowVariants = [];
6578
- for (const combination of positionCombinations) {
6579
- const variant = [...baseElbow];
6580
- for (let segmentIndex = 0; segmentIndex < movableSegments.length; segmentIndex++) {
6581
- const segment = movableSegments[segmentIndex];
6582
- const newPosition = combination[segmentIndex];
6583
- const elbowIndex = segmentIndex + 1;
6584
- if (segment.freedom === "x+" || segment.freedom === "x-") {
6585
- variant[elbowIndex] = { ...variant[elbowIndex], x: newPosition };
6586
- variant[elbowIndex + 1] = { ...variant[elbowIndex + 1], x: newPosition };
6657
+ {
6658
+ const key = keyForPolyline(elbow);
6659
+ seen.add(key);
6660
+ elbowVariants.push(elbow.map((p) => ({ ...p })));
6661
+ }
6662
+ for (const combo of combos) {
6663
+ if (combo.length === 0) continue;
6664
+ const variant = elbow.map((p) => ({ ...p }));
6665
+ for (let k = 0; k < movableIdx.length; k++) {
6666
+ const segIndex = movableIdx[k];
6667
+ const axis = axes[k];
6668
+ const newPos = combo[k];
6669
+ if (typeof newPos !== "number" || Number.isNaN(newPos)) continue;
6670
+ if (axis === "y") {
6671
+ variant[segIndex] = { ...variant[segIndex], y: newPos };
6672
+ variant[segIndex + 1] = { ...variant[segIndex + 1], y: newPos };
6587
6673
  } else {
6588
- variant[elbowIndex] = { ...variant[elbowIndex], y: newPosition };
6589
- variant[elbowIndex + 1] = { ...variant[elbowIndex + 1], y: newPosition };
6674
+ variant[segIndex] = { ...variant[segIndex], x: newPos };
6675
+ variant[segIndex + 1] = { ...variant[segIndex + 1], x: newPos };
6590
6676
  }
6591
6677
  }
6592
- elbowVariants.push(variant);
6678
+ try {
6679
+ assertOrthogonalPolyline(variant);
6680
+ } catch {
6681
+ continue;
6682
+ }
6683
+ const key = keyForPolyline(variant);
6684
+ if (!seen.has(key)) {
6685
+ seen.add(key);
6686
+ elbowVariants.push(variant);
6687
+ }
6593
6688
  }
6594
- return {
6595
- elbowVariants,
6596
- movableSegments
6597
- };
6689
+ return { elbowVariants, movableSegments };
6598
6690
  };
6599
6691
 
6600
6692
  // lib/solvers/GuidelinesSolver/visualizeGuidelines.ts
@@ -6642,6 +6734,7 @@ var SchematicTraceSingleLineSolver = class extends BaseSolver {
6642
6734
  chipMap;
6643
6735
  movableSegments;
6644
6736
  baseElbow;
6737
+ allCandidatePaths;
6645
6738
  queuedCandidatePaths;
6646
6739
  chipObstacleSpatialIndex;
6647
6740
  solvedTracePath = null;
@@ -6691,7 +6784,8 @@ var SchematicTraceSingleLineSolver = class extends BaseSolver {
6691
6784
  }
6692
6785
  return len;
6693
6786
  };
6694
- this.queuedCandidatePaths = [this.baseElbow, ...elbowVariants].sort(
6787
+ this.allCandidatePaths = [this.baseElbow, ...elbowVariants];
6788
+ this.queuedCandidatePaths = [...this.allCandidatePaths].sort(
6695
6789
  (a, b) => getPathLength(a) - getPathLength(b)
6696
6790
  );
6697
6791
  }
@@ -6728,12 +6822,12 @@ var SchematicTraceSingleLineSolver = class extends BaseSolver {
6728
6822
  const bounds = getInputChipBounds(this.chipMap[startPin.chipId]);
6729
6823
  const dx = end.x - start.x;
6730
6824
  const dy = end.y - start.y;
6731
- const EPS = 1e-9;
6825
+ const EPS2 = 1e-9;
6732
6826
  const onLeft = Math.abs(start.x - bounds.minX) < 1e-9;
6733
6827
  const onRight = Math.abs(start.x - bounds.maxX) < 1e-9;
6734
6828
  const onBottom = Math.abs(start.y - bounds.minY) < 1e-9;
6735
6829
  const onTop = Math.abs(start.y - bounds.maxY) < 1e-9;
6736
- const entersInterior = onLeft && dx > EPS || onRight && dx < -EPS || onBottom && dy > EPS || onTop && dy < -EPS;
6830
+ const entersInterior = onLeft && dx > EPS2 || onRight && dx < -EPS2 || onBottom && dy > EPS2 || onTop && dy < -EPS2;
6737
6831
  if (entersInterior) return;
6738
6832
  if (!excludeChipIds.includes(startPin.chipId)) {
6739
6833
  excludeChipIds.push(startPin.chipId);
@@ -6748,12 +6842,12 @@ var SchematicTraceSingleLineSolver = class extends BaseSolver {
6748
6842
  const bounds = getInputChipBounds(this.chipMap[endPin.chipId]);
6749
6843
  const dx = start.x - end.x;
6750
6844
  const dy = start.y - end.y;
6751
- const EPS = 1e-9;
6845
+ const EPS2 = 1e-9;
6752
6846
  const onLeft = Math.abs(end.x - bounds.minX) < 1e-9;
6753
6847
  const onRight = Math.abs(end.x - bounds.maxX) < 1e-9;
6754
6848
  const onBottom = Math.abs(end.y - bounds.minY) < 1e-9;
6755
6849
  const onTop = Math.abs(end.y - bounds.maxY) < 1e-9;
6756
- const entersInterior = onLeft && dx > EPS || onRight && dx < -EPS || onBottom && dy > EPS || onTop && dy < -EPS;
6850
+ const entersInterior = onLeft && dx > EPS2 || onRight && dx < -EPS2 || onBottom && dy > EPS2 || onTop && dy < -EPS2;
6757
6851
  if (entersInterior) return;
6758
6852
  if (!excludeChipIds.includes(endPin.chipId)) {
6759
6853
  excludeChipIds.push(endPin.chipId);
@@ -6956,13 +7050,13 @@ var TraceOverlapIssueSolver = class extends BaseSolver {
6956
7050
  }
6957
7051
  }
6958
7052
  _step() {
6959
- const EPS = 1e-6;
7053
+ const EPS2 = 1e-6;
6960
7054
  const offsets = this.overlappingTraceSegments.map((_, idx) => {
6961
7055
  const n = Math.floor(idx / 2) + 1;
6962
7056
  const signed = idx % 2 === 0 ? -n : n;
6963
7057
  return signed * this.SHIFT_DISTANCE;
6964
7058
  });
6965
- const eq = (a, b) => Math.abs(a - b) < EPS;
7059
+ const eq = (a, b) => Math.abs(a - b) < EPS2;
6966
7060
  const samePoint = (p, q) => !!p && !!q && eq(p.x, q.x) && eq(p.y, q.y);
6967
7061
  this.overlappingTraceSegments.forEach((group, gidx) => {
6968
7062
  const offset = offsets[gidx];
@@ -6985,8 +7079,8 @@ var TraceOverlapIssueSolver = class extends BaseSolver {
6985
7079
  if (si === 0 || si === pts.length - 2) continue;
6986
7080
  const start = pts[si];
6987
7081
  const end = pts[si + 1];
6988
- const isVertical = Math.abs(start.x - end.x) < EPS;
6989
- const isHorizontal = Math.abs(start.y - end.y) < EPS;
7082
+ const isVertical = Math.abs(start.x - end.x) < EPS2;
7083
+ const isHorizontal = Math.abs(start.y - end.y) < EPS2;
6990
7084
  if (!isVertical && !isHorizontal) continue;
6991
7085
  if (isVertical) {
6992
7086
  if (!appliedX.has(si)) {
@@ -7095,7 +7189,7 @@ var TraceOverlapShiftSolver = class extends BaseSolver {
7095
7189
  return islands;
7096
7190
  }
7097
7191
  findNextOverlapIssue() {
7098
- const EPS = 1e-6;
7192
+ const EPS2 = 1e-6;
7099
7193
  const netIds = Object.keys(this.traceNetIslands);
7100
7194
  for (let i = 0; i < netIds.length; i++) {
7101
7195
  for (let j = i + 1; j < netIds.length; j++) {
@@ -7113,7 +7207,7 @@ var TraceOverlapShiftSolver = class extends BaseSolver {
7113
7207
  const minB = Math.min(b1, b2);
7114
7208
  const maxB = Math.max(b1, b2);
7115
7209
  const overlap = Math.min(maxA, maxB) - Math.max(minA, minB);
7116
- return overlap > EPS;
7210
+ return overlap > EPS2;
7117
7211
  };
7118
7212
  for (let pa = 0; pa < pathsA.length; pa++) {
7119
7213
  const pathA = pathsA[pa];
@@ -7121,8 +7215,8 @@ var TraceOverlapShiftSolver = class extends BaseSolver {
7121
7215
  for (let sa = 0; sa < ptsA.length - 1; sa++) {
7122
7216
  const a1 = ptsA[sa];
7123
7217
  const a2 = ptsA[sa + 1];
7124
- const aVert = Math.abs(a1.x - a2.x) < EPS;
7125
- const aHorz = Math.abs(a1.y - a2.y) < EPS;
7218
+ const aVert = Math.abs(a1.x - a2.x) < EPS2;
7219
+ const aHorz = Math.abs(a1.y - a2.y) < EPS2;
7126
7220
  if (!aVert && !aHorz) continue;
7127
7221
  for (let pb = 0; pb < pathsB.length; pb++) {
7128
7222
  const pathB = pathsB[pb];
@@ -7130,11 +7224,11 @@ var TraceOverlapShiftSolver = class extends BaseSolver {
7130
7224
  for (let sb = 0; sb < ptsB.length - 1; sb++) {
7131
7225
  const b1 = ptsB[sb];
7132
7226
  const b2 = ptsB[sb + 1];
7133
- const bVert = Math.abs(b1.x - b2.x) < EPS;
7134
- const bHorz = Math.abs(b1.y - b2.y) < EPS;
7227
+ const bVert = Math.abs(b1.x - b2.x) < EPS2;
7228
+ const bHorz = Math.abs(b1.y - b2.y) < EPS2;
7135
7229
  if (!bVert && !bHorz) continue;
7136
7230
  if (aVert && bVert) {
7137
- if (Math.abs(a1.x - b1.x) < EPS) {
7231
+ if (Math.abs(a1.x - b1.x) < EPS2) {
7138
7232
  if (overlaps1D(a1.y, a2.y, b1.y, b2.y)) {
7139
7233
  const keyA = `${pa}:${sa}`;
7140
7234
  const keyB = `${pb}:${sb}`;
@@ -7155,7 +7249,7 @@ var TraceOverlapShiftSolver = class extends BaseSolver {
7155
7249
  }
7156
7250
  }
7157
7251
  } else if (aHorz && bHorz) {
7158
- if (Math.abs(a1.y - b1.y) < EPS) {
7252
+ if (Math.abs(a1.y - b1.y) < EPS2) {
7159
7253
  if (overlaps1D(a1.x, a2.x, b1.x, b2.x)) {
7160
7254
  const keyA = `${pa}:${sa}`;
7161
7255
  const keyB = `${pb}:${sb}`;
@@ -7270,24 +7364,24 @@ function getRectBounds(center, w, h) {
7270
7364
  }
7271
7365
 
7272
7366
  // lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/collisions.ts
7273
- function segmentIntersectsRect(p1, p2, rect, EPS = 1e-9) {
7274
- const isVert = Math.abs(p1.x - p2.x) < EPS;
7275
- const isHorz = Math.abs(p1.y - p2.y) < EPS;
7367
+ function segmentIntersectsRect(p1, p2, rect, EPS2 = 1e-9) {
7368
+ const isVert = Math.abs(p1.x - p2.x) < EPS2;
7369
+ const isHorz = Math.abs(p1.y - p2.y) < EPS2;
7276
7370
  if (!isVert && !isHorz) return false;
7277
7371
  if (isVert) {
7278
7372
  const x = p1.x;
7279
- if (x < rect.minX - EPS || x > rect.maxX + EPS) return false;
7373
+ if (x < rect.minX - EPS2 || x > rect.maxX + EPS2) return false;
7280
7374
  const segMinY = Math.min(p1.y, p2.y);
7281
7375
  const segMaxY = Math.max(p1.y, p2.y);
7282
7376
  const overlap = Math.min(segMaxY, rect.maxY) - Math.max(segMinY, rect.minY);
7283
- return overlap > EPS;
7377
+ return overlap > EPS2;
7284
7378
  } else {
7285
7379
  const y = p1.y;
7286
- if (y < rect.minY - EPS || y > rect.maxY + EPS) return false;
7380
+ if (y < rect.minY - EPS2 || y > rect.maxY + EPS2) return false;
7287
7381
  const segMinX = Math.min(p1.x, p2.x);
7288
7382
  const segMaxX = Math.max(p1.x, p2.x);
7289
7383
  const overlap = Math.min(segMaxX, rect.maxX) - Math.max(segMinX, rect.minX);
7290
- return overlap > EPS;
7384
+ return overlap > EPS2;
7291
7385
  }
7292
7386
  }
7293
7387
  function rectIntersectsAnyTrace(bounds, inputTraceMap, hostPathId, hostSegIndex) {
@@ -7612,14 +7706,14 @@ var SingleNetLabelPlacementSolver = class extends BaseSolver {
7612
7706
  };
7613
7707
  let bestCandidate = null;
7614
7708
  let bestScore = -Infinity;
7615
- const EPS = 1e-6;
7709
+ const EPS2 = 1e-6;
7616
7710
  for (const curr of tracesToScan) {
7617
7711
  const pts = curr.tracePath.slice();
7618
7712
  for (let si = 0; si < pts.length - 1; si++) {
7619
7713
  const a = pts[si];
7620
7714
  const b = pts[si + 1];
7621
- const isH = Math.abs(a.y - b.y) < EPS;
7622
- const isV = Math.abs(a.x - b.x) < EPS;
7715
+ const isH = Math.abs(a.y - b.y) < EPS2;
7716
+ const isV = Math.abs(a.x - b.x) < EPS2;
7623
7717
  if (!isH && !isV) continue;
7624
7718
  const segmentAllowed = isH ? ["y+", "y-"] : ["x+", "x-"];
7625
7719
  const candidateOrients = orientations.filter(
@@ -96,6 +96,15 @@ export class MspConnectionPairSolver extends BaseSolver {
96
96
 
97
97
  if (directlyConnectedPins.length === 2) {
98
98
  const [pin1, pin2] = directlyConnectedPins
99
+ const p1 = this.pinMap[pin1!]!
100
+ const p2 = this.pinMap[pin2!]!
101
+ // Enforce max pair distance (use Manhattan to match orthogonal routing metric)
102
+ const manhattanDist = Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y)
103
+ if (manhattanDist > this.maxMspPairDistance) {
104
+ // Too far apart; skip creating an MSP pair for this net
105
+ return
106
+ }
107
+
99
108
  const globalConnNetId = this.globalConnMap.getNetConnectedToId(pin1!)!
100
109
  const userNetId =
101
110
  this.userNetIdByPinId[pin1!] ?? this.userNetIdByPinId[pin2!]
@@ -105,7 +114,7 @@ export class MspConnectionPairSolver extends BaseSolver {
105
114
  dcConnNetId: dcNetId,
106
115
  globalConnNetId,
107
116
  userNetId,
108
- pins: [this.pinMap[pin1!]!, this.pinMap[pin2!]!],
117
+ pins: [p1, p2],
109
118
  })
110
119
 
111
120
  return
@@ -24,6 +24,7 @@ export class SchematicTraceSingleLineSolver extends BaseSolver {
24
24
  movableSegments: Array<MovableSegment>
25
25
  baseElbow: Point[]
26
26
 
27
+ allCandidatePaths: Array<Point[]>
27
28
  queuedCandidatePaths: Array<Point[]>
28
29
 
29
30
  chipObstacleSpatialIndex: ChipObstacleSpatialIndex
@@ -91,7 +92,8 @@ export class SchematicTraceSingleLineSolver extends BaseSolver {
91
92
  return len
92
93
  }
93
94
 
94
- this.queuedCandidatePaths = [this.baseElbow, ...elbowVariants].sort(
95
+ this.allCandidatePaths = [this.baseElbow, ...elbowVariants]
96
+ this.queuedCandidatePaths = [...this.allCandidatePaths].sort(
95
97
  (a, b) => getPathLength(a) - getPathLength(b),
96
98
  )
97
99
  }