@tscircuit/schematic-trace-solver 0.0.27 → 0.0.29

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
@@ -154,6 +154,9 @@ interface MovableSegment {
154
154
  };
155
155
  }
156
156
 
157
+ type ChipPin = InputPin & {
158
+ chipId: ChipId;
159
+ };
157
160
  declare class SchematicTraceSingleLineSolver extends BaseSolver {
158
161
  pins: MspConnectionPair["pins"];
159
162
  inputProblem: InputProblem;
@@ -168,6 +171,7 @@ declare class SchematicTraceSingleLineSolver extends BaseSolver {
168
171
  x: number;
169
172
  y: number;
170
173
  }[] | null;
174
+ pinIdMap: Map<PinId, ChipPin>;
171
175
  constructor(params: {
172
176
  pins: MspConnectionPair["pins"];
173
177
  guidelines: Guideline[];
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 = [];
@@ -668,6 +735,86 @@ var visualizeGuidelines = ({
668
735
  return graphics;
669
736
  };
670
737
 
738
+ // lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/getRestrictedCenterLines.ts
739
+ var getRestrictedCenterLines = (params) => {
740
+ const { pins, inputProblem, pinIdMap, chipMap } = params;
741
+ const findAllDirectlyConnectedPins = (startPinId) => {
742
+ const visited = /* @__PURE__ */ new Set();
743
+ const queue = [startPinId];
744
+ visited.add(startPinId);
745
+ const directConns = inputProblem.directConnections || [];
746
+ while (queue.length) {
747
+ const cur = queue.shift();
748
+ for (const dc of directConns) {
749
+ if (dc.pinIds.includes(cur)) {
750
+ for (const p of dc.pinIds) {
751
+ if (!visited.has(p)) {
752
+ visited.add(p);
753
+ queue.push(p);
754
+ }
755
+ }
756
+ }
757
+ }
758
+ }
759
+ return visited;
760
+ };
761
+ const p0 = pins[0].pinId;
762
+ const p1 = pins[1].pinId;
763
+ const relatedPinIds = /* @__PURE__ */ new Set([
764
+ ...findAllDirectlyConnectedPins(p0),
765
+ ...findAllDirectlyConnectedPins(p1)
766
+ ]);
767
+ const restrictedCenterLines = /* @__PURE__ */ new Map();
768
+ const chipFacingMap = /* @__PURE__ */ new Map();
769
+ const chipsOfFacingPins = new Set(pins.map((p) => p.chipId));
770
+ for (const pinId of relatedPinIds) {
771
+ const pin = pinIdMap.get(pinId);
772
+ if (!pin) continue;
773
+ const chip = chipMap[pin.chipId];
774
+ if (!chip) continue;
775
+ const facing = pin._facingDirection ?? getPinDirection(pin, chip);
776
+ let entry = chipFacingMap.get(chip.chipId);
777
+ if (!entry) {
778
+ entry = { center: chip.center };
779
+ const counts = { xPos: 0, xNeg: 0, yPos: 0, yNeg: 0 };
780
+ for (const cp of chip.pins) {
781
+ const cpFacing = cp._facingDirection ?? getPinDirection(cp, chip);
782
+ if (cpFacing === "x+") counts.xPos++;
783
+ if (cpFacing === "x-") counts.xNeg++;
784
+ if (cpFacing === "y+") counts.yPos++;
785
+ if (cpFacing === "y-") counts.yNeg++;
786
+ }
787
+ entry.counts = counts;
788
+ chipFacingMap.set(chip.chipId, entry);
789
+ }
790
+ if (facing === "x+") entry.hasXPos = true;
791
+ if (facing === "x-") entry.hasXNeg = true;
792
+ if (facing === "y+") entry.hasYPos = true;
793
+ if (facing === "y-") entry.hasYNeg = true;
794
+ }
795
+ for (const [chipId, faces] of chipFacingMap) {
796
+ const axes = /* @__PURE__ */ new Set();
797
+ const rcl = { axes };
798
+ const counts = faces.counts;
799
+ const anySideHasMultiplePins = !!(counts && (counts.xPos > 1 || counts.xNeg > 1 || counts.yPos > 1 || counts.yNeg > 1));
800
+ const skipCenterRestriction = !anySideHasMultiplePins && chipsOfFacingPins.has(chipId);
801
+ if (!skipCenterRestriction) {
802
+ if (faces.hasXPos && faces.hasXNeg) {
803
+ rcl.x = faces.center.x;
804
+ axes.add("x");
805
+ }
806
+ if (faces.hasYPos && faces.hasYNeg) {
807
+ rcl.y = faces.center.y;
808
+ axes.add("y");
809
+ }
810
+ }
811
+ if (axes.size > 0) {
812
+ restrictedCenterLines.set(chipId, rcl);
813
+ }
814
+ }
815
+ return restrictedCenterLines;
816
+ };
817
+
671
818
  // lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver.ts
672
819
  var SchematicTraceSingleLineSolver = class extends BaseSolver {
673
820
  pins;
@@ -680,6 +827,8 @@ var SchematicTraceSingleLineSolver = class extends BaseSolver {
680
827
  queuedCandidatePaths;
681
828
  chipObstacleSpatialIndex;
682
829
  solvedTracePath = null;
830
+ // map of pinId -> pin (with chipId attached)
831
+ pinIdMap = /* @__PURE__ */ new Map();
683
832
  constructor(params) {
684
833
  super();
685
834
  this.pins = params.pins;
@@ -690,6 +839,11 @@ var SchematicTraceSingleLineSolver = class extends BaseSolver {
690
839
  if (!this.inputProblem._chipObstacleSpatialIndex) {
691
840
  this.inputProblem._chipObstacleSpatialIndex = this.chipObstacleSpatialIndex;
692
841
  }
842
+ for (const chip of this.inputProblem.chips) {
843
+ for (const pin of chip.pins) {
844
+ this.pinIdMap.set(pin.pinId, { ...pin, chipId: chip.chipId });
845
+ }
846
+ }
693
847
  for (const pin of this.pins) {
694
848
  if (!pin._facingDirection) {
695
849
  const chip = this.chipMap[pin.chipId];
@@ -746,10 +900,25 @@ var SchematicTraceSingleLineSolver = class extends BaseSolver {
746
900
  return;
747
901
  }
748
902
  const nextCandidatePath = this.queuedCandidatePaths.shift();
903
+ const restrictedCenterLines = getRestrictedCenterLines({
904
+ pins: this.pins,
905
+ inputProblem: this.inputProblem,
906
+ pinIdMap: this.pinIdMap,
907
+ chipMap: this.chipMap
908
+ });
749
909
  for (let i = 0; i < nextCandidatePath.length - 1; i++) {
750
910
  const start = nextCandidatePath[i];
751
911
  const end = nextCandidatePath[i + 1];
752
912
  let excludeChipIds = [];
913
+ const EPS2 = 1e-9;
914
+ for (const [, rcl] of restrictedCenterLines) {
915
+ if (rcl.axes.has("x") && typeof rcl.x === "number") {
916
+ if ((start.x - rcl.x) * (end.x - rcl.x) < -EPS2) return;
917
+ }
918
+ if (rcl.axes.has("y") && typeof rcl.y === "number") {
919
+ if ((start.y - rcl.y) * (end.y - rcl.y) < -EPS2) return;
920
+ }
921
+ }
753
922
  const isStartPin = this.pins.some(
754
923
  (pin) => Math.abs(pin.x - start.x) < 1e-10 && Math.abs(pin.y - start.y) < 1e-10
755
924
  );
@@ -764,12 +933,12 @@ var SchematicTraceSingleLineSolver = class extends BaseSolver {
764
933
  const bounds = getInputChipBounds(this.chipMap[startPin.chipId]);
765
934
  const dx = end.x - start.x;
766
935
  const dy = end.y - start.y;
767
- const EPS2 = 1e-9;
936
+ const EPS3 = 1e-9;
768
937
  const onLeft = Math.abs(start.x - bounds.minX) < 1e-9;
769
938
  const onRight = Math.abs(start.x - bounds.maxX) < 1e-9;
770
939
  const onBottom = Math.abs(start.y - bounds.minY) < 1e-9;
771
940
  const onTop = Math.abs(start.y - bounds.maxY) < 1e-9;
772
- const entersInterior = onLeft && dx > EPS2 || onRight && dx < -EPS2 || onBottom && dy > EPS2 || onTop && dy < -EPS2;
941
+ const entersInterior = onLeft && dx > EPS3 || onRight && dx < -EPS3 || onBottom && dy > EPS3 || onTop && dy < -EPS3;
773
942
  if (entersInterior) return;
774
943
  if (!excludeChipIds.includes(startPin.chipId)) {
775
944
  excludeChipIds.push(startPin.chipId);
@@ -784,12 +953,12 @@ var SchematicTraceSingleLineSolver = class extends BaseSolver {
784
953
  const bounds = getInputChipBounds(this.chipMap[endPin.chipId]);
785
954
  const dx = start.x - end.x;
786
955
  const dy = start.y - end.y;
787
- const EPS2 = 1e-9;
956
+ const EPS3 = 1e-9;
788
957
  const onLeft = Math.abs(end.x - bounds.minX) < 1e-9;
789
958
  const onRight = Math.abs(end.x - bounds.maxX) < 1e-9;
790
959
  const onBottom = Math.abs(end.y - bounds.minY) < 1e-9;
791
960
  const onTop = Math.abs(end.y - bounds.maxY) < 1e-9;
792
- const entersInterior = onLeft && dx > EPS2 || onRight && dx < -EPS2 || onBottom && dy > EPS2 || onTop && dy < -EPS2;
961
+ const entersInterior = onLeft && dx > EPS3 || onRight && dx < -EPS3 || onBottom && dy > EPS3 || onTop && dy < -EPS3;
793
962
  if (entersInterior) return;
794
963
  if (!excludeChipIds.includes(endPin.chipId)) {
795
964
  excludeChipIds.push(endPin.chipId);
@@ -4,7 +4,13 @@ import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
4
4
  import type { Guideline } from "lib/solvers/GuidelinesSolver/GuidelinesSolver"
5
5
  import type { MspConnectionPair } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver"
6
6
  import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
7
- import type { InputChip, InputProblem } from "lib/types/InputProblem"
7
+ import type {
8
+ ChipId,
9
+ InputChip,
10
+ InputPin,
11
+ InputProblem,
12
+ PinId,
13
+ } from "lib/types/InputProblem"
8
14
  import { calculateElbow } from "calculate-elbow"
9
15
  import { getPinDirection } from "./getPinDirection"
10
16
  import {
@@ -15,6 +21,9 @@ import type { Point } from "@tscircuit/math-utils"
15
21
  import { visualizeGuidelines } from "lib/solvers/GuidelinesSolver/visualizeGuidelines"
16
22
  import { getInputChipBounds } from "lib/solvers/GuidelinesSolver/getInputChipBounds"
17
23
  import { getColorFromString } from "lib/utils/getColorFromString"
24
+ import { getRestrictedCenterLines } from "./getRestrictedCenterLines"
25
+
26
+ type ChipPin = InputPin & { chipId: ChipId }
18
27
 
19
28
  export class SchematicTraceSingleLineSolver extends BaseSolver {
20
29
  pins: MspConnectionPair["pins"]
@@ -31,6 +40,9 @@ export class SchematicTraceSingleLineSolver extends BaseSolver {
31
40
 
32
41
  solvedTracePath: { x: number; y: number }[] | null = null
33
42
 
43
+ // map of pinId -> pin (with chipId attached)
44
+ pinIdMap: Map<PinId, ChipPin> = new Map()
45
+
34
46
  constructor(params: {
35
47
  pins: MspConnectionPair["pins"]
36
48
  guidelines: Guideline[]
@@ -51,6 +63,13 @@ export class SchematicTraceSingleLineSolver extends BaseSolver {
51
63
  this.chipObstacleSpatialIndex
52
64
  }
53
65
 
66
+ // Build a lookup of all pins by id and attach chipId to each pin entry
67
+ for (const chip of this.inputProblem.chips) {
68
+ for (const pin of chip.pins) {
69
+ this.pinIdMap.set(pin.pinId, { ...pin, chipId: chip.chipId })
70
+ }
71
+ }
72
+
54
73
  for (const pin of this.pins) {
55
74
  if (!pin._facingDirection) {
56
75
  const chip = this.chipMap[pin.chipId]
@@ -118,6 +137,13 @@ export class SchematicTraceSingleLineSolver extends BaseSolver {
118
137
 
119
138
  const nextCandidatePath = this.queuedCandidatePaths.shift()!
120
139
 
140
+ const restrictedCenterLines = getRestrictedCenterLines({
141
+ pins: this.pins,
142
+ inputProblem: this.inputProblem,
143
+ pinIdMap: this.pinIdMap,
144
+ chipMap: this.chipMap,
145
+ })
146
+
121
147
  for (let i = 0; i < nextCandidatePath.length - 1; i++) {
122
148
  const start = nextCandidatePath[i]
123
149
  const end = nextCandidatePath[i + 1]
@@ -125,6 +151,19 @@ export class SchematicTraceSingleLineSolver extends BaseSolver {
125
151
  // Determine which chips to exclude for this specific segment
126
152
  let excludeChipIds: string[] = []
127
153
 
154
+ // If this segment would cross any restricted center line, reject the candidate path.
155
+ const EPS = 1e-9
156
+ for (const [, rcl] of restrictedCenterLines) {
157
+ if (rcl.axes.has("x") && typeof rcl.x === "number") {
158
+ // segment strictly crosses vertical center line
159
+ if ((start.x - rcl.x) * (end.x - rcl.x) < -EPS) return
160
+ }
161
+ if (rcl.axes.has("y") && typeof rcl.y === "number") {
162
+ // segment strictly crosses horizontal center line
163
+ if ((start.y - rcl.y) * (end.y - rcl.y) < -EPS) return
164
+ }
165
+ }
166
+
128
167
  // Always exclude chips that contain the start or end points of this segment
129
168
  // if those points are actually pin locations
130
169
  const isStartPin = this.pins.some(
@@ -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[] = []
@@ -0,0 +1,139 @@
1
+ import type {
2
+ ChipId,
3
+ InputChip,
4
+ InputPin,
5
+ InputProblem,
6
+ PinId,
7
+ } from "lib/types/InputProblem"
8
+ import { getPinDirection } from "./getPinDirection"
9
+
10
+ type ChipPin = InputPin & { chipId: ChipId }
11
+
12
+ /**
13
+ * A line that should not be crossed by a trace segment.
14
+ * This is used to prevent traces from crossing through the center of a chip when
15
+ * connecting pins on that same chip. Right now this is only enabled when a chip
16
+ * has multiple pins on at least one side.
17
+ */
18
+ export type RestrictedCenterLine = {
19
+ x?: number
20
+ y?: number
21
+ axes: Set<"x" | "y">
22
+ }
23
+
24
+ export const getRestrictedCenterLines = (params: {
25
+ pins: Array<InputPin & { chipId: string }>
26
+ inputProblem: InputProblem
27
+ pinIdMap: Map<PinId, ChipPin>
28
+ chipMap: Record<string, InputChip>
29
+ }): Map<ChipId, RestrictedCenterLine> => {
30
+ const { pins, inputProblem, pinIdMap, chipMap } = params
31
+
32
+ // Determine set of related pin IDs (closure over directConnections) for both endpoints
33
+ const findAllDirectlyConnectedPins = (startPinId: string) => {
34
+ const visited = new Set<string>()
35
+ const queue: string[] = [startPinId]
36
+ visited.add(startPinId)
37
+ const directConns = inputProblem.directConnections || []
38
+ while (queue.length) {
39
+ const cur = queue.shift()!
40
+ for (const dc of directConns) {
41
+ if (dc.pinIds.includes(cur)) {
42
+ for (const p of dc.pinIds) {
43
+ if (!visited.has(p)) {
44
+ visited.add(p)
45
+ queue.push(p)
46
+ }
47
+ }
48
+ }
49
+ }
50
+ }
51
+ return visited
52
+ }
53
+
54
+ const p0 = pins[0].pinId
55
+ const p1 = pins[1].pinId
56
+ const relatedPinIds = new Set<string>([
57
+ ...findAllDirectlyConnectedPins(p0),
58
+ ...findAllDirectlyConnectedPins(p1),
59
+ ])
60
+
61
+ const restrictedCenterLines = new Map<ChipId, RestrictedCenterLine>()
62
+
63
+ // Collect facing-signs per chip
64
+ const chipFacingMap = new Map<
65
+ string,
66
+ {
67
+ hasXPos?: boolean
68
+ hasXNeg?: boolean
69
+ hasYPos?: boolean
70
+ hasYNeg?: boolean
71
+ center: { x: number; y: number }
72
+ counts?: { xPos: number; xNeg: number; yPos: number; yNeg: number }
73
+ }
74
+ >()
75
+
76
+ const chipsOfFacingPins = new Set<string>(pins.map((p) => p.chipId))
77
+
78
+ for (const pinId of relatedPinIds) {
79
+ const pin = pinIdMap.get(pinId)
80
+ if (!pin) continue
81
+ const chip = chipMap[pin.chipId]
82
+ if (!chip) continue
83
+ const facing = pin._facingDirection ?? getPinDirection(pin, chip)
84
+ let entry = chipFacingMap.get(chip.chipId)
85
+ if (!entry) {
86
+ entry = { center: chip.center }
87
+
88
+ const counts = { xPos: 0, xNeg: 0, yPos: 0, yNeg: 0 }
89
+ for (const cp of chip.pins) {
90
+ const cpFacing = cp._facingDirection ?? getPinDirection(cp, chip)
91
+ if (cpFacing === "x+") counts.xPos++
92
+ if (cpFacing === "x-") counts.xNeg++
93
+ if (cpFacing === "y+") counts.yPos++
94
+ if (cpFacing === "y-") counts.yNeg++
95
+ }
96
+ entry.counts = counts
97
+
98
+ chipFacingMap.set(chip.chipId, entry)
99
+ }
100
+ if (facing === "x+") entry.hasXPos = true
101
+ if (facing === "x-") entry.hasXNeg = true
102
+ if (facing === "y+") entry.hasYPos = true
103
+ if (facing === "y-") entry.hasYNeg = true
104
+ }
105
+
106
+ // Only mark a center as restricted on an axis if both signs for that axis
107
+ // are present among related pins on the chip.
108
+ for (const [chipId, faces] of chipFacingMap) {
109
+ const axes = new Set<"x" | "y">()
110
+ const rcl: RestrictedCenterLine = { axes }
111
+
112
+ // determine whether any side on this chip has more than one pin
113
+ const counts = faces.counts
114
+ const anySideHasMultiplePins = !!(
115
+ counts &&
116
+ (counts.xPos > 1 || counts.xNeg > 1 || counts.yPos > 1 || counts.yNeg > 1)
117
+ )
118
+
119
+ const skipCenterRestriction =
120
+ !anySideHasMultiplePins && chipsOfFacingPins.has(chipId)
121
+
122
+ if (!skipCenterRestriction) {
123
+ if (faces.hasXPos && faces.hasXNeg) {
124
+ rcl.x = faces.center.x
125
+ axes.add("x")
126
+ }
127
+ if (faces.hasYPos && faces.hasYNeg) {
128
+ rcl.y = faces.center.y
129
+ axes.add("y")
130
+ }
131
+ }
132
+
133
+ if (axes.size > 0) {
134
+ restrictedCenterLines.set(chipId, rcl)
135
+ }
136
+ }
137
+
138
+ return restrictedCenterLines
139
+ }
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.27",
4
+ "version": "0.0.29",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",