@tscircuit/schematic-trace-solver 0.0.164 → 0.0.166

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 (23) hide show
  1. package/dist/index.d.ts +5 -1
  2. package/dist/index.js +370 -24
  3. package/lib/solvers/NetLabelToTraceSolver/NetLabelToTraceSolver.ts +225 -24
  4. package/lib/solvers/SameNetJunctionAlignmentSolver/SameNetJunctionAlignmentSolver.ts +11 -5
  5. package/lib/solvers/SameNetJunctionAlignmentSolver/alignSameNetJunctions.ts +1 -1
  6. package/lib/solvers/SameNetJunctionAlignmentSolver/collapseSameNetCycles.ts +310 -0
  7. package/package.json +1 -1
  8. package/tests/assets/example51.json +6 -0
  9. package/tests/bug-reports/bug-report-20260721T221026Z/__snapshots__/bug-report-20260721T221026Z.snap.svg +2 -2
  10. package/tests/examples/__snapshots__/example51.snap.svg +42 -78
  11. package/tests/examples/example51.test.ts +24 -0
  12. package/tests/fixtures/convertSolverOutputToCircuitJson.ts +22 -3
  13. package/tests/repros/__snapshots__/board-1273-trace-overlap-cycle.snap.svg +2 -2
  14. package/tests/repros/__snapshots__/repro-bluetooth-controller-ground-decoupling-groups.snap.svg +1 -15
  15. package/tests/repros/__snapshots__/repro-core-ground-inline-label-fallback.snap.svg +2 -16
  16. package/tests/repros/__snapshots__/repro-isolated-rs485-isow7841.snap.svg +10 -10
  17. package/tests/repros/__snapshots__/repro-nrf52810-clock-routing.snap.svg +18 -18
  18. package/tests/repros/__snapshots__/repro-pga300-redundant-ground-traces.snap.svg +1 -15
  19. package/tests/repros/__snapshots__/repro-pmp11282-isolated-dcdc.snap.svg +33 -47
  20. package/tests/repros/repro-board-648-esp12f-section.test.ts +3 -0
  21. package/tests/repros/repro-isolated-rs485-isow7841.test.ts +33 -1
  22. package/tests/repros/repro-nrf52810-clock-routing.test.ts +26 -1
  23. package/tests/solvers/SameNetJunctionAlignmentSolver/collapse-same-net-cycles.test.ts +154 -0
