@tscircuit/schematic-trace-solver 0.0.117 → 0.0.119

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
@@ -721,12 +721,13 @@ interface AvailableNetOrientationSolverParams {
721
721
  type CandidateLabel = {
722
722
  orientation: FacingDirection;
723
723
  anchorPoint: Point;
724
+ connectorSource?: Point;
724
725
  center: Point;
725
726
  width: number;
726
727
  height: number;
727
728
  };
728
729
  type CandidateStatus$2 = "valid" | "chip-collision" | "text-collision" | "trace-collision" | "trace-clearance-violation" | "netlabel-collision";
729
- type CandidatePhase = "rotate" | "trace-anchor" | "shift" | "lateral-shift";
730
+ type CandidatePhase = "rotate" | "trace-anchor" | "outward-trace-anchor" | "shift" | "lateral-shift";
730
731
  type EvaluatedCandidate = CandidateLabel & {
731
732
  status: CandidateStatus$2;
732
733
  selected: boolean;
@@ -744,6 +745,8 @@ declare class AvailableNetOrientationSolver extends BaseSolver {
744
745
  currentLabelIndex: number | null;
745
746
  currentLabel: NetLabelPlacement | null;
746
747
  currentCandidateResults: EvaluatedCandidate[];
748
+ blockedStandaloneLabelIndex: number | null;
749
+ blockedStandaloneLabelTargetAnchorX: number | null;
747
750
  private traceMap;
748
751
  private chipObstacleSpatialIndex;
749
752
  private maxSearchDistance;
@@ -766,6 +769,7 @@ declare class AvailableNetOrientationSolver extends BaseSolver {
766
769
  private hasTraceContinuingInOrientation;
767
770
  private findValidRotatedCandidate;
768
771
  private findValidTraceAnchorCandidate;
772
+ private findValidOutwardTraceAnchorCandidate;
769
773
  private getTraceAnchorCandidatePoints;
770
774
  private getAvailableOrientations;
771
775
  private findValidShiftedCandidate;
@@ -799,6 +803,10 @@ declare class AvailableNetOrientationSolver extends BaseSolver {
799
803
  private getLabelTraceClearanceBounds;
800
804
  private shouldCheckTraceClearanceForLabel;
801
805
  private hasRoutedLabelOnSameNet;
806
+ private isOutwardHorizontalFallback;
807
+ private hasPortOnlyLabelOnSameNet;
808
+ private hasOppositeVerticalRailOnSameChipSide;
809
+ private getSingleChipSideForNetConnection;
802
810
  private isPortOnlyLabel;
803
811
  private intersectsAnyOtherNetLabel;
804
812
  private sharesChipBoundary;
package/dist/index.js CHANGED
@@ -7848,6 +7848,8 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
7848
7848
  currentLabelIndex = null;
7849
7849
  currentLabel = null;
7850
7850
  currentCandidateResults = [];
7851
+ blockedStandaloneLabelIndex = null;
7852
+ blockedStandaloneLabelTargetAnchorX = null;
7851
7853
  traceMap;
7852
7854
  chipObstacleSpatialIndex;
7853
7855
  maxSearchDistance;
@@ -7901,6 +7903,8 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
7901
7903
  return this.isPortOnlyLabel(label) && this.isStandaloneSinglePinNetLabel(label) && orientations.length === 1 && isYOrientation(orientations[0]) && this.labelIntersectsDifferentNetTrace(label);
7902
7904
  });
7903
7905
  if (blockedStandaloneLabelIndex === void 0) return indices;
7906
+ this.blockedStandaloneLabelIndex = blockedStandaloneLabelIndex;
7907
+ this.blockedStandaloneLabelTargetAnchorX = null;
7904
7908
  const blockedLabel = this.outputNetLabelPlacements[blockedStandaloneLabelIndex];
7905
7909
  const requiredOrientation = this.getAvailableOrientations(blockedLabel)[0];
7906
7910
  const chipSide = this.getChipSideForPoint(blockedLabel.anchorPoint);
@@ -7920,6 +7924,22 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
7920
7924
  const bY = this.outputNetLabelPlacements[b].anchorPoint.y;
7921
7925
  return requiredOrientation === "y+" ? bY - aY : aY - bY;
7922
7926
  });
7927
+ const blockedLabelWidth = this.getNetLabelWidth(blockedLabel);
7928
+ let columnLabelIndex;
7929
+ if (blockedLabelWidth !== void 0) {
7930
+ columnLabelIndex = labelsToOrder.find((index) => {
7931
+ if (index === blockedStandaloneLabelIndex) return false;
7932
+ const label = this.outputNetLabelPlacements[index];
7933
+ return this.getNetLabelWidth(label) === blockedLabelWidth;
7934
+ });
7935
+ }
7936
+ if (columnLabelIndex !== void 0) {
7937
+ const columnLabel = this.outputNetLabelPlacements[columnLabelIndex];
7938
+ this.blockedStandaloneLabelTargetAnchorX = this.getSearchStartAnchor(
7939
+ columnLabel,
7940
+ requiredOrientation
7941
+ ).x;
7942
+ }
7923
7943
  for (let i = 0; i < affectedSlots.length; i++) {
7924
7944
  indices[affectedSlots[i]] = labelsToOrder[i];
7925
7945
  }
@@ -7975,9 +7995,20 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
7975
7995
  findCorrectedCandidate(label, labelIndex) {
7976
7996
  const orientations = this.getAvailableOrientations(label);
7977
7997
  const requiredOrientation = orientations[0];
7978
- const isTwoPinNet = this.inputProblem.netConnections.some(
7979
- (connection) => connection.netId === label.netId && connection.pinIds.length === 2
7998
+ const netConnection = this.inputProblem.netConnections.find(
7999
+ (connection) => connection.netId === label.netId
7980
8000
  );
8001
+ const isTwoPinNet = netConnection?.pinIds.length === 2;
8002
+ const isDistanceSplitUpwardRail = (netConnection?.pinIds.length ?? 0) > 2 && requiredOrientation === "y+" && this.hasPortOnlyLabelOnSameNet(label);
8003
+ const isPairedSameSidePowerRail = isYOrientation(requiredOrientation) && this.hasOppositeVerticalRailOnSameChipSide(label, requiredOrientation);
8004
+ if (orientations.length === 1 && isYOrientation(requiredOrientation) && this.isOutwardHorizontalFallback(label) && this.hasTraceContinuingInOrientation(label, requiredOrientation) && (isDistanceSplitUpwardRail || isPairedSameSidePowerRail)) {
8005
+ const traceAnchorCandidate2 = this.findValidOutwardTraceAnchorCandidate(
8006
+ label,
8007
+ requiredOrientation,
8008
+ labelIndex
8009
+ );
8010
+ if (traceAnchorCandidate2) return traceAnchorCandidate2;
8011
+ }
7981
8012
  if (isTwoPinNet && orientations.length === 1 && isYOrientation(requiredOrientation) && this.hasTraceContinuingInOrientation(label, requiredOrientation)) {
7982
8013
  const alignedCandidate = this.findValidCandidateInShiftColumn({
7983
8014
  label,
@@ -8079,6 +8110,72 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
8079
8110
  }
8080
8111
  return null;
8081
8112
  }
8113
+ findValidOutwardTraceAnchorCandidate(label, orientation, labelIndex) {
8114
+ const direction = dir(orientation);
8115
+ const candidatePoints = this.getTraceAnchorCandidatePoints(label).sort(
8116
+ (a, b) => {
8117
+ const aAlongDirection = a.x * direction.x + a.y * direction.y;
8118
+ const bAlongDirection = b.x * direction.x + b.y * direction.y;
8119
+ return bAlongDirection - aAlongDirection;
8120
+ }
8121
+ );
8122
+ for (const connectorSource of candidatePoints) {
8123
+ const preservedColumnAnchor = this.getSearchStartAnchor(
8124
+ label,
8125
+ orientation
8126
+ );
8127
+ const preservedColumnCandidate = this.createCandidate(
8128
+ label,
8129
+ {
8130
+ x: preservedColumnAnchor.x,
8131
+ y: connectorSource.y
8132
+ },
8133
+ orientation,
8134
+ connectorSource
8135
+ );
8136
+ const preservedColumnResult = this.evaluateCandidate(
8137
+ preservedColumnCandidate,
8138
+ label,
8139
+ labelIndex,
8140
+ "outward-trace-anchor"
8141
+ );
8142
+ this.currentCandidateResults.push(preservedColumnResult);
8143
+ if (preservedColumnResult.status === "valid") {
8144
+ preservedColumnResult.selected = true;
8145
+ return preservedColumnResult;
8146
+ }
8147
+ const outwardDirection = this.getPerpendicularOutwardDirection(
8148
+ connectorSource,
8149
+ orientation
8150
+ );
8151
+ if (outwardDirection.x === 0 && outwardDirection.y === 0) continue;
8152
+ for (let distance5 = LABEL_SEARCH_STEP; distance5 <= this.maxSearchDistance + EPS11; distance5 += LABEL_SEARCH_STEP) {
8153
+ const anchorPoint = {
8154
+ x: connectorSource.x + outwardDirection.x * distance5,
8155
+ y: connectorSource.y + outwardDirection.y * distance5
8156
+ };
8157
+ const candidate = this.createCandidate(
8158
+ label,
8159
+ anchorPoint,
8160
+ orientation,
8161
+ connectorSource
8162
+ );
8163
+ const result = this.evaluateCandidate(
8164
+ candidate,
8165
+ label,
8166
+ labelIndex,
8167
+ "outward-trace-anchor",
8168
+ distance5
8169
+ );
8170
+ this.currentCandidateResults.push(result);
8171
+ if (result.status === "valid") {
8172
+ result.selected = true;
8173
+ return result;
8174
+ }
8175
+ }
8176
+ }
8177
+ return null;
8178
+ }
8082
8179
  getTraceAnchorCandidatePoints(label) {
8083
8180
  const seen = /* @__PURE__ */ new Set();
8084
8181
  const points = [];
@@ -8236,7 +8333,7 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
8236
8333
  ]);
8237
8334
  }
8238
8335
  return getConnectorTracePath(
8239
- label.anchorPoint,
8336
+ candidate.connectorSource ?? label.anchorPoint,
8240
8337
  candidate.anchorPoint,
8241
8338
  candidate.orientation
8242
8339
  );
@@ -8270,13 +8367,17 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
8270
8367
  netLabelWidth: this.getNetLabelWidth(label),
8271
8368
  netLabelHeight: this.getNetLabelHeight(label)
8272
8369
  });
