@tscircuit/hypergraph 0.0.34 → 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
@@ -248,6 +248,13 @@ declare class HyperGraphSolver<RegionType extends Region = Region, RegionPortTyp
248
248
  * when there is no direct port-assignment conflict.
249
249
  */
250
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;
251
258
  computeG(candidate: CandidateType): number;
252
259
  /**
253
260
  * Return a subset of the candidates for entering a region. These candidates
@@ -317,6 +324,11 @@ interface JRegion extends Region {
317
324
  x: number;
318
325
  y: number;
319
326
  }[];
327
+ polygonPerimeterCache?: {
328
+ edgeLengths: number[];
329
+ cumulative: number[];
330
+ perimeter: number;
331
+ };
320
332
  isPad: boolean;
321
333
  isThroughJumper?: boolean;
322
334
  isConnectionRegion?: boolean;
@@ -324,6 +336,8 @@ interface JRegion extends Region {
324
336
  };
325
337
  }
326
338
  interface JPort extends RegionPort {
339
+ region1T?: number;
340
+ region2T?: number;
327
341
  d: {
328
342
  x: number;
329
343
  y: number;
@@ -383,6 +397,7 @@ declare class JumperGraphSolver extends HyperGraphSolver<JRegion, JPort> {
383
397
  private populateDistanceToEndMaps;
384
398
  estimateCostToEnd(port: JPort): number;
385
399
  getPortUsagePenalty(port: JPort): number;
400
+ isTransitionAllowed(region: JRegion, port1: JPort, port2: JPort): boolean;
386
401
  computeIncreasedRegionCostIfPortsAreUsed(region: JRegion, port1: JPort, port2: JPort): number;
387
402
  getRipsRequiredForPortUsage(region: JRegion, port1: JPort, port2: JPort): RegionPortAssignment[];
388
403
  isRipRequiredForPortUsage(region: JRegion, _port1: JPort, _port2: JPort): boolean;
package/dist/index.js CHANGED
@@ -337,6 +337,15 @@ var HyperGraphSolver = class extends BaseSolver {
337
337
  isRipRequiredForPortUsage(_region, _port1, _port2) {
338
338
  return false;
339
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
+ }
340
349
  computeG(candidate) {
341
350
  return candidate.parent.g + this.computeIncreasedRegionCostIfPortsAreUsed(
342
351
  candidate.lastRegion,
@@ -405,6 +414,13 @@ var HyperGraphSolver = class extends BaseSolver {
405
414
  const nextCandidatesByRegion = {};
406
415
  for (const port of currentRegion.ports) {
407
416
  if (port === currentCandidate.port) continue;
417
+ if (!this.isTransitionAllowed(
418
+ currentRegion,
419
+ currentPort,
420
+ port
421
+ )) {
422
+ continue;
423
+ }
408
424
  const ripRequired = port.assignment && port.assignment.connection.mutuallyConnectedNetworkId !== this.currentConnection.mutuallyConnectedNetworkId || this.isRipRequiredForPortUsage(
409
425
  currentRegion,
410
426
  currentPort,
@@ -772,42 +788,156 @@ var rotateGraph90Degrees = (graph) => {
772
788
  };
773
789
 
774
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
+ }
775
802
  function perimeterT(p, xmin, xmax, ymin, ymax) {
776
803
  const W = xmax - xmin;
777
804
  const H = ymax - ymin;
778
805
  const eps = 1e-6;
779
- if (Math.abs(p.y - ymax) < eps) {
780
- return p.x - xmin;
781
- }
782
- if (Math.abs(p.x - xmax) < eps) {
783
- return W + (ymax - p.y);
784
- }
785
- if (Math.abs(p.y - ymin) < eps) {
786
- return W + H + (xmax - p.x);
787
- }
788
- if (Math.abs(p.x - xmin) < eps) {
789
- return 2 * W + H + (p.y - ymin);
790
- }
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);
791
810
  const distTop = Math.abs(p.y - ymax);
792
811
  const distRight = Math.abs(p.x - xmax);
793
812
  const distBottom = Math.abs(p.y - ymin);
794
813
  const distLeft = Math.abs(p.x - xmin);
795
814
  const minDist = Math.min(distTop, distRight, distBottom, distLeft);
796
- if (minDist === distTop) {
797
- return Math.max(0, Math.min(W, p.x - xmin));
798
- }
799
- if (minDist === distRight) {
800
- return W + Math.max(0, Math.min(H, ymax - p.y));
801
- }
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));
802
817
  if (minDist === distBottom) {
803
818
  return W + H + Math.max(0, Math.min(W, xmax - p.x));
804
819
  }
805
820
  return 2 * W + H + Math.max(0, Math.min(H, p.y - ymin));
806
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
+ }
807
908
  function areCoincident(t1, t2, eps = 1e-6) {
808
909
  return Math.abs(t1 - t2) < eps;
809
910
  }
810
- function chordsCross(chord1, chord2) {
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
+ }
811
941
  const [a, b] = chord1[0] < chord1[1] ? chord1 : [chord1[1], chord1[0]];
812
942
  const [c, d] = chord2[0] < chord2[1] ? chord2 : [chord2[1], chord2[0]];
813
943
  if (areCoincident(a, c) || areCoincident(a, d) || areCoincident(b, c) || areCoincident(b, d)) {
@@ -818,29 +948,23 @@ function chordsCross(chord1, chord2) {
818
948
 
819
949
  // lib/JumperGraphSolver/computeCrossingAssignments.ts
820
950
  function computeCrossingAssignments(region, port1, port2) {
821
- const { minX: xmin, maxX: xmax, minY: ymin, maxY: ymax } = region.d.bounds;
822
- const t1 = perimeterT(port1.d, xmin, xmax, ymin, ymax);
823
- const t2 = perimeterT(port2.d, xmin, xmax, ymin, ymax);
951
+ const perimeter = getRegionPerimeter(region);
952
+ const t1 = getPortPerimeterTInRegion(port1, region);
953
+ const t2 = getPortPerimeterTInRegion(port2, region);
824
954
  const newChord = [t1, t2];
825
955
  const crossingAssignments = [];
826
956
  const assignments = region.assignments ?? [];
827
957
  for (const assignment of assignments) {
828
- const existingT1 = perimeterT(
829
- assignment.regionPort1.d,
830
- xmin,
831
- xmax,
832
- ymin,
833
- ymax
958
+ const existingT1 = getPortPerimeterTInRegion(
959
+ assignment.regionPort1,
960
+ region
834
961
  );
835
- const existingT2 = perimeterT(
836
- assignment.regionPort2.d,
837
- xmin,
838
- xmax,
839
- ymin,
840
- ymax
962
+ const existingT2 = getPortPerimeterTInRegion(
963
+ assignment.regionPort2,
964
+ region
841
965
  );
842
966
  const existingChord = [existingT1, existingT2];
843
- if (chordsCross(newChord, existingChord)) {
967
+ if (chordsCross(newChord, existingChord, perimeter)) {
844
968
  crossingAssignments.push(assignment);
845
969
  }
846
970
  }
@@ -849,29 +973,23 @@ function computeCrossingAssignments(region, port1, port2) {
849
973
 
850
974
  // lib/JumperGraphSolver/computeDifferentNetCrossings.ts
851
975
  function computeDifferentNetCrossings(region, port1, port2) {
852
- const { minX: xmin, maxX: xmax, minY: ymin, maxY: ymax } = region.d.bounds;
853
- const t1 = perimeterT(port1.d, xmin, xmax, ymin, ymax);
854
- const t2 = perimeterT(port2.d, xmin, xmax, ymin, ymax);
976
+ const perimeter = getRegionPerimeter(region);
977
+ const t1 = getPortPerimeterTInRegion(port1, region);
978
+ const t2 = getPortPerimeterTInRegion(port2, region);
855
979
  const newChord = [t1, t2];
856
980
  let crossings = 0;
857
981
  const assignments = region.assignments ?? [];
858
982
  for (const assignment of assignments) {
859
- const existingT1 = perimeterT(
860
- assignment.regionPort1.d,
861
- xmin,
862
- xmax,
863
- ymin,
864
- ymax
983
+ const existingT1 = getPortPerimeterTInRegion(
984
+ assignment.regionPort1,
985
+ region
865
986
  );
866
- const existingT2 = perimeterT(
867
- assignment.regionPort2.d,
868
- xmin,
869
- xmax,
870
- ymin,
871
- ymax
987
+ const existingT2 = getPortPerimeterTInRegion(
988
+ assignment.regionPort2,
989
+ region
872
990
  );
873
991
  const existingChord = [existingT1, existingT2];
874
- if (chordsCross(newChord, existingChord)) {
992
+ if (chordsCross(newChord, existingChord, perimeter)) {
875
993
  crossings++;
876
994
  }
877
995
  }
@@ -909,6 +1027,7 @@ function countInputConnectionCrossings(graph, connections) {
909
1027
  }
910
1028
  }
911
1029
  const chords = [];
1030
+ const perimeter = 2 * (maxX - minX) + 2 * (maxY - minY);
912
1031
  for (const conn of connections) {
913
1032
  let startCenter;
914
1033
  let endCenter;
@@ -931,7 +1050,7 @@ function countInputConnectionCrossings(graph, connections) {
931
1050
  let crossings = 0;
932
1051
  for (let i = 0; i < chords.length; i++) {
933
1052
  for (let j = i + 1; j < chords.length; j++) {
934
- if (chordsCross(chords[i], chords[j])) {
1053
+ if (chordsCross(chords[i], chords[j], perimeter)) {
935
1054
  crossings++;
936
1055
  }
937
1056
  }
@@ -972,9 +1091,7 @@ var visualizeJumperGraph = (graph, options) => {
972
1091
  const points = polygon2;
973
1092
  graphics.polygons.push({
974
1093
  points,
975
- fill,
976
- stroke: "rgba(128, 128, 128, 0.5)",
977
- strokeWidth: 0.03
1094
+ fill
978
1095
  });
979
1096
  } else {
980
1097
  graphics.rects.push({
@@ -1065,6 +1182,12 @@ var visualizeJumperGraphSolver = (solver) => {
1065
1182
  hidePortPoints: true
1066
1183
  } : {}
1067
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
+ }
1068
1191
  if (solver.currentConnection && !solver.solved) {
1069
1192
  const connectionColor = getConnectionColor(
1070
1193
  solver.currentConnection.connectionId
@@ -1230,11 +1353,35 @@ var JumperGraphSolver = class extends HyperGraphSolver {
1230
1353
  const ripCount = port.ripCount ?? 0;
1231
1354
  return ripCount * this.portUsagePenalty + ripCount * this.portUsagePenaltySq;
1232
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
+ }
1233
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
+ }
1234
1375
  const crossings = computeDifferentNetCrossings(region, port1, port2);
1235
1376
  return crossings * this.crossingPenalty + crossings * this.crossingPenaltySq;
1236
1377
  }
1237
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
+ }
1238
1385
  const crossingAssignments = computeCrossingAssignments(region, port1, port2);
1239
1386
  const conflictingAssignments = crossingAssignments.filter(
1240
1387
  (a) => a.connection.mutuallyConnectedNetworkId !== this.currentConnection.mutuallyConnectedNetworkId
@@ -1249,7 +1396,7 @@ var JumperGraphSolver = class extends HyperGraphSolver {
1249
1396
  return conflictingAssignments;
1250
1397
  }
1251
1398
  isRipRequiredForPortUsage(region, _port1, _port2) {
1252
- if (!region.d.isThroughJumper) return false;
1399
+ if (!region.d.isThroughJumper && !region.d.isPad) return false;
1253
1400
  for (const assignment of region.assignments ?? []) {
1254
1401
  if (assignment.connection.mutuallyConnectedNetworkId !== this.currentConnection.mutuallyConnectedNetworkId) {
1255
1402
  return true;
@@ -1301,23 +1448,123 @@ var createConnectionRegion = (regionId, x, y) => {
1301
1448
  };
1302
1449
 
1303
1450
  // lib/JumperGraphSolver/jumper-graph-generator/findBoundaryRegion.ts
1304
- var findBoundaryRegion = (x, y, regions, graphBounds) => {
1305
- for (const region of regions) {
1306
- if (region.d.isPad || region.d.isThroughJumper) continue;
1307
- const bounds = region.d.bounds;
1308
- if (Math.abs(x - bounds.minX) < 0.01 && y >= bounds.minY && y <= bounds.maxY) {
1309
- return { region, portPosition: { x: bounds.minX, y } };
1310
- }
1311
- if (Math.abs(x - bounds.maxX) < 0.01 && y >= bounds.minY && y <= bounds.maxY) {
1312
- 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
+ }
1313
1504
  }
1314
- if (Math.abs(y - bounds.minY) < 0.01 && x >= bounds.minX && x <= bounds.maxX) {
1315
- 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
+ }
1316
1536
  }
1317
- if (Math.abs(y - bounds.maxY) < 0.01 && x >= bounds.minX && x <= bounds.maxX) {
1318
- 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 };
1319
1562
  }
1320
1563
  }
1564
+ return best;
1565
+ };
1566
+ var findBoundaryRegion = (x, y, regions, graphBounds) => {
1567
+ const preferredSides = getBoundarySidesForPoint(x, y, graphBounds);
1321
1568
  let closestRegion = null;
1322
1569
  let closestDistance = Number.POSITIVE_INFINITY;
1323
1570
  let closestPortPosition = { x, y };
@@ -1326,23 +1573,19 @@ var findBoundaryRegion = (x, y, regions, graphBounds) => {
1326
1573
  const bounds = region.d.bounds;
1327
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;
1328
1575
  if (!isOuterRegion) continue;
1329
- const clampedX = Math.max(bounds.minX, Math.min(bounds.maxX, x));
1330
- const clampedY = Math.max(bounds.minY, Math.min(bounds.maxY, y));
1331
- 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);
1332
1585
  if (dist < closestDistance) {
1333
1586
  closestDistance = dist;
1334
1587
  closestRegion = region;
1335
- if (x < bounds.minX) {
1336
- closestPortPosition = { x: bounds.minX, y: clampedY };
1337
- } else if (x > bounds.maxX) {
1338
- closestPortPosition = { x: bounds.maxX, y: clampedY };
1339
- } else if (y < bounds.minY) {
1340
- closestPortPosition = { x: clampedX, y: bounds.minY };
1341
- } else if (y > bounds.maxY) {
1342
- closestPortPosition = { x: clampedX, y: bounds.maxY };
1343
- } else {
1344
- closestPortPosition = { x: clampedX, y: clampedY };
1345
- }
1588
+ closestPortPosition = { x: projection.x, y: projection.y };
1346
1589
  }
1347
1590
  }
1348
1591
  if (closestRegion) {
@@ -4976,64 +5219,28 @@ function generateDefaultViaTopologyGrid(opts) {
4976
5219
  }
4977
5220
 
4978
5221
  // lib/ViaGraphSolver/polygonPerimeterUtils.ts
4979
- function polygonPerimeterT(p, polygon2) {
4980
- const n = polygon2.length;
4981
- let bestDist = Infinity;
4982
- let bestEdgeIndex = 0;
4983
- let bestT = 0;
4984
- for (let i = 0; i < n; i++) {
4985
- const a2 = polygon2[i];
4986
- const b2 = polygon2[(i + 1) % n];
4987
- const dx = b2.x - a2.x;
4988
- const dy = b2.y - a2.y;
4989
- const lenSq = dx * dx + dy * dy;
4990
- if (lenSq < 1e-10) continue;
4991
- const t = Math.max(
4992
- 0,
4993
- Math.min(1, ((p.x - a2.x) * dx + (p.y - a2.y) * dy) / lenSq)
4994
- );
4995
- const projX = a2.x + t * dx;
4996
- const projY = a2.y + t * dy;
4997
- const dist = Math.sqrt((p.x - projX) ** 2 + (p.y - projY) ** 2);
4998
- if (dist < bestDist) {
4999
- bestDist = dist;
5000
- bestEdgeIndex = i;
5001
- bestT = t;
5002
- }
5003
- }
5004
- let cumulative = 0;
5005
- for (let i = 0; i < bestEdgeIndex; i++) {
5006
- const a2 = polygon2[i];
5007
- const b2 = polygon2[(i + 1) % n];
5008
- cumulative += Math.sqrt((b2.x - a2.x) ** 2 + (b2.y - a2.y) ** 2);
5009
- }
5010
- const a = polygon2[bestEdgeIndex];
5011
- const b = polygon2[(bestEdgeIndex + 1) % n];
5012
- const edgeLen = Math.sqrt((b.x - a.x) ** 2 + (b.y - a.y) ** 2);
5013
- cumulative += bestT * edgeLen;
5014
- return cumulative;
5015
- }
5016
5222
  function computeDifferentNetCrossingsForPolygon(region, port1, port2) {
5017
5223
  const polygon2 = region.d.polygon;
5018
5224
  if (!polygon2 || polygon2.length < 3) {
5019
5225
  return 0;
5020
5226
  }
5021
- const t1 = polygonPerimeterT(port1.d, polygon2);
5022
- const t2 = polygonPerimeterT(port2.d, polygon2);
5227
+ const perimeter = getRegionPerimeter(region);
5228
+ const t1 = getPortPerimeterTInRegion(port1, region);
5229
+ const t2 = getPortPerimeterTInRegion(port2, region);
5023
5230
  const newChord = [t1, t2];
5024
5231
  let crossings = 0;
5025
5232
  const assignments = region.assignments ?? [];
5026
5233
  for (const assignment of assignments) {
5027
- const existingT1 = polygonPerimeterT(
5028
- assignment.regionPort1.d,
5029
- polygon2
5234
+ const existingT1 = getPortPerimeterTInRegion(
5235
+ assignment.regionPort1,
5236
+ region
5030
5237
  );
5031
- const existingT2 = polygonPerimeterT(
5032
- assignment.regionPort2.d,
5033
- polygon2
5238
+ const existingT2 = getPortPerimeterTInRegion(
5239
+ assignment.regionPort2,
5240
+ region
5034
5241
  );
5035
5242
  const existingChord = [existingT1, existingT2];
5036
- if (chordsCross(newChord, existingChord)) {
5243
+ if (chordsCross(newChord, existingChord, perimeter)) {
5037
5244
  crossings++;
5038
5245
  }
5039
5246
  }
@@ -5044,22 +5251,23 @@ function computeCrossingAssignmentsForPolygon(region, port1, port2) {
5044
5251
  if (!polygon2 || polygon2.length < 3) {
5045
5252
  return [];
5046
5253
  }
5047
- const t1 = polygonPerimeterT(port1.d, polygon2);
5048
- const t2 = polygonPerimeterT(port2.d, polygon2);
5254
+ const perimeter = getRegionPerimeter(region);
5255
+ const t1 = getPortPerimeterTInRegion(port1, region);
5256
+ const t2 = getPortPerimeterTInRegion(port2, region);
5049
5257
  const newChord = [t1, t2];
5050
5258
  const crossingAssignments = [];
5051
5259
  const assignments = region.assignments ?? [];
5052
5260
  for (const assignment of assignments) {
5053
- const existingT1 = polygonPerimeterT(
5054
- assignment.regionPort1.d,
5055
- polygon2
5261
+ const existingT1 = getPortPerimeterTInRegion(
5262
+ assignment.regionPort1,
5263
+ region
5056
5264
  );
5057
- const existingT2 = polygonPerimeterT(
5058
- assignment.regionPort2.d,
5059
- polygon2
5265
+ const existingT2 = getPortPerimeterTInRegion(
5266
+ assignment.regionPort2,
5267
+ region
5060
5268
  );
5061
5269
  const existingChord = [existingT1, existingT2];
5062
- if (chordsCross(newChord, existingChord)) {
5270
+ if (chordsCross(newChord, existingChord, perimeter)) {
5063
5271
  crossingAssignments.push(assignment);
5064
5272
  }
5065
5273
  }
@@ -5106,6 +5314,12 @@ var visualizeViaGraphSolver = (solver) => {
5106
5314
  hidePortPoints: true
5107
5315
  } : {}
5108
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
+ }
5109
5323
  const outerIds = /* @__PURE__ */ new Set(["T", "B", "L", "R"]);
5110
5324
  let netColorIndex = 0;
5111
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.34",
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",