package/dist/index.d.ts CHANGED
@@ -1394,11 +1394,15 @@ declare class NetLabelToTraceSolver extends BaseSolver {
1394
1394
  activeSubSolver: SchematicTraceSingleLineSolver2 | null;
1395
1395
  constructor(input: InlineNetLabelOutput);
1396
1396
  getConstructorParams(): [InlineNetLabelOutput];
1397
- private isEligiblePortOnlyDirectConnectionLabel;
1397
+ private isPortOnlyFallbackLabel;
1398
+ private isDirectConnectionLabel;
1399
+ private getMultiPinNetConnection;
1398
1400
  private buildCandidatePairs;
1401
+ private buildRoutedComponentCandidates;
1399
1402
  private isSupersededConnectorTrace;
1400
1403
  private routeIntersectsRemainingLabels;
1401
1404
  private tryAcceptCurrentRoute;
1405
+ private areCandidatePinsAlreadyConnected;
1402
1406
  _step(): void;
1403
1407
  getOutput(): {
1404
1408
  traces: SolvedTracePath[];
package/dist/index.js CHANGED
@@ -13009,6 +13009,214 @@ var alignSameNetJunctions = ({
13009
13009
  };
13010
13010
  };
13011
13011
 
13012
+ // lib/solvers/SameNetJunctionAlignmentSolver/collapseSameNetCycles.ts
13013
+ import { getSegmentIntersection as getSegmentIntersection4 } from "@tscircuit/math-utils/line-intersections";
13014
+ var TRACE_LENGTH_EPSILON = 1e-6;
13015
+ var getTracePathStartingAtPin = (trace, pin) => {
13016
+ const pathStart = trace.tracePath[0];
13017
+ const pathEnd = trace.tracePath.at(-1);
13018
+ if (!pathStart || !pathEnd) return null;
13019
+ if (pointsEqual(pathStart, pin)) return trace.tracePath;
13020
+ if (pointsEqual(pathEnd, pin)) return [...trace.tracePath].reverse();
13021
+ return null;
13022
+ };
13023
+ var getPerpendicularPathCrossings = (targetPath, donorPath) => {
13024
+ const crossings = [];
13025
+ const sharedEndpoint = targetPath[0];
13026
+ for (let targetSegmentIndex = 0; targetSegmentIndex < targetPath.length - 1; targetSegmentIndex++) {
13027
+ const targetStart = targetPath[targetSegmentIndex];
13028
+ const targetEnd = targetPath[targetSegmentIndex + 1];
13029
+ const targetOrientation = getRailOrientation(targetStart, targetEnd);
13030
+ if (!targetOrientation) continue;
13031
+ for (let donorSegmentIndex = 0; donorSegmentIndex < donorPath.length - 1; donorSegmentIndex++) {
13032
+ const donorStart = donorPath[donorSegmentIndex];
13033
+ const donorEnd = donorPath[donorSegmentIndex + 1];
13034
+ const donorOrientation = getRailOrientation(donorStart, donorEnd);
13035
+ if (!donorOrientation || donorOrientation === targetOrientation) continue;
13036
+ const intersectionPoint = getSegmentIntersection4(
13037
+ targetStart,
13038
+ targetEnd,
13039
+ donorStart,
13040
+ donorEnd
13041
+ );
13042
+ if (!intersectionPoint || pointsEqual(intersectionPoint, sharedEndpoint)) {
13043
+ continue;
13044
+ }
13045
+ crossings.push({
13046
+ intersectionPoint,
13047
+ targetSegmentIndex,
13048
+ donorSegmentIndex
13049
+ });
13050
+ }
13051
+ }
13052
+ return crossings;
13053
+ };
13054
+ var buildCollapsedCyclePath = ({
13055
+ targetTrace,
13056
+ targetPath,
13057
+ donorPath,
13058
+ crossing
13059
+ }) => {
13060
+ const pathFromSharedPin = simplifyPath([
13061
+ ...donorPath.slice(0, crossing.donorSegmentIndex + 1),
13062
+ crossing.intersectionPoint,
13063
+ ...targetPath.slice(crossing.targetSegmentIndex + 1)
13064
+ ]);
13065
+ if (pointsEqual(targetTrace.tracePath[0], targetPath[0])) {
13066
+ return pathFromSharedPin;
13067
+ }
13068
+ return pathFromSharedPin.reverse();
13069
+ };
13070
+ var getNetLabelPlacementsForCycleCollapse = ({
13071
+ targetTrace,
13072
+ tracePath,
13073
+ netLabelPlacements
13074
+ }) => {
13075
+ const candidateNetLabelPlacements = moveAttachedLabelsToReroutedTrace({
13076
+ trace: targetTrace,
13077
+ originalTracePath: targetTrace.tracePath,
13078
+ reroutedTracePath: tracePath,
13079
+ netLabelPlacements
13080
+ });
13081
+ for (let labelIndex = 0; labelIndex < netLabelPlacements.length; labelIndex++) {
13082
+ const label = netLabelPlacements[labelIndex];
13083
+ if (label.globalConnNetId !== targetTrace.globalConnNetId) continue;
13084
+ if (!tracePathContainsPoint(targetTrace.tracePath, label.anchorPoint)) {
13085
+ continue;
13086
+ }
13087
+ const candidateLabel = candidateNetLabelPlacements[labelIndex];
13088
+ if (!tracePathContainsPoint(tracePath, candidateLabel.anchorPoint)) {
13089
+ return null;
13090
+ }
13091
+ }
13092
+ const otherNetLabelPlacements = candidateNetLabelPlacements.filter(
13093
+ (label) => label.globalConnNetId !== targetTrace.globalConnNetId
13094
+ );
13095
+ if (pathIntersectsAnyNetLabel({
13096
+ path: tracePath,
13097
+ netLabelPlacements: otherNetLabelPlacements
13098
+ })) {
13099
+ return null;
13100
+ }
13101
+ const sameNetLabelPlacements = candidateNetLabelPlacements.filter(
13102
+ (label) => label.globalConnNetId === targetTrace.globalConnNetId
13103
+ );
13104
+ if (pathEntersAnyNetLabel({
13105
+ path: tracePath,
13106
+ netLabelPlacements: sameNetLabelPlacements
13107
+ })) {
13108
+ return null;
13109
+ }
13110
+ return candidateNetLabelPlacements;
13111
+ };
13112
+ var candidateIsBetter = (candidate, bestCandidate) => {
13113
+ if (!bestCandidate) return true;
13114
+ const visibleLengthDelta = candidate.netVisibleLength - bestCandidate.netVisibleLength;
13115
+ if (Math.abs(visibleLengthDelta) > TRACE_LENGTH_EPSILON) {
13116
+ return visibleLengthDelta < 0;
13117
+ }
13118
+ return candidate.netVisibleSegmentCount < bestCandidate.netVisibleSegmentCount;
13119
+ };
13120
+ var getBestCycleCollapseCandidate = ({
13121
+ targetTrace,
13122
+ traces,
13123
+ netLabelPlacements
13124
+ }) => {
13125
+ const sameNetTraces = traces.filter(
13126
+ (trace) => trace.globalConnNetId === targetTrace.globalConnNetId
13127
+ );
13128
+ const baselineNetVisibleLength = getVisibleTraceLength(sameNetTraces);
13129
+ const baselineTargetVisibleLength = getVisibleTraceLength([targetTrace]);
13130
+ let bestCandidate = null;
13131
+ for (const donorTrace of sameNetTraces) {
13132
+ if (donorTrace.mspPairId === targetTrace.mspPairId) continue;
13133
+ const sharedPin = getSharedPin({
13134
+ donorTrace,
13135
+ branchTrace: targetTrace
13136
+ });
13137
+ if (!sharedPin) continue;
13138
+ const targetPath = getTracePathStartingAtPin(targetTrace, sharedPin);
13139
+ const donorPath = getTracePathStartingAtPin(donorTrace, sharedPin);
13140
+ if (!targetPath || !donorPath) continue;
13141
+ const crossings = getPerpendicularPathCrossings(targetPath, donorPath);
13142
+ for (const crossing of crossings) {
13143
+ const tracePath = buildCollapsedCyclePath({
13144
+ targetTrace,
13145
+ targetPath,
13146
+ donorPath,
13147
+ crossing
13148
+ });
13149
+ const candidateTrace = { ...targetTrace, tracePath };
13150
+ const candidateTargetVisibleLength = getVisibleTraceLength([
13151
+ candidateTrace
13152
+ ]);
13153
+ if (candidateTargetVisibleLength > baselineTargetVisibleLength + TRACE_LENGTH_EPSILON) {
13154
+ continue;
13155
+ }
13156
+ const candidateNetLabelPlacements = getNetLabelPlacementsForCycleCollapse(
13157
+ {
13158
+ targetTrace,
13159
+ tracePath,
13160
+ netLabelPlacements
13161
+ }
13162
+ );
13163
+ if (!candidateNetLabelPlacements) continue;
13164
+ const candidateNetTraces = sameNetTraces.map((trace) => {
13165
+ if (trace.mspPairId === targetTrace.mspPairId) return candidateTrace;
13166
+ return trace;
13167
+ });
13168
+ const netVisibleLength = getVisibleTraceLength(candidateNetTraces);
13169
+ if (netVisibleLength >= baselineNetVisibleLength - TRACE_LENGTH_EPSILON) {
13170
+ continue;
13171
+ }
13172
+ const netVisibleSegmentCount = getVisibleTraceSegmentCount(candidateNetTraces);
13173
+ const candidate = {
13174
+ tracePath,
13175
+ netLabelPlacements: candidateNetLabelPlacements,
13176
+ netVisibleLength,
13177
+ netVisibleSegmentCount
13178
+ };
13179
+ if (candidateIsBetter(candidate, bestCandidate)) {
13180
+ bestCandidate = candidate;
13181
+ }
13182
+ }
13183
+ }
13184
+ return bestCandidate;
13185
+ };
13186
+ var collapseSameNetCycles = ({
13187
+ traces,
13188
+ netLabelPlacements
13189
+ }) => {
13190
+ const outputTraces = [...traces];
13191
+ let outputNetLabelPlacements = [...netLabelPlacements];
13192
+ let collapsedCycleCount = 0;
13193
+ let traceIndex = 0;
13194
+ while (traceIndex < outputTraces.length) {
13195
+ const targetTrace = outputTraces[traceIndex];
13196
+ const candidate = getBestCycleCollapseCandidate({
13197
+ targetTrace,
13198
+ traces: outputTraces,
13199
+ netLabelPlacements: outputNetLabelPlacements
13200
+ });
13201
+ if (!candidate) {
13202
+ traceIndex++;
13203
+ continue;
13204
+ }
13205
+ outputTraces[traceIndex] = {
13206
+ ...targetTrace,
13207
+ tracePath: candidate.tracePath
13208
+ };
13209
+ outputNetLabelPlacements = candidate.netLabelPlacements;
13210
+ collapsedCycleCount++;
13211
+ traceIndex = 0;
13212
+ }
13213
+ return {
13214
+ traces: outputTraces,
13215
+ netLabelPlacements: outputNetLabelPlacements,
13216
+ collapsedCycleCount
13217
+ };
13218
+ };
13219
+
13012
13220
  // lib/solvers/SameNetJunctionAlignmentSolver/placeGroundRailLabelsAtOuterEnd.ts
13013
13221
  var placeGroundRailLabelsAtOuterEnd = ({
13014
13222
  inputProblem,
@@ -13090,14 +13298,19 @@ var SameNetJunctionAlignmentSolver = class extends BaseSolver {
13090
13298
  this.outputNetLabelPlacements = input.netLabelPlacements;
13091
13299
  }
13092
13300
  _step() {
13093
- const result = alignSameNetJunctions(this.input);
13094
- this.outputTraces = result.traces;
13301
+ const alignment = alignSameNetJunctions(this.input);
13302
+ const cycleCollapse = collapseSameNetCycles({
13303
+ traces: alignment.traces,
13304
+ netLabelPlacements: alignment.netLabelPlacements
13305
+ });
13306
+ this.outputTraces = cycleCollapse.traces;
13095
13307
  this.outputNetLabelPlacements = placeGroundRailLabelsAtOuterEnd({
13096
13308
  inputProblem: this.input.inputProblem,
13097
- traces: result.traces,
13098
- netLabelPlacements: result.netLabelPlacements
13309
+ traces: cycleCollapse.traces,
13310
+ netLabelPlacements: cycleCollapse.netLabelPlacements
13099
13311
  });
13100
- this.stats.alignedJunctionCount = result.alignedJunctionCount;
13312
+ this.stats.alignedJunctionCount = alignment.alignedJunctionCount;
13313
+ this.stats.collapsedCycleCount = cycleCollapse.collapsedCycleCount;
13101
13314
  this.solved = true;
13102
13315
  }
13103
13316
  getOutput() {
@@ -15051,6 +15264,8 @@ var getTraceRecoveryConnectivityMaps = (inputProblem) => {
15051
15264
  // lib/solvers/NetLabelToTraceSolver/NetLabelToTraceSolver.ts
15052
15265
  var AVAILABLE_NET_ORIENTATION_PREFIX = "available-net-orientation-";
15053
15266
  var RECOVERED_TRACE_PREFIX = "net-label-to-trace-";
15267
+ var MAX_NAMED_NET_RECOVERY_PERPENDICULAR_OFFSET = 0.05;
15268
+ var MAX_ROUTED_COMPONENT_RECOVERY_PERPENDICULAR_OFFSET = 0.25;
15054
15269
  var getCanonicalPairKey = (firstPinId, secondPinId) => [firstPinId, secondPinId].sort().join("--");
15055
15270
  var pathIntersectsRenderedLabel = (path, label) => {
15056
15271
  let width = label.width;
@@ -15071,12 +15286,21 @@ var pathIntersectsRenderedLabel = (path, label) => {
15071
15286
  }
15072
15287
  return false;
15073
15288
  };
15074
- var getPerpendicularOffset = (firstPin, secondPin) => {
15075
- const xDistance = Math.abs(firstPin.x - secondPin.x);
15076
- const yDistance = Math.abs(firstPin.y - secondPin.y);
15289
+ var getPerpendicularOffset = (firstPoint, secondPoint) => {
15290
+ const xDistance = Math.abs(firstPoint.x - secondPoint.x);
15291
+ const yDistance = Math.abs(firstPoint.y - secondPoint.y);
15077
15292
  if (xDistance >= yDistance) return yDistance;
15078
15293
  return xDistance;
15079
15294
  };
15295
+ var arePinsCoFacingAlongSeparationAxis = (firstPin, secondPin) => {
15296
+ if (firstPin._facingDirection !== secondPin._facingDirection) return false;
15297
+ const xDistance = Math.abs(firstPin.x - secondPin.x);
15298
+ const yDistance = Math.abs(firstPin.y - secondPin.y);
15299
+ if (xDistance >= yDistance) {
15300
+ return firstPin._facingDirection === "x+" || firstPin._facingDirection === "x-";
15301
+ }
15302
+ return firstPin._facingDirection === "y+" || firstPin._facingDirection === "y-";
15303
+ };
15080
15304
  var NetLabelToTraceSolver = class extends BaseSolver {
15081
15305
  constructor(input) {
15082
15306
  super();
@@ -15104,26 +15328,36 @@ var NetLabelToTraceSolver = class extends BaseSolver {
15104
15328
  getConstructorParams() {
15105
15329
  return [this.input];
15106
15330
  }
15107
- isEligiblePortOnlyDirectConnectionLabel(label, groundGlobalConnNetId) {
15108
- if (label.pinIds.length !== 1 || label.mspConnectionPairIds.length !== 0 || !label.netId || label.netId === "GND" || label.globalConnNetId === groundGlobalConnNetId) {
15109
- return false;
15110
- }
15331
+ isPortOnlyFallbackLabel(label, groundGlobalConnNetIds) {
15332
+ return !(label.pinIds.length !== 1 || label.mspConnectionPairIds.length !== 0 || !label.netId || groundGlobalConnNetIds.has(label.globalConnNetId));
15333
+ }
15334
+ isDirectConnectionLabel(label) {
15335
+ if (!label.netId || label.pinIds.length !== 1) return false;
15111
15336
  const pinId = label.pinIds[0];
15112
15337
  return this.inputProblem.directConnections.some(
15113
15338
  (connection) => connection.netId === label.netId && connection.pinIds.includes(pinId)
15114
15339
  );
15115
15340
  }
15341
+ getMultiPinNetConnection(label) {
15342
+ if (!label.netId || label.pinIds.length !== 1) return void 0;
15343
+ const pinId = label.pinIds[0];
15344
+ return this.inputProblem.netConnections.find(
15345
+ (connection) => connection.isGround === false && connection.pinIds.length > 2 && connection.netId === label.netId && connection.pinIds.includes(pinId)
15346
+ );
15347
+ }
15116
15348
  buildCandidatePairs() {
15117
15349
  const { netConnMap } = getConnectivityMapsFromInputProblem(
15118
15350
  this.inputProblem
15119
15351
  );
15120
- const groundGlobalConnNetId = netConnMap.getNetConnectedToId("GND") ?? void 0;
15352
+ const groundGlobalConnNetIds = /* @__PURE__ */ new Set();
15353
+ for (const connection of this.inputProblem.netConnections) {
15354
+ if (!connection.isGround) continue;
15355
+ const globalConnNetId = netConnMap.getNetConnectedToId(connection.netId);
15356
+ if (globalConnNetId) groundGlobalConnNetIds.add(globalConnNetId);
15357
+ }
15121
15358
  const labelsByGlobalNet = /* @__PURE__ */ new Map();
15122
15359
  for (const label of this.input.netLabelPlacements) {
15123
- if (!this.isEligiblePortOnlyDirectConnectionLabel(
15124
- label,
15125
- groundGlobalConnNetId
15126
- )) {
15360
+ if (!this.isPortOnlyFallbackLabel(label, groundGlobalConnNetIds) || !this.isDirectConnectionLabel(label) && !this.getMultiPinNetConnection(label)) {
15127
15361
  continue;
15128
15362
  }
15129
15363
  const labels = labelsByGlobalNet.get(label.globalConnNetId) ?? [];
@@ -15139,6 +15373,16 @@ var NetLabelToTraceSolver = class extends BaseSolver {
15139
15373
  const firstPin = this.pinMap.get(firstLabel.pinIds[0]);
15140
15374
  const secondPin = this.pinMap.get(secondLabel.pinIds[0]);
15141
15375
  if (!firstPin || !secondPin) continue;
15376
+ const perpendicularOffset = getPerpendicularOffset(
15377
+ firstPin,
15378
+ secondPin
15379
+ );
15380
+ const bothLabelsBelongToDirectConnections = this.isDirectConnectionLabel(firstLabel) && this.isDirectConnectionLabel(secondLabel);
15381
+ const firstMultiPinNetConnection = this.getMultiPinNetConnection(firstLabel);
15382
+ const secondMultiPinNetConnection = this.getMultiPinNetConnection(secondLabel);
15383
+ if (!bothLabelsBelongToDirectConnections && (!firstMultiPinNetConnection || firstMultiPinNetConnection !== secondMultiPinNetConnection || perpendicularOffset > MAX_NAMED_NET_RECOVERY_PERPENDICULAR_OFFSET)) {
15384
+ continue;
15385
+ }
15142
15386
  if (arePinsInDifferentSchematicSections(
15143
15387
  this.inputProblem,
15144
15388
  firstPin,
@@ -15159,18 +15403,103 @@ var NetLabelToTraceSolver = class extends BaseSolver {
15159
15403
  firstLabel,
15160
15404
  secondLabel,
15161
15405
  pins: [firstPin, secondPin],
15162
- perpendicularOffset: getPerpendicularOffset(firstPin, secondPin),
15406
+ perpendicularOffset,
15163
15407
  routeDistance: Math.abs(firstPin.x - secondPin.x) + Math.abs(firstPin.y - secondPin.y),
15164
- key: getCanonicalPairKey(firstPin.pinId, secondPin.pinId)
15408
+ key: getCanonicalPairKey(firstPin.pinId, secondPin.pinId),
15409
+ recoveryMode: "fallback_labels"
15165
15410
  });
15166
15411
  }
15167
15412
  }
15168
15413
  }
15414
+ candidates.push(...this.buildRoutedComponentCandidates());
15169
15415
  candidates.sort(
15170
15416
  (first, second) => first.perpendicularOffset - second.perpendicularOffset || first.routeDistance - second.routeDistance || first.key.localeCompare(second.key)
15171
15417
  );
15172
15418
  return candidates;
15173
15419
  }
15420
+ buildRoutedComponentCandidates() {
15421
+ const { netConnMap } = getConnectivityMapsFromInputProblem(
15422
+ this.inputProblem
15423
+ );
15424
+ const candidates = [];
15425
+ for (const connection of this.inputProblem.netConnections) {
15426
+ if (connection.pinIds.length <= 2 || connection.isGround !== false)
15427
+ continue;
15428
+ const globalConnNetId = netConnMap.getNetConnectedToId(connection.netId);
15429
+ if (!globalConnNetId) continue;
15430
+ const traceConnectedPinComponents = getTraceConnectedPinComponents({
15431
+ pinIds: connection.pinIds,
15432
+ traces: this.outputTraces.filter(
15433
+ (trace) => trace.globalConnNetId === globalConnNetId
15434
+ )
15435
+ }).filter((component) => component.traces.length > 0);
15436
+ for (let firstIndex = 0; firstIndex < traceConnectedPinComponents.length; firstIndex++) {
15437
+ for (let secondIndex = firstIndex + 1; secondIndex < traceConnectedPinComponents.length; secondIndex++) {
15438
+ const firstComponent = traceConnectedPinComponents[firstIndex];
15439
+ const secondComponent = traceConnectedPinComponents[secondIndex];
15440
+ const firstLabel = this.input.netLabelPlacements.find(
15441
+ (label) => label.globalConnNetId === globalConnNetId && label.mspConnectionPairIds.length > 0 && label.pinIds.some(
15442
+ (pinId) => firstComponent.pinIds.includes(pinId)
15443
+ )
15444
+ );
15445
+ const secondLabel = this.input.netLabelPlacements.find(
15446
+ (label) => label.globalConnNetId === globalConnNetId && label.mspConnectionPairIds.length > 0 && label.pinIds.some(
15447
+ (pinId) => secondComponent.pinIds.includes(pinId)
15448
+ )
15449
+ );
15450
+ if (!firstLabel || !secondLabel || firstLabel === secondLabel)
15451
+ continue;
15452
+ if (getPerpendicularOffset(
15453
+ firstLabel.anchorPoint,
15454
+ secondLabel.anchorPoint
15455
+ ) > MAX_ROUTED_COMPONENT_RECOVERY_PERPENDICULAR_OFFSET) {
15456
+ continue;
15457
+ }
15458
+ let bestCandidate;
15459
+ for (const firstPinId of firstComponent.pinIds) {
15460
+ for (const secondPinId of secondComponent.pinIds) {
15461
+ const firstPin = this.pinMap.get(firstPinId);
15462
+ const secondPin = this.pinMap.get(secondPinId);
15463
+ if (!firstPin || !secondPin) continue;
15464
+ const perpendicularOffset = getPerpendicularOffset(
15465
+ firstPin,
15466
+ secondPin
15467
+ );
15468
+ if (perpendicularOffset > MAX_ROUTED_COMPONENT_RECOVERY_PERPENDICULAR_OFFSET || arePinsCoFacingAlongSeparationAxis(firstPin, secondPin) || arePinsInDifferentSchematicSections(
15469
+ this.inputProblem,
15470
+ firstPin,
15471
+ secondPin
15472
+ ) || doesPairCrossRestrictedCenterLines({
15473
+ inputProblem: this.inputProblem,
15474
+ chipMap: this.chipMap,
15475
+ pinIdMap: this.pinMap,
15476
+ p1: firstPin,
15477
+ p2: secondPin
15478
+ })) {
15479
+ continue;
15480
+ }
15481
+ const routeDistance = Math.abs(firstPin.x - secondPin.x) + Math.abs(firstPin.y - secondPin.y);
15482
+ const candidate = {
15483
+ firstLabel,
15484
+ secondLabel,
15485
+ pins: [firstPin, secondPin],
15486
+ perpendicularOffset,
15487
+ routeDistance,
15488
+ key: getCanonicalPairKey(firstPin.pinId, secondPin.pinId),
15489
+ recoveryMode: "routed_components",
15490
+ netConnectionPinIds: connection.pinIds
15491
+ };
15492
+ if (!bestCandidate || candidate.routeDistance < bestCandidate.routeDistance || candidate.routeDistance === bestCandidate.routeDistance && (candidate.perpendicularOffset < bestCandidate.perpendicularOffset || candidate.perpendicularOffset === bestCandidate.perpendicularOffset && candidate.key.localeCompare(bestCandidate.key) < 0)) {
15493
+ bestCandidate = candidate;
15494
+ }
15495
+ }
15496
+ }
15497
+ if (bestCandidate) candidates.push(bestCandidate);
15498
+ }
15499
+ }
15500
+ }
15501
+ return candidates;
15502
+ }
15174
15503
  isSupersededConnectorTrace(trace, candidate) {
15175
15504
  if (!trace.mspPairId.startsWith(AVAILABLE_NET_ORIENTATION_PREFIX)) {
15176
15505
  return false;
@@ -15196,7 +15525,13 @@ var NetLabelToTraceSolver = class extends BaseSolver {
15196
15525
  const retainedTraces = this.outputTraces.filter(
15197
15526
  (trace) => !this.isSupersededConnectorTrace(trace, candidate)
15198
15527
  );
15199
- if (doesTraceOverlapWithExistingTraces(tracePath, retainedTraces) || this.routeIntersectsRemainingLabels(tracePath, candidate)) {
15528
+ let collisionTraces = retainedTraces;
15529
+ if (candidate.recoveryMode === "routed_components") {
15530
+ collisionTraces = retainedTraces.filter(
15531
+ (trace) => trace.globalConnNetId !== candidate.firstLabel.globalConnNetId
15532
+ );
15533
+ }
15534
+ if (doesTraceOverlapWithExistingTraces(tracePath, collisionTraces) || this.routeIntersectsRemainingLabels(tracePath, candidate)) {
15200
15535
  return;
15201
15536
  }
15202
15537
  const [firstPin, secondPin] = candidate.pins;
@@ -15211,11 +15546,22 @@ var NetLabelToTraceSolver = class extends BaseSolver {
15211
15546
  pinIds: [firstPin.pinId, secondPin.pinId]
15212
15547
  };
15213
15548
  this.outputTraces = [...retainedTraces, recoveredTrace];
15214
- this.outputNetLabelPlacements = this.outputNetLabelPlacements.filter(
15215
- (label) => label !== candidate.firstLabel && label !== candidate.secondLabel
15216
- );
15549
+ if (candidate.recoveryMode === "fallback_labels") {
15550
+ this.outputNetLabelPlacements = this.outputNetLabelPlacements.filter(
15551
+ (label) => label !== candidate.firstLabel && label !== candidate.secondLabel
15552
+ );
15553
+ }
15217
15554
  this.stats.recoveredTraceCount++;
15218
15555
  }
15556
+ areCandidatePinsAlreadyConnected(candidate) {
15557
+ if (!candidate.netConnectionPinIds) return false;
15558
+ return getTraceConnectedPinComponents({
15559
+ pinIds: candidate.netConnectionPinIds,
15560
+ traces: this.outputTraces
15561
+ }).some(
15562
+ (component) => component.pinIds.includes(candidate.pins[0].pinId) && component.pinIds.includes(candidate.pins[1].pinId)
15563
+ );
15564
+ }
15219
15565
  _step() {
15220
15566
  if (this.activeSubSolver) {
15221
15567
  this.activeSubSolver.step();
@@ -15230,7 +15576,7 @@ var NetLabelToTraceSolver = class extends BaseSolver {
15230
15576
  return;
15231
15577
  }
15232
15578
  let candidate = this.queuedCandidates.shift();
15233
- while (candidate && (!this.outputNetLabelPlacements.includes(candidate.firstLabel) || !this.outputNetLabelPlacements.includes(candidate.secondLabel))) {
15579
+ while (candidate && (!this.outputNetLabelPlacements.includes(candidate.firstLabel) || !this.outputNetLabelPlacements.includes(candidate.secondLabel) || this.areCandidatePinsAlreadyConnected(candidate))) {
15234
15580
  candidate = this.queuedCandidates.shift();
15235
15581
  }
15236
15582
  if (!candidate) {