8273
- return this.getSideOffsetAnchor({
8370
+ const searchStartAnchor = this.getSideOffsetAnchor({
8274
8371
  anchorPoint,
8275
8372
  labelAnchorPoint: label.anchorPoint,
8276
8373
  orientation,
8277
8374
  width,
8278
8375
  height
8279
8376
  });
8377
+ if (this.blockedStandaloneLabelIndex !== null && label === this.outputNetLabelPlacements[this.blockedStandaloneLabelIndex] && isYOrientation(orientation) && this.blockedStandaloneLabelTargetAnchorX !== null) {
8378
+ searchStartAnchor.x = this.blockedStandaloneLabelTargetAnchorX;
8379
+ }
8380
+ return searchStartAnchor;
8280
8381
  }
8281
8382
  getWickOffsetAnchor(anchorPoint, orientation) {
8282
8383
  const direction = dir(orientation);
@@ -8366,7 +8467,7 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
8366
8467
  }
8367
8468
  return this.getSearchDistanceLimit(label, orientation);
8368
8469
  }
8369
- createCandidate(label, anchorPoint, orientation) {
8470
+ createCandidate(label, anchorPoint, orientation, connectorSource) {
8370
8471
  const { width, height } = getDimsForOrientation({
8371
8472
  orientation,
8372
8473
  netLabelWidth: this.getNetLabelWidth(label),
@@ -8375,6 +8476,7 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
8375
8476
  return {
8376
8477
  orientation,
8377
8478
  anchorPoint,
8479
+ connectorSource,
8378
8480
  width,
8379
8481
  height,
8380
8482
  center: getCenterFromAnchor(anchorPoint, orientation, width, height)
@@ -8528,6 +8630,59 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
8528
8630
  }
8529
8631
  return false;
8530
8632
  }
8633
+ isOutwardHorizontalFallback(label) {
8634
+ const chipSide = this.getChipSideForPoint(label.anchorPoint);
8635
+ return chipSide === "left" && label.orientation === "x-" || chipSide === "right" && label.orientation === "x+";
8636
+ }
8637
+ hasPortOnlyLabelOnSameNet(label) {
8638
+ return this.outputNetLabelPlacements.some(
8639
+ (otherLabel) => otherLabel !== label && otherLabel.globalConnNetId === label.globalConnNetId && this.isPortOnlyLabel(otherLabel)
8640
+ );
8641
+ }
8642
+ hasOppositeVerticalRailOnSameChipSide(label, orientation) {
8643
+ const currentConnection = this.inputProblem.netConnections.find(
8644
+ (connection) => connection.netId === label.netId
8645
+ );
8646
+ if (!currentConnection) return false;
8647
+ const currentSide = this.getSingleChipSideForNetConnection(currentConnection);
8648
+ if (!currentSide) return false;
8649
+ const oppositeOrientation = orientation === "y+" ? "y-" : "y+";
8650
+ return this.inputProblem.netConnections.some((connection) => {
8651
+ if (connection === currentConnection || connection.pinIds.length < 2) {
8652
+ return false;
8653
+ }
8654
+ const availableOrientations = this.inputProblem.availableNetLabelOrientations[connection.netId] ?? [];
8655
+ if (availableOrientations.length !== 1 || availableOrientations[0] !== oppositeOrientation) {
8656
+ return false;
8657
+ }
8658
+ const connectionSide = this.getSingleChipSideForNetConnection(connection);
8659
+ return connectionSide?.chipId === currentSide.chipId && connectionSide.side === currentSide.side;
8660
+ });
8661
+ }
8662
+ getSingleChipSideForNetConnection(connection) {
8663
+ if (connection.pinIds.length < 2) return null;
8664
+ const pins = connection.pinIds.map((pinId) => this.pinMap[pinId]);
8665
+ if (pins.some((pin) => !pin)) return null;
8666
+ const chipIds = new Set(pins.map((pin) => pin.chipId));
8667
+ if (chipIds.size !== 1) return null;
8668
+ const chipId = pins[0].chipId;
8669
+ const chip = this.chipObstacleSpatialIndex.chips.find(
8670
+ (candidate) => candidate.chipId === chipId
8671
+ );
8672
+ if (!chip) return null;
8673
+ const sides = new Set(
8674
+ pins.map((pin) => {
8675
+ if (Math.abs(pin.x - chip.bounds.minX) <= EPS11) return "left";
8676
+ if (Math.abs(pin.x - chip.bounds.maxX) <= EPS11) return "right";
8677
+ return null;
8678
+ })
8679
+ );
8680
+ if (sides.size !== 1 || sides.has(null)) return null;
8681
+ return {
8682
+ chipId,
8683
+ side: [...sides][0]
8684
+ };
8685
+ }
8531
8686
  isPortOnlyLabel(label) {
8532
8687
  return label.mspConnectionPairIds.length === 0;
8533
8688
  }
@@ -8536,6 +8691,10 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
8536
8691
  if (i === labelIndex) continue;
8537
8692
  const label = this.outputNetLabelPlacements[i];
8538
8693
  const otherBounds = getRectBounds(label.center, label.width, label.height);
8694
+ if (i === this.blockedStandaloneLabelIndex && isYOrientation(label.orientation)) {
8695
+ otherBounds.minX -= LABEL_SEARCH_STEP;
8696
+ otherBounds.maxX += LABEL_SEARCH_STEP;
8697
+ }
8539
8698
  if (rectsOverlap2(bounds, otherBounds)) return true;
8540
8699
  }
8541
8700
  return false;
@@ -9,7 +9,11 @@ import {
9
9
  getRectBounds,
10
10
  } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
11
11
  import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
12
- import type { InputPin, InputProblem } from "lib/types/InputProblem"
12
+ import type {
13
+ InputNetConnection,
14
+ InputPin,
15
+ InputProblem,
16
+ } from "lib/types/InputProblem"
13
17
  import { dir, type FacingDirection } from "lib/utils/dir"
14
18
  import { rectIntersectsAnyTextBox } from "lib/utils/textBoxBounds"
15
19
  import { EPS, LABEL_SEARCH_STEP, WICK_CLEARANCE } from "./constants"
@@ -51,6 +55,8 @@ export class AvailableNetOrientationSolver extends BaseSolver {
51
55
  currentLabel: NetLabelPlacement | null = null
52
56
  currentCandidateResults: EvaluatedCandidate[] = []
53
57
 
58
+ blockedStandaloneLabelIndex: number | null = null
59
+ blockedStandaloneLabelTargetAnchorX: number | null = null
54
60
  private traceMap: Record<string, SolvedTracePath>
55
61
  private chipObstacleSpatialIndex: ChipObstacleSpatialIndex
56
62
  private maxSearchDistance: number
@@ -122,6 +128,8 @@ export class AvailableNetOrientationSolver extends BaseSolver {
122
128
  )
123
129
  })
124
130
  if (blockedStandaloneLabelIndex === undefined) return indices
131
+ this.blockedStandaloneLabelIndex = blockedStandaloneLabelIndex
132
+ this.blockedStandaloneLabelTargetAnchorX = null
125
133
 
126
134
  const blockedLabel =
127
135
  this.outputNetLabelPlacements[blockedStandaloneLabelIndex]!
@@ -152,6 +160,23 @@ export class AvailableNetOrientationSolver extends BaseSolver {
152
160
  const bY = this.outputNetLabelPlacements[b]!.anchorPoint.y
153
161
  return requiredOrientation === "y+" ? bY - aY : aY - bY
154
162
  })
