@tscircuit/schematic-trace-solver 0.0.196 → 0.0.198

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 (29) hide show
  1. package/dist/index.d.ts +5 -0
  2. package/dist/index.js +120 -7
  3. package/lib/solvers/NetLabelToTraceSolver/NetLabelToTraceSolver.ts +42 -1
  4. package/lib/solvers/NetLabelToTraceSolver/getSimpleElbowPath.ts +31 -0
  5. package/lib/solvers/RailNetLabelCornerPlacementSolver/RailNetLabelCornerPlacementSolver.ts +128 -2
  6. package/lib/solvers/RailNetLabelCornerPlacementSolver/types.ts +1 -0
  7. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +2 -0
  8. package/package.json +1 -1
  9. package/site/SchematicTracePipelineSolver/repro-am3352-sheet3-power-rails.page.tsx +7 -0
  10. package/site/bug-reports/bug-report-20260911T195723Z.page.tsx +4 -0
  11. package/site/bug-reports/enc-scl-hop.page.tsx +6 -0
  12. package/tests/assets/enc-scl-hop.json +185 -0
  13. package/tests/bug-reports/bug-report-20260707T134549Z/__snapshots__/bug-report-20260707T134549Z.snap.svg +13 -31
  14. package/tests/bug-reports/bug-report-20260901T064117Z/__snapshots__/bug-report-20260901T064117Z.snap.svg +25 -61
  15. package/tests/bug-reports/bug-report-20260911T195723Z/__snapshots__/bug-report-20260911T195723Z.snap.svg +559 -0
  16. package/tests/bug-reports/bug-report-20260911T195723Z/bug-report-20260911T195723Z.json +1792 -0
  17. package/tests/bug-reports/bug-report-20260911T195723Z/bug-report-20260911T195723Z.test.ts +35 -0
  18. package/tests/repros/__snapshots__/repro-am3352-sheet3-power-rails.snap.svg +187 -0
  19. package/tests/repros/__snapshots__/repro-mcp73831-charger-traces.snap.svg +5 -23
  20. package/tests/repros/__snapshots__/repro-normal-net-label-branch-from-junction.snap.svg +31 -31
  21. package/tests/repros/__snapshots__/repro-trellis-core-gnd-net-label-overlap.snap.svg +25 -61
  22. package/tests/repros/__snapshots__/rp2040-robot-controller-disconnected-gnd.snap.svg +12 -26
  23. package/tests/repros/assets/repro-am3352-sheet3-power-rails.input.json +698 -0
  24. package/tests/repros/repro-am3352-sheet3-power-rails.md +66 -0
  25. package/tests/repros/repro-am3352-sheet3-power-rails.test.ts +39 -0
  26. package/tests/repros/repro-normal-net-label-branch-from-junction.test.ts +13 -0
  27. package/tests/solvers/SchematicTracePipelineSolver/__snapshots__/enc-scl-hop.snap.svg +101 -0
  28. package/tests/solvers/SchematicTracePipelineSolver/__snapshots__/explicit-hop.snap.svg +101 -0
  29. package/tests/solvers/SchematicTracePipelineSolver/enc-scl-hop.test.ts +139 -0
package/dist/index.d.ts CHANGED
@@ -963,6 +963,7 @@ interface RailNetLabelCornerPlacementSolverParams {
963
963
  inputProblem: InputProblem;
964
964
  traces: SolvedTracePath[];
965
965
  netLabelPlacements: NetLabelPlacement[];
966
+ netLabelConnectorTraceIds?: ReadonlySet<string>;
966
967
  }
