@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 +4 -0
- package/dist/index.js +177 -8
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver.ts +40 -1
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/generateElbowVariants.ts +129 -6
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/getRestrictedCenterLines.ts +139 -0
- package/package.json +1 -1
- package/site/SchematicTracePipelineSolver/SchematicTracePipelineSolver01.page.tsx +143 -0
- package/site/examples/example08.page.tsx +1 -1
- package/tests/examples/__snapshots__/example01.snap.svg +29 -32
- package/tests/examples/__snapshots__/example02.snap.svg +5 -5
- package/tests/examples/__snapshots__/example04.snap.svg +12 -12
- package/tests/examples/__snapshots__/example05.snap.svg +35 -35
- package/tests/examples/__snapshots__/example06.snap.svg +14 -14
- package/tests/examples/__snapshots__/example08.snap.svg +23 -29
- package/tests/examples/__snapshots__/example11.snap.svg +33 -39
- package/tests/examples/__snapshots__/example12.snap.svg +29 -32
- package/tests/examples/__snapshots__/example13.snap.svg +86 -83
- package/tests/examples/__snapshots__/example15.snap.svg +235 -0
- package/tests/examples/example15.test.tsx +187 -0
- package/tests/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver_repro03.test.ts +0 -0
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
|
-
|
|
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 =
|
|
561
|
-
const lastMovableIndex = nPts -
|
|
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
|
|
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 >
|
|
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
|
|
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 >
|
|
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 {
|
|
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 (
|
|
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
|
|
170
|
-
// movable indices i in [
|
|
171
|
-
const firstMovableIndex =
|
|
172
|
-
const lastMovableIndex = nPts -
|
|
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
|
+
}
|