@tscircuit/hypergraph 0.0.34 → 0.0.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +76 -215
  2. package/dist/index.js +634 -369
  3. package/package.json +2 -2
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) {
@@ -3640,48 +3883,24 @@ var via_tile_default = {
3640
3883
  y: 0.582296
3641
3884
  },
3642
3885
  {
3643
- x: 1.31705,
3644
- y: 0.74908
3645
- },
3646
- {
3647
- x: 1.425309,
3648
- y: 0.74908
3649
- },
3650
- {
3651
- x: 1.762721,
3652
- y: 1.086492
3653
- },
3654
- {
3655
- x: 1.762721,
3656
- y: 1.563668
3657
- },
3658
- {
3659
- x: 1.425309,
3660
- y: 1.90108
3886
+ x: 1.712721,
3887
+ y: 0.811183
3661
3888
  },
3662
3889
  {
3663
- x: 1.266397,
3664
- y: 1.90108
3890
+ x: 1.712721,
3891
+ y: 1.542957
3665
3892
  },
3666
3893
  {
3667
- x: 0.781911,
3668
- y: 2.385567
3894
+ x: 0.920111,
3895
+ y: 2.335567
3669
3896
  },
3670
3897
  {
3671
- x: 0.013957,
3672
- y: 2.385567
3673
- },
3674
- {
3675
- x: -0.753066,
3676
- y: 1.618543
3677
- },
3678
- {
3679
- x: -0.753066,
3680
- y: 1.360375
3898
+ x: 0.034666,
3899
+ y: 2.335566
3681
3900
  },
3682
3901
  {
3683
3902
  x: -0.703066,
3684
- y: 1.310375
3903
+ y: 1.597834
3685
3904
  },
3686
3905
  {
3687
3906
  x: -0.703066,
@@ -3704,32 +3923,20 @@ var via_tile_default = {
3704
3923
  y: -1.893395
3705
3924
  },
3706
3925
  {
3707
- x: -2.2,
3708
- y: -1.4
3709
- },
3710
- {
3711
- x: -2.2,
3712
- y: 0.903041
3713
- },
3714
- {
3715
- x: -1.603041,
3716
- y: 1.5
3926
+ x: -2.10103,
3927
+ y: -1.49897
3717
3928
  },
3718
3929
  {
3719
- x: -1.516757,
3720
- y: 1.5
3930
+ x: -2.10103,
3931
+ y: 1.002011
3721
3932
  },
3722
3933
  {
3723
- x: -1.430473,
3724
- y: 1.586284
3934
+ x: -1.708294,
3935
+ y: 1.394747
3725
3936
  },
3726
3937
  {
3727
- x: -0.978975,
3728
- y: 1.586284
3729
- },
3730
- {
3731
- x: -0.703066,
3732
- y: 1.310375
3938
+ x: -0.897813,
3939
+ y: 1.394747
3733
3940
  },
3734
3941
  {
3735
3942
  x: -0.703066,
@@ -3739,81 +3946,37 @@ var via_tile_default = {
3739
3946
  },
3740
3947
  {
3741
3948
  routeId: "Net5:route_0",
3742
- fromPort: "a3797f13-73f9-48c3-a448-bea3980cdd65",
3743
- toPort: "b0c56bb1-ea80-4154-9bb7-cb0fd760de8f",
3949
+ fromPort: "4e57cee8-0910-4c83-9ec1-1ed2c9d16dcc",
3950
+ toPort: "a3797f13-73f9-48c3-a448-bea3980cdd65",
3744
3951
  layer: "bottom",
3745
3952
  segments: [
3746
3953
  {
3747
- x: -1.115754,
3748
- y: -0.799377
3749
- },
3750
- {
3751
- x: -0.764375,
3752
- y: -0.447998
3753
- },
3754
- {
3755
- x: -0.636191,
3756
- y: -0.447998
3757
- },
3758
- {
3759
- x: -0.368136,
3760
- y: -0.179943
3761
- },
3762
- {
3763
- x: -0.368136,
3764
- y: -0.178529
3765
- },
3766
- {
3767
- x: 0.556834,
3768
- y: 0.746441
3954
+ x: 1.268717,
3955
+ y: -1.698536
3769
3956
  },
3770
3957
  {
3771
- x: 0.397934,
3772
- y: 0.905341
3958
+ x: 0.369558,
3959
+ y: -0.799377
3773
3960
  },
3774
3961
  {
3775
- x: 0.397934,
3776
- y: 1.458567
3962
+ x: -1.115754,
3963
+ y: -0.799377
3777
3964
  }
3778
3965
  ]
3779
3966
  },
3780
3967
  {
3781
3968
  routeId: "Net5:route_1",
3782
- fromPort: "4e57cee8-0910-4c83-9ec1-1ed2c9d16dcc",
3969
+ fromPort: "a3797f13-73f9-48c3-a448-bea3980cdd65",
3783
3970
  toPort: "b0c56bb1-ea80-4154-9bb7-cb0fd760de8f",
3784
3971
  layer: "bottom",
3785
3972
  segments: [
3786
3973
  {
3787
- x: 1.268717,
3788
- y: -1.698536
3789
- },
3790
- {
3791
- x: 1.405649,
3792
- y: -1.561604
3793
- },
3794
- {
3795
- x: 1.393076,
3796
- y: -1.561604
3797
- },
3798
- {
3799
- x: 1.055664,
3800
- y: -1.224192
3801
- },
3802
- {
3803
- x: 1.055664,
3804
- y: -0.905992
3805
- },
3806
- {
3807
- x: 0.556834,
3808
- y: -0.407162
3809
- },
3810
- {
3811
- x: 0.556834,
3812
- y: 0.746441
3974
+ x: -1.115754,
3975
+ y: -0.799377
3813
3976
  },
3814
3977
  {
3815
3978
  x: 0.397934,
3816
- y: 0.905341
3979
+ y: 0.714311
3817
3980
  },
3818
3981
  {
3819
3982
  x: 0.397934,
@@ -3871,28 +4034,16 @@ var via_tile_default = {
3871
4034
  x: 0.159346,
3872
4035
  y: 2.034567
3873
4036
  },
3874
- {
3875
- x: 0.06239,
3876
- y: 1.93761
3877
- },
3878
4037
  {
3879
4038
  x: -0.178066,
3880
4039
  y: 1.697155
3881
4040
  },
3882
4041
  {
3883
4042
  x: -0.178066,
3884
- y: 0.824715
4043
+ y: 0.981123
3885
4044
  },
3886
4045
  {
3887
- x: -1.042874,
3888
- y: -0.040093
3889
- },
3890
- {
3891
- x: -1.169705,
3892
- y: -0.040093
3893
- },
3894
- {
3895
- x: -1.209798,
4046
+ x: -1.159189,
3896
4047
  y: 0
3897
4048
  },
3898
4049
  {
@@ -3912,69 +4063,25 @@ var via_tile_default = {
3912
4063
  y: -0.164505
3913
4064
  },
3914
4065
  {
3915
- x: 1.562188,
3916
- y: -0.173664
3917
- },
3918
- {
3919
- x: 1.634312,
3920
- y: -0.173664
3921
- },
3922
- {
3923
- x: 2.207664,
3924
- y: -0.747016
3925
- },
3926
- {
3927
- x: 2.207664,
3928
- y: -1.224192
3929
- },
3930
- {
3931
- x: 1.870252,
3932
- y: -1.561604
4066
+ x: 1.565592,
4067
+ y: -0.164505
3933
4068
  },
3934
4069
  {
3935
- x: 1.844717,
3936
- y: -1.561604
4070
+ x: 2.181408,
4071
+ y: -0.780321
3937
4072
  },
3938
4073
  {
3939
- x: 1.844717,
3940
- y: -1.937124
4074
+ x: 2.181408,
4075
+ y: -1.600433
3941
4076
  },
3942
4077
  {
3943
4078
  x: 1.507305,
3944
4079
  y: -2.274536
3945
4080
  },
3946
4081
  {
3947
- x: 1.030129,
4082
+ x: 0.630282,
3948
4083
  y: -2.274536
3949
4084
  },
3950
- {
3951
- x: 0.692717,
3952
- y: -1.937124
3953
- },
3954
- {
3955
- x: 0.692717,
3956
- y: -1.739534
3957
- },
3958
- {
3959
- x: 0.475553,
3960
- y: -1.739534
3961
- },
3962
- {
3963
- x: 0.470734,
3964
- y: -1.744353
3965
- },
3966
- {
3967
- x: 0.455647,
3968
- y: -1.744353
3969
- },
3970
- {
3971
- x: 0.30246,
3972
- y: -1.89754
3973
- },
3974
- {
3975
- x: 0.253286,
3976
- y: -1.89754
3977
- },
3978
4085
  {
3979
4086
  x: 0.222457,
3980
4087
  y: -1.866711
@@ -3992,23 +4099,15 @@ var via_tile_default = {
3992
4099
  y: -1.866711
3993
4100
  },
3994
4101
  {
3995
- x: 0.208345,
4102
+ x: 0.137528,
3996
4103
  y: -1.866711
3997
4104
  },
3998
4105
  {
3999
- x: 0.2,
4000
- y: -1.858366
4001
- },
4002
- {
4003
- x: 0.2,
4004
- y: -1.2
4005
- },
4006
- {
4007
- x: -0.701789,
4008
- y: -1.2
4106
+ x: -0.329183,
4107
+ y: -1.4
4009
4108
  },
4010
4109
  {
4011
- x: -0.869184,
4110
+ x: -0.361788,
4012
4111
  y: -1.367395
4013
4112
  },
4014
4113
  {
@@ -4016,28 +4115,16 @@ var via_tile_default = {
4016
4115
  y: -1.367395
4017
4116
  },
4018
4117
  {
4019
- x: -1.025859,
4020
- y: -1.375377
4118
+ x: -1.237643,
4119
+ y: -1.587161
4021
4120
  },
4022
4121
  {
4023
- x: -1.354342,
4024
- y: -1.375377
4025
- },
4026
- {
4027
- x: -1.529719,
4028
- y: -1.2
4029
- },
4030
- {
4031
- x: -1.636994,
4032
- y: -1.2
4033
- },
4034
- {
4035
- x: -1.718497,
4036
- y: -1.118497
4122
+ x: -1.587161,
4123
+ y: -1.587161
4037
4124
  },
4038
4125
  {
4039
4126
  x: -1.8,
4040
- y: -1.2
4127
+ y: -1.374322
4041
4128
  },
4042
4129
  {
4043
4130
  x: -1.8,
@@ -4049,19 +4136,11 @@ var via_tile_default = {
4049
4136
  },
4050
4137
  {
4051
4138
  x: -1.80003,
4052
- y: -0.559789
4139
+ y: -0.6
4053
4140
  },
4054
4141
  {
4055
- x: -1.8,
4056
- y: -0.559759
4057
- },
4058
- {
4059
- x: -1.8,
4060
- y: 0.877363
4061
- },
4062
- {
4063
- x: -1.614161,
4064
- y: 1.063202
4142
+ x: -1.80003,
4143
+ y: 0.488423
4065
4144
  },
4066
4145
  {
4067
4146
  x: -1.419706,
@@ -4976,64 +5055,28 @@ function generateDefaultViaTopologyGrid(opts) {
4976
5055
  }
4977
5056
 
4978
5057
  // 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
5058
  function computeDifferentNetCrossingsForPolygon(region, port1, port2) {
5017
5059
  const polygon2 = region.d.polygon;
5018
5060
  if (!polygon2 || polygon2.length < 3) {
5019
5061
  return 0;
5020
5062
  }
5021
- const t1 = polygonPerimeterT(port1.d, polygon2);
5022
- const t2 = polygonPerimeterT(port2.d, polygon2);
5063
+ const perimeter = getRegionPerimeter(region);
5064
+ const t1 = getPortPerimeterTInRegion(port1, region);
5065
+ const t2 = getPortPerimeterTInRegion(port2, region);
5023
5066
  const newChord = [t1, t2];
5024
5067
  let crossings = 0;
5025
5068
  const assignments = region.assignments ?? [];
5026
5069
  for (const assignment of assignments) {
5027
- const existingT1 = polygonPerimeterT(
5028
- assignment.regionPort1.d,
5029
- polygon2
5070
+ const existingT1 = getPortPerimeterTInRegion(
5071
+ assignment.regionPort1,
5072
+ region
5030
5073
  );
5031
- const existingT2 = polygonPerimeterT(
5032
- assignment.regionPort2.d,
5033
- polygon2
5074
+ const existingT2 = getPortPerimeterTInRegion(
5075
+ assignment.regionPort2,
5076
+ region
5034
5077
  );
5035
5078
  const existingChord = [existingT1, existingT2];
5036
- if (chordsCross(newChord, existingChord)) {
5079
+ if (chordsCross(newChord, existingChord, perimeter)) {
5037
5080
  crossings++;
5038
5081
  }
5039
5082
  }
@@ -5044,28 +5087,170 @@ function computeCrossingAssignmentsForPolygon(region, port1, port2) {
5044
5087
  if (!polygon2 || polygon2.length < 3) {
5045
5088
  return [];
5046
5089
  }
5047
- const t1 = polygonPerimeterT(port1.d, polygon2);
5048
- const t2 = polygonPerimeterT(port2.d, polygon2);
5090
+ const perimeter = getRegionPerimeter(region);
5091
+ const t1 = getPortPerimeterTInRegion(port1, region);
5092
+ const t2 = getPortPerimeterTInRegion(port2, region);
5049
5093
  const newChord = [t1, t2];
5050
5094
  const crossingAssignments = [];
5051
5095
  const assignments = region.assignments ?? [];
5052
5096
  for (const assignment of assignments) {
5053
- const existingT1 = polygonPerimeterT(
5054
- assignment.regionPort1.d,
5055
- polygon2
5097
+ const existingT1 = getPortPerimeterTInRegion(
5098
+ assignment.regionPort1,
5099
+ region
5056
5100
  );
5057
- const existingT2 = polygonPerimeterT(
5058
- assignment.regionPort2.d,
5059
- polygon2
5101
+ const existingT2 = getPortPerimeterTInRegion(
5102
+ assignment.regionPort2,
5103
+ region
5060
5104
  );
5061
5105
  const existingChord = [existingT1, existingT2];
5062
- if (chordsCross(newChord, existingChord)) {
5106
+ if (chordsCross(newChord, existingChord, perimeter)) {
5063
5107
  crossingAssignments.push(assignment);
5064
5108
  }
5065
5109
  }
5066
5110
  return crossingAssignments;
5067
5111
  }
5068
5112
 
5113
+ // lib/ViaGraphSolver/resolveSolvedRoutePoints.ts
5114
+ var POINT_EPSILON = 1e-6;
5115
+ function arePointsEqual(a, b) {
5116
+ return Math.abs(a.x - b.x) <= POINT_EPSILON && Math.abs(a.y - b.y) <= POINT_EPSILON;
5117
+ }
5118
+ function appendPoint(points, point2) {
5119
+ const lastPoint = points[points.length - 1];
5120
+ if (lastPoint && arePointsEqual(lastPoint, point2)) return;
5121
+ points.push(point2);
5122
+ }
5123
+ function findNearestVia(vias, point2) {
5124
+ let bestVia = null;
5125
+ let bestDistance = Infinity;
5126
+ for (const via of vias) {
5127
+ const dx = via.position.x - point2.x;
5128
+ const dy = via.position.y - point2.y;
5129
+ const distance3 = Math.hypot(dx, dy);
5130
+ if (distance3 < bestDistance) {
5131
+ bestDistance = distance3;
5132
+ bestVia = via;
5133
+ }
5134
+ }
5135
+ return bestVia;
5136
+ }
5137
+ function parseViaRegionNetName(regionId) {
5138
+ const marker = ":v:";
5139
+ const markerIndex = regionId.lastIndexOf(marker);
5140
+ if (markerIndex !== -1) {
5141
+ return regionId.slice(markerIndex + marker.length);
5142
+ }
5143
+ const lastColonIndex = regionId.lastIndexOf(":");
5144
+ if (lastColonIndex === -1) return regionId;
5145
+ return regionId.slice(lastColonIndex + 1);
5146
+ }
5147
+ function parseViaRegionTilePrefix(regionId) {
5148
+ const marker = ":v:";
5149
+ const markerIndex = regionId.lastIndexOf(marker);
5150
+ if (markerIndex <= 0) return null;
5151
+ return regionId.slice(0, markerIndex);
5152
+ }
5153
+ function getBottomRouteSegmentsForVias(viaTile, vias) {
5154
+ const viaIdSet = new Set(vias.map((via) => via.viaId));
5155
+ return viaTile.routeSegments.filter(
5156
+ (routeSegment) => routeSegment.layer === "bottom" && viaIdSet.has(routeSegment.fromPort) && viaIdSet.has(routeSegment.toPort) && routeSegment.segments.length >= 2
5157
+ );
5158
+ }
5159
+ function selectViasForTraversedRegion(viaTile, viaRegion) {
5160
+ const netName = parseViaRegionNetName(viaRegion.regionId);
5161
+ if (!netName) return [];
5162
+ const viasForNet = viaTile.viasByNet[netName];
5163
+ if (!viasForNet || viasForNet.length === 0) return [];
5164
+ const tilePrefix = parseViaRegionTilePrefix(viaRegion.regionId);
5165
+ if (!tilePrefix) return viasForNet;
5166
+ const tileScopedVias = viasForNet.filter(
5167
+ (via) => via.viaId.startsWith(`${tilePrefix}:`)
5168
+ );
5169
+ return tileScopedVias.length > 0 ? tileScopedVias : viasForNet;
5170
+ }
5171
+ function normalizeSegmentPoints(points) {
5172
+ const normalized = [];
5173
+ for (const point2 of points) appendPoint(normalized, point2);
5174
+ return normalized;
5175
+ }
5176
+ function appendLineSegment(lineSegments, points, layer) {
5177
+ const normalized = normalizeSegmentPoints(points);
5178
+ if (normalized.length < 2) return;
5179
+ const lastLine = lineSegments[lineSegments.length - 1];
5180
+ if (!lastLine || lastLine.layer !== layer) {
5181
+ lineSegments.push({ points: normalized, layer });
5182
+ return;
5183
+ }
5184
+ const lastPoint = lastLine.points[lastLine.points.length - 1];
5185
+ const firstPoint = normalized[0];
5186
+ if (!lastPoint || !firstPoint || !arePointsEqual(lastPoint, firstPoint)) {
5187
+ lineSegments.push({ points: normalized, layer });
5188
+ return;
5189
+ }
5190
+ const continuation = normalized.slice(1);
5191
+ for (const point2 of continuation) {
5192
+ appendPoint(lastLine.points, point2);
5193
+ }
5194
+ }
5195
+ function flattenLineSegments(lineSegments) {
5196
+ const points = [];
5197
+ for (const lineSegment of lineSegments) {
5198
+ for (const point2 of lineSegment.points) {
5199
+ appendPoint(points, point2);
5200
+ }
5201
+ }
5202
+ return points;
5203
+ }
5204
+ function resolveSolvedRouteLineSegments(solvedRoute, viaTile) {
5205
+ if (solvedRoute.path.length === 0) return [];
5206
+ const path = solvedRoute.path;
5207
+ const lineSegments = [];
5208
+ const drawnViaRegionIds = /* @__PURE__ */ new Set();
5209
+ for (let index = 1; index < path.length; index++) {
5210
+ const previousCandidate = path[index - 1];
5211
+ const currentCandidate = path[index];
5212
+ const previousPoint = {
5213
+ x: previousCandidate.port.d.x,
5214
+ y: previousCandidate.port.d.y
5215
+ };
5216
+ const currentPoint = {
5217
+ x: currentCandidate.port.d.x,
5218
+ y: currentCandidate.port.d.y
5219
+ };
5220
+ const traversedRegion = currentCandidate.lastRegion;
5221
+ const isViaRegionTraversal = !!viaTile && !!traversedRegion?.d?.isViaRegion;
5222
+ if (!isViaRegionTraversal) {
5223
+ appendLineSegment(lineSegments, [previousPoint, currentPoint], "top");
5224
+ continue;
5225
+ }
5226
+ const viasForRegion = selectViasForTraversedRegion(viaTile, traversedRegion);
5227
+ if (viasForRegion.length === 0) continue;
5228
+ const entryVia = findNearestVia(viasForRegion, previousPoint);
5229
+ const exitVia = findNearestVia(viasForRegion, currentPoint);
5230
+ if (entryVia) {
5231
+ appendLineSegment(lineSegments, [previousPoint, entryVia.position], "top");
5232
+ }
5233
+ const bottomRouteSegments = getBottomRouteSegmentsForVias(
5234
+ viaTile,
5235
+ viasForRegion
5236
+ );
5237
+ if (bottomRouteSegments.length > 0 && !drawnViaRegionIds.has(traversedRegion.regionId)) {
5238
+ drawnViaRegionIds.add(traversedRegion.regionId);
5239
+ for (const routeSegment of bottomRouteSegments) {
5240
+ appendLineSegment(lineSegments, routeSegment.segments, "bottom");
5241
+ }
5242
+ }
5243
+ if (exitVia) {
5244
+ appendLineSegment(lineSegments, [exitVia.position, currentPoint], "top");
5245
+ }
5246
+ }
5247
+ return lineSegments;
5248
+ }
5249
+ function resolveSolvedRoutePoints(solvedRoute, viaTile) {
5250
+ const lineSegments = resolveSolvedRouteLineSegments(solvedRoute, viaTile);
5251
+ return flattenLineSegments(lineSegments);
5252
+ }
5253
+
5069
5254
  // lib/ViaGraphSolver/visualizeViaGraphSolver.ts
5070
5255
  var getConnectionColor2 = (connectionId, alpha = 0.8) => {
5071
5256
  let hash = 0;
@@ -5093,6 +5278,8 @@ var NET_COLOR_PALETTE = [
5093
5278
  "rgba(230, 126, 34, 0.35)"
5094
5279
  // dark orange
5095
5280
  ];
5281
+ var BOTTOM_LAYER_TRACE_COLOR = "rgba(52, 152, 219, 0.95)";
5282
+ var BOTTOM_LAYER_TRACE_DASH = "3 2";
5096
5283
  var visualizeViaGraphSolver = (solver) => {
5097
5284
  const graph = {
5098
5285
  regions: solver.graph.regions,
@@ -5106,6 +5293,12 @@ var visualizeViaGraphSolver = (solver) => {
5106
5293
  hidePortPoints: true
5107
5294
  } : {}
5108
5295
  });
5296
+ if (solver.iterations === 0) {
5297
+ for (const polygon2 of graphics.polygons) {
5298
+ polygon2.stroke = "rgba(128, 128, 128, 0.5)";
5299
+ polygon2.strokeWidth = 0.03;
5300
+ }
5301
+ }
5109
5302
  const outerIds = /* @__PURE__ */ new Set(["T", "B", "L", "R"]);
5110
5303
  let netColorIndex = 0;
5111
5304
  const netColorMap = /* @__PURE__ */ new Map();
@@ -5166,15 +5359,13 @@ var visualizeViaGraphSolver = (solver) => {
5166
5359
  const connectionColor = getConnectionColor2(
5167
5360
  solvedRoute.connection.connectionId
5168
5361
  );
5169
- const pathPoints = [];
5170
- for (const candidate of solvedRoute.path) {
5171
- const port = candidate.port;
5172
- pathPoints.push({ x: port.d.x, y: port.d.y });
5173
- }
5174
- if (pathPoints.length > 0) {
5362
+ const lineSegments = solver.getSolvedRouteLineSegments(solvedRoute);
5363
+ for (const lineSegment of lineSegments) {
5364
+ const isBottomLayer = lineSegment.layer === "bottom";
5175
5365
  graphics.lines.push({
5176
- points: pathPoints,
5177
- strokeColor: connectionColor
5366
+ points: lineSegment.points,
5367
+ strokeColor: isBottomLayer ? BOTTOM_LAYER_TRACE_COLOR : connectionColor,
5368
+ ...isBottomLayer ? { strokeDash: BOTTOM_LAYER_TRACE_DASH } : {}
5178
5369
  });
5179
5370
  }
5180
5371
  }
@@ -5346,6 +5537,12 @@ var ViaGraphSolver = class extends HyperGraphSolver {
5346
5537
  }
5347
5538
  routeSolvedHook(solvedRoute) {
5348
5539
  }
5540
+ getSolvedRoutePoints(solvedRoute) {
5541
+ return resolveSolvedRoutePoints(solvedRoute, this.viaTile);
5542
+ }
5543
+ getSolvedRouteLineSegments(solvedRoute) {
5544
+ return resolveSolvedRouteLineSegments(solvedRoute, this.viaTile);
5545
+ }
5349
5546
  routeStartedHook(connection) {
5350
5547
  }
5351
5548
  visualize() {
@@ -14646,6 +14843,35 @@ function centroid(points) {
14646
14843
  }
14647
14844
  return { x: cx / points.length, y: cy / points.length };
14648
14845
  }
14846
+ function classifySideFromBounds(point2, bounds) {
14847
+ const distances = {
14848
+ left: Math.abs(point2.x - bounds.minX),
14849
+ right: Math.abs(point2.x - bounds.maxX),
14850
+ bottom: Math.abs(point2.y - bounds.minY),
14851
+ top: Math.abs(point2.y - bounds.maxY)
14852
+ };
14853
+ let bestSide = "left";
14854
+ let bestDistance = distances.left;
14855
+ for (const side of ["right", "bottom", "top"]) {
14856
+ if (distances[side] < bestDistance) {
14857
+ bestSide = side;
14858
+ bestDistance = distances[side];
14859
+ }
14860
+ }
14861
+ return bestSide;
14862
+ }
14863
+ function toCandidateKey(regionId, point2) {
14864
+ return `${regionId}:${point2.x.toFixed(6)},${point2.y.toFixed(6)}`;
14865
+ }
14866
+ function compareCandidateQuality(a, b) {
14867
+ if (Math.abs(a.primaryDistance - b.primaryDistance) > 1e-6) {
14868
+ return b.primaryDistance - a.primaryDistance;
14869
+ }
14870
+ if (Math.abs(a.orthDistance - b.orthDistance) > 1e-6) {
14871
+ return a.orthDistance - b.orthDistance;
14872
+ }
14873
+ return a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
14874
+ }
14649
14875
  function createRegionFromPolygon(regionId, polygon2, opts) {
14650
14876
  const bounds = boundsFromPolygon(polygon2);
14651
14877
  return {
@@ -14834,6 +15060,8 @@ function generateConvexViaTopologyRegions(opts) {
14834
15060
  }
14835
15061
  }
14836
15062
  for (const viaRegion of viaRegions) {
15063
+ const viaCenter = viaRegion.d.center;
15064
+ const candidates = [];
14837
15065
  for (const convexRegion of convexRegions) {
14838
15066
  const sharedEdges = findSharedEdges(
14839
15067
  viaRegion.d.polygon,
@@ -14843,18 +15071,55 @@ function generateConvexViaTopologyRegions(opts) {
14843
15071
  for (const edge of sharedEdges) {
14844
15072
  const portPositions = createPortsAlongEdge(edge, portPitch);
14845
15073
  for (const pos of portPositions) {
14846
- const port = {
14847
- portId: `via-convex:${viaRegion.regionId}-${convexRegion.regionId}:${portIdCounter++}`,
14848
- region1: viaRegion,
14849
- region2: convexRegion,
14850
- d: { x: pos.x, y: pos.y }
14851
- };
14852
- viaRegion.ports.push(port);
14853
- convexRegion.ports.push(port);
14854
- allPorts.push(port);
15074
+ const dx = pos.x - viaCenter.x;
15075
+ const dy = pos.y - viaCenter.y;
15076
+ const side = classifySideFromBounds(pos, viaRegion.d.bounds);
15077
+ const primaryDistance = side === "left" || side === "right" ? Math.abs(dx) : Math.abs(dy);
15078
+ const orthDistance = side === "left" || side === "right" ? Math.abs(dy) : Math.abs(dx);
15079
+ candidates.push({
15080
+ convexRegion,
15081
+ position: pos,
15082
+ side,
15083
+ primaryDistance,
15084
+ orthDistance,
15085
+ key: toCandidateKey(convexRegion.regionId, pos)
15086
+ });
14855
15087
  }
14856
15088
  }
14857
15089
  }
15090
+ if (candidates.length === 0) continue;
15091
+ const selectedCandidates = [];
15092
+ const selectedKeys = /* @__PURE__ */ new Set();
15093
+ const addCandidate = (candidate) => {
15094
+ if (!candidate) return;
15095
+ if (selectedKeys.has(candidate.key)) return;
15096
+ selectedCandidates.push(candidate);
15097
+ selectedKeys.add(candidate.key);
15098
+ };
15099
+ for (const side of ["top", "bottom", "left", "right"]) {
15100
+ const sideCandidate = [...candidates].filter((candidate) => candidate.side === side).sort(compareCandidateQuality)[0];
15101
+ addCandidate(sideCandidate);
15102
+ }
15103
+ if (selectedCandidates.length < 4) {
15104
+ for (const candidate of [...candidates].sort(compareCandidateQuality)) {
15105
+ addCandidate(candidate);
15106
+ if (selectedCandidates.length >= 4) break;
15107
+ }
15108
+ }
15109
+ for (const selectedCandidate of selectedCandidates.slice(0, 4)) {
15110
+ const port = {
15111
+ portId: `via-convex:${viaRegion.regionId}-${selectedCandidate.convexRegion.regionId}:${portIdCounter++}`,
15112
+ region1: viaRegion,
15113
+ region2: selectedCandidate.convexRegion,
15114
+ d: {
15115
+ x: selectedCandidate.position.x,
15116
+ y: selectedCandidate.position.y
15117
+ }
15118
+ };
15119
+ viaRegion.ports.push(port);
15120
+ selectedCandidate.convexRegion.ports.push(port);
15121
+ allPorts.push(port);
15122
+ }
14858
15123
  }
14859
15124
  return {
14860
15125
  regions: allRegions,