@tscircuit/hypergraph 0.0.33 → 0.0.35

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
@@ -241,6 +241,20 @@ declare class HyperGraphSolver<RegionType extends Region = Region, RegionPortTyp
241
241
  * a route would require ripping other routes due to problematic crossings.
242
242
  */
243
243
  getRipsRequiredForPortUsage(_region: RegionType, _port1: RegionPortType, _port2: RegionPortType): RegionPortAssignment[];
244
+ /**
245
+ * OPTIONALLY OVERRIDE THIS
246
+ *
247
+ * Return true if using the candidate transition should incur ripCost even
248
+ * when there is no direct port-assignment conflict.
249
+ */
250
+ isRipRequiredForPortUsage(_region: RegionType, _port1: RegionPortType, _port2: RegionPortType): boolean;
251
+ /**
252
+ * OPTIONALLY OVERRIDE THIS
253
+ *
254
+ * Return false to prevent transitioning through a region from `_port1` to
255
+ * `_port2`.
256
+ */
257
+ isTransitionAllowed(_region: RegionType, _port1: RegionPortType, _port2: RegionPortType): boolean;
244
258
  computeG(candidate: CandidateType): number;
245
259
  /**
246
260
  * Return a subset of the candidates for entering a region. These candidates
@@ -310,6 +324,11 @@ interface JRegion extends Region {
310
324
  x: number;
311
325
  y: number;
312
326
  }[];
327
+ polygonPerimeterCache?: {
328
+ edgeLengths: number[];
329
+ cumulative: number[];
330
+ perimeter: number;
331
+ };
313
332
  isPad: boolean;
314
333
  isThroughJumper?: boolean;
315
334
  isConnectionRegion?: boolean;
@@ -317,6 +336,8 @@ interface JRegion extends Region {
317
336
  };
318
337
  }
319
338
  interface JPort extends RegionPort {
339
+ region1T?: number;
340
+ region2T?: number;
320
341
  d: {
321
342
  x: number;
322
343
  y: number;
@@ -376,8 +397,10 @@ declare class JumperGraphSolver extends HyperGraphSolver<JRegion, JPort> {
376
397
  private populateDistanceToEndMaps;
377
398
  estimateCostToEnd(port: JPort): number;
378
399
  getPortUsagePenalty(port: JPort): number;
400
+ isTransitionAllowed(region: JRegion, port1: JPort, port2: JPort): boolean;
379
401
  computeIncreasedRegionCostIfPortsAreUsed(region: JRegion, port1: JPort, port2: JPort): number;
380
402
  getRipsRequiredForPortUsage(region: JRegion, port1: JPort, port2: JPort): RegionPortAssignment[];
403
+ isRipRequiredForPortUsage(region: JRegion, _port1: JPort, _port2: JPort): boolean;
381
404
  routeSolvedHook(solvedRoute: SolvedRoute): void;
382
405
  routeStartedHook(connection: Connection): void;
383
406
  visualize(): GraphicsObject;
package/dist/index.js CHANGED
@@ -31,6 +31,28 @@ var convertHyperGraphToSerializedHyperGraph = (graph) => {
31
31
  // lib/HyperGraphSolver.ts
32
32
  import { BaseSolver } from "@tscircuit/solver-utils";
33
33
 
34
+ // lib/convertSerializedConnectionsToConnections.ts
35
+ var convertSerializedConnectionsToConnections = (inputConnections, graph) => {
36
+ const connections = [];
37
+ for (const inputConn of inputConnections) {
38
+ if ("startRegionId" in inputConn) {
39
+ connections.push({
40
+ connectionId: inputConn.connectionId,
41
+ mutuallyConnectedNetworkId: inputConn.connectionId,
42
+ startRegion: graph.regions.find(
43
+ (region) => region.regionId === inputConn.startRegionId
44
+ ),
45
+ endRegion: graph.regions.find(
46
+ (region) => region.regionId === inputConn.endRegionId
47
+ )
48
+ });
49
+ } else {
50
+ connections.push(inputConn);
51
+ }
52
+ }
53
+ return connections;
54
+ };
55
+
34
56
  // lib/convertSerializedHyperGraphToHyperGraph.ts
35
57
  var convertSerializedHyperGraphToHyperGraph = (inputGraph) => {
36
58
  if (inputGraph.ports.length > 0 && "region1" in inputGraph.ports[0] && typeof inputGraph.ports[0].region1 === "object") {
@@ -65,28 +87,6 @@ var convertSerializedHyperGraphToHyperGraph = (inputGraph) => {
65
87
  };
66
88
  };
67
89
 
68
- // lib/convertSerializedConnectionsToConnections.ts
69
- var convertSerializedConnectionsToConnections = (inputConnections, graph) => {
70
- const connections = [];
71
- for (const inputConn of inputConnections) {
72
- if ("startRegionId" in inputConn) {
73
- connections.push({
74
- connectionId: inputConn.connectionId,
75
- mutuallyConnectedNetworkId: inputConn.connectionId,
76
- startRegion: graph.regions.find(
77
- (region) => region.regionId === inputConn.startRegionId
78
- ),
79
- endRegion: graph.regions.find(
80
- (region) => region.regionId === inputConn.endRegionId
81
- )
82
- });
83
- } else {
84
- connections.push(inputConn);
85
- }
86
- }
87
- return connections;
88
- };
89
-
90
90
  // lib/PriorityQueue.ts
91
91
  var PriorityQueue = class {
92
92
  // Store the heap as an array. Index 0 is the root (highest priority/smallest 'f').
@@ -328,6 +328,24 @@ var HyperGraphSolver = class extends BaseSolver {
328
328
  getRipsRequiredForPortUsage(_region, _port1, _port2) {
329
329
  return [];
330
330
  }
331
+ /**
332
+ * OPTIONALLY OVERRIDE THIS
333
+ *
334
+ * Return true if using the candidate transition should incur ripCost even
335
+ * when there is no direct port-assignment conflict.
336
+ */
337
+ isRipRequiredForPortUsage(_region, _port1, _port2) {
338
+ return false;
339
+ }
340
+ /**
341
+ * OPTIONALLY OVERRIDE THIS
342
+ *
343
+ * Return false to prevent transitioning through a region from `_port1` to
344
+ * `_port2`.
345
+ */
346
+ isTransitionAllowed(_region, _port1, _port2) {
347
+ return true;
348
+ }
331
349
  computeG(candidate) {
332
350
  return candidate.parent.g + this.computeIncreasedRegionCostIfPortsAreUsed(
333
351
  candidate.lastRegion,
@@ -396,7 +414,18 @@ var HyperGraphSolver = class extends BaseSolver {
396
414
  const nextCandidatesByRegion = {};
397
415
  for (const port of currentRegion.ports) {
398
416
  if (port === currentCandidate.port) continue;
399
- const ripRequired = port.assignment && port.assignment.connection.mutuallyConnectedNetworkId !== this.currentConnection.mutuallyConnectedNetworkId;
417
+ if (!this.isTransitionAllowed(
418
+ currentRegion,
419
+ currentPort,
420
+ port
421
+ )) {
422
+ continue;
423
+ }
424
+ const ripRequired = port.assignment && port.assignment.connection.mutuallyConnectedNetworkId !== this.currentConnection.mutuallyConnectedNetworkId || this.isRipRequiredForPortUsage(
425
+ currentRegion,
426
+ currentPort,
427
+ port
428
+ );
400
429
  const newCandidate = {
401
430
  port,
402
431
  hops: currentCandidate.hops + 1,
@@ -758,6 +787,277 @@ var rotateGraph90Degrees = (graph) => {
758
787
  return applyTransformToGraph(graph, matrix2);
759
788
  };
760
789
 
790
+ // lib/JumperGraphSolver/perimeterChordUtils.ts
791
+ function clamp2(value, min, max) {
792
+ return Math.max(min, Math.min(max, value));
793
+ }
794
+ function dist2(a, b) {
795
+ const dx = a.x - b.x;
796
+ const dy = a.y - b.y;
797
+ return dx * dx + dy * dy;
798
+ }
799
+ function getRectanglePerimeter(xmin, xmax, ymin, ymax) {
800
+ return 2 * (xmax - xmin) + 2 * (ymax - ymin);
801
+ }
802
+ function perimeterT(p, xmin, xmax, ymin, ymax) {
803
+ const W = xmax - xmin;
804
+ const H = ymax - ymin;
805
+ const eps = 1e-6;
806
+ if (Math.abs(p.y - ymax) < eps) return p.x - xmin;
807
+ if (Math.abs(p.x - xmax) < eps) return W + (ymax - p.y);
808
+ if (Math.abs(p.y - ymin) < eps) return W + H + (xmax - p.x);
809
+ if (Math.abs(p.x - xmin) < eps) return 2 * W + H + (p.y - ymin);
810
+ const distTop = Math.abs(p.y - ymax);
811
+ const distRight = Math.abs(p.x - xmax);
812
+ const distBottom = Math.abs(p.y - ymin);
813
+ const distLeft = Math.abs(p.x - xmin);
814
+ const minDist = Math.min(distTop, distRight, distBottom, distLeft);
815
+ if (minDist === distTop) return Math.max(0, Math.min(W, p.x - xmin));
816
+ if (minDist === distRight) return W + Math.max(0, Math.min(H, ymax - p.y));
817
+ if (minDist === distBottom) {
818
+ return W + H + Math.max(0, Math.min(W, xmax - p.x));
819
+ }
820
+ return 2 * W + H + Math.max(0, Math.min(H, p.y - ymin));
821
+ }
822
+ function projectToSegment(p, a, b) {
823
+ const abx = b.x - a.x;
824
+ const aby = b.y - a.y;
825
+ const apx = p.x - a.x;
826
+ const apy = p.y - a.y;
827
+ const ab2 = abx * abx + aby * aby;
828
+ const u = ab2 > 0 ? clamp2((apx * abx + apy * aby) / ab2, 0, 1) : 0;
829
+ const q = { x: a.x + u * abx, y: a.y + u * aby };
830
+ return { u, d2: dist2(p, q) };
831
+ }
832
+ function createPolygonPerimeterCache(polygon2) {
833
+ const n = polygon2.length;
834
+ const edgeLengths = new Array(n);
835
+ const cumulative = new Array(n + 1);
836
+ cumulative[0] = 0;
837
+ for (let i = 0; i < n; i++) {
838
+ const a = polygon2[i];
839
+ const b = polygon2[(i + 1) % n];
840
+ const edgeLength = Math.hypot(b.x - a.x, b.y - a.y);
841
+ edgeLengths[i] = edgeLength;
842
+ cumulative[i + 1] = cumulative[i] + edgeLength;
843
+ }
844
+ return { edgeLengths, cumulative, perimeter: cumulative[n] };
845
+ }
846
+ function perimeterTPolygonWithCache(p, polygon2, cache, eps = 1e-6) {
847
+ let bestEdgeIndex = 0;
848
+ let bestU = 0;
849
+ let bestD2 = Number.POSITIVE_INFINITY;
850
+ const eps2 = eps * eps;
851
+ for (let i = 0; i < polygon2.length; i++) {
852
+ const a = polygon2[i];
853
+ const b = polygon2[(i + 1) % polygon2.length];
854
+ const projected = projectToSegment(p, a, b);
855
+ if (projected.d2 <= eps2) {
856
+ bestEdgeIndex = i;
857
+ bestU = projected.u;
858
+ bestD2 = projected.d2;
859
+ break;
860
+ }
861
+ if (projected.d2 < bestD2) {
862
+ bestEdgeIndex = i;
863
+ bestU = projected.u;
864
+ bestD2 = projected.d2;
865
+ }
866
+ }
867
+ return cache.cumulative[bestEdgeIndex] + bestU * cache.edgeLengths[bestEdgeIndex];
868
+ }
869
+ function getRegionPolygonCache(region) {
870
+ const polygon2 = region.d.polygon;
871
+ if (!polygon2 || polygon2.length < 3) return null;
872
+ const existing = region.d.polygonPerimeterCache;
873
+ if (existing) return existing;
874
+ const cache = createPolygonPerimeterCache(polygon2);
875
+ region.d.polygonPerimeterCache = cache;
876
+ return cache;
877
+ }
878
+ function getRegionPerimeter(region) {
879
+ const polygonCache = getRegionPolygonCache(region);
880
+ if (polygonCache) return polygonCache.perimeter;
881
+ const { minX, maxX, minY, maxY } = region.d.bounds;
882
+ return getRectanglePerimeter(minX, maxX, minY, maxY);
883
+ }
884
+ function getPortPerimeterTInRegion(port, region) {
885
+ if (port.region1 === region) {
886
+ if (typeof port.region1T === "number") return port.region1T;
887
+ const t = getPointPerimeterTInRegion(port.d, region);
888
+ port.region1T = t;
889
+ return t;
890
+ }
891
+ if (port.region2 === region) {
892
+ if (typeof port.region2T === "number") return port.region2T;
893
+ const t = getPointPerimeterTInRegion(port.d, region);
894
+ port.region2T = t;
895
+ return t;
896
+ }
897
+ return getPointPerimeterTInRegion(port.d, region);
898
+ }
899
+ function getPointPerimeterTInRegion(p, region) {
900
+ const polygon2 = region.d.polygon;
901
+ if (polygon2 && polygon2.length >= 3) {
902
+ const cache = getRegionPolygonCache(region);
903
+ if (cache) return perimeterTPolygonWithCache(p, polygon2, cache);
904
+ }
905
+ const { minX, maxX, minY, maxY } = region.d.bounds;
906
+ return perimeterT(p, minX, maxX, minY, maxY);
907
+ }
908
+ function areCoincident(t1, t2, eps = 1e-6) {
909
+ return Math.abs(t1 - t2) < eps;
910
+ }
911
+ function normalizeMod(value, modulus) {
912
+ return (value % modulus + modulus) % modulus;
913
+ }
914
+ function areCoincidentOnCircle(t1, t2, perimeter, eps) {
915
+ const delta = Math.abs(normalizeMod(t1 - t2, perimeter));
916
+ return delta < eps || perimeter - delta < eps;
917
+ }
918
+ function betweenMod(x, start, end, perimeter, eps) {
919
+ const nx = normalizeMod(x, perimeter);
920
+ const ns = normalizeMod(start, perimeter);
921
+ const ne = normalizeMod(end, perimeter);
922
+ if (Math.abs(ns - ne) < eps) return false;
923
+ if (ns < ne) return ns < nx && nx < ne;
924
+ return nx > ns || nx < ne;
925
+ }
926
+ function chordsCross(chord1, chord2, perimeter) {
927
+ if (typeof perimeter === "number" && perimeter > 0) {
928
+ let [a2, b2] = chord1;
929
+ let [c2, d2] = chord2;
930
+ a2 = normalizeMod(a2, perimeter);
931
+ b2 = normalizeMod(b2, perimeter);
932
+ c2 = normalizeMod(c2, perimeter);
933
+ d2 = normalizeMod(d2, perimeter);
934
+ if (areCoincidentOnCircle(a2, c2, perimeter, 1e-6) || areCoincidentOnCircle(a2, d2, perimeter, 1e-6) || areCoincidentOnCircle(b2, c2, perimeter, 1e-6) || areCoincidentOnCircle(b2, d2, perimeter, 1e-6)) {
935
+ return false;
936
+ }
937
+ const cInside = betweenMod(c2, a2, b2, perimeter, 1e-12);
938
+ const dInside = betweenMod(d2, a2, b2, perimeter, 1e-12);
939
+ return cInside !== dInside;
940
+ }
941
+ const [a, b] = chord1[0] < chord1[1] ? chord1 : [chord1[1], chord1[0]];
942
+ const [c, d] = chord2[0] < chord2[1] ? chord2 : [chord2[1], chord2[0]];
943
+ if (areCoincident(a, c) || areCoincident(a, d) || areCoincident(b, c) || areCoincident(b, d)) {
944
+ return false;
945
+ }
946
+ return a < c && c < b && b < d || c < a && a < d && d < b;
947
+ }
948
+
949
+ // lib/JumperGraphSolver/computeCrossingAssignments.ts
950
+ function computeCrossingAssignments(region, port1, port2) {
951
+ const perimeter = getRegionPerimeter(region);
952
+ const t1 = getPortPerimeterTInRegion(port1, region);
953
+ const t2 = getPortPerimeterTInRegion(port2, region);
954
+ const newChord = [t1, t2];
955
+ const crossingAssignments = [];
956
+ const assignments = region.assignments ?? [];
957
+ for (const assignment of assignments) {
958
+ const existingT1 = getPortPerimeterTInRegion(
959
+ assignment.regionPort1,
960
+ region
961
+ );
962
+ const existingT2 = getPortPerimeterTInRegion(
963
+ assignment.regionPort2,
964
+ region
965
+ );
966
+ const existingChord = [existingT1, existingT2];
967
+ if (chordsCross(newChord, existingChord, perimeter)) {
968
+ crossingAssignments.push(assignment);
969
+ }
970
+ }
971
+ return crossingAssignments;
972
+ }
973
+
974
+ // lib/JumperGraphSolver/computeDifferentNetCrossings.ts
975
+ function computeDifferentNetCrossings(region, port1, port2) {
976
+ const perimeter = getRegionPerimeter(region);
977
+ const t1 = getPortPerimeterTInRegion(port1, region);
978
+ const t2 = getPortPerimeterTInRegion(port2, region);
979
+ const newChord = [t1, t2];
980
+ let crossings = 0;
981
+ const assignments = region.assignments ?? [];
982
+ for (const assignment of assignments) {
983
+ const existingT1 = getPortPerimeterTInRegion(
984
+ assignment.regionPort1,
985
+ region
986
+ );
987
+ const existingT2 = getPortPerimeterTInRegion(
988
+ assignment.regionPort2,
989
+ region
990
+ );
991
+ const existingChord = [existingT1, existingT2];
992
+ if (chordsCross(newChord, existingChord, perimeter)) {
993
+ crossings++;
994
+ }
995
+ }
996
+ return crossings;
997
+ }
998
+
999
+ // lib/JumperGraphSolver/countInputConnectionCrossings.ts
1000
+ function countInputConnectionCrossings(graph, connections) {
1001
+ if (connections.length < 2) {
1002
+ return 0;
1003
+ }
1004
+ let minX = Infinity;
1005
+ let maxX = -Infinity;
1006
+ let minY = Infinity;
1007
+ let maxY = -Infinity;
1008
+ for (const region of graph.regions) {
1009
+ const jRegion = region;
1010
+ if (jRegion.d?.bounds) {
1011
+ minX = Math.min(minX, jRegion.d.bounds.minX);
1012
+ maxX = Math.max(maxX, jRegion.d.bounds.maxX);
1013
+ minY = Math.min(minY, jRegion.d.bounds.minY);
1014
+ maxY = Math.max(maxY, jRegion.d.bounds.maxY);
1015
+ } else if (jRegion.d?.center) {
1016
+ minX = Math.min(minX, jRegion.d.center.x);
1017
+ maxX = Math.max(maxX, jRegion.d.center.x);
1018
+ minY = Math.min(minY, jRegion.d.center.y);
1019
+ maxY = Math.max(maxY, jRegion.d.center.y);
1020
+ }
1021
+ }
1022
+ const regionCenterMap = /* @__PURE__ */ new Map();
1023
+ for (const region of graph.regions) {
1024
+ const jRegion = region;
1025
+ if (jRegion.d?.center) {
1026
+ regionCenterMap.set(region.regionId, jRegion.d.center);
1027
+ }
1028
+ }
1029
+ const chords = [];
1030
+ const perimeter = 2 * (maxX - minX) + 2 * (maxY - minY);
1031
+ for (const conn of connections) {
1032
+ let startCenter;
1033
+ let endCenter;
1034
+ if ("startRegion" in conn && conn.startRegion) {
1035
+ const startRegion = conn.startRegion;
1036
+ const endRegion = conn.endRegion;
1037
+ startCenter = startRegion.d?.center;
1038
+ endCenter = endRegion.d?.center;
1039
+ } else if ("startRegionId" in conn) {
1040
+ startCenter = regionCenterMap.get(conn.startRegionId);
1041
+ endCenter = regionCenterMap.get(conn.endRegionId);
1042
+ }
1043
+ if (!startCenter || !endCenter) {
1044
+ continue;
1045
+ }
1046
+ const t1 = perimeterT(startCenter, minX, maxX, minY, maxY);
1047
+ const t2 = perimeterT(endCenter, minX, maxX, minY, maxY);
1048
+ chords.push([t1, t2]);
1049
+ }
1050
+ let crossings = 0;
1051
+ for (let i = 0; i < chords.length; i++) {
1052
+ for (let j = i + 1; j < chords.length; j++) {
1053
+ if (chordsCross(chords[i], chords[j], perimeter)) {
1054
+ crossings++;
1055
+ }
1056
+ }
1057
+ }
1058
+ return crossings;
1059
+ }
1060
+
761
1061
  // lib/JumperGraphSolver/visualizeJumperGraph.ts
762
1062
  var visualizeJumperGraph = (graph, options) => {
763
1063
  const graphics = {
@@ -791,9 +1091,7 @@ var visualizeJumperGraph = (graph, options) => {
791
1091
  const points = polygon2;
792
1092
  graphics.polygons.push({
793
1093
  points,
794
- fill,
795
- stroke: "rgba(128, 128, 128, 0.5)",
796
- strokeWidth: 0.03
1094
+ fill
797
1095
  });
798
1096
  } else {
799
1097
  graphics.rects.push({
@@ -884,6 +1182,12 @@ var visualizeJumperGraphSolver = (solver) => {
884
1182
  hidePortPoints: true
885
1183
  } : {}
886
1184
  });
1185
+ if (solver.iterations === 0) {
1186
+ for (const polygon2 of graphics.polygons) {
1187
+ polygon2.stroke = "rgba(128, 128, 128, 0.5)";
1188
+ polygon2.strokeWidth = 0.03;
1189
+ }
1190
+ }
887
1191
  if (solver.currentConnection && !solver.solved) {
888
1192
  const connectionColor = getConnectionColor(
889
1193
  solver.currentConnection.connectionId
@@ -971,174 +1275,6 @@ var visualizeJumperGraphSolver = (solver) => {
971
1275
  return graphics;
972
1276
  };
973
1277
 
974
- // lib/JumperGraphSolver/perimeterChordUtils.ts
975
- function perimeterT(p, xmin, xmax, ymin, ymax) {
976
- const W = xmax - xmin;
977
- const H = ymax - ymin;
978
- const eps = 1e-6;
979
- if (Math.abs(p.y - ymax) < eps) {
980
- return p.x - xmin;
981
- }
982
- if (Math.abs(p.x - xmax) < eps) {
983
- return W + (ymax - p.y);
984
- }
985
- if (Math.abs(p.y - ymin) < eps) {
986
- return W + H + (xmax - p.x);
987
- }
988
- if (Math.abs(p.x - xmin) < eps) {
989
- return 2 * W + H + (p.y - ymin);
990
- }
991
- const distTop = Math.abs(p.y - ymax);
992
- const distRight = Math.abs(p.x - xmax);
993
- const distBottom = Math.abs(p.y - ymin);
994
- const distLeft = Math.abs(p.x - xmin);
995
- const minDist = Math.min(distTop, distRight, distBottom, distLeft);
996
- if (minDist === distTop) {
997
- return Math.max(0, Math.min(W, p.x - xmin));
998
- }
999
- if (minDist === distRight) {
1000
- return W + Math.max(0, Math.min(H, ymax - p.y));
1001
- }
1002
- if (minDist === distBottom) {
1003
- return W + H + Math.max(0, Math.min(W, xmax - p.x));
1004
- }
1005
- return 2 * W + H + Math.max(0, Math.min(H, p.y - ymin));
1006
- }
1007
- function areCoincident(t1, t2, eps = 1e-6) {
1008
- return Math.abs(t1 - t2) < eps;
1009
- }
1010
- function chordsCross(chord1, chord2) {
1011
- const [a, b] = chord1[0] < chord1[1] ? chord1 : [chord1[1], chord1[0]];
1012
- const [c, d] = chord2[0] < chord2[1] ? chord2 : [chord2[1], chord2[0]];
1013
- if (areCoincident(a, c) || areCoincident(a, d) || areCoincident(b, c) || areCoincident(b, d)) {
1014
- return false;
1015
- }
1016
- return a < c && c < b && b < d || c < a && a < d && d < b;
1017
- }
1018
-
1019
- // lib/JumperGraphSolver/computeDifferentNetCrossings.ts
1020
- function computeDifferentNetCrossings(region, port1, port2) {
1021
- const { minX: xmin, maxX: xmax, minY: ymin, maxY: ymax } = region.d.bounds;
1022
- const t1 = perimeterT(port1.d, xmin, xmax, ymin, ymax);
1023
- const t2 = perimeterT(port2.d, xmin, xmax, ymin, ymax);
1024
- const newChord = [t1, t2];
1025
- let crossings = 0;
1026
- const assignments = region.assignments ?? [];
1027
- for (const assignment of assignments) {
1028
- const existingT1 = perimeterT(
1029
- assignment.regionPort1.d,
1030
- xmin,
1031
- xmax,
1032
- ymin,
1033
- ymax
1034
- );
1035
- const existingT2 = perimeterT(
1036
- assignment.regionPort2.d,
1037
- xmin,
1038
- xmax,
1039
- ymin,
1040
- ymax
1041
- );
1042
- const existingChord = [existingT1, existingT2];
1043
- if (chordsCross(newChord, existingChord)) {
1044
- crossings++;
1045
- }
1046
- }
1047
- return crossings;
1048
- }
1049
-
1050
- // lib/JumperGraphSolver/computeCrossingAssignments.ts
1051
- function computeCrossingAssignments(region, port1, port2) {
1052
- const { minX: xmin, maxX: xmax, minY: ymin, maxY: ymax } = region.d.bounds;
1053
- const t1 = perimeterT(port1.d, xmin, xmax, ymin, ymax);
1054
- const t2 = perimeterT(port2.d, xmin, xmax, ymin, ymax);
1055
- const newChord = [t1, t2];
1056
- const crossingAssignments = [];
1057
- const assignments = region.assignments ?? [];
1058
- for (const assignment of assignments) {
1059
- const existingT1 = perimeterT(
1060
- assignment.regionPort1.d,
1061
- xmin,
1062
- xmax,
1063
- ymin,
1064
- ymax
1065
- );
1066
- const existingT2 = perimeterT(
1067
- assignment.regionPort2.d,
1068
- xmin,
1069
- xmax,
1070
- ymin,
1071
- ymax
1072
- );
1073
- const existingChord = [existingT1, existingT2];
1074
- if (chordsCross(newChord, existingChord)) {
1075
- crossingAssignments.push(assignment);
1076
- }
1077
- }
1078
- return crossingAssignments;
1079
- }
1080
-
1081
- // lib/JumperGraphSolver/countInputConnectionCrossings.ts
1082
- function countInputConnectionCrossings(graph, connections) {
1083
- if (connections.length < 2) {
1084
- return 0;
1085
- }
1086
- let minX = Infinity;
1087
- let maxX = -Infinity;
1088
- let minY = Infinity;
1089
- let maxY = -Infinity;
1090
- for (const region of graph.regions) {
1091
- const jRegion = region;
1092
- if (jRegion.d?.bounds) {
1093
- minX = Math.min(minX, jRegion.d.bounds.minX);
1094
- maxX = Math.max(maxX, jRegion.d.bounds.maxX);
1095
- minY = Math.min(minY, jRegion.d.bounds.minY);
1096
- maxY = Math.max(maxY, jRegion.d.bounds.maxY);
1097
- } else if (jRegion.d?.center) {
1098
- minX = Math.min(minX, jRegion.d.center.x);
1099
- maxX = Math.max(maxX, jRegion.d.center.x);
1100
- minY = Math.min(minY, jRegion.d.center.y);
1101
- maxY = Math.max(maxY, jRegion.d.center.y);
1102
- }
1103
- }
1104
- const regionCenterMap = /* @__PURE__ */ new Map();
1105
- for (const region of graph.regions) {
1106
- const jRegion = region;
1107
- if (jRegion.d?.center) {
1108
- regionCenterMap.set(region.regionId, jRegion.d.center);
1109
- }
1110
- }
1111
- const chords = [];
1112
- for (const conn of connections) {
1113
- let startCenter;
1114
- let endCenter;
1115
- if ("startRegion" in conn && conn.startRegion) {
1116
- const startRegion = conn.startRegion;
1117
- const endRegion = conn.endRegion;
1118
- startCenter = startRegion.d?.center;
1119
- endCenter = endRegion.d?.center;
1120
- } else if ("startRegionId" in conn) {
1121
- startCenter = regionCenterMap.get(conn.startRegionId);
1122
- endCenter = regionCenterMap.get(conn.endRegionId);
1123
- }
1124
- if (!startCenter || !endCenter) {
1125
- continue;
1126
- }
1127
- const t1 = perimeterT(startCenter, minX, maxX, minY, maxY);
1128
- const t2 = perimeterT(endCenter, minX, maxX, minY, maxY);
1129
- chords.push([t1, t2]);
1130
- }
1131
- let crossings = 0;
1132
- for (let i = 0; i < chords.length; i++) {
1133
- for (let j = i + 1; j < chords.length; j++) {
1134
- if (chordsCross(chords[i], chords[j])) {
1135
- crossings++;
1136
- }
1137
- }
1138
- }
1139
- return crossings;
1140
- }
1141
-
1142
1278
  // lib/JumperGraphSolver/JumperGraphSolver.ts
1143
1279
  var JUMPER_GRAPH_SOLVER_DEFAULTS = {
1144
1280
  portUsagePenalty: 0.034685181009478865,
@@ -1217,15 +1353,56 @@ var JumperGraphSolver = class extends HyperGraphSolver {
1217
1353
  const ripCount = port.ripCount ?? 0;
1218
1354
  return ripCount * this.portUsagePenalty + ripCount * this.portUsagePenaltySq;
1219
1355
  }
1356
+ isTransitionAllowed(region, port1, port2) {
1357
+ if (!region.d.isPad) return true;
1358
+ const usesThroughJumper = (port) => {
1359
+ const otherRegion = port.region1 === region ? port.region2 : port.region1;
1360
+ return Boolean(otherRegion.d.isThroughJumper);
1361
+ };
1362
+ return usesThroughJumper(port1) || usesThroughJumper(port2);
1363
+ }
1220
1364
  computeIncreasedRegionCostIfPortsAreUsed(region, port1, port2) {
1365
+ if (region.d.isPad) {
1366
+ const assignments = region.assignments ?? [];
1367
+ const differentNetCount = assignments.filter(
1368
+ (a) => a.connection.mutuallyConnectedNetworkId !== this.currentConnection.mutuallyConnectedNetworkId
1369
+ ).length;
1370
+ if (differentNetCount > 0) {
1371
+ return differentNetCount * this.crossingPenalty + differentNetCount * this.crossingPenaltySq;
1372
+ }
1373
+ return 0;
1374
+ }
1221
1375
  const crossings = computeDifferentNetCrossings(region, port1, port2);
1222
1376
  return crossings * this.crossingPenalty + crossings * this.crossingPenaltySq;
1223
1377
  }
1224
1378
  getRipsRequiredForPortUsage(region, port1, port2) {
1379
+ if (region.d.isPad) {
1380
+ const assignments = region.assignments ?? [];
1381
+ return assignments.filter(
1382
+ (a) => a.connection.mutuallyConnectedNetworkId !== this.currentConnection.mutuallyConnectedNetworkId
1383
+ );
1384
+ }
1225
1385
  const crossingAssignments = computeCrossingAssignments(region, port1, port2);
1226
- return crossingAssignments.filter(
1386
+ const conflictingAssignments = crossingAssignments.filter(
1227
1387
  (a) => a.connection.mutuallyConnectedNetworkId !== this.currentConnection.mutuallyConnectedNetworkId
1228
1388
  );
1389
+ if (!region.d.isThroughJumper) return conflictingAssignments;
1390
+ for (const assignment of region.assignments ?? []) {
1391
+ if (assignment.connection.mutuallyConnectedNetworkId === this.currentConnection.mutuallyConnectedNetworkId) {
1392
+ continue;
1393
+ }
1394
+ conflictingAssignments.push(assignment);
1395
+ }
1396
+ return conflictingAssignments;
1397
+ }
1398
+ isRipRequiredForPortUsage(region, _port1, _port2) {
1399
+ if (!region.d.isThroughJumper && !region.d.isPad) return false;
1400
+ for (const assignment of region.assignments ?? []) {
1401
+ if (assignment.connection.mutuallyConnectedNetworkId !== this.currentConnection.mutuallyConnectedNetworkId) {
1402
+ return true;
1403
+ }
1404
+ }
1405
+ return false;
1229
1406
  }
1230
1407
  routeSolvedHook(solvedRoute) {
1231
1408
  }
@@ -1271,23 +1448,123 @@ var createConnectionRegion = (regionId, x, y) => {
1271
1448
  };
1272
1449
 
1273
1450
  // lib/JumperGraphSolver/jumper-graph-generator/findBoundaryRegion.ts
1274
- var findBoundaryRegion = (x, y, regions, graphBounds) => {
1275
- for (const region of regions) {
1276
- if (region.d.isPad || region.d.isThroughJumper) continue;
1277
- const bounds = region.d.bounds;
1278
- if (Math.abs(x - bounds.minX) < 0.01 && y >= bounds.minY && y <= bounds.maxY) {
1279
- return { region, portPosition: { x: bounds.minX, y } };
1280
- }
1281
- if (Math.abs(x - bounds.maxX) < 0.01 && y >= bounds.minY && y <= bounds.maxY) {
1282
- return { region, portPosition: { x: bounds.maxX, y } };
1451
+ var EPS = 0.01;
1452
+ var clamp3 = (value, min, max) => {
1453
+ return Math.max(min, Math.min(max, value));
1454
+ };
1455
+ var getBoundarySidesForPoint = (x, y, graphBounds) => {
1456
+ const sides = [];
1457
+ if (Math.abs(x - graphBounds.minX) < EPS) sides.push("left");
1458
+ if (Math.abs(x - graphBounds.maxX) < EPS) sides.push("right");
1459
+ if (Math.abs(y - graphBounds.maxY) < EPS) sides.push("top");
1460
+ if (Math.abs(y - graphBounds.minY) < EPS) sides.push("bottom");
1461
+ return sides;
1462
+ };
1463
+ var isPointOnSide = (p, side, b) => {
1464
+ if (side === "left") return Math.abs(p.x - b.minX) < EPS;
1465
+ if (side === "right") return Math.abs(p.x - b.maxX) < EPS;
1466
+ if (side === "top") return Math.abs(p.y - b.maxY) < EPS;
1467
+ return Math.abs(p.y - b.minY) < EPS;
1468
+ };
1469
+ var projectToSegment2 = (x, y, a, b) => {
1470
+ const abx = b.x - a.x;
1471
+ const aby = b.y - a.y;
1472
+ const apx = x - a.x;
1473
+ const apy = y - a.y;
1474
+ const ab2 = abx * abx + aby * aby;
1475
+ const t = ab2 > 0 ? clamp3((apx * abx + apy * aby) / ab2, 0, 1) : 0;
1476
+ const px = a.x + t * abx;
1477
+ const py = a.y + t * aby;
1478
+ const dx = x - px;
1479
+ const dy = y - py;
1480
+ return {
1481
+ x: px,
1482
+ y: py,
1483
+ d2: dx * dx + dy * dy
1484
+ };
1485
+ };
1486
+ var getRegionBoundaryProjection = (x, y, region, graphBounds, preferredSides) => {
1487
+ const polygon2 = region.d.polygon;
1488
+ if (polygon2 && polygon2.length >= 3) {
1489
+ const sideSet = new Set(preferredSides);
1490
+ let best2 = null;
1491
+ for (let i = 0; i < polygon2.length; i++) {
1492
+ const a = polygon2[i];
1493
+ const b = polygon2[(i + 1) % polygon2.length];
1494
+ if (preferredSides.length > 0) {
1495
+ const edgeOnPreferredSide = preferredSides.some(
1496
+ (side) => isPointOnSide(a, side, graphBounds) && isPointOnSide(b, side, graphBounds) && sideSet.has(side)
1497
+ );
1498
+ if (!edgeOnPreferredSide) continue;
1499
+ }
1500
+ const p = projectToSegment2(x, y, a, b);
1501
+ if (!best2 || p.d2 < best2.d2) {
1502
+ best2 = p;
1503
+ }
1283
1504
  }
1284
- if (Math.abs(y - bounds.minY) < 0.01 && x >= bounds.minX && x <= bounds.maxX) {
1285
- return { region, portPosition: { x, y: bounds.minY } };
1505
+ if (best2) return best2;
1506
+ }
1507
+ const bounds = region.d.bounds;
1508
+ const sideCandidates = [];
1509
+ if (preferredSides.length > 0) {
1510
+ for (const side of preferredSides) {
1511
+ if (side === "left") {
1512
+ sideCandidates.push({
1513
+ side,
1514
+ x: bounds.minX,
1515
+ y: clamp3(y, bounds.minY, bounds.maxY)
1516
+ });
1517
+ } else if (side === "right") {
1518
+ sideCandidates.push({
1519
+ side,
1520
+ x: bounds.maxX,
1521
+ y: clamp3(y, bounds.minY, bounds.maxY)
1522
+ });
1523
+ } else if (side === "top") {
1524
+ sideCandidates.push({
1525
+ side,
1526
+ x: clamp3(x, bounds.minX, bounds.maxX),
1527
+ y: bounds.maxY
1528
+ });
1529
+ } else {
1530
+ sideCandidates.push({
1531
+ side,
1532
+ x: clamp3(x, bounds.minX, bounds.maxX),
1533
+ y: bounds.minY
1534
+ });
1535
+ }
1286
1536
  }
1287
- if (Math.abs(y - bounds.maxY) < 0.01 && x >= bounds.minX && x <= bounds.maxX) {
1288
- return { region, portPosition: { x, y: bounds.maxY } };
1537
+ }
1538
+ if (sideCandidates.length === 0) {
1539
+ sideCandidates.push(
1540
+ { side: "left", x: bounds.minX, y: clamp3(y, bounds.minY, bounds.maxY) },
1541
+ {
1542
+ side: "right",
1543
+ x: bounds.maxX,
1544
+ y: clamp3(y, bounds.minY, bounds.maxY)
1545
+ },
1546
+ { side: "top", x: clamp3(x, bounds.minX, bounds.maxX), y: bounds.maxY },
1547
+ {
1548
+ side: "bottom",
1549
+ x: clamp3(x, bounds.minX, bounds.maxX),
1550
+ y: bounds.minY
1551
+ }
1552
+ );
1553
+ }
1554
+ let best = null;
1555
+ for (const c of sideCandidates) {
1556
+ if (preferredSides.length > 0 && !preferredSides.includes(c.side)) continue;
1557
+ const dx = x - c.x;
1558
+ const dy = y - c.y;
1559
+ const d2 = dx * dx + dy * dy;
1560
+ if (!best || d2 < best.d2) {
1561
+ best = { x: c.x, y: c.y, d2 };
1289
1562
  }
1290
1563
  }
1564
+ return best;
1565
+ };
1566
+ var findBoundaryRegion = (x, y, regions, graphBounds) => {
1567
+ const preferredSides = getBoundarySidesForPoint(x, y, graphBounds);
1291
1568
  let closestRegion = null;
1292
1569
  let closestDistance = Number.POSITIVE_INFINITY;
1293
1570
  let closestPortPosition = { x, y };
@@ -1296,23 +1573,19 @@ var findBoundaryRegion = (x, y, regions, graphBounds) => {
1296
1573
  const bounds = region.d.bounds;
1297
1574
  const isOuterRegion = Math.abs(bounds.minX - graphBounds.minX) < 0.01 || Math.abs(bounds.maxX - graphBounds.maxX) < 0.01 || Math.abs(bounds.minY - graphBounds.minY) < 0.01 || Math.abs(bounds.maxY - graphBounds.maxY) < 0.01;
1298
1575
  if (!isOuterRegion) continue;
1299
- const clampedX = Math.max(bounds.minX, Math.min(bounds.maxX, x));
1300
- const clampedY = Math.max(bounds.minY, Math.min(bounds.maxY, y));
1301
- const dist = Math.sqrt((x - clampedX) ** 2 + (y - clampedY) ** 2);
1576
+ const projection = getRegionBoundaryProjection(
1577
+ x,
1578
+ y,
1579
+ region,
1580
+ graphBounds,
1581
+ preferredSides
1582
+ );
1583
+ if (!projection) continue;
1584
+ const dist = Math.sqrt(projection.d2);
1302
1585
  if (dist < closestDistance) {
1303
1586
  closestDistance = dist;
1304
1587
  closestRegion = region;
1305
- if (x < bounds.minX) {
1306
- closestPortPosition = { x: bounds.minX, y: clampedY };
1307
- } else if (x > bounds.maxX) {
1308
- closestPortPosition = { x: bounds.maxX, y: clampedY };
1309
- } else if (y < bounds.minY) {
1310
- closestPortPosition = { x: clampedX, y: bounds.minY };
1311
- } else if (y > bounds.maxY) {
1312
- closestPortPosition = { x: clampedX, y: bounds.maxY };
1313
- } else {
1314
- closestPortPosition = { x: clampedX, y: clampedY };
1315
- }
1588
+ closestPortPosition = { x: projection.x, y: projection.y };
1316
1589
  }
1317
1590
  }
1318
1591
  if (closestRegion) {
@@ -4946,64 +5219,28 @@ function generateDefaultViaTopologyGrid(opts) {
4946
5219
  }
4947
5220
 
4948
5221
  // lib/ViaGraphSolver/polygonPerimeterUtils.ts
4949
- function polygonPerimeterT(p, polygon2) {
4950
- const n = polygon2.length;
4951
- let bestDist = Infinity;
4952
- let bestEdgeIndex = 0;
4953
- let bestT = 0;
4954
- for (let i = 0; i < n; i++) {
4955
- const a2 = polygon2[i];
4956
- const b2 = polygon2[(i + 1) % n];
4957
- const dx = b2.x - a2.x;
4958
- const dy = b2.y - a2.y;
4959
- const lenSq = dx * dx + dy * dy;
4960
- if (lenSq < 1e-10) continue;
4961
- const t = Math.max(
4962
- 0,
4963
- Math.min(1, ((p.x - a2.x) * dx + (p.y - a2.y) * dy) / lenSq)
4964
- );
4965
- const projX = a2.x + t * dx;
4966
- const projY = a2.y + t * dy;
4967
- const dist = Math.sqrt((p.x - projX) ** 2 + (p.y - projY) ** 2);
4968
- if (dist < bestDist) {
4969
- bestDist = dist;
4970
- bestEdgeIndex = i;
4971
- bestT = t;
4972
- }
4973
- }
4974
- let cumulative = 0;
4975
- for (let i = 0; i < bestEdgeIndex; i++) {
4976
- const a2 = polygon2[i];
4977
- const b2 = polygon2[(i + 1) % n];
4978
- cumulative += Math.sqrt((b2.x - a2.x) ** 2 + (b2.y - a2.y) ** 2);
4979
- }
4980
- const a = polygon2[bestEdgeIndex];
4981
- const b = polygon2[(bestEdgeIndex + 1) % n];
4982
- const edgeLen = Math.sqrt((b.x - a.x) ** 2 + (b.y - a.y) ** 2);
4983
- cumulative += bestT * edgeLen;
4984
- return cumulative;
4985
- }
4986
5222
  function computeDifferentNetCrossingsForPolygon(region, port1, port2) {
4987
5223
  const polygon2 = region.d.polygon;
4988
5224
  if (!polygon2 || polygon2.length < 3) {
4989
5225
  return 0;
4990
5226
  }
4991
- const t1 = polygonPerimeterT(port1.d, polygon2);
4992
- const t2 = polygonPerimeterT(port2.d, polygon2);
5227
+ const perimeter = getRegionPerimeter(region);
5228
+ const t1 = getPortPerimeterTInRegion(port1, region);
5229
+ const t2 = getPortPerimeterTInRegion(port2, region);
4993
5230
  const newChord = [t1, t2];
4994
5231
  let crossings = 0;
4995
5232
  const assignments = region.assignments ?? [];
4996
5233
  for (const assignment of assignments) {
4997
- const existingT1 = polygonPerimeterT(
4998
- assignment.regionPort1.d,
4999
- polygon2
5234
+ const existingT1 = getPortPerimeterTInRegion(
5235
+ assignment.regionPort1,
5236
+ region
5000
5237
  );
5001
- const existingT2 = polygonPerimeterT(
5002
- assignment.regionPort2.d,
5003
- polygon2
5238
+ const existingT2 = getPortPerimeterTInRegion(
5239
+ assignment.regionPort2,
5240
+ region
5004
5241
  );
5005
5242
  const existingChord = [existingT1, existingT2];
5006
- if (chordsCross(newChord, existingChord)) {
5243
+ if (chordsCross(newChord, existingChord, perimeter)) {
5007
5244
  crossings++;
5008
5245
  }
5009
5246
  }
@@ -5014,22 +5251,23 @@ function computeCrossingAssignmentsForPolygon(region, port1, port2) {
5014
5251
  if (!polygon2 || polygon2.length < 3) {
5015
5252
  return [];
5016
5253
  }
5017
- const t1 = polygonPerimeterT(port1.d, polygon2);
5018
- const t2 = polygonPerimeterT(port2.d, polygon2);
5254
+ const perimeter = getRegionPerimeter(region);
5255
+ const t1 = getPortPerimeterTInRegion(port1, region);
5256
+ const t2 = getPortPerimeterTInRegion(port2, region);
5019
5257
  const newChord = [t1, t2];
5020
5258
  const crossingAssignments = [];
5021
5259
  const assignments = region.assignments ?? [];
5022
5260
  for (const assignment of assignments) {
5023
- const existingT1 = polygonPerimeterT(
5024
- assignment.regionPort1.d,
5025
- polygon2
5261
+ const existingT1 = getPortPerimeterTInRegion(
5262
+ assignment.regionPort1,
5263
+ region
5026
5264
  );
5027
- const existingT2 = polygonPerimeterT(
5028
- assignment.regionPort2.d,
5029
- polygon2
5265
+ const existingT2 = getPortPerimeterTInRegion(
5266
+ assignment.regionPort2,
5267
+ region
5030
5268
  );
5031
5269
  const existingChord = [existingT1, existingT2];
5032
- if (chordsCross(newChord, existingChord)) {
5270
+ if (chordsCross(newChord, existingChord, perimeter)) {
5033
5271
  crossingAssignments.push(assignment);
5034
5272
  }
5035
5273
  }
@@ -5076,6 +5314,12 @@ var visualizeViaGraphSolver = (solver) => {
5076
5314
  hidePortPoints: true
5077
5315
  } : {}
5078
5316
  });
5317
+ if (solver.iterations === 0) {
5318
+ for (const polygon2 of graphics.polygons) {
5319
+ polygon2.stroke = "rgba(128, 128, 128, 0.5)";
5320
+ polygon2.strokeWidth = 0.03;
5321
+ }
5322
+ }
5079
5323
  const outerIds = /* @__PURE__ */ new Set(["T", "B", "L", "R"]);
5080
5324
  let netColorIndex = 0;
5081
5325
  const netColorMap = /* @__PURE__ */ new Map();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tscircuit/hypergraph",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.33",
4
+ "version": "0.0.35",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",
@@ -17,7 +17,7 @@
17
17
  "devDependencies": {
18
18
  "@biomejs/biome": "^2.3.11",
19
19
  "@tscircuit/find-convex-regions": "^0.0.7",
20
- "@tscircuit/jumper-topology-generator": "^0.0.1",
20
+ "@tscircuit/jumper-topology-generator": "^0.0.2",
21
21
  "@tscircuit/math-utils": "^0.0.29",
22
22
  "@types/bun": "latest",
23
23
  "bun-match-svg": "^0.0.15",