@tscircuit/schematic-trace-solver 0.0.28 → 0.0.30

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.js CHANGED
@@ -469,6 +469,68 @@ var dir = (facingDirection) => {
469
469
  // lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/generateElbowVariants.ts
470
470
  var EPS = 1e-6;
471
471
  var MIN_LEN = 1e-6;
472
+ var checkIfUTurnNeeded = (elbow) => {
473
+ if (elbow.length !== 4) return false;
474
+ const [start, p1, p2, end] = elbow;
475
+ const startOrient = orientationOf(start, p1);
476
+ const startFacing = startOrient === "horizontal" ? p1.x > start.x ? "x+" : "x-" : p1.y > start.y ? "y+" : "y-";
477
+ const endOrient = orientationOf(p2, end);
478
+ const endFacing = endOrient === "horizontal" ? end.x > p2.x ? "x+" : "x-" : end.y > p2.y ? "y+" : "y-";
479
+ if (startFacing === "x+" && end.x < p1.x - EPS) return true;
480
+ if (startFacing === "x-" && end.x > p1.x + EPS) return true;
481
+ if (startFacing === "y+" && end.y < p1.y - EPS) return true;
482
+ if (startFacing === "y-" && end.y > p1.y + EPS) return true;
483
+ if (endFacing === "x-" && p2.x > end.x + EPS && startFacing === "x+")
484
+ return true;
485
+ if (endFacing === "x+" && p2.x < end.x - EPS && startFacing === "x-")
486
+ return true;
487
+ if (endFacing === "y-" && p2.y > end.y + EPS && startFacing === "y+")
488
+ return true;
489
+ if (endFacing === "y+" && p2.y < end.y - EPS && startFacing === "y-")
490
+ return true;
491
+ return false;
492
+ };
493
+ var expandToUTurn = (elbow) => {
494
+ if (elbow.length !== 4) return elbow;
495
+ const [start, p1, p2, end] = elbow;
496
+ const overshoot = Math.max(
497
+ Math.abs(start.x - p1.x),
498
+ Math.abs(start.y - p1.y),
499
+ 0.2
500
+ // minimum overshoot
501
+ );
502
+ const startOrient = orientationOf(start, p1);
503
+ const expanded = [{ ...start }];
504
+ if (startOrient === "horizontal") {
505
+ const startDir = p1.x > start.x ? 1 : -1;
506
+ expanded.push({ x: start.x + startDir * overshoot, y: start.y });
507
+ const verticalSpace = Math.max(
508
+ overshoot * 2,
509
+ Math.abs(end.y - start.y) + overshoot
510
+ );
511
+ const midY = end.y > start.y ? start.y - verticalSpace : start.y + verticalSpace;
512
+ expanded.push({ x: start.x + startDir * overshoot, y: midY });
513
+ const endOrient = orientationOf(p2, end);
514
+ const endApproachX = endOrient === "horizontal" ? end.x > p2.x ? end.x - overshoot : end.x + overshoot : end.x;
515
+ expanded.push({ x: endApproachX, y: midY });
516
+ expanded.push({ x: endApproachX, y: end.y });
517
+ } else {
518
+ const startDir = p1.y > start.y ? 1 : -1;
519
+ expanded.push({ x: start.x, y: start.y + startDir * overshoot });
520
+ const horizontalSpace = Math.max(
521
+ overshoot * 2,
522
+ Math.abs(end.x - start.x) + overshoot
523
+ );
524
+ const midX = end.x > start.x ? start.x - horizontalSpace : start.x + horizontalSpace;
525
+ expanded.push({ x: midX, y: start.y + startDir * overshoot });
526
+ const endOrient = orientationOf(p2, end);
527
+ const endApproachY = endOrient === "vertical" ? end.y > p2.y ? end.y - overshoot : end.y + overshoot : end.y;
528
+ expanded.push({ x: midX, y: endApproachY });
529
+ expanded.push({ x: end.x, y: endApproachY });
530
+ }
531
+ expanded.push({ ...end });
532
+ return expanded;
533
+ };
472
534
  var isAxisAligned = (a, b) => Math.abs(a.x - b.x) < EPS || Math.abs(a.y - b.y) < EPS;
473
535
  var orientationOf = (a, b) => {
474
536
  const dx = Math.abs(a.x - b.x);
@@ -550,15 +612,20 @@ var generateElbowVariants = ({
550
612
  const elbow = preprocessElbow(baseElbow);
551
613
  assertOrthogonalPolyline(elbow);
552
614
  const nPts = elbow.length;
553
- const nSegs = nPts - 1;
554
- if (nSegs < 5) {
615
+ if (nPts < 4) {
555
616
  return {
556
617
  elbowVariants: [elbow.map((p) => ({ ...p }))],
557
618
  movableSegments: []
558
619
  };
620
+ } else if (nPts === 4) {
621
+ const needsUTurn = checkIfUTurnNeeded(elbow);
622
+ if (needsUTurn) {
623
+ const expandedElbow = expandToUTurn(elbow);
624
+ return generateElbowVariants({ baseElbow: expandedElbow, guidelines });
625
+ }
559
626
  }
560
- const firstMovableIndex = 2;
561
- const lastMovableIndex = nPts - 4;
627
+ const firstMovableIndex = 1;
628
+ const lastMovableIndex = nPts - 3;
562
629
  const movableSegments = [];
563
630
  const movableIdx = [];
564
631
  const axes = [];
@@ -1427,10 +1494,11 @@ function rectIntersectsAnyTrace(bounds, inputTraceMap, hostPathId, hostSegIndex)
1427
1494
  const pts = solved.tracePath;
1428
1495
  for (let i = 0; i < pts.length - 1; i++) {
1429
1496
  if (pairId === hostPathId && i === hostSegIndex) continue;
1430
- if (segmentIntersectsRect(pts[i], pts[i + 1], bounds)) return true;
1497
+ if (segmentIntersectsRect(pts[i], pts[i + 1], bounds))
1498
+ return { hasIntersection: true, mspPairId: pairId, segIndex: i };
1431
1499
  }
1432
1500
  }
1433
- return false;
1501
+ return { hasIntersection: false };
1434
1502
  }
1435
1503
 
1436
1504
  // lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/host.ts
@@ -1515,14 +1583,16 @@ function solveNetLabelPlacementForPortOnlyPin(params) {
1515
1583
  };
1516
1584
  }
1517
1585
  let pin = null;
1586
+ let pinFacingDirection = null;
1518
1587
  for (const chip of inputProblem.chips) {
1519
1588
  const p = chip.pins.find((pp) => pp.pinId === pinId);
1520
1589
  if (p) {
1521
1590
  pin = { x: p.x, y: p.y };
1591
+ pinFacingDirection = p._facingDirection || getPinDirection(p, chip);
1522
1592
  break;
1523
1593
  }
1524
1594
  }
1525
- if (!pin) {
1595
+ if (!pin || !pinFacingDirection) {
1526
1596
  return {
1527
1597
  placement: null,
1528
1598
  testedCandidates: [],
@@ -1534,21 +1604,21 @@ function solveNetLabelPlacementForPortOnlyPin(params) {
1534
1604
  const outwardOf = (o) => o === "x+" ? { x: 1, y: 0 } : o === "x-" ? { x: -1, y: 0 } : o === "y+" ? { x: 0, y: 1 } : { x: 0, y: -1 };
1535
1605
  const testedCandidates = [];
1536
1606
  for (const orientation of orientations) {
1537
- const { width, height } = getDimsForOrientation(orientation);
1538
- const baseCenter = getCenterFromAnchor(anchor, orientation, width, height);
1539
- const outward = outwardOf(orientation);
1540
- const offset = 1e-3;
1607
+ const { width: width2, height: height2 } = getDimsForOrientation(orientation);
1608
+ const baseCenter2 = getCenterFromAnchor(anchor, orientation, width2, height2);
1609
+ const outward2 = outwardOf(orientation);
1610
+ const offset2 = 1e-3;
1541
1611
  const center = {
1542
- x: baseCenter.x + outward.x * offset,
1543
- y: baseCenter.y + outward.y * offset
1612
+ x: baseCenter2.x + outward2.x * offset2,
1613
+ y: baseCenter2.y + outward2.y * offset2
1544
1614
  };
1545
- const bounds = getRectBounds(center, width, height);
1615
+ const bounds = getRectBounds(center, width2, height2);
1546
1616
  const chips = chipObstacleSpatialIndex.getChipsInBounds(bounds);
1547
1617
  if (chips.length > 0) {
1548
1618
  testedCandidates.push({
1549
1619
  center,
1550
- width,
1551
- height,
1620
+ width: width2,
1621
+ height: height2,
1552
1622
  bounds,
1553
1623
  anchor,
1554
1624
  orientation,
@@ -1557,16 +1627,17 @@ function solveNetLabelPlacementForPortOnlyPin(params) {
1557
1627
  });
1558
1628
  continue;
1559
1629
  }
1560
- if (rectIntersectsAnyTrace(
1630
+ const traceIntersectionResult = rectIntersectsAnyTrace(
1561
1631
  bounds,
1562
1632
  inputTraceMap,
1563
1633
  "",
1564
1634
  -1
1565
- )) {
1635
+ );
1636
+ if (traceIntersectionResult.hasIntersection) {
1566
1637
  testedCandidates.push({
1567
1638
  center,
1568
- width,
1569
- height,
1639
+ width: width2,
1640
+ height: height2,
1570
1641
  bounds,
1571
1642
  anchor,
1572
1643
  orientation,
@@ -1577,8 +1648,8 @@ function solveNetLabelPlacementForPortOnlyPin(params) {
1577
1648
  }
1578
1649
  testedCandidates.push({
1579
1650
  center,
1580
- width,
1581
- height,
1651
+ width: width2,
1652
+ height: height2,
1582
1653
  bounds,
1583
1654
  anchor,
1584
1655
  orientation,
@@ -1593,17 +1664,39 @@ function solveNetLabelPlacementForPortOnlyPin(params) {
1593
1664
  pinIds: [pinId],
1594
1665
  orientation,
1595
1666
  anchorPoint: anchor,
1596
- width,
1597
- height,
1667
+ width: width2,
1668
+ height: height2,
1598
1669
  center
1599
1670
  };
1600
1671
  return { placement, testedCandidates };
1601
1672
  }
1602
- return {
1603
- placement: null,
1604
- testedCandidates,
1605
- error: "Could not place net label at port without collisions"
1673
+ const fallbackOrientation = pinFacingDirection;
1674
+ const { width, height } = getDimsForOrientation(fallbackOrientation);
1675
+ const baseCenter = getCenterFromAnchor(
1676
+ anchor,
1677
+ fallbackOrientation,
1678
+ width,
1679
+ height
1680
+ );
1681
+ const outward = outwardOf(fallbackOrientation);
1682
+ const offset = 1e-3;
1683
+ const fallbackCenter = {
1684
+ x: baseCenter.x + outward.x * offset,
1685
+ y: baseCenter.y + outward.y * offset
1686
+ };
1687
+ const fallbackPlacement = {
1688
+ globalConnNetId: overlappingSameNetTraceGroup.globalConnNetId,
1689
+ dcConnNetId: void 0,
1690
+ netId: overlappingSameNetTraceGroup.netId,
1691
+ mspConnectionPairIds: [],
1692
+ pinIds: [pinId],
1693
+ orientation: fallbackOrientation,
1694
+ anchorPoint: anchor,
1695
+ width,
1696
+ height,
1697
+ center: fallbackCenter
1606
1698
  };
1699
+ return { placement: fallbackPlacement, testedCandidates };
1607
1700
  }
1608
1701
 
1609
1702
  // lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/SingleNetLabelPlacementSolver_visualize.ts
@@ -1619,12 +1712,11 @@ function visualizeSingleNetLabelPlacementSolver(solver) {
1619
1712
  const groupStroke = getColorFromString(groupId, 0.9);
1620
1713
  const groupFill = getColorFromString(groupId, 0.5);
1621
1714
  for (const trace of Object.values(solver.inputTraceMap)) {
1622
- if (trace.globalConnNetId !== groupId) continue;
1623
1715
  const isHost = host ? trace.mspPairId === host.mspPairId : false;
1624
1716
  graphics.lines.push({
1625
- points: trace.tracePath,
1626
- strokeColor: isHost ? groupStroke : groupFill,
1627
- strokeDash: isHost ? void 0 : "4 2"
1717
+ points: trace.tracePath
1718
+ // strokeColor: isHost ? groupStroke : groupFill,
1719
+ // strokeDash: isHost ? undefined : "4 2",
1628
1720
  });
1629
1721
  }
1630
1722
  for (const c of solver.testedCandidates) {
@@ -1796,12 +1888,13 @@ var SingleNetLabelPlacementSolver = class extends BaseSolver {
1796
1888
  });
1797
1889
  continue;
1798
1890
  }
1799
- if (rectIntersectsAnyTrace(
1891
+ const traceIntersectionResult = rectIntersectsAnyTrace(
1800
1892
  bounds,
1801
1893
  this.inputTraceMap,
1802
1894
  curr.mspPairId,
1803
1895
  si
1804
- )) {
1896
+ );
1897
+ if (traceIntersectionResult.hasIntersection) {
1805
1898
  this.testedCandidates.push({
1806
1899
  center: testCenter,
1807
1900
  width,
@@ -261,14 +261,13 @@ export class SingleNetLabelPlacementSolver extends BaseSolver {
261
261
  }
262
262
 
263
263
  // Trace collision check (ignore the host segment)
264
- if (
265
- rectIntersectsAnyTrace(
266
- bounds,
267
- this.inputTraceMap,
268
- curr.mspPairId,
269
- si,
270
- )
271
- ) {
264
+ const traceIntersectionResult = rectIntersectsAnyTrace(
265
+ bounds,
266
+ this.inputTraceMap,
267
+ curr.mspPairId,
268
+ si,
269
+ )
270
+ if (traceIntersectionResult.hasIntersection) {
272
271
  this.testedCandidates.push({
273
272
  center: testCenter,
274
273
  width,
@@ -21,12 +21,11 @@ export function visualizeSingleNetLabelPlacementSolver(
21
21
  const groupFill = getColorFromString(groupId, 0.5)
22
22
 
23
23
  for (const trace of Object.values(solver.inputTraceMap)) {
24
- if (trace.globalConnNetId !== groupId) continue
25
24
  const isHost = host ? trace.mspPairId === host.mspPairId : false
26
25
  graphics.lines!.push({
27
26
  points: trace.tracePath,
28
- strokeColor: isHost ? groupStroke : groupFill,
29
- strokeDash: isHost ? undefined : "4 2",
27
+ // strokeColor: isHost ? groupStroke : groupFill,
28
+ // strokeDash: isHost ? undefined : "4 2",
30
29
  } as any)
31
30
  }
32
31
 
@@ -33,13 +33,20 @@ export function rectIntersectsAnyTrace(
33
33
  inputTraceMap: Record<MspConnectionPairId, SolvedTracePath>,
34
34
  hostPathId?: MspConnectionPairId,
35
35
  hostSegIndex?: number,
36
- ): boolean {
36
+ ):
37
+ | {
38
+ hasIntersection: true
39
+ mspPairId: MspConnectionPairId
40
+ segIndex: number
41
+ }
42
+ | { hasIntersection: false } {
37
43
  for (const [pairId, solved] of Object.entries(inputTraceMap)) {
38
44
  const pts = solved.tracePath
39
45
  for (let i = 0; i < pts.length - 1; i++) {
40
46
  if (pairId === hostPathId && i === hostSegIndex) continue
41
- if (segmentIntersectsRect(pts[i]!, pts[i + 1]!, bounds)) return true
47
+ if (segmentIntersectsRect(pts[i]!, pts[i + 1]!, bounds))
48
+ return { hasIntersection: true, mspPairId: pairId, segIndex: i }
42
49
  }
43
50
  }
44
- return false
51
+ return { hasIntersection: false }
45
52
  }
@@ -3,6 +3,7 @@ import type { MspConnectionPairId } from "lib/solvers/MspConnectionPairSolver/Ms
3
3
  import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
4
4
  import type { FacingDirection } from "lib/utils/dir"
5
5
  import { ChipObstacleSpatialIndex } from "lib/data-structures/ChipObstacleSpatialIndex"
6
+ import { getPinDirection } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/getPinDirection"
6
7
  import type {
7
8
  NetLabelPlacement,
8
9
  OverlappingSameNetTraceGroup,
@@ -51,16 +52,18 @@ export function solveNetLabelPlacementForPortOnlyPin(params: {
51
52
  }
52
53
  }
53
54
 
54
- // Find pin coordinates
55
+ // Find pin coordinates and facing direction
55
56
  let pin: { x: number; y: number } | null = null
57
+ let pinFacingDirection: FacingDirection | null = null
56
58
  for (const chip of inputProblem.chips) {
57
59
  const p = chip.pins.find((pp) => pp.pinId === pinId)
58
60
  if (p) {
59
61
  pin = { x: p.x, y: p.y }
62
+ pinFacingDirection = p._facingDirection || getPinDirection(p, chip)
60
63
  break
61
64
  }
62
65
  }
63
- if (!pin) {
66
+ if (!pin || !pinFacingDirection) {
64
67
  return {
65
68
  placement: null,
66
69
  testedCandidates: [],
@@ -123,14 +126,13 @@ export function solveNetLabelPlacementForPortOnlyPin(params: {
123
126
  }
124
127
 
125
128
  // Trace collision check
126
- if (
127
- rectIntersectsAnyTrace(
128
- bounds,
129
- inputTraceMap,
130
- "" as MspConnectionPairId,
131
- -1,
132
- )
133
- ) {
129
+ const traceIntersectionResult = rectIntersectsAnyTrace(
130
+ bounds,
131
+ inputTraceMap,
132
+ "" as MspConnectionPairId,
133
+ -1,
134
+ )
135
+ if (traceIntersectionResult.hasIntersection) {
134
136
  testedCandidates.push({
135
137
  center,
136
138
  width,
@@ -172,9 +174,34 @@ export function solveNetLabelPlacementForPortOnlyPin(params: {
172
174
  return { placement, testedCandidates }
173
175
  }
174
176
 
175
- return {
176
- placement: null,
177
- testedCandidates,
178
- error: "Could not place net label at port without collisions",
177
+ // If no valid placements found, return placement using pin's facing direction
178
+ const fallbackOrientation = pinFacingDirection
179
+ const { width, height } = getDimsForOrientation(fallbackOrientation)
180
+ const baseCenter = getCenterFromAnchor(
181
+ anchor,
182
+ fallbackOrientation,
183
+ width,
184
+ height,
185
+ )
186
+ const outward = outwardOf(fallbackOrientation)
187
+ const offset = 1e-3
188
+ const fallbackCenter = {
189
+ x: baseCenter.x + outward.x * offset,
190
+ y: baseCenter.y + outward.y * offset,
179
191
  }
192
+
193
+ const fallbackPlacement: NetLabelPlacement = {
194
+ globalConnNetId: overlappingSameNetTraceGroup.globalConnNetId,
195
+ dcConnNetId: undefined,
196
+ netId: overlappingSameNetTraceGroup.netId,
197
+ mspConnectionPairIds: [],
198
+ pinIds: [pinId],
199
+ orientation: fallbackOrientation,
200
+ anchorPoint: anchor,
201
+ width,
202
+ height,
203
+ center: fallbackCenter,
204
+ }
205
+
206
+ return { placement: fallbackPlacement, testedCandidates }
180
207
  }
@@ -14,6 +14,123 @@ export interface MovableSegment {
14
14
  const EPS = 1e-6 // numeric tolerance for equality
15
15
  const MIN_LEN = 1e-6 // forbid near-zero length segments (adjacent collapses)
16
16
 
17
+ const checkIfUTurnNeeded = (elbow: Point[]): boolean => {
18
+ if (elbow.length !== 4) return false
19
+
20
+ const [start, p1, p2, end] = elbow
21
+
22
+ // Determine facing directions from the path segments
23
+ const startOrient = orientationOf(start, p1)
24
+ const startFacing: FacingDirection =
25
+ startOrient === "horizontal"
26
+ ? p1.x > start.x
27
+ ? "x+"
28
+ : "x-"
29
+ : p1.y > start.y
30
+ ? "y+"
31
+ : "y-"
32
+
33
+ const endOrient = orientationOf(p2, end)
34
+ const endFacing: FacingDirection =
35
+ endOrient === "horizontal"
36
+ ? end.x > p2.x
37
+ ? "x+"
38
+ : "x-"
39
+ : end.y > p2.y
40
+ ? "y+"
41
+ : "y-"
42
+
43
+ // Check if path overshoots and needs to turn back
44
+ if (startFacing === "x+" && end.x < p1.x - EPS) return true
45
+ if (startFacing === "x-" && end.x > p1.x + EPS) return true
46
+ if (startFacing === "y+" && end.y < p1.y - EPS) return true
47
+ if (startFacing === "y-" && end.y > p1.y + EPS) return true
48
+
49
+ // Check if path segments conflict (p2 is past the end in the facing direction)
50
+ if (endFacing === "x-" && p2.x > end.x + EPS && startFacing === "x+")
51
+ return true
52
+ if (endFacing === "x+" && p2.x < end.x - EPS && startFacing === "x-")
53
+ return true
54
+ if (endFacing === "y-" && p2.y > end.y + EPS && startFacing === "y+")
55
+ return true
56
+ if (endFacing === "y+" && p2.y < end.y - EPS && startFacing === "y-")
57
+ return true
58
+
59
+ return false
60
+ }
61
+
62
+ const expandToUTurn = (elbow: Point[]): Point[] => {
63
+ if (elbow.length !== 4) return elbow
64
+
65
+ const [start, p1, p2, end] = elbow
66
+
67
+ // Calculate overshoot distance from existing segments
68
+ const overshoot = Math.max(
69
+ Math.abs(start.x - p1.x),
70
+ Math.abs(start.y - p1.y),
71
+ 0.2, // minimum overshoot
72
+ )
73
+
74
+ const startOrient = orientationOf(start, p1)
75
+ const expanded: Point[] = [{ ...start }]
76
+
77
+ if (startOrient === "horizontal") {
78
+ const startDir = p1.x > start.x ? 1 : -1
79
+ expanded.push({ x: start.x + startDir * overshoot, y: start.y })
80
+
81
+ // Move away vertically to create space for U-turn
82
+ const verticalSpace = Math.max(
83
+ overshoot * 2,
84
+ Math.abs(end.y - start.y) + overshoot,
85
+ )
86
+ const midY =
87
+ end.y > start.y ? start.y - verticalSpace : start.y + verticalSpace
88
+
89
+ expanded.push({ x: start.x + startDir * overshoot, y: midY })
90
+
91
+ // Approach end from its required direction
92
+ const endOrient = orientationOf(p2, end)
93
+ const endApproachX =
94
+ endOrient === "horizontal"
95
+ ? end.x > p2.x
96
+ ? end.x - overshoot
97
+ : end.x + overshoot
98
+ : end.x
99
+
100
+ expanded.push({ x: endApproachX, y: midY })
101
+ expanded.push({ x: endApproachX, y: end.y })
102
+ } else {
103
+ // Vertical start
104
+ const startDir = p1.y > start.y ? 1 : -1
105
+ expanded.push({ x: start.x, y: start.y + startDir * overshoot })
106
+
107
+ // Move away horizontally to create space for U-turn
108
+ const horizontalSpace = Math.max(
109
+ overshoot * 2,
110
+ Math.abs(end.x - start.x) + overshoot,
111
+ )
112
+ const midX =
113
+ end.x > start.x ? start.x - horizontalSpace : start.x + horizontalSpace
114
+
115
+ expanded.push({ x: midX, y: start.y + startDir * overshoot })
116
+
117
+ // Approach end from its required direction
118
+ const endOrient = orientationOf(p2, end)
119
+ const endApproachY =
120
+ endOrient === "vertical"
121
+ ? end.y > p2.y
122
+ ? end.y - overshoot
123
+ : end.y + overshoot
124
+ : end.y
125
+
126
+ expanded.push({ x: midX, y: endApproachY })
127
+ expanded.push({ x: end.x, y: endApproachY })
128
+ }
129
+
130
+ expanded.push({ ...end })
131
+ return expanded
132
+ }
133
+
17
134
  /** True if segment [a,b] is axis-aligned (horizontal or vertical). */
18
135
  const isAxisAligned = (a: Point, b: Point): boolean =>
19
136
  Math.abs(a.x - b.x) < EPS || Math.abs(a.y - b.y) < EPS
@@ -155,21 +272,27 @@ export const generateElbowVariants = ({
155
272
  assertOrthogonalPolyline(elbow)
156
273
 
157
274
  const nPts = elbow.length
158
- const nSegs = nPts - 1
159
275
 
160
276
  // No interior segments to move if path is too short.
161
- if (nSegs < 5) {
277
+ if (nPts < 4) {
162
278
  return {
163
279
  elbowVariants: [elbow.map((p) => ({ ...p }))],
164
280
  movableSegments: [],
165
281
  }
282
+ } else if (nPts === 4) {
283
+ const needsUTurn = checkIfUTurnNeeded(elbow)
284
+ if (needsUTurn) {
285
+ const expandedElbow = expandToUTurn(elbow)
286
+ // Recursively call with the expanded path
287
+ return generateElbowVariants({ baseElbow: expandedElbow, guidelines })
288
+ }
166
289
  }
167
290
 
168
291
  // 2) Choose which segments are allowed to move.
169
- // We avoid segments adjacent to the first and last segment:
170
- // movable indices i in [2 .. (nPts - 4)] inclusive (segment i connects P[i] -> P[i+1]).
171
- const firstMovableIndex = 2
172
- const lastMovableIndex = nPts - 4
292
+ // We avoid moving the very first and last segments of the:
293
+ // movable indices i in [1 .. (nPts - 3)] inclusive (segment i connects P[i] -> P[i+1]).
294
+ const firstMovableIndex = 1
295
+ const lastMovableIndex = nPts - 3
173
296
 
174
297
  const movableSegments: MovableSegment[] = []
175
298
  const movableIdx: number[] = []
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.28",
4
+ "version": "0.0.30",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",
@@ -88,7 +88,7 @@ export const inputProblem: InputProblem = {
88
88
  ],
89
89
  netConnections: [],
90
90
  availableNetLabelOrientations: {},
91
- maxMspPairDistance: 2,
91
+ maxMspPairDistance: 5,
92
92
  }
93
93
 
94
94
  export default () => <PipelineDebugger inputProblem={inputProblem} />