@tscircuit/schematic-trace-solver 0.0.184 → 0.0.186

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
@@ -936,6 +936,16 @@ declare class AvailableNetOrientationSolver extends BaseSolver {
936
936
  private hasOppositeVerticalRailOnSameChipSide;
937
937
  private getSingleChipSideForNetConnection;
938
938
  private isPortOnlyLabel;
939
+ private getNetConnectionForLabel;
940
+ private isGroundLabel;
941
+ /**
942
+ * A vertical-only input stores its text extent in netLabelHeight. If its
943
+ * initial port-only placement is horizontal, use that extent for collision
944
+ * checks without changing the placement dimensions returned by the solver.
945
+ */
946
+ private getHorizontalConstraintMismatchBounds;
947
+ private includeHorizontalConstraintMismatches;
948
+ private getLabelBoundsForCollisionWithGround;
939
949
  private shouldIgnorePendingCrowdedLabel;
940
950
  private intersectsAnyOtherNetLabel;
941
951
  private sharesChipBoundary;
package/dist/index.js CHANGED
@@ -9413,6 +9413,17 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9413
9413
  if (this.getTextBlockedMultiPinRailTraces(label, orientations[0])) {
9414
9414
  return true;
9415
9415
  }
9416
+ const hasSingleVerticalOrientation = orientations.length === 1 && isYOrientation(orientations[0]);
9417
+ if (hasSingleVerticalOrientation && this.isGroundLabel(label)) {
9418
+ const bounds2 = getRectBounds(label.center, label.width, label.height);
9419
+ const overlapsHorizontalConstraintMismatch = this.outputNetLabelPlacements.some((otherLabel, otherIndex) => {
9420
+ if (otherIndex === labelIndex) return false;
9421
+ if (otherLabel.globalConnNetId === label.globalConnNetId) return false;
9422
+ const otherBounds = this.getHorizontalConstraintMismatchBounds(otherLabel);
9423
+ return otherBounds ? rectsOverlap2(bounds2, otherBounds) : false;
9424
+ });
9425
+ if (overlapsHorizontalConstraintMismatch) return true;
9426
+ }
9416
9427
  if (!this.crowdedPortOnlyLabelIndices.has(labelIndex)) return false;
9417
9428
  const bounds = getRectBounds(label.center, label.width, label.height);
9418
9429
  return this.outputNetLabelPlacements.some((otherLabel, otherIndex) => {
@@ -10386,15 +10397,17 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
10386
10397
  return "text-collision";
10387
10398
  }
10388
10399
  }
10389
- for (const i of this.obstacleIndex.getLabelIndicesNearTracePath(
10390
- connectorTrace
10391
- )) {
10400
+ const nearbyLabelIndices = this.includeHorizontalConstraintMismatches(
10401
+ this.obstacleIndex.getLabelIndicesNearTracePath(connectorTrace),
10402
+ this.isGroundLabel(label)
10403
+ );
10404
+ for (const i of nearbyLabelIndices) {
10392
10405
  if (i === labelIndex) continue;
10393
10406
  if (this.shouldIgnorePendingCrowdedLabel(labelIndex, i)) continue;
10394
10407
  const otherLabel = this.outputNetLabelPlacements[i];
10395
10408
  if (tracePathIntersectsBounds(
10396
10409
  connectorTrace,
10397
- getRectBounds(otherLabel.center, otherLabel.width, otherLabel.height)
10410
+ this.getLabelBoundsForCollisionWithGround(otherLabel, label)
10398
10411
  )) {
10399
10412
  return "netlabel-collision";
10400
10413
  }
@@ -10445,7 +10458,7 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
10445
10458
  if (this.isTraceTooCloseToLabel(bounds, label)) {
10446
10459
  return "trace-clearance-violation";
10447
10460
  }
10448
- if (this.intersectsAnyOtherNetLabel(bounds, labelIndex)) {
10461
+ if (this.intersectsAnyOtherNetLabel(bounds, labelIndex, label)) {
10449
10462
  return "netlabel-collision";
10450
10463
  }
10451
10464
  return "valid";
@@ -10645,22 +10658,95 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
10645
10658
  isPortOnlyLabel(label) {
10646
10659
  return label.mspConnectionPairIds.length === 0;
10647
10660
  }
10661
+ getNetConnectionForLabel(label) {
10662
+ return label.netId ? this.inputProblem.netConnections.find(
10663
+ (connection) => connection.netId === label.netId
10664
+ ) : this.inputProblem.netConnections.find(
10665
+ (connection) => connection.pinIds.some((pinId) => label.pinIds.includes(pinId))
10666
+ );
10667
+ }
10668
+ isGroundLabel(label) {
10669
+ return this.getNetConnectionForLabel(label)?.isGround === true;
10670
+ }
10671
+ /**
10672
+ * A vertical-only input stores its text extent in netLabelHeight. If its
10673
+ * initial port-only placement is horizontal, use that extent for collision
10674
+ * checks without changing the placement dimensions returned by the solver.
10675
+ */
10676
+ getHorizontalConstraintMismatchBounds(label) {
10677
+ if (!this.isPortOnlyLabel(label)) return null;
10678
+ if (label.orientation !== "x+" && label.orientation !== "x-") return null;
10679
+ const effectiveNetId = label.netId ?? label.globalConnNetId;
10680
+ const orientations = this.inputProblem.availableNetLabelOrientations[effectiveNetId] ?? [];
10681
+ if (orientations.length === 0 || orientations.some((orientation) => !isYOrientation(orientation))) {
10682
+ return null;
10683
+ }
10684
+ const renderedWidth = this.getNetConnectionForLabel(label)?.netLabelHeight;
10685
+ if (renderedWidth === void 0 || renderedWidth <= label.width + label.height + EPS13) {
10686
+ return null;
10687
+ }
10688
+ const placementBaseCenter = getCenterFromAnchor(
10689
+ label.anchorPoint,
10690
+ label.orientation,
10691
+ label.width,
10692
+ label.height
10693
+ );
10694
+ const renderedBaseCenter = getCenterFromAnchor(
10695
+ label.anchorPoint,
10696
+ label.orientation,
10697
+ renderedWidth,
10698
+ label.height
10699
+ );
10700
+ return getRectBounds(
10701
+ {
10702
+ x: renderedBaseCenter.x + label.center.x - placementBaseCenter.x,
10703
+ y: renderedBaseCenter.y + label.center.y - placementBaseCenter.y
10704
+ },
10705
+ renderedWidth,
10706
+ label.height
10707
+ );
10708
+ }
10709
+ includeHorizontalConstraintMismatches(labelIndices, include) {
10710
+ if (!include) return [...labelIndices].sort((a, b) => a - b);
10711
+ const result = new Set(labelIndices);
10712
+ for (let index = 0; index < this.outputNetLabelPlacements.length; index++) {
10713
+ if (this.getHorizontalConstraintMismatchBounds(
10714
+ this.outputNetLabelPlacements[index]
10715
+ )) {
10716
+ result.add(index);
10717
+ }
10718
+ }
10719
+ return [...result].sort((a, b) => a - b);
10720
+ }
10721
+ getLabelBoundsForCollisionWithGround(label, currentLabel) {
10722
+ if (this.isGroundLabel(currentLabel)) {
10723
+ const adjustedBounds = this.getHorizontalConstraintMismatchBounds(label);
10724
+ if (adjustedBounds) return adjustedBounds;
10725
+ }
10726
+ return getRectBounds(label.center, label.width, label.height);
10727
+ }
10648
10728
  shouldIgnorePendingCrowdedLabel(labelIndex, otherLabelIndex) {
10649
10729
  return this.crowdedPortOnlyLabelIndices.has(labelIndex) && this.crowdedPortOnlyLabelIndices.has(otherLabelIndex) && this.queuedLabelIndices.includes(otherLabelIndex);
10650
10730
  }
10651
- intersectsAnyOtherNetLabel(bounds, labelIndex) {
10731
+ intersectsAnyOtherNetLabel(bounds, labelIndex, currentLabel) {
10652
10732
  const searchBounds = {
10653
10733
  minX: bounds.minX - LABEL_SEARCH_STEP,
10654
10734
  minY: bounds.minY - LABEL_SEARCH_STEP,
10655
10735
  maxX: bounds.maxX + LABEL_SEARCH_STEP,
10656
10736
  maxY: bounds.maxY + LABEL_SEARCH_STEP
10657
10737
  };
10658
- const nearbyLabelIndices = this.obstacleIndex.getLabelIndicesInBounds(searchBounds).sort((a, b) => a - b);
10738
+ const nearbyLabelIndices = this.includeHorizontalConstraintMismatches(
10739
+ this.obstacleIndex.getLabelIndicesInBounds(searchBounds),
10740
+ this.isGroundLabel(currentLabel)
10741
+ );
10659
10742
  for (const i of nearbyLabelIndices) {
10660
10743
  if (i === labelIndex) continue;
10661
10744
  if (this.shouldIgnorePendingCrowdedLabel(labelIndex, i)) continue;
10662
10745
  const label = this.outputNetLabelPlacements[i];
10663
- const otherBounds = getRectBounds(label.center, label.width, label.height);
10746
+ const otherBounds = this.getLabelBoundsForCollisionWithGround(
10747
+ label,
10748
+ currentLabel
10749
+ );
10664
10750
  if (i === this.blockedStandaloneLabelIndex && isYOrientation(label.orientation)) {
10665
10751
  otherBounds.minX -= LABEL_SEARCH_STEP;
10666
10752
  otherBounds.maxX += LABEL_SEARCH_STEP;
@@ -12659,6 +12745,7 @@ ${c.status}`
12659
12745
 
12660
12746
  // lib/solvers/UnroutedTraceRecoverySolver/UnroutedTraceRecoverySolver.ts
12661
12747
  import { distance as distance6, doSegmentsIntersect as doSegmentsIntersect3 } from "@tscircuit/math-utils";
12748
+ import { calculateElbow as calculateElbow3 } from "calculate-elbow";
12662
12749
  var ROUTE_CLEARANCE = 0.2;
12663
12750
  var COORDINATE_TOLERANCE = 1e-9;
12664
12751
  var GROUND_NET_ID = "GND";
@@ -12711,7 +12798,7 @@ var getOuterBounds = (obstacles) => {
12711
12798
  maxY: Math.max(...obstacles.map((obstacle) => obstacle.maxY))
12712
12799
  };
12713
12800
  };
12714
- var getPerimeterCandidates = ({
12801
+ var getPinConnectionCandidates = ({
12715
12802
  connectionPair,
12716
12803
  obstacles
12717
12804
  }) => {
@@ -12733,7 +12820,13 @@ var getPerimeterCandidates = ({
12733
12820
  outerBounds.minX - ROUTE_CLEARANCE,
12734
12821
  outerBounds.maxX + ROUTE_CLEARANCE
12735
12822
  ];
12736
- const candidates = [];
12823
+ const candidates = [
12824
+ calculateElbow3(
12825
+ { ...firstPin, facingDirection: firstPin._facingDirection },
12826
+ { ...secondPin, facingDirection: secondPin._facingDirection },
12827
+ { overshoot: ROUTE_CLEARANCE }
12828
+ )
12829
+ ];
12737
12830
  for (const channelY of horizontalChannels) {
12738
12831
  candidates.push(
12739
12832
  removeConsecutiveDuplicatePoints([
@@ -13053,11 +13146,11 @@ var UnroutedTraceRecoverySolver = class extends BaseSolver {
13053
13146
  obstacles,
13054
13147
  maxConnectionDistance: this.maxConnectionDistance
13055
13148
  });
13056
- const perimeterCandidates = getPerimeterCandidates({
13149
+ const pinConnectionCandidates = getPinConnectionCandidates({
13057
13150
  connectionPair,
13058
13151
  obstacles
13059
13152
  });
13060
- const candidates = [...junctionCandidates, ...perimeterCandidates];
13153
+ const candidates = [...junctionCandidates, ...pinConnectionCandidates];
13061
13154
  const rejectComponentBoundaryTravel = hasParallelFailedConnection({
13062
13155
  connectionPair,
13063
13156
  failedConnectionPairs: this.failedConnectionPairs
@@ -268,6 +268,22 @@ export class AvailableNetOrientationSolver extends BaseSolver {
268
268
  if (this.getTextBlockedMultiPinRailTraces(label, orientations[0]!)) {
269
269
  return true
270
270
  }
271
+
272
+ const hasSingleVerticalOrientation =
273
+ orientations.length === 1 && isYOrientation(orientations[0]!)
274
+ if (hasSingleVerticalOrientation && this.isGroundLabel(label)) {
275
+ const bounds = getRectBounds(label.center, label.width, label.height)
276
+ const overlapsHorizontalConstraintMismatch =
277
+ this.outputNetLabelPlacements.some((otherLabel, otherIndex) => {
278
+ if (otherIndex === labelIndex) return false
279
+ if (otherLabel.globalConnNetId === label.globalConnNetId) return false
280
+ const otherBounds =
281
+ this.getHorizontalConstraintMismatchBounds(otherLabel)
282
+ return otherBounds ? rectsOverlap(bounds, otherBounds) : false
283
+ })
284
+ if (overlapsHorizontalConstraintMismatch) return true
285
+ }
286
+
271
287
  if (!this.crowdedPortOnlyLabelIndices.has(labelIndex)) return false
272
288
 
273
289
  const bounds = getRectBounds(label.center, label.width, label.height)
@@ -1627,16 +1643,18 @@ export class AvailableNetOrientationSolver extends BaseSolver {
1627
1643
  }
1628
1644
  }
1629
1645
 
1630
- for (const i of this.obstacleIndex.getLabelIndicesNearTracePath(
1631
- connectorTrace,
1632
- )) {
1646
+ const nearbyLabelIndices = this.includeHorizontalConstraintMismatches(
1647
+ this.obstacleIndex.getLabelIndicesNearTracePath(connectorTrace),
1648
+ this.isGroundLabel(label),
1649
+ )
1650
+ for (const i of nearbyLabelIndices) {
1633
1651
  if (i === labelIndex) continue
1634
1652
  if (this.shouldIgnorePendingCrowdedLabel(labelIndex, i)) continue
1635
1653
  const otherLabel = this.outputNetLabelPlacements[i]!
1636
1654
  if (
1637
1655
  tracePathIntersectsBounds(
1638
1656
  connectorTrace,
1639
- getRectBounds(otherLabel.center, otherLabel.width, otherLabel.height),
1657
+ this.getLabelBoundsForCollisionWithGround(otherLabel, label),
1640
1658
  )
1641
1659
  ) {
1642
1660
  return "netlabel-collision"
@@ -1719,7 +1737,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
1719
1737
  if (this.isTraceTooCloseToLabel(bounds, label)) {
1720
1738
  return "trace-clearance-violation"
1721
1739
  }
1722
- if (this.intersectsAnyOtherNetLabel(bounds, labelIndex)) {
1740
+ if (this.intersectsAnyOtherNetLabel(bounds, labelIndex, label)) {
1723
1741
  return "netlabel-collision"
1724
1742
  }
1725
1743
  return "valid"
@@ -2020,6 +2038,99 @@ export class AvailableNetOrientationSolver extends BaseSolver {
2020
2038
  return label.mspConnectionPairIds.length === 0
2021
2039
  }
2022
2040
 
2041
+ private getNetConnectionForLabel(label: NetLabelPlacement) {
2042
+ return label.netId
2043
+ ? this.inputProblem.netConnections.find(
2044
+ (connection) => connection.netId === label.netId,
2045
+ )
2046
+ : this.inputProblem.netConnections.find((connection) =>
2047
+ connection.pinIds.some((pinId) => label.pinIds.includes(pinId)),
2048
+ )
2049
+ }
2050
+
2051
+ private isGroundLabel(label: NetLabelPlacement) {
2052
+ return this.getNetConnectionForLabel(label)?.isGround === true
2053
+ }
2054
+
2055
+ /**
2056
+ * A vertical-only input stores its text extent in netLabelHeight. If its
2057
+ * initial port-only placement is horizontal, use that extent for collision
2058
+ * checks without changing the placement dimensions returned by the solver.
2059
+ */
2060
+ private getHorizontalConstraintMismatchBounds(label: NetLabelPlacement) {
2061
+ if (!this.isPortOnlyLabel(label)) return null
2062
+ if (label.orientation !== "x+" && label.orientation !== "x-") return null
2063
+
2064
+ const effectiveNetId = label.netId ?? label.globalConnNetId
2065
+ const orientations =
2066
+ this.inputProblem.availableNetLabelOrientations[effectiveNetId] ?? []
2067
+ if (
2068
+ orientations.length === 0 ||
2069
+ orientations.some((orientation) => !isYOrientation(orientation))
2070
+ ) {
2071
+ return null
2072
+ }
2073
+
2074
+ const renderedWidth = this.getNetConnectionForLabel(label)?.netLabelHeight
2075
+ if (
2076
+ renderedWidth === undefined ||
2077
+ renderedWidth <= label.width + label.height + EPS
2078
+ ) {
2079
+ return null
2080
+ }
2081
+
2082
+ const placementBaseCenter = getCenterFromAnchor(
2083
+ label.anchorPoint,
2084
+ label.orientation,
2085
+ label.width,
2086
+ label.height,
2087
+ )
2088
+ const renderedBaseCenter = getCenterFromAnchor(
2089
+ label.anchorPoint,
2090
+ label.orientation,
2091
+ renderedWidth,
2092
+ label.height,
2093
+ )
2094
+ return getRectBounds(
2095
+ {
2096
+ x: renderedBaseCenter.x + label.center.x - placementBaseCenter.x,
2097
+ y: renderedBaseCenter.y + label.center.y - placementBaseCenter.y,
2098
+ },
2099
+ renderedWidth,
2100
+ label.height,
2101
+ )
2102
+ }
2103
+
2104
+ private includeHorizontalConstraintMismatches(
2105
+ labelIndices: number[],
2106
+ include: boolean,
2107
+ ) {
2108
+ if (!include) return [...labelIndices].sort((a, b) => a - b)
2109
+
2110
+ const result = new Set(labelIndices)
2111
+ for (let index = 0; index < this.outputNetLabelPlacements.length; index++) {
2112
+ if (
2113
+ this.getHorizontalConstraintMismatchBounds(
2114
+ this.outputNetLabelPlacements[index]!,
2115
+ )
2116
+ ) {
2117
+ result.add(index)
2118
+ }
2119
+ }
2120
+ return [...result].sort((a, b) => a - b)
2121
+ }
2122
+
2123
+ private getLabelBoundsForCollisionWithGround(
2124
+ label: NetLabelPlacement,
2125
+ currentLabel: NetLabelPlacement,
2126
+ ) {
2127
+ if (this.isGroundLabel(currentLabel)) {
2128
+ const adjustedBounds = this.getHorizontalConstraintMismatchBounds(label)
2129
+ if (adjustedBounds) return adjustedBounds
2130
+ }
2131
+ return getRectBounds(label.center, label.width, label.height)
2132
+ }
2133
+
2023
2134
  private shouldIgnorePendingCrowdedLabel(
2024
2135
  labelIndex: number,
2025
2136
  otherLabelIndex: number,
@@ -2031,22 +2142,30 @@ export class AvailableNetOrientationSolver extends BaseSolver {
2031
2142
  )
2032
2143
  }
2033
2144
 
2034
- private intersectsAnyOtherNetLabel(bounds: Bounds, labelIndex: number) {
2145
+ private intersectsAnyOtherNetLabel(
2146
+ bounds: Bounds,
2147
+ labelIndex: number,
2148
+ currentLabel: NetLabelPlacement,
2149
+ ) {
2035
2150
  const searchBounds = {
2036
2151
  minX: bounds.minX - LABEL_SEARCH_STEP,
2037
2152
  minY: bounds.minY - LABEL_SEARCH_STEP,
2038
2153
  maxX: bounds.maxX + LABEL_SEARCH_STEP,
2039
2154
  maxY: bounds.maxY + LABEL_SEARCH_STEP,
2040
2155
  }
2041
- const nearbyLabelIndices = this.obstacleIndex
2042
- .getLabelIndicesInBounds(searchBounds)
2043
- .sort((a, b) => a - b)
2156
+ const nearbyLabelIndices = this.includeHorizontalConstraintMismatches(
2157
+ this.obstacleIndex.getLabelIndicesInBounds(searchBounds),
2158
+ this.isGroundLabel(currentLabel),
2159
+ )
2044
2160
 
2045
2161
  for (const i of nearbyLabelIndices) {
2046
2162
  if (i === labelIndex) continue
2047
2163
  if (this.shouldIgnorePendingCrowdedLabel(labelIndex, i)) continue
2048
2164
  const label = this.outputNetLabelPlacements[i]!
2049
- const otherBounds = getRectBounds(label.center, label.width, label.height)
2165
+ const otherBounds = this.getLabelBoundsForCollisionWithGround(
2166
+ label,
2167
+ currentLabel,
2168
+ )
2050
2169
  if (
2051
2170
  i === this.blockedStandaloneLabelIndex &&
2052
2171
  isYOrientation(label.orientation)
@@ -1,5 +1,6 @@
1
1
  import type { Point } from "@tscircuit/math-utils"
2
2
  import { distance, doSegmentsIntersect } from "@tscircuit/math-utils"
3
+ import { calculateElbow } from "calculate-elbow"
3
4
  import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
4
5
  import { getConnectivityMapsFromInputProblem } from "lib/solvers/MspConnectionPairSolver/getConnectivityMapFromInputProblem"
5
6
  import {
@@ -86,7 +87,7 @@ const getOuterBounds = (obstacles: ObstacleRect[]) => {
86
87
  }
87
88
  }
88
89
 
89
- const getPerimeterCandidates = ({
90
+ const getPinConnectionCandidates = ({
90
91
  connectionPair,
91
92
  obstacles,
92
93
  }: {
@@ -111,7 +112,14 @@ const getPerimeterCandidates = ({
111
112
  outerBounds.minX - ROUTE_CLEARANCE,
112
113
  outerBounds.maxX + ROUTE_CLEARANCE,
113
114
  ]
114
- const candidates: Point[][] = []
115
+ // Recovery uses actual text bounds, so try local elbows before the perimeter.
116
+ const candidates: Point[][] = [
117
+ calculateElbow(
118
+ { ...firstPin, facingDirection: firstPin._facingDirection! },
119
+ { ...secondPin, facingDirection: secondPin._facingDirection! },
120
+ { overshoot: ROUTE_CLEARANCE },
121
+ ),
122
+ ]
115
123
 
116
124
  for (const channelY of horizontalChannels) {
117
125
  candidates.push(
@@ -526,11 +534,11 @@ export class UnroutedTraceRecoverySolver extends BaseSolver {
526
534
  obstacles,
527
535
  maxConnectionDistance: this.maxConnectionDistance,
528
536
  })
529
- const perimeterCandidates = getPerimeterCandidates({
537
+ const pinConnectionCandidates = getPinConnectionCandidates({
530
538
  connectionPair,
531
539
  obstacles,
532
540
  })
533
- const candidates = [...junctionCandidates, ...perimeterCandidates]
541
+ const candidates = [...junctionCandidates, ...pinConnectionCandidates]
534
542
  const rejectComponentBoundaryTravel = hasParallelFailedConnection({
535
543
  connectionPair,
536
544
  failedConnectionPairs: this.failedConnectionPairs,
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.184",
8
+ "version": "0.0.186",
9
9
  "type": "module",
10
10
  "scripts": {
11
11
  "start": "cosmos",
@@ -0,0 +1,4 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/bug-reports/bug-report-20260905T041712Z/bug-report-20260905T041712Z.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />