@tscircuit/schematic-trace-solver 0.0.114 → 0.0.116

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 (27) hide show
  1. package/dist/index.d.ts +22 -9
  2. package/dist/index.js +248 -3
  3. package/lib/solvers/SameNetJunctionAlignmentSolver/SameNetJunctionAlignmentSolver.ts +71 -0
  4. package/lib/solvers/SameNetJunctionAlignmentSolver/alignSameNetJunctions.ts +223 -0
  5. package/lib/solvers/SameNetJunctionAlignmentSolver/pathIntersectsAnyNetLabel.ts +22 -0
  6. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +9 -9
  7. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect.ts +25 -9
  8. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +17 -0
  9. package/package.json +2 -2
  10. package/site/bug-reports/bug-report-20260730T061837Z.page.tsx +4 -0
  11. package/tests/bug-reports/bug-report-20260721T221026Z/__snapshots__/bug-report-20260721T221026Z.snap.svg +1 -1
  12. package/tests/bug-reports/bug-report-20260728T144234Z/__snapshots__/bug-report-20260728T144234Z.snap.svg +1 -1
  13. package/tests/bug-reports/bug-report-20260728T144234Z/bug-report-20260728T144234Z.test.ts +6 -0
  14. package/tests/bug-reports/bug-report-20260730T061837Z/__snapshots__/bug-report-20260730T061837Z.snap.svg +123 -0
  15. package/tests/bug-reports/bug-report-20260730T061837Z/bug-report-20260730T061837Z.json +614 -0
  16. package/tests/bug-reports/bug-report-20260730T061837Z/bug-report-20260730T061837Z.test.ts +34 -0
  17. package/tests/repros/__snapshots__/board-15984-same-net-junction.snap.svg +110 -0
  18. package/tests/repros/__snapshots__/repro-type-c-16pin-self-connection.snap.svg +54 -0
  19. package/tests/repros/__snapshots__/repro-type-c-16pin-to-resistor.snap.svg +56 -0
  20. package/tests/repros/__snapshots__/repro48-555-timer-vcc-rail-label.snap.svg +87 -0
  21. package/tests/repros/assets/board-15984-schematic-trace-input.json +518 -0
  22. package/tests/repros/assets/repro48-555-timer-vcc-rail-label.input.json +276 -0
  23. package/tests/repros/board-15984-same-net-junction.test.ts +30 -0
  24. package/tests/repros/repro-type-c-16pin-self-connection.test.ts +97 -0
  25. package/tests/repros/repro-type-c-16pin-to-resistor.test.ts +120 -0
  26. package/tests/repros/repro48-555-timer-vcc-rail-label.test.ts +23 -0
  27. package/tests/solvers/SchematicTraceSingleLineSolver2/text-box-obstacle-gap.test.ts +43 -0
package/dist/index.d.ts CHANGED
@@ -146,17 +146,11 @@ type RectPadding = {
146
146
  maxY?: number;
147
147
  };
148
148
 
149
- type RectBounds = {
150
- minX: number;
151
- minY: number;
152
- maxX: number;
153
- maxY: number;
154
- };
155
- type ChipObstacleRect = RectBounds & {
149
+ type ChipObstacleRect = Bounds & {
156
150
  kind: "chip";
157
151
  chipId: string;
158
152
  };
159
- type TextBoxObstacleRect = RectBounds & {
153
+ type TextBoxObstacleRect = Bounds & {
160
154
  kind: "text_box";
161
155
  textBox: NonNullable<InputProblem["textBoxes"]>[number];
162
156
  };
@@ -1053,6 +1047,24 @@ declare class UnroutedTraceRecoverySolver extends BaseSolver {
1053
1047
  visualize(): graphics_debug.GraphicsObject;
1054
1048
  }
1055
1049
 
1050
+ interface SameNetJunctionAlignmentSolverInput {
1051
+ inputProblem: InputProblem;
1052
+ traces: SolvedTracePath[];
1053
+ netLabelPlacements: NetLabelPlacement[];
1054
+ }
1055
+ /** Turns separate same-net load traces into one shared rail with short junction branches. */
1056
+ declare class SameNetJunctionAlignmentSolver extends BaseSolver {
1057
+ private input;
1058
+ outputTraces: SolvedTracePath[];
1059
+ constructor(input: SameNetJunctionAlignmentSolverInput);
1060
+ _step(): void;
1061
+ getOutput(): {
1062
+ traces: SolvedTracePath[];
1063
+ netLabelPlacements: NetLabelPlacement[];
1064
+ };
1065
+ visualize(): GraphicsObject;
1066
+ }
1067
+
1056
1068
  /**
1057
1069
  * Pipeline solver that runs a series of solvers to find the best schematic layout.
1058
1070
  * Coordinates the entire layout process from chip partitioning through final packing.
@@ -1087,13 +1099,14 @@ declare class SchematicTracePipelineSolver extends BaseSolver {
1087
1099
  netLabelTraceCollisionSolver?: NetLabelTraceCollisionSolver;
1088
1100
  traceCleanupSolver2?: TraceCleanupSolver;
1089
1101
  netLabelNetLabelCollisionSolver?: NetLabelNetLabelCollisionSolver;
1102
+ sameNetJunctionAlignmentSolver?: SameNetJunctionAlignmentSolver;
1090
1103
  startTimeOfPhase: Record<string, number>;
1091
1104
  endTimeOfPhase: Record<string, number>;
1092
1105
  timeSpentOnPhase: Record<string, number>;
1093
1106
  firstIterationOfPhase: Record<string, number>;
1094
1107
  inputProblem: InputProblem;
1095
1108
  hideRatsNet: boolean;
1096
- pipelineDef: (PipelineStep<typeof MspConnectionPairSolver> | PipelineStep<typeof SchematicTraceLinesSolver> | PipelineStep<typeof LongDistancePairSolver> | PipelineStep<typeof UnroutedTraceRecoverySolver> | PipelineStep<typeof TraceOverlapShiftSolver> | PipelineStep<typeof NetLabelPlacementSolver> | PipelineStep<typeof TraceLabelOverlapAvoidanceSolver> | PipelineStep<typeof TraceCleanupSolver> | PipelineStep<typeof Example28Solver> | PipelineStep<typeof AvailableNetOrientationSolver> | PipelineStep<typeof RailNetLabelCornerPlacementSolver> | PipelineStep<typeof TraceAnchoredNetLabelOverlapSolver> | PipelineStep<typeof NetLabelTraceCollisionSolver> | PipelineStep<typeof NetLabelNetLabelCollisionSolver>)[];
1109
+ pipelineDef: (PipelineStep<typeof MspConnectionPairSolver> | PipelineStep<typeof SchematicTraceLinesSolver> | PipelineStep<typeof LongDistancePairSolver> | PipelineStep<typeof UnroutedTraceRecoverySolver> | PipelineStep<typeof TraceOverlapShiftSolver> | PipelineStep<typeof NetLabelPlacementSolver> | PipelineStep<typeof TraceLabelOverlapAvoidanceSolver> | PipelineStep<typeof TraceCleanupSolver> | PipelineStep<typeof Example28Solver> | PipelineStep<typeof AvailableNetOrientationSolver> | PipelineStep<typeof RailNetLabelCornerPlacementSolver> | PipelineStep<typeof TraceAnchoredNetLabelOverlapSolver> | PipelineStep<typeof NetLabelTraceCollisionSolver> | PipelineStep<typeof NetLabelNetLabelCollisionSolver> | PipelineStep<typeof SameNetJunctionAlignmentSolver>)[];
1097
1110
  constructor(inputProblem: InputProblem, opts?: Options);
1098
1111
  getConstructorParams(): ConstructorParameters<typeof SchematicTracePipelineSolver>;
1099
1112
  currentPipelineStepIndex: number;
package/dist/index.js CHANGED
@@ -1116,6 +1116,9 @@ var generateInternalSegmentCollisionDetours = ({
1116
1116
  ]).filter(hasOnlyNonzeroOrthogonalSegments);
1117
1117
  };
1118
1118
 
1119
+ // lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect.ts
1120
+ import { boundsDistance } from "@tscircuit/math-utils";
1121
+
1119
1122
  // lib/solvers/GuidelinesSolver/getInputChipBounds.ts
1120
1123
  function getInputChipBounds(chip) {
1121
1124
  const halfWidth = chip.width / 2;
@@ -1129,6 +1132,7 @@ function getInputChipBounds(chip) {
1129
1132
  }
1130
1133
 
1131
1134
  // lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect.ts
1135
+ var MIN_ROUTABLE_OBSTACLE_GAP = 0.05;
1132
1136
  var isTextBoxObstacle = (obstacle) => obstacle.kind === "text_box";
1133
1137
  var chipToRect = (chip) => {
1134
1138
  const b = getInputChipBounds(chip);
@@ -1137,7 +1141,17 @@ var chipToRect = (chip) => {
1137
1141
  var getObstacleRects = (problem, opts = {}) => {
1138
1142
  const chipRects = problem.chips.map(chipToRect);
1139
1143
  const textBoxRects = (problem.textBoxes ?? []).map((textBox) => {
1140
- const b = getTextBoxBounds(textBox, opts.textBoxPadding);
1144
+ const textBounds = getTextBoxBounds(textBox, opts.textBoxPadding);
1145
+ const attachedChipBounds = chipRects.find(
1146
+ (chip) => chip.chipId === textBox.chipId
1147
+ );
1148
+ const gap = attachedChipBounds ? boundsDistance(textBounds, attachedChipBounds) : Number.POSITIVE_INFINITY;
1149
+ const b = attachedChipBounds && gap > 0 && gap < MIN_ROUTABLE_OBSTACLE_GAP ? {
1150
+ minX: Math.min(textBounds.minX, attachedChipBounds.maxX),
1151
+ maxX: Math.max(textBounds.maxX, attachedChipBounds.minX),
1152
+ minY: Math.min(textBounds.minY, attachedChipBounds.maxY),
1153
+ maxY: Math.max(textBounds.maxY, attachedChipBounds.minY)
1154
+ } : textBounds;
1141
1155
  return {
1142
1156
  kind: "text_box",
1143
1157
  textBox,
@@ -1207,9 +1221,9 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
1207
1221
  this.textObstacles = new Set(this.obstacles.filter(isTextBoxObstacle));
1208
1222
  const endpointChipIds = new Set(this.pins.map((pin) => pin.chipId));
1209
1223
  this.endpointTextObstacles = new Set(
1210
- endpointChipIds.size > 1 ? this.obstacles.filter(isTextBoxObstacle).filter(
1224
+ this.obstacles.filter(isTextBoxObstacle).filter(
1211
1225
  (obstacle) => obstacle.textBox.chipId !== void 0 && endpointChipIds.has(obstacle.textBox.chipId)
1212
- ) : []
1226
+ )
1213
1227
  );
1214
1228
  const [pin1, pin2] = this.pins;
1215
1229
  const directShortPath = calculateDirectShortPath(pin1, pin2);
@@ -10703,6 +10717,222 @@ var UnroutedTraceRecoverySolver = class extends BaseSolver {
10703
10717
  }
10704
10718
  };
10705
10719
 
10720
+ // lib/solvers/SameNetJunctionAlignmentSolver/pathIntersectsAnyNetLabel.ts
10721
+ var pathIntersectsAnyNetLabel = ({
10722
+ path,
10723
+ netLabelPlacements
10724
+ }) => {
10725
+ for (const label of netLabelPlacements) {
10726
+ const labelBounds = getRectBounds(label.center, label.width, label.height);
10727
+ for (let index = 0; index < path.length - 1; index++) {
10728
+ if (segmentIntersectsRect2(path[index], path[index + 1], labelBounds)) {
10729
+ return true;
10730
+ }
10731
+ }
10732
+ }
10733
+ return false;
10734
+ };
10735
+
10736
+ // lib/solvers/SameNetJunctionAlignmentSolver/alignSameNetJunctions.ts
10737
+ var MAX_ALIGNED_LOAD_PIN_OFFSET = 0.2;
10738
+ var getSharedPin = ({
10739
+ donorTrace,
10740
+ branchTrace
10741
+ }) => {
10742
+ const branchPinIds = new Set(branchTrace.pins.map((pin) => pin.pinId));
10743
+ return donorTrace.pins.find((pin) => branchPinIds.has(pin.pinId)) ?? null;
10744
+ };
10745
+ var getOtherPin = ({
10746
+ trace,
10747
+ sharedPin
10748
+ }) => trace.pins.find((pin) => pin.pinId !== sharedPin.pinId) ?? null;
10749
+ var getLongestHorizontalSegment = (trace) => {
10750
+ let longest = null;
10751
+ for (let index = 0; index < trace.tracePath.length - 1; index++) {
10752
+ const start = trace.tracePath[index];
10753
+ const end = trace.tracePath[index + 1];
10754
+ if (!isHorizontal2(start, end)) continue;
10755
+ if (!longest || Math.abs(end.x - start.x) > Math.abs(longest.end.x - longest.start.x)) {
10756
+ longest = { start, end };
10757
+ }
10758
+ }
10759
+ return longest;
10760
+ };
10761
+ var getJunctionPoint = ({
10762
+ segment,
10763
+ sharedPin
10764
+ }) => {
10765
+ const startDistance = Math.abs(segment.start.x - sharedPin.x);
10766
+ const endDistance = Math.abs(segment.end.x - sharedPin.x);
10767
+ if (startDistance <= endDistance) return segment.start;
10768
+ return segment.end;
10769
+ };
10770
+ var railIsOnFacingSide = ({
10771
+ railY,
10772
+ pin
10773
+ }) => {
10774
+ if (pin._facingDirection === "y+") return railY > pin.y;
10775
+ return false;
10776
+ };
10777
+ var getAlignedBranchPath = ({
10778
+ donorTrace,
10779
+ branchTrace
10780
+ }) => {
10781
+ const sharedPin = getSharedPin({ donorTrace, branchTrace });
10782
+ if (!sharedPin) return null;
10783
+ const donorOtherPin = getOtherPin({ trace: donorTrace, sharedPin });
10784
+ if (!donorOtherPin) return null;
10785
+ const otherPin = getOtherPin({ trace: branchTrace, sharedPin });
10786
+ if (!otherPin) return null;
10787
+ if (Math.abs(sharedPin.y - otherPin.y) > MAX_ALIGNED_LOAD_PIN_OFFSET) {
10788
+ return null;
10789
+ }
10790
+ const donorRail = getLongestHorizontalSegment(donorTrace);
10791
+ if (!donorRail) return null;
10792
+ const branchRail = getLongestHorizontalSegment(branchTrace);
10793
+ if (branchRail && nearlyEqual(branchRail.start.y, donorRail.start.y)) {
10794
+ return null;
10795
+ }
10796
+ if (!railIsOnFacingSide({ railY: donorRail.start.y, pin: otherPin })) {
10797
+ return null;
10798
+ }
10799
+ const junction = getJunctionPoint({ segment: donorRail, sharedPin });
10800
+ const extendsDonorRail = donorOtherPin.x < junction.x && otherPin.x > junction.x || donorOtherPin.x > junction.x && otherPin.x < junction.x;
10801
+ if (!extendsDonorRail) return null;
10802
+ const sharedToOther = simplifyPath([
10803
+ { x: sharedPin.x, y: sharedPin.y },
10804
+ { x: junction.x, y: sharedPin.y },
10805
+ { x: junction.x, y: junction.y },
10806
+ { x: otherPin.x, y: junction.y },
10807
+ { x: otherPin.x, y: otherPin.y }
10808
+ ]);
10809
+ if (branchTrace.pins[0].pinId === sharedPin.pinId) return sharedToOther;
10810
+ return [...sharedToOther].reverse();
10811
+ };
10812
+ var candidateIsClear = ({
10813
+ candidateTrace,
10814
+ traces,
10815
+ inputProblem,
10816
+ netLabelPlacements
10817
+ }) => {
10818
+ const obstacles = getObstacleRects(inputProblem);
10819
+ if (isPathCollidingWithObstacles(candidateTrace.tracePath, obstacles)) {
10820
+ return false;
10821
+ }
10822
+ const otherNetTraces = traces.filter(
10823
+ (trace) => trace.globalConnNetId !== candidateTrace.globalConnNetId
10824
+ );
10825
+ if (doesPathCoincideWithTraces(candidateTrace.tracePath, otherNetTraces)) {
10826
+ return false;
10827
+ }
10828
+ return !pathIntersectsAnyNetLabel({
10829
+ path: candidateTrace.tracePath,
10830
+ netLabelPlacements
10831
+ });
10832
+ };
10833
+ var alignSameNetJunctions = ({
10834
+ inputProblem,
10835
+ traces,
10836
+ netLabelPlacements
10837
+ }) => {
10838
+ let outputTraces = [...traces];
10839
+ const alignedBranchTraceIds = /* @__PURE__ */ new Set();
10840
+ let alignedJunctionCount = 0;
10841
+ for (const donorTraceId of traces.map((trace) => trace.mspPairId)) {
10842
+ const donorTrace = outputTraces.find(
10843
+ (trace) => trace.mspPairId === donorTraceId
10844
+ );
10845
+ for (const branchTrace of outputTraces) {
10846
+ if (alignedBranchTraceIds.has(branchTrace.mspPairId)) continue;
10847
+ if (donorTrace.mspPairId === branchTrace.mspPairId) continue;
10848
+ if (donorTrace.globalConnNetId !== branchTrace.globalConnNetId) continue;
10849
+ const candidatePath = getAlignedBranchPath({ donorTrace, branchTrace });
10850
+ if (!candidatePath) continue;
10851
+ const candidateTrace = { ...branchTrace, tracePath: candidatePath };
10852
+ const originalPair = [donorTrace, branchTrace];
10853
+ const candidatePair = [donorTrace, candidateTrace];
10854
+ const removesVisibleSegment = getVisibleTraceSegmentCount(candidatePair) < getVisibleTraceSegmentCount(originalPair);
10855
+ const shortensVisibleTrace = getVisibleTraceLength(candidatePair) < getVisibleTraceLength(originalPair) && !nearlyEqual(
10856
+ getVisibleTraceLength(candidatePair),
10857
+ getVisibleTraceLength(originalPair)
10858
+ );
10859
+ if (!removesVisibleSegment && !shortensVisibleTrace) {
10860
+ continue;
10861
+ }
10862
+ if (!candidateIsClear({
10863
+ candidateTrace,
10864
+ traces: outputTraces,
10865
+ inputProblem,
10866
+ netLabelPlacements
10867
+ })) {
10868
+ continue;
10869
+ }
10870
+ outputTraces = outputTraces.map((trace) => {
10871
+ if (trace.mspPairId === branchTrace.mspPairId) return candidateTrace;
10872
+ return trace;
10873
+ });
10874
+ alignedBranchTraceIds.add(branchTrace.mspPairId);
10875
+ alignedJunctionCount++;
10876
+ }
10877
+ }
10878
+ return { traces: outputTraces, alignedJunctionCount };
10879
+ };
10880
+
10881
+ // lib/solvers/SameNetJunctionAlignmentSolver/SameNetJunctionAlignmentSolver.ts
10882
+ var SameNetJunctionAlignmentSolver = class extends BaseSolver {
10883
+ input;
10884
+ outputTraces;
10885
+ constructor(input) {
10886
+ super();
10887
+ this.input = input;
10888
+ this.outputTraces = input.traces;
10889
+ }
10890
+ _step() {
10891
+ const result = alignSameNetJunctions(this.input);
10892
+ this.outputTraces = result.traces;
10893
+ this.stats.alignedJunctionCount = result.alignedJunctionCount;
10894
+ this.solved = true;
10895
+ }
10896
+ getOutput() {
10897
+ return {
10898
+ traces: this.outputTraces,
10899
+ netLabelPlacements: this.input.netLabelPlacements
10900
+ };
10901
+ }
10902
+ visualize() {
10903
+ const graphics = visualizeInputProblem(this.input.inputProblem);
10904
+ graphics.lines ??= [];
10905
+ graphics.rects ??= [];
10906
+ graphics.points ??= [];
10907
+ for (const trace of this.outputTraces) {
10908
+ graphics.lines.push({
10909
+ points: trace.tracePath,
10910
+ strokeColor: "purple"
10911
+ });
10912
+ }
10913
+ for (const label of this.input.netLabelPlacements) {
10914
+ const labelRect = {
10915
+ center: label.center,
10916
+ width: label.width,
10917
+ height: label.height,
10918
+ fill: getColorFromString(label.globalConnNetId, 0.35),
10919
+ strokeColor: getColorFromString(label.globalConnNetId, 0.9),
10920
+ label: `netId: ${label.netId}
10921
+ globalConnNetId: ${label.globalConnNetId}`
10922
+ };
10923
+ graphics.rects.push(labelRect);
10924
+ graphics.points.push({
10925
+ x: label.anchorPoint.x,
10926
+ y: label.anchorPoint.y,
10927
+ color: getColorFromString(label.globalConnNetId, 0.9),
10928
+ label: `anchorPoint
10929
+ orientation: ${label.orientation}`
10930
+ });
10931
+ }
10932
+ return graphics;
10933
+ }
10934
+ };
10935
+
10706
10936
  // lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts
10707
10937
  function definePipelineStep(solverName, solverClass, getConstructorParams, opts = {}) {
10708
10938
  return {
@@ -10733,6 +10963,7 @@ var SchematicTracePipelineSolver = class extends BaseSolver {
10733
10963
  netLabelTraceCollisionSolver;
10734
10964
  traceCleanupSolver2;
10735
10965
  netLabelNetLabelCollisionSolver;
10966
+ sameNetJunctionAlignmentSolver;
10736
10967
  startTimeOfPhase;
10737
10968
  endTimeOfPhase;
10738
10969
  timeSpentOnPhase;
@@ -11005,6 +11236,20 @@ var SchematicTracePipelineSolver = class extends BaseSolver {
11005
11236
  netLabelPlacements: instance.netLabelTraceCollisionSolver.getOutput().netLabelPlacements
11006
11237
  }
11007
11238
  ]
11239
+ ),
11240
+ definePipelineStep(
11241
+ "sameNetJunctionAlignmentSolver",
11242
+ SameNetJunctionAlignmentSolver,
11243
+ (instance) => {
11244
+ const collisionOutput = instance.netLabelNetLabelCollisionSolver.getOutput();
11245
+ return [
11246
+ {
11247
+ inputProblem: instance.inputProblem,
11248
+ traces: instance.netLabelNetLabelCollisionSolver.traces,
11249
+ netLabelPlacements: collisionOutput.netLabelPlacements
11250
+ }
11251
+ ];
11252
+ }
11008
11253
  )
11009
11254
  ];
11010
11255
  constructor(inputProblem, opts) {
@@ -0,0 +1,71 @@
1
+ import type { GraphicsObject, Rect } from "graphics-debug"
2
+ import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
3
+ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
4
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
5
+ import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
6
+ import type { InputProblem } from "lib/types/InputProblem"
7
+ import { getColorFromString } from "lib/utils/getColorFromString"
8
+ import { alignSameNetJunctions } from "./alignSameNetJunctions"
9
+
10
+ interface SameNetJunctionAlignmentSolverInput {
11
+ inputProblem: InputProblem
12
+ traces: SolvedTracePath[]
13
+ netLabelPlacements: NetLabelPlacement[]
14
+ }
15
+
16
+ /** Turns separate same-net load traces into one shared rail with short junction branches. */
17
+ export class SameNetJunctionAlignmentSolver extends BaseSolver {
18
+ private input: SameNetJunctionAlignmentSolverInput
19
+ outputTraces: SolvedTracePath[]
20
+
21
+ constructor(input: SameNetJunctionAlignmentSolverInput) {
22
+ super()
23
+ this.input = input
24
+ this.outputTraces = input.traces
25
+ }
26
+
27
+ override _step() {
28
+ const result = alignSameNetJunctions(this.input)
29
+ this.outputTraces = result.traces
30
+ this.stats.alignedJunctionCount = result.alignedJunctionCount
31
+ this.solved = true
32
+ }
33
+
34
+ getOutput() {
35
+ return {
36
+ traces: this.outputTraces,
37
+ netLabelPlacements: this.input.netLabelPlacements,
38
+ }
39
+ }
40
+
41
+ override visualize(): GraphicsObject {
42
+ const graphics = visualizeInputProblem(this.input.inputProblem)
43
+ graphics.lines ??= []
44
+ graphics.rects ??= []
45
+ graphics.points ??= []
46
+ for (const trace of this.outputTraces) {
47
+ graphics.lines.push({
48
+ points: trace.tracePath,
49
+ strokeColor: "purple",
50
+ })
51
+ }
52
+ for (const label of this.input.netLabelPlacements) {
53
+ const labelRect: Rect & { strokeColor: string } = {
54
+ center: label.center,
55
+ width: label.width,
56
+ height: label.height,
57
+ fill: getColorFromString(label.globalConnNetId, 0.35),
58
+ strokeColor: getColorFromString(label.globalConnNetId, 0.9),
59
+ label: `netId: ${label.netId}\nglobalConnNetId: ${label.globalConnNetId}`,
60
+ }
61
+ graphics.rects.push(labelRect)
62
+ graphics.points.push({
63
+ x: label.anchorPoint.x,
64
+ y: label.anchorPoint.y,
65
+ color: getColorFromString(label.globalConnNetId, 0.9),
66
+ label: `anchorPoint\norientation: ${label.orientation}`,
67
+ })
68
+ }
69
+ return graphics
70
+ }
71
+ }
@@ -0,0 +1,223 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
3
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
4
+ import { isPathCollidingWithObstacles } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
5
+ import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
6
+ import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath"
7
+ import {
8
+ getVisibleTraceLength,
9
+ getVisibleTraceSegmentCount,
10
+ isHorizontal,
11
+ nearlyEqual,
12
+ } from "lib/solvers/TraceCleanupSolver/sameNetRailAlignment/geometry"
13
+ import type { InputPin, InputProblem } from "lib/types/InputProblem"
14
+ import { doesPathCoincideWithTraces } from "lib/utils/doesPathCoincideWithTraces"
15
+ import { pathIntersectsAnyNetLabel } from "./pathIntersectsAnyNetLabel"
16
+
17
+ interface AlignSameNetJunctionsInput {
18
+ inputProblem: InputProblem
19
+ traces: SolvedTracePath[]
20
+ netLabelPlacements: NetLabelPlacement[]
21
+ }
22
+
23
+ interface HorizontalSegment {
24
+ start: Point
25
+ end: Point
26
+ }
27
+
28
+ const MAX_ALIGNED_LOAD_PIN_OFFSET = 0.2
29
+
30
+ const getSharedPin = ({
31
+ donorTrace,
32
+ branchTrace,
33
+ }: {
34
+ donorTrace: SolvedTracePath
35
+ branchTrace: SolvedTracePath
36
+ }): (InputPin & { chipId: string }) | null => {
37
+ const branchPinIds = new Set(branchTrace.pins.map((pin) => pin.pinId))
38
+ return donorTrace.pins.find((pin) => branchPinIds.has(pin.pinId)) ?? null
39
+ }
40
+
41
+ const getOtherPin = ({
42
+ trace,
43
+ sharedPin,
44
+ }: {
45
+ trace: SolvedTracePath
46
+ sharedPin: InputPin
47
+ }) => trace.pins.find((pin) => pin.pinId !== sharedPin.pinId) ?? null
48
+
49
+ const getLongestHorizontalSegment = (
50
+ trace: SolvedTracePath,
51
+ ): HorizontalSegment | null => {
52
+ let longest: HorizontalSegment | null = null
53
+ for (let index = 0; index < trace.tracePath.length - 1; index++) {
54
+ const start = trace.tracePath[index]!
55
+ const end = trace.tracePath[index + 1]!
56
+ if (!isHorizontal(start, end)) continue
57
+ if (
58
+ !longest ||
59
+ Math.abs(end.x - start.x) > Math.abs(longest.end.x - longest.start.x)
60
+ ) {
61
+ longest = { start, end }
62
+ }
63
+ }
64
+ return longest
65
+ }
66
+
67
+ const getJunctionPoint = ({
68
+ segment,
69
+ sharedPin,
70
+ }: {
71
+ segment: HorizontalSegment
72
+ sharedPin: Point
73
+ }) => {
74
+ const startDistance = Math.abs(segment.start.x - sharedPin.x)
75
+ const endDistance = Math.abs(segment.end.x - sharedPin.x)
76
+ if (startDistance <= endDistance) return segment.start
77
+ return segment.end
78
+ }
79
+
80
+ const railIsOnFacingSide = ({
81
+ railY,
82
+ pin,
83
+ }: {
84
+ railY: number
85
+ pin: InputPin
86
+ }) => {
87
+ if (pin._facingDirection === "y+") return railY > pin.y
88
+ return false
89
+ }
90
+
91
+ const getAlignedBranchPath = ({
92
+ donorTrace,
93
+ branchTrace,
94
+ }: {
95
+ donorTrace: SolvedTracePath
96
+ branchTrace: SolvedTracePath
97
+ }): Point[] | null => {
98
+ const sharedPin = getSharedPin({ donorTrace, branchTrace })
99
+ if (!sharedPin) return null
100
+ const donorOtherPin = getOtherPin({ trace: donorTrace, sharedPin })
101
+ if (!donorOtherPin) return null
102
+ const otherPin = getOtherPin({ trace: branchTrace, sharedPin })
103
+ if (!otherPin) return null
104
+ if (Math.abs(sharedPin.y - otherPin.y) > MAX_ALIGNED_LOAD_PIN_OFFSET) {
105
+ return null
106
+ }
107
+
108
+ const donorRail = getLongestHorizontalSegment(donorTrace)
109
+ if (!donorRail) return null
110
+ const branchRail = getLongestHorizontalSegment(branchTrace)
111
+ if (branchRail && nearlyEqual(branchRail.start.y, donorRail.start.y)) {
112
+ return null
113
+ }
114
+ if (!railIsOnFacingSide({ railY: donorRail.start.y, pin: otherPin })) {
115
+ return null
116
+ }
117
+
118
+ const junction = getJunctionPoint({ segment: donorRail, sharedPin })
119
+ const extendsDonorRail =
120
+ (donorOtherPin.x < junction.x && otherPin.x > junction.x) ||
121
+ (donorOtherPin.x > junction.x && otherPin.x < junction.x)
122
+ if (!extendsDonorRail) return null
123
+
124
+ const sharedToOther = simplifyPath([
125
+ { x: sharedPin.x, y: sharedPin.y },
126
+ { x: junction.x, y: sharedPin.y },
127
+ { x: junction.x, y: junction.y },
128
+ { x: otherPin.x, y: junction.y },
129
+ { x: otherPin.x, y: otherPin.y },
130
+ ])
131
+
132
+ if (branchTrace.pins[0].pinId === sharedPin.pinId) return sharedToOther
133
+ return [...sharedToOther].reverse()
134
+ }
135
+
136
+ const candidateIsClear = ({
137
+ candidateTrace,
138
+ traces,
139
+ inputProblem,
140
+ netLabelPlacements,
141
+ }: {
142
+ candidateTrace: SolvedTracePath
143
+ traces: SolvedTracePath[]
144
+ inputProblem: InputProblem
145
+ netLabelPlacements: NetLabelPlacement[]
146
+ }) => {
147
+ const obstacles = getObstacleRects(inputProblem)
148
+ if (isPathCollidingWithObstacles(candidateTrace.tracePath, obstacles)) {
149
+ return false
150
+ }
151
+
152
+ const otherNetTraces = traces.filter(
153
+ (trace) => trace.globalConnNetId !== candidateTrace.globalConnNetId,
154
+ )
155
+ if (doesPathCoincideWithTraces(candidateTrace.tracePath, otherNetTraces)) {
156
+ return false
157
+ }
158
+
159
+ return !pathIntersectsAnyNetLabel({
160
+ path: candidateTrace.tracePath,
161
+ netLabelPlacements,
162
+ })
163
+ }
164
+
165
+ export const alignSameNetJunctions = ({
166
+ inputProblem,
167
+ traces,
168
+ netLabelPlacements,
169
+ }: AlignSameNetJunctionsInput) => {
170
+ let outputTraces = [...traces]
171
+ const alignedBranchTraceIds = new Set<string>()
172
+ let alignedJunctionCount = 0
173
+
174
+ // Reuse each aligned branch as the rail for the next load in the chain.
175
+ for (const donorTraceId of traces.map((trace) => trace.mspPairId)) {
176
+ const donorTrace = outputTraces.find(
177
+ (trace) => trace.mspPairId === donorTraceId,
178
+ )!
179
+ for (const branchTrace of outputTraces) {
180
+ if (alignedBranchTraceIds.has(branchTrace.mspPairId)) continue
181
+ if (donorTrace.mspPairId === branchTrace.mspPairId) continue
182
+ if (donorTrace.globalConnNetId !== branchTrace.globalConnNetId) continue
183
+
184
+ const candidatePath = getAlignedBranchPath({ donorTrace, branchTrace })
185
+ if (!candidatePath) continue
186
+ const candidateTrace = { ...branchTrace, tracePath: candidatePath }
187
+ const originalPair = [donorTrace, branchTrace]
188
+ const candidatePair = [donorTrace, candidateTrace]
189
+ const removesVisibleSegment =
190
+ getVisibleTraceSegmentCount(candidatePair) <
191
+ getVisibleTraceSegmentCount(originalPair)
192
+ const shortensVisibleTrace =
193
+ getVisibleTraceLength(candidatePair) <
194
+ getVisibleTraceLength(originalPair) &&
195
+ !nearlyEqual(
196
+ getVisibleTraceLength(candidatePair),
197
+ getVisibleTraceLength(originalPair),
198
+ )
199
+ if (!removesVisibleSegment && !shortensVisibleTrace) {
200
+ continue
201
+ }
202
+ if (
203
+ !candidateIsClear({
204
+ candidateTrace,
205
+ traces: outputTraces,
206
+ inputProblem,
207
+ netLabelPlacements,
208
+ })
209
+ ) {
210
+ continue
211
+ }
212
+
213
+ outputTraces = outputTraces.map((trace) => {
214
+ if (trace.mspPairId === branchTrace.mspPairId) return candidateTrace
215
+ return trace
216
+ })
217
+ alignedBranchTraceIds.add(branchTrace.mspPairId)
218
+ alignedJunctionCount++
219
+ }
220
+ }
221
+
222
+ return { traces: outputTraces, alignedJunctionCount }
223
+ }
@@ -0,0 +1,22 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
3
+ import { segmentIntersectsRect } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/collisions"
4
+ import { getRectBounds } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
5
+
6
+ export const pathIntersectsAnyNetLabel = ({
7
+ path,
8
+ netLabelPlacements,
9
+ }: {
10
+ path: Point[]
11
+ netLabelPlacements: NetLabelPlacement[]
12
+ }) => {
13
+ for (const label of netLabelPlacements) {
14
+ const labelBounds = getRectBounds(label.center, label.width, label.height)
15
+ for (let index = 0; index < path.length - 1; index++) {
16
+ if (segmentIntersectsRect(path[index], path[index + 1], labelBounds)) {
17
+ return true
18
+ }
19
+ }
20
+ }
21
+ return false
22
+ }