163
+ const blockedLabelWidth = this.getNetLabelWidth(blockedLabel)
164
+ let columnLabelIndex: number | undefined
165
+ if (blockedLabelWidth !== undefined) {
166
+ columnLabelIndex = labelsToOrder.find((index) => {
167
+ if (index === blockedStandaloneLabelIndex) return false
168
+ const label = this.outputNetLabelPlacements[index]!
169
+ return this.getNetLabelWidth(label) === blockedLabelWidth
170
+ })
171
+ }
172
+ if (columnLabelIndex !== undefined) {
173
+ const columnLabel = this.outputNetLabelPlacements[columnLabelIndex]!
174
+ // Reuse the established same-width column instead of a nearby search step.
175
+ this.blockedStandaloneLabelTargetAnchorX = this.getSearchStartAnchor(
176
+ columnLabel,
177
+ requiredOrientation,
178
+ ).x
179
+ }
155
180
  for (let i = 0; i < affectedSlots.length; i++) {
156
181
  indices[affectedSlots[i]!] = labelsToOrder[i]!
157
182
  }
@@ -229,10 +254,35 @@ export class AvailableNetOrientationSolver extends BaseSolver {
229
254
  private findCorrectedCandidate(label: NetLabelPlacement, labelIndex: number) {
230
255
  const orientations = this.getAvailableOrientations(label)
231
256
  const requiredOrientation = orientations[0]!
232
- const isTwoPinNet = this.inputProblem.netConnections.some(
233
- (connection) =>
234
- connection.netId === label.netId && connection.pinIds.length === 2,
257
+ const netConnection = this.inputProblem.netConnections.find(
258
+ (connection) => connection.netId === label.netId,
235
259
  )
260
+ const isTwoPinNet = netConnection?.pinIds.length === 2
261
+ const isDistanceSplitUpwardRail =
262
+ (netConnection?.pinIds.length ?? 0) > 2 &&
263
+ requiredOrientation === "y+" &&
264
+ this.hasPortOnlyLabelOnSameNet(label)
265
+ const isPairedSameSidePowerRail =
266
+ isYOrientation(requiredOrientation) &&
267
+ this.hasOppositeVerticalRailOnSameChipSide(label, requiredOrientation)
268
+ if (
269
+ orientations.length === 1 &&
270
+ isYOrientation(requiredOrientation) &&
271
+ this.isOutwardHorizontalFallback(label) &&
272
+ this.hasTraceContinuingInOrientation(label, requiredOrientation) &&
273
+ (isDistanceSplitUpwardRail || isPairedSameSidePowerRail)
274
+ ) {
275
+ // Keep the established outward column, but attach at the furthest trace
276
+ // point in the required vertical direction. This places y+ labels above
277
+ // a rail and y- labels below it while using a short horizontal connector.
278
+ const traceAnchorCandidate = this.findValidOutwardTraceAnchorCandidate(
279
+ label,
280
+ requiredOrientation,
281
+ labelIndex,
282
+ )
283
+ if (traceAnchorCandidate) return traceAnchorCandidate
284
+ }
285
+
236
286
  if (
237
287
  isTwoPinNet &&
238
288
  orientations.length === 1 &&
@@ -369,6 +419,86 @@ export class AvailableNetOrientationSolver extends BaseSolver {
369
419
  return null
370
420
  }
371
421
 
422
+ private findValidOutwardTraceAnchorCandidate(
423
+ label: NetLabelPlacement,
424
+ orientation: FacingDirection,
425
+ labelIndex: number,
426
+ ) {
427
+ const direction = dir(orientation)
428
+ const candidatePoints = this.getTraceAnchorCandidatePoints(label).sort(
429
+ (a, b) => {
430
+ const aAlongDirection = a.x * direction.x + a.y * direction.y
431
+ const bAlongDirection = b.x * direction.x + b.y * direction.y
432
+ return bAlongDirection - aAlongDirection
433
+ },
434
+ )
435
+
436
+ for (const connectorSource of candidatePoints) {
437
+ const preservedColumnAnchor = this.getSearchStartAnchor(
438
+ label,
439
+ orientation,
440
+ )
441
+ const preservedColumnCandidate = this.createCandidate(
442
+ label,
443
+ {
444
+ x: preservedColumnAnchor.x,
445
+ y: connectorSource.y,
446
+ },
447
+ orientation,
448
+ connectorSource,
449
+ )
450
+ const preservedColumnResult = this.evaluateCandidate(
451
+ preservedColumnCandidate,
452
+ label,
453
+ labelIndex,
454
+ "outward-trace-anchor",
455
+ )
456
+ this.currentCandidateResults.push(preservedColumnResult)
457
+ if (preservedColumnResult.status === "valid") {
458
+ preservedColumnResult.selected = true
459
+ return preservedColumnResult
460
+ }
461
+
462
+ const outwardDirection = this.getPerpendicularOutwardDirection(
463
+ connectorSource,
464
+ orientation,
465
+ )
466
+ if (outwardDirection.x === 0 && outwardDirection.y === 0) continue
467
+
468
+ for (
469
+ let distance = LABEL_SEARCH_STEP;
470
+ distance <= this.maxSearchDistance + EPS;
471
+ distance += LABEL_SEARCH_STEP
472
+ ) {
473
+ const anchorPoint = {
474
+ x: connectorSource.x + outwardDirection.x * distance,
475
+ y: connectorSource.y + outwardDirection.y * distance,
476
+ }
477
+ const candidate = this.createCandidate(
478
+ label,
479
+ anchorPoint,
480
+ orientation,
481
+ connectorSource,
482
+ )
483
+ const result = this.evaluateCandidate(
484
+ candidate,
485
+ label,
486
+ labelIndex,
487
+ "outward-trace-anchor",
488
+ distance,
489
+ )
490
+ this.currentCandidateResults.push(result)
491
+
492
+ if (result.status === "valid") {
493
+ result.selected = true
494
+ return result
495
+ }
496
+ }
497
+ }
498
+
499
+ return null
500
+ }
501
+
372
502
  private getTraceAnchorCandidatePoints(label: NetLabelPlacement) {
373
503
  const seen = new Set<string>()
374
504
  const points: Point[] = []
@@ -571,7 +701,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
571
701
  label: NetLabelPlacement,
572
702
  candidate: Pick<
573
703
  EvaluatedCandidate,
574
- "anchorPoint" | "orientation" | "phase"
704
+ "anchorPoint" | "connectorSource" | "orientation" | "phase"
575
705
  >,
576
706
  ) {
577
707
  if (candidate.phase === "lateral-shift") {
@@ -591,7 +721,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
591
721
  }
592
722
 
593
723
  return getConnectorTracePath(
594
- label.anchorPoint,
724
+ candidate.connectorSource ?? label.anchorPoint,
595
725
  candidate.anchorPoint,
596
726
  candidate.orientation,
597
727
  )
@@ -637,13 +767,23 @@ export class AvailableNetOrientationSolver extends BaseSolver {
637
767
  netLabelHeight: this.getNetLabelHeight(label),
638
768
  })
639
769
 
640
- return this.getSideOffsetAnchor({
770
+ const searchStartAnchor = this.getSideOffsetAnchor({
641
771
  anchorPoint,
642
772
  labelAnchorPoint: label.anchorPoint,
643
773
  orientation,
644
774
  width,
645
775
  height,
646
776
  })
777
+ if (
778
+ this.blockedStandaloneLabelIndex !== null &&
779
+ label ===
780
+ this.outputNetLabelPlacements[this.blockedStandaloneLabelIndex] &&
781
+ isYOrientation(orientation) &&
782
+ this.blockedStandaloneLabelTargetAnchorX !== null
783
+ ) {
784
+ searchStartAnchor.x = this.blockedStandaloneLabelTargetAnchorX
785
+ }
786
+ return searchStartAnchor
647
787
  }
648
788
 
649
789
  private getWickOffsetAnchor(
@@ -779,6 +919,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
779
919
  label: NetLabelPlacement,
780
920
  anchorPoint: Point,
781
921
  orientation: FacingDirection,
922
+ connectorSource?: Point,
782
923
  ): CandidateLabel {
783
924
  const { width, height } = getDimsForOrientation({
784
925
  orientation,
@@ -788,6 +929,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
788
929
  return {
789
930
  orientation,
790
931
  anchorPoint,
932
+ connectorSource,
791
933
  width,
792
934
  height,
793
935
  center: getCenterFromAnchor(anchorPoint, orientation, width, height),
@@ -998,6 +1140,89 @@ export class AvailableNetOrientationSolver extends BaseSolver {
998
1140
  return false
999
1141
  }
1000
1142
 
1143
+ private isOutwardHorizontalFallback(label: NetLabelPlacement) {
1144
+ const chipSide = this.getChipSideForPoint(label.anchorPoint)
1145
+ return (
1146
+ (chipSide === "left" && label.orientation === "x-") ||
1147
+ (chipSide === "right" && label.orientation === "x+")
1148
+ )
1149
+ }
1150
+
1151
+ private hasPortOnlyLabelOnSameNet(label: NetLabelPlacement) {
1152
+ return this.outputNetLabelPlacements.some(
1153
+ (otherLabel) =>
1154
+ otherLabel !== label &&
1155
+ otherLabel.globalConnNetId === label.globalConnNetId &&
1156
+ this.isPortOnlyLabel(otherLabel),
1157
+ )
1158
+ }
1159
+
1160
+ private hasOppositeVerticalRailOnSameChipSide(
1161
+ label: NetLabelPlacement,
1162
+ orientation: "y+" | "y-",
1163
+ ) {
1164
+ const currentConnection = this.inputProblem.netConnections.find(
1165
+ (connection) => connection.netId === label.netId,
1166
+ )
1167
+ if (!currentConnection) return false
1168
+
1169
+ const currentSide =
1170
+ this.getSingleChipSideForNetConnection(currentConnection)
1171
+ if (!currentSide) return false
1172
+
1173
+ const oppositeOrientation = orientation === "y+" ? "y-" : "y+"
1174
+ return this.inputProblem.netConnections.some((connection) => {
1175
+ if (connection === currentConnection || connection.pinIds.length < 2) {
1176
+ return false
1177
+ }
1178
+ const availableOrientations =
1179
+ this.inputProblem.availableNetLabelOrientations[connection.netId] ?? []
1180
+ if (
1181
+ availableOrientations.length !== 1 ||
1182
+ availableOrientations[0] !== oppositeOrientation
1183
+ ) {
1184
+ return false
1185
+ }
1186
+
1187
+ const connectionSide = this.getSingleChipSideForNetConnection(connection)
1188
+ return (
1189
+ connectionSide?.chipId === currentSide.chipId &&
1190
+ connectionSide.side === currentSide.side
1191
+ )
1192
+ })
1193
+ }
1194
+
1195
+ private getSingleChipSideForNetConnection(
1196
+ connection: InputNetConnection,
1197
+ ): { chipId: string; side: "left" | "right" } | null {
1198
+ if (connection.pinIds.length < 2) return null
1199
+
1200
+ const pins = connection.pinIds.map((pinId) => this.pinMap[pinId])
1201
+ if (pins.some((pin) => !pin)) return null
1202
+ const chipIds = new Set(pins.map((pin) => pin!.chipId))
1203
+ if (chipIds.size !== 1) return null
1204
+
1205
+ const chipId = pins[0]!.chipId
1206
+ const chip = this.chipObstacleSpatialIndex.chips.find(
1207
+ (candidate) => candidate.chipId === chipId,
1208
+ )
1209
+ if (!chip) return null
1210
+
1211
+ const sides = new Set(
1212
+ pins.map((pin) => {
1213
+ if (Math.abs(pin!.x - chip.bounds.minX) <= EPS) return "left"
1214
+ if (Math.abs(pin!.x - chip.bounds.maxX) <= EPS) return "right"
1215
+ return null
1216
+ }),
1217
+ )
1218
+ if (sides.size !== 1 || sides.has(null)) return null
1219
+
1220
+ return {
1221
+ chipId,
1222
+ side: [...sides][0] as "left" | "right",
1223
+ }
1224
+ }
1225
+
1001
1226
  private isPortOnlyLabel(label: NetLabelPlacement) {
1002
1227
  return label.mspConnectionPairIds.length === 0
1003
1228
  }
@@ -1007,6 +1232,14 @@ export class AvailableNetOrientationSolver extends BaseSolver {
1007
1232
  if (i === labelIndex) continue
1008
1233
  const label = this.outputNetLabelPlacements[i]!
1009
1234
  const otherBounds = getRectBounds(label.center, label.width, label.height)
1235
+ if (
1236
+ i === this.blockedStandaloneLabelIndex &&
1237
+ isYOrientation(label.orientation)
1238
+ ) {
1239
+ // Keep later labels one search step away from the blocked label.
1240
+ otherBounds.minX -= LABEL_SEARCH_STEP
1241
+ otherBounds.maxX += LABEL_SEARCH_STEP
1242
+ }
1010
1243
  if (rectsOverlap(bounds, otherBounds)) return true
1011
1244
  }
1012
1245
  return false
@@ -22,6 +22,7 @@ export type ChipSide = "left" | "right" | "top" | "bottom"
22
22
  export type CandidateLabel = {
23
23
  orientation: FacingDirection
24
24
  anchorPoint: Point
25
+ connectorSource?: Point
25
26
  center: Point
26
27
  width: number
27
28
  height: number
@@ -38,6 +39,7 @@ export type CandidateStatus =
38
39
  export type CandidatePhase =
39
40
  | "rotate"
40
41
  | "trace-anchor"
42
+ | "outward-trace-anchor"
41
43
  | "shift"
42
44
  | "lateral-shift"
43
45
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tscircuit/schematic-trace-solver",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.117",
4
+ "version": "0.0.119",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",