967
968
  type TraceCornerCandidate = {
968
969
  anchorPoint: Point;
@@ -996,6 +997,7 @@ declare class RailNetLabelCornerPlacementSolver extends BaseSolver {
996
997
  private queuedCornerCandidates;
997
998
  private shouldAdvanceToNextLabel;
998
999
  private traceMap;
1000
+ private netLabelConnectorTraceIds;
999
1001
  constructor(params: RailNetLabelCornerPlacementSolverParams);
1000
1002
  getConstructorParams(): ConstructorParameters<typeof RailNetLabelCornerPlacementSolver>[0];
1001
1003
  _step(): void;
@@ -1015,6 +1017,8 @@ declare class RailNetLabelCornerPlacementSolver extends BaseSolver {
1015
1017
  private intersectsAnyChip;
1016
1018
  private intersectsAnyOtherNetLabel;
1017
1019
  private getCornerCandidatesForLabel;
1020
+ private getStraightConnectorCandidates;
1021
+ private isComponentAdjacentCorner;
1018
1022
  private isConfiguredRailLabel;
1019
1023
  private isLabelCrossedByOtherNetTrace;
1020
1024
  private pointsEqual;
@@ -1451,6 +1455,7 @@ declare class NetLabelToTraceSolver extends BaseSolver {
1451
1455
  private isPortOnlyFallbackLabel;
1452
1456
  private isDirectConnectionLabel;
1453
1457
  private getMultiPinNetConnection;
1458
+ private getTwoPinNetConnection;
1454
1459
  private buildCandidatePairs;
1455
1460
  private buildRoutedComponentCandidates;
1456
1461
  private isSupersededConnectorTrace;
package/dist/index.js CHANGED
@@ -11181,6 +11181,7 @@ var RailNetLabelCornerPlacementSolver = class extends BaseSolver {
11181
11181
  queuedCornerCandidates = [];
11182
11182
  shouldAdvanceToNextLabel = false;
11183
11183
  traceMap;
11184
+ netLabelConnectorTraceIds;
11184
11185
  constructor(params) {
11185
11186
  super();
11186
11187
  this.inputProblem = params.inputProblem;
@@ -11190,6 +11191,7 @@ var RailNetLabelCornerPlacementSolver = class extends BaseSolver {
11190
11191
  this.traceMap = Object.fromEntries(
11191
11192
  params.traces.map((trace) => [trace.mspPairId, trace])
11192
11193
  );
11194
+ this.netLabelConnectorTraceIds = params.netLabelConnectorTraceIds ?? /* @__PURE__ */ new Set();
11193
11195
  this.queuedLabelIndices = this.getProcessableLabelIndices();
11194
11196
  this.prepareNextLabel();
11195
11197
  }
@@ -11197,7 +11199,8 @@ var RailNetLabelCornerPlacementSolver = class extends BaseSolver {
11197
11199
  return {
11198
11200
  inputProblem: this.inputProblem,
11199
11201
  traces: this.traces,
11200
- netLabelPlacements: this.netLabelPlacements
11202
+ netLabelPlacements: this.netLabelPlacements,
11203
+ netLabelConnectorTraceIds: this.netLabelConnectorTraceIds
11201
11204
  };
11202
11205
  }
11203
11206
  _step() {
@@ -11248,7 +11251,10 @@ var RailNetLabelCornerPlacementSolver = class extends BaseSolver {
11248
11251
  this.currentLabelIndex = labelIndex;
11249
11252
  this.currentLabel = label;
11250
11253
  this.currentCandidateResults = [];
11251
- this.queuedCornerCandidates = this.getCornerCandidatesForLabel(label);
11254
+ this.queuedCornerCandidates = [
11255
+ ...this.getStraightConnectorCandidates(label),
11256
+ ...this.getCornerCandidatesForLabel(label)
11257
+ ];
11252
11258
  return true;
11253
11259
  }
11254
11260
  advanceToNextLabel() {
@@ -11330,7 +11336,7 @@ var RailNetLabelCornerPlacementSolver = class extends BaseSolver {
11330
11336
  }
11331
11337
  shouldProcessLabel(label) {
11332
11338
  const isVerticalRailLabel = label.orientation === "y+" || label.orientation === "y-";
11333
- return isVerticalRailLabel && this.getCornerCandidatesForLabel(label).length > 0;
11339
+ return isVerticalRailLabel && (this.getStraightConnectorCandidates(label).length > 0 || this.getCornerCandidatesForLabel(label).length > 0);
11334
11340
  }
11335
11341
  intersectsAnyChip(bounds) {
11336
11342
  return this.inputProblem.chips.some(
@@ -11387,6 +11393,77 @@ var RailNetLabelCornerPlacementSolver = class extends BaseSolver {
11387
11393
  ...railAlignedCandidates.sort((a, b) => a.distance - b.distance)
11388
11394
  ];
11389
11395
  }
11396
+ getStraightConnectorCandidates(label) {
11397
+ if (label.pinIds.length !== 2 || !this.isConfiguredRailLabel(label)) {
11398
+ return [];
11399
+ }
11400
+ const pins = this.inputProblem.chips.flatMap((chip) => chip.pins).filter((pin) => label.pinIds.includes(pin.pinId));
11401
+ if (pins.length !== 2 || pins[0]._facingDirection !== pins[1]._facingDirection || pins[0]._facingDirection !== "x+" && pins[0]._facingDirection !== "x-" || Math.abs(pins[0].y - pins[1].y) > EPS8) {
11402
+ return [];
11403
+ }
11404
+ const configuredOrientations = this.inputProblem.availableNetLabelOrientations[label.netId];
11405
+ if (configuredOrientations?.length !== 1 || configuredOrientations[0] !== label.orientation) {
11406
+ return [];
11407
+ }
11408
+ const netConnection = this.inputProblem.netConnections.find(
11409
+ (connection) => connection.netId === label.netId
11410
+ );
11411
+ if (netConnection?.isGround) return [];
11412
+ const connector = this.traces.find(
11413
+ (trace) => this.netLabelConnectorTraceIds.has(trace.mspPairId) && trace.globalConnNetId === label.globalConnNetId && label.pinIds.every((pinId) => trace.pinIds.includes(pinId)) && trace.tracePath.length >= 2 && (this.pointsEqual(trace.tracePath[0], label.anchorPoint) || this.pointsEqual(trace.tracePath.at(-1), label.anchorPoint))
11414
+ );
11415
+ if (!connector) return [];
11416
+ const sourcePoint = this.pointsEqual(
11417
+ connector.tracePath[0],
11418
+ label.anchorPoint
11419
+ ) ? connector.tracePath.at(-1) : connector.tracePath[0];
11420
+ if (!this.isComponentAdjacentCorner(label, sourcePoint)) return [];
11421
+ const direction = label.orientation === "y+" ? 1 : -1;
11422
+ const maxSearchDistance = Math.min(
11423
+ getMaxSearchDistance(this.inputProblem),
11424
+ label.height * 2
11425
+ );
11426
+ for (let distance7 = WICK_CLEARANCE + LABEL_SEARCH_STEP; distance7 <= maxSearchDistance + EPS8; distance7 += LABEL_SEARCH_STEP) {
11427
+ const anchorPoint = {
11428
+ x: sourcePoint.x,
11429
+ y: sourcePoint.y + direction * distance7
11430
+ };
11431
+ const center = getCenterFromAnchor(
11432
+ anchorPoint,
11433
+ label.orientation,
11434
+ label.width,
11435
+ label.height
11436
+ );
11437
+ if (this.intersectsAnyChip(getRectBounds(center, label.width, label.height))) {
11438
+ continue;
11439
+ }
11440
+ return [
11441
+ {
11442
+ anchorPoint,
11443
+ traceId: connector.mspPairId,
11444
+ distance: getDistance(anchorPoint, label.anchorPoint),
11445
+ pinAligned: true,
11446
+ reroutedTracePath: [sourcePoint, anchorPoint]
11447
+ }
11448
+ ];
11449
+ }
11450
+ return [];
11451
+ }
11452
+ isComponentAdjacentCorner(label, corner) {
11453
+ return label.mspConnectionPairIds.some((traceId) => {
11454
+ const trace = this.traceMap[traceId];
11455
+ if (!trace || trace.tracePath.length < 2) return false;
11456
+ return [
11457
+ { point: trace.tracePath[0], neighbor: trace.tracePath[1] },
11458
+ {
11459
+ point: trace.tracePath.at(-1),
11460
+ neighbor: trace.tracePath.at(-2)
11461
+ }
11462
+ ].some(
11463
+ ({ point, neighbor }) => this.pointsEqual(neighbor, corner) && trace.pins.some((pin) => this.pointsEqual(pin, point))
11464
+ );
11465
+ });
11466
+ }
11390
11467
  isConfiguredRailLabel(label) {
11391
11468
  if (!label.netId) return false;
11392
11469
  return this.inputProblem.availableNetLabelOrientations[label.netId]?.some(
@@ -17900,6 +17977,23 @@ var getTraceRecoveryConnectivityMaps = (inputProblem) => {
17900
17977
  return { chipMap, pinMap };
17901
17978
  };
17902
17979
 
17980
+ // lib/solvers/NetLabelToTraceSolver/getSimpleElbowPath.ts
17981
+ var getSimpleElbowPath = (first, second) => {
17982
+ const firstDirection = dir(first._facingDirection);
17983
+ const secondDirection = dir(second._facingDirection);
17984
+ if (firstDirection.x * secondDirection.x || firstDirection.y * secondDirection.y)
17985
+ return void 0;
17986
+ const elbow = firstDirection.x ? { x: second.x, y: first.y } : { x: first.x, y: second.y };
17987
+ for (const [pin, direction] of [
17988
+ [first, firstDirection],
17989
+ [second, secondDirection]
17990
+ ]) {
17991
+ if ((elbow.x - pin.x) * direction.x + (elbow.y - pin.y) * direction.y <= 1e-6)
17992
+ return void 0;
17993
+ }
17994
+ return [{ x: first.x, y: first.y }, elbow, { x: second.x, y: second.y }];
17995
+ };
17996
+
17903
17997
  // lib/solvers/NetLabelToTraceSolver/reduceTraceCrossings.ts
17904
17998
  var EPS18 = 1e-9;
17905
17999
  var isHorizontal4 = (start, end) => Math.abs(start.y - end.y) < EPS18;
@@ -18074,6 +18168,11 @@ var NetLabelToTraceSolver = class extends BaseSolver {
18074
18168
  (connection) => connection.isGround === false && connection.pinIds.length > 2 && connection.netId === label.netId && connection.pinIds.includes(pinId)
18075
18169
  );
18076
18170
  }
18171
+ getTwoPinNetConnection(label) {
18172
+ return this.inputProblem.netConnections.find(
18173
+ (connection) => !connection.isGround && connection.pinIds.length === 2 && connection.netId === label.netId && connection.pinIds.includes(label.pinIds[0])
18174
+ );
18175
+ }
18077
18176
  buildCandidatePairs() {
18078
18177
  const { netConnMap } = getConnectivityMapsFromInputProblem(
18079
18178
  this.inputProblem
@@ -18086,7 +18185,7 @@ var NetLabelToTraceSolver = class extends BaseSolver {
18086
18185
  }
18087
18186
  const labelsByGlobalNet = /* @__PURE__ */ new Map();
18088
18187
  for (const label of this.input.netLabelPlacements) {
18089
- if (!this.isPortOnlyFallbackLabel(label, groundGlobalConnNetIds) || !this.isDirectConnectionLabel(label) && !this.getMultiPinNetConnection(label)) {
18188
+ if (!this.isPortOnlyFallbackLabel(label, groundGlobalConnNetIds) || !this.isDirectConnectionLabel(label) && !this.getMultiPinNetConnection(label) && !this.getTwoPinNetConnection(label)) {
18090
18189
  continue;
18091
18190
  }
18092
18191
  const labels = labelsByGlobalNet.get(label.globalConnNetId) ?? [];
@@ -18110,7 +18209,9 @@ var NetLabelToTraceSolver = class extends BaseSolver {
18110
18209
  const bothLabelsBelongToDirectConnections = this.isDirectConnectionLabel(firstLabel) && this.isDirectConnectionLabel(secondLabel);
18111
18210
  const firstMultiPinNetConnection = this.getMultiPinNetConnection(firstLabel);
18112
18211
  const secondMultiPinNetConnection = this.getMultiPinNetConnection(secondLabel);
18113
- if (!bothLabelsBelongToDirectConnections && (!firstMultiPinNetConnection || firstMultiPinNetConnection !== secondMultiPinNetConnection || perpendicularOffset > MAX_NAMED_NET_RECOVERY_PERPENDICULAR_OFFSET)) {
18212
+ const twoPinNetConnection = this.getTwoPinNetConnection(firstLabel);
18213
+ const simpleElbowPath = !bothLabelsBelongToDirectConnections && twoPinNetConnection && firstPin.chipId !== secondPin.chipId && twoPinNetConnection === this.getTwoPinNetConnection(secondLabel) ? getSimpleElbowPath(firstPin, secondPin) : void 0;
18214
+ if (!simpleElbowPath && !bothLabelsBelongToDirectConnections && (!firstMultiPinNetConnection || firstMultiPinNetConnection !== secondMultiPinNetConnection || perpendicularOffset > MAX_NAMED_NET_RECOVERY_PERPENDICULAR_OFFSET)) {
18114
18215
  continue;
18115
18216
  }
18116
18217
  if (arePinsInDifferentSchematicSections(
@@ -18136,7 +18237,8 @@ var NetLabelToTraceSolver = class extends BaseSolver {
18136
18237
  perpendicularOffset,
18137
18238
  routeDistance: Math.abs(firstPin.x - secondPin.x) + Math.abs(firstPin.y - secondPin.y),
18138
18239
  key: getCanonicalPairKey(firstPin.pinId, secondPin.pinId),
18139
- recoveryMode: "fallback_labels"
18240
+ recoveryMode: "fallback_labels",
18241
+ simpleElbowPath
18140
18242
  });
18141
18243
  }
18142
18244
  }
@@ -18298,6 +18400,16 @@ var NetLabelToTraceSolver = class extends BaseSolver {
18298
18400
  (trace) => trace.globalConnNetId !== candidate.firstLabel.globalConnNetId
18299
18401
  );
18300
18402
  }
18403
+ if (candidate.simpleElbowPath) {
18404
+ tracePath = candidate.simpleElbowPath;
18405
+ if (findFirstCollision(tracePath, this.activeSubSolver.obstacles) || collisionTraces.reduce(
18406
+ (count, trace) => count + findPerpendicularPathCrossings(tracePath, trace.tracePath, {
18407
+ includeTerminalSegments: true
18408
+ }).length,
18409
+ 0
18410
+ ) > 1)
18411
+ return;
18412
+ }
18301
18413
  if (doesTraceRecoveryPathConflict(tracePath, collisionTraces) || this.routeIntersectsRemainingLabels(tracePath, candidate)) {
18302
18414
  return;
18303
18415
  }
@@ -18675,7 +18787,8 @@ var SchematicTracePipelineSolver = class extends BaseSolver {
18675
18787
  traces: Object.values(
18676
18788
  instance.postLabelTraceOverlapShiftSolver.correctedTraceMap
18677
18789
  ),
18678
- netLabelPlacements: instance.availableNetOrientationSolver.outputNetLabelPlacements
18790
+ netLabelPlacements: instance.availableNetOrientationSolver.outputNetLabelPlacements,
18791
+ netLabelConnectorTraceIds: instance.availableNetOrientationSolver.netLabelConnectorTraceIds
18679
18792
  }
18680
18793
  ];
18681
18794
  }
@@ -27,6 +27,8 @@ import {
27
27
  type InlineNetLabelPlacement,
28
28
  visualizeInlineNetLabelOutput,
29
29
  } from "../InlineNetLabelSolver/InlineNetLabelSolver"
30
+ import { getSimpleElbowPath } from "./getSimpleElbowPath"
31
+ import { findPerpendicularPathCrossings } from "../TraceCleanupSolver/sub-solver/findIntersectionsWithObstacles"
30
32
  import { reduceTraceCrossings } from "./reduceTraceCrossings"
31
33
 
32
34
  type GlobalConnNetId = NetLabelPlacement["globalConnNetId"]
@@ -43,6 +45,7 @@ interface CandidatePair {
43
45
  | "routed_components"
44
46
  | "routed_direct_connection"
45
47
  netConnectionPinIds?: PinId[]
48
+ simpleElbowPath?: Point[]
46
49
  }
47
50
 
48
51
  const RECOVERED_TRACE_PREFIX = "net-label-to-trace-"
@@ -153,6 +156,16 @@ export class NetLabelToTraceSolver extends BaseSolver {
153
156
  )
154
157
  }
155
158
 
159
+ private getTwoPinNetConnection(label: NetLabelPlacement) {
160
+ return this.inputProblem.netConnections.find(
161
+ (connection) =>
162
+ !connection.isGround &&
163
+ connection.pinIds.length === 2 &&
164
+ connection.netId === label.netId &&
165
+ connection.pinIds.includes(label.pinIds[0]!),
166
+ )
167
+ }
168
+
156
169
  private buildCandidatePairs() {
157
170
  const { netConnMap } = getConnectivityMapsFromInputProblem(
158
171
  this.inputProblem,
@@ -169,7 +182,8 @@ export class NetLabelToTraceSolver extends BaseSolver {
169
182
  if (
170
183
  !this.isPortOnlyFallbackLabel(label, groundGlobalConnNetIds) ||
171
184
  (!this.isDirectConnectionLabel(label) &&
172
- !this.getMultiPinNetConnection(label))
185
+ !this.getMultiPinNetConnection(label) &&
186
+ !this.getTwoPinNetConnection(label))
173
187
  ) {
174
188
  continue
175
189
  }
@@ -203,7 +217,16 @@ export class NetLabelToTraceSolver extends BaseSolver {
203
217
  this.getMultiPinNetConnection(firstLabel)
204
218
  const secondMultiPinNetConnection =
205
219
  this.getMultiPinNetConnection(secondLabel)
220
+ const twoPinNetConnection = this.getTwoPinNetConnection(firstLabel)
221
+ const simpleElbowPath =
222
+ !bothLabelsBelongToDirectConnections &&
223
+ twoPinNetConnection &&
224
+ firstPin.chipId !== secondPin.chipId &&
225
+ twoPinNetConnection === this.getTwoPinNetConnection(secondLabel)
226
+ ? getSimpleElbowPath(firstPin, secondPin)
227
+ : undefined
206
228
  if (
229
+ !simpleElbowPath &&
207
230
  !bothLabelsBelongToDirectConnections &&
208
231
  (!firstMultiPinNetConnection ||
209
232
  firstMultiPinNetConnection !== secondMultiPinNetConnection ||
@@ -242,6 +265,7 @@ export class NetLabelToTraceSolver extends BaseSolver {
242
265
  Math.abs(firstPin.y - secondPin.y),
243
266
  key: getCanonicalPairKey(firstPin.pinId, secondPin.pinId),
244
267
  recoveryMode: "fallback_labels",
268
+ simpleElbowPath,
245
269
  })
246
270
  }
247
271
  }
@@ -474,6 +498,23 @@ export class NetLabelToTraceSolver extends BaseSolver {
474
498
  trace.globalConnNetId !== candidate.firstLabel.globalConnNetId,
475
499
  )
476
500
  }
501
+ // Only recover a named two-pin elbow when the shortest, direction-respecting
502
+ // path is clear. Do not replace labels with obstacle detours or many hops.
503
+ if (candidate.simpleElbowPath) {
504
+ tracePath = candidate.simpleElbowPath
505
+ if (
506
+ findFirstCollision(tracePath, this.activeSubSolver!.obstacles) ||
507
+ collisionTraces.reduce(
508
+ (count, trace) =>
509
+ count +
510
+ findPerpendicularPathCrossings(tracePath!, trace.tracePath, {
511
+ includeTerminalSegments: true,
512
+ }).length,
513
+ 0,
514
+ ) > 1
515
+ )
516
+ return
517
+ }
477
518
  if (
478
519
  doesTraceRecoveryPathConflict(tracePath, collisionTraces) ||
479
520
  this.routeIntersectsRemainingLabels(tracePath, candidate)
@@ -0,0 +1,31 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+ import type { TraceRecoveryPin } from "lib/solvers/NetLabelTraceRecovery/getTraceRecoveryConnectivityMaps"
3
+ import { dir } from "lib/utils/dir"
4
+
5
+ /** The single elbow reached by extending both pins outwards, if it exists. */
6
+ export const getSimpleElbowPath = (
7
+ first: TraceRecoveryPin,
8
+ second: TraceRecoveryPin,
9
+ ): Point[] | undefined => {
10
+ const firstDirection = dir(first._facingDirection)
11
+ const secondDirection = dir(second._facingDirection)
12
+ if (
13
+ firstDirection.x * secondDirection.x ||
14
+ firstDirection.y * secondDirection.y
15
+ )
16
+ return undefined
17
+ const elbow = firstDirection.x
18
+ ? { x: second.x, y: first.y }
19
+ : { x: first.x, y: second.y }
20
+ for (const [pin, direction] of [
21
+ [first, firstDirection],
22
+ [second, secondDirection],
23
+ ] as const) {
24
+ if (
25
+ (elbow.x - pin.x) * direction.x + (elbow.y - pin.y) * direction.y <=
26
+ 1e-6
27
+ )
28
+ return undefined
29
+ }
30
+ return [{ x: first.x, y: first.y }, elbow, { x: second.x, y: second.y }]
31
+ }
@@ -1,9 +1,14 @@
1
1
  import type { GraphicsObject } from "graphics-debug"
2
2
  import type { Point } from "@tscircuit/math-utils"
3
3
  import {
4
+ getMaxSearchDistance,
4
5
  segmentCrossesBoundsInterior,
5
6
  traceCrossesBoundsInterior,
6
7
  } from "lib/solvers/AvailableNetOrientationSolver/geometry"
8
+ import {
9
+ LABEL_SEARCH_STEP,
10
+ WICK_CLEARANCE,
11
+ } from "lib/solvers/AvailableNetOrientationSolver/constants"
7
12
  import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
8
13
  import { moveAttachedLabelsToReroutedTrace } from "lib/solvers/Example28Solver/labelMovement"
9
14
  import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
@@ -51,6 +56,7 @@ export class RailNetLabelCornerPlacementSolver extends BaseSolver {
51
56
  private queuedCornerCandidates: TraceCornerCandidate[] = []
52
57
  private shouldAdvanceToNextLabel = false
53
58
  private traceMap: Record<string, SolvedTracePath>
59
+ private netLabelConnectorTraceIds: ReadonlySet<string>
54
60
 
55
61
  constructor(params: RailNetLabelCornerPlacementSolverParams) {
56
62
  super()
@@ -61,6 +67,8 @@ export class RailNetLabelCornerPlacementSolver extends BaseSolver {
61
67
  this.traceMap = Object.fromEntries(
62
68
  params.traces.map((trace) => [trace.mspPairId, trace]),
63
69
  )
70
+ this.netLabelConnectorTraceIds =
71
+ params.netLabelConnectorTraceIds ?? new Set()
64
72
  this.queuedLabelIndices = this.getProcessableLabelIndices()
65
73
  this.prepareNextLabel()
66
74
  }
@@ -72,6 +80,7 @@ export class RailNetLabelCornerPlacementSolver extends BaseSolver {
72
80
  inputProblem: this.inputProblem,
73
81
  traces: this.traces,
74
82
  netLabelPlacements: this.netLabelPlacements,
83
+ netLabelConnectorTraceIds: this.netLabelConnectorTraceIds,
75
84
  }
76
85
  }
77
86
 
@@ -135,7 +144,10 @@ export class RailNetLabelCornerPlacementSolver extends BaseSolver {
135
144
  this.currentLabelIndex = labelIndex
136
145
  this.currentLabel = label
137
146
  this.currentCandidateResults = []
138
- this.queuedCornerCandidates = this.getCornerCandidatesForLabel(label)
147
+ this.queuedCornerCandidates = [
148
+ ...this.getStraightConnectorCandidates(label),
149
+ ...this.getCornerCandidatesForLabel(label),
150
+ ]
139
151
 
140
152
  return true
141
153
  }
@@ -250,7 +262,9 @@ export class RailNetLabelCornerPlacementSolver extends BaseSolver {
250
262
  const isVerticalRailLabel =
251
263
  label.orientation === "y+" || label.orientation === "y-"
252
264
  return (
253
- isVerticalRailLabel && this.getCornerCandidatesForLabel(label).length > 0
265
+ isVerticalRailLabel &&
266
+ (this.getStraightConnectorCandidates(label).length > 0 ||
267
+ this.getCornerCandidatesForLabel(label).length > 0)
254
268
  )
255
269
  }
256
270
 
@@ -323,6 +337,118 @@ export class RailNetLabelCornerPlacementSolver extends BaseSolver {
323
337
  ]
324
338
  }
325
339
 
340
+ private getStraightConnectorCandidates(
341
+ label: NetLabelPlacement,
342
+ ): TraceCornerCandidate[] {
343
+ if (label.pinIds.length !== 2 || !this.isConfiguredRailLabel(label)) {
344
+ return []
345
+ }
346
+
347
+ const pins = this.inputProblem.chips
348
+ .flatMap((chip) => chip.pins)
349
+ .filter((pin) => label.pinIds.includes(pin.pinId))
350
+ if (
351
+ pins.length !== 2 ||
352
+ pins[0]!._facingDirection !== pins[1]!._facingDirection ||
353
+ (pins[0]!._facingDirection !== "x+" &&
354
+ pins[0]!._facingDirection !== "x-") ||
355
+ Math.abs(pins[0]!.y - pins[1]!.y) > EPS
356
+ ) {
357
+ return []
358
+ }
359
+
360
+ const configuredOrientations =
361
+ this.inputProblem.availableNetLabelOrientations[label.netId!]
362
+ if (
363
+ configuredOrientations?.length !== 1 ||
364
+ configuredOrientations[0] !== label.orientation
365
+ ) {
366
+ return []
367
+ }
368
+
369
+ const netConnection = this.inputProblem.netConnections.find(
370
+ (connection) => connection.netId === label.netId,
371
+ )
372
+ if (netConnection?.isGround) return []
373
+
374
+ const connector = this.traces.find(
375
+ (trace) =>
376
+ this.netLabelConnectorTraceIds.has(trace.mspPairId) &&
377
+ trace.globalConnNetId === label.globalConnNetId &&
378
+ label.pinIds.every((pinId) => trace.pinIds.includes(pinId)) &&
379
+ trace.tracePath.length >= 2 &&
380
+ (this.pointsEqual(trace.tracePath[0]!, label.anchorPoint) ||
381
+ this.pointsEqual(trace.tracePath.at(-1)!, label.anchorPoint)),
382
+ )
383
+ if (!connector) return []
384
+
385
+ const sourcePoint = this.pointsEqual(
386
+ connector.tracePath[0]!,
387
+ label.anchorPoint,
388
+ )
389
+ ? connector.tracePath.at(-1)!
390
+ : connector.tracePath[0]!
391
+ if (!this.isComponentAdjacentCorner(label, sourcePoint)) return []
392
+
393
+ const direction = label.orientation === "y+" ? 1 : -1
394
+ const maxSearchDistance = Math.min(
395
+ getMaxSearchDistance(this.inputProblem),
396
+ label.height * 2,
397
+ )
398
+ for (
399
+ let distance = WICK_CLEARANCE + LABEL_SEARCH_STEP;
400
+ distance <= maxSearchDistance + EPS;
401
+ distance += LABEL_SEARCH_STEP
402
+ ) {
403
+ const anchorPoint = {
404
+ x: sourcePoint.x,
405
+ y: sourcePoint.y + direction * distance,
406
+ }
407
+ const center = getCenterFromAnchor(
408
+ anchorPoint,
409
+ label.orientation,
410
+ label.width,
411
+ label.height,
412
+ )
413
+ if (
414
+ this.intersectsAnyChip(getRectBounds(center, label.width, label.height))
415
+ ) {
416
+ continue
417
+ }
418
+
419
+ return [
420
+ {
421
+ anchorPoint,
422
+ traceId: connector.mspPairId,
423
+ distance: getDistance(anchorPoint, label.anchorPoint),
424
+ pinAligned: true,
425
+ reroutedTracePath: [sourcePoint, anchorPoint],
426
+ },
427
+ ]
428
+ }
429
+
430
+ return []
431
+ }
432
+
433
+ private isComponentAdjacentCorner(label: NetLabelPlacement, corner: Point) {
434
+ return label.mspConnectionPairIds.some((traceId) => {
435
+ const trace = this.traceMap[traceId]
436
+ if (!trace || trace.tracePath.length < 2) return false
437
+
438
+ return [
439
+ { point: trace.tracePath[0]!, neighbor: trace.tracePath[1]! },
440
+ {
441
+ point: trace.tracePath.at(-1)!,
442
+ neighbor: trace.tracePath.at(-2)!,
443
+ },
444
+ ].some(
445
+ ({ point, neighbor }) =>
446
+ this.pointsEqual(neighbor, corner) &&
447
+ trace.pins.some((pin) => this.pointsEqual(pin, point)),
448
+ )
449
+ })
450
+ }
451
+
326
452
  private isConfiguredRailLabel(label: NetLabelPlacement) {
327
453
  if (!label.netId) return false
328
454
  return this.inputProblem.availableNetLabelOrientations[label.netId]?.some(
@@ -7,6 +7,7 @@ export interface RailNetLabelCornerPlacementSolverParams {
7
7
  inputProblem: InputProblem
8
8
  traces: SolvedTracePath[]
9
9
  netLabelPlacements: NetLabelPlacement[]
10
+ netLabelConnectorTraceIds?: ReadonlySet<string>
10
11
  }
11
12
 
12
13
  export type Bounds = {
@@ -402,6 +402,8 @@ export class SchematicTracePipelineSolver extends BaseSolver {
402
402
  ),
403
403
  netLabelPlacements:
404
404
  instance.availableNetOrientationSolver!.outputNetLabelPlacements,
405
+ netLabelConnectorTraceIds:
406
+ instance.availableNetOrientationSolver!.netLabelConnectorTraceIds,
405
407
  },
406
408
  ]
407
409
  },
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.196",
8
+ "version": "0.0.198",
9
9
  "type": "module",
10
10
  "scripts": {
11
11
  "start": "cosmos",
@@ -0,0 +1,7 @@
1
+ import type { InputProblem } from "lib/types/InputProblem"
2
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
3
+ import input from "../../tests/repros/assets/repro-am3352-sheet3-power-rails.input.json"
4
+
5
+ export default () => (
6
+ <PipelineDebugger inputProblem={structuredClone(input) as InputProblem} />
7
+ )
@@ -0,0 +1,4 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/bug-reports/bug-report-20260911T195723Z/bug-report-20260911T195723Z.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />
@@ -0,0 +1,6 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/assets/enc-scl-hop.json"
3
+
4
+ export default () => (
5
+ <PipelineDebugger inputProblem={inputProblem as any} hideRatsNet />
6
+ )