@tscircuit/rectdiff 0.0.24 → 0.0.26

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 (31) hide show
  1. package/.github/workflows/bun-pver-release.yml +45 -24
  2. package/AGENTS.md +23 -0
  3. package/dist/index.d.ts +26 -0
  4. package/dist/index.js +414 -191
  5. package/lib/RectDiffPipeline.ts +23 -0
  6. package/lib/solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver.ts +311 -0
  7. package/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts +23 -23
  8. package/lib/solvers/RectDiffSeedingSolver/computeCandidates3D.ts +9 -10
  9. package/lib/solvers/RectDiffSeedingSolver/computeEdgeCandidates3D.ts +22 -19
  10. package/lib/solvers/RectDiffSeedingSolver/longestFreeSpanAroundZ.ts +1 -1
  11. package/lib/types/srj-types.ts +1 -0
  12. package/lib/utils/expandRectFromSeed.ts +8 -10
  13. package/lib/utils/isFullyOccupiedAtPoint.ts +2 -2
  14. package/lib/utils/rectdiff-geometry.ts +13 -20
  15. package/package.json +3 -1
  16. package/pages/pour.page.tsx +18 -0
  17. package/scripts/benchmark-slow-problem.ts +94 -0
  18. package/test-assets/bugreport49-634662.json +412 -0
  19. package/test-assets/keyboard4.json +16165 -0
  20. package/tests/solver/__snapshots__/rectDiffGridSolverPipeline.snap.svg +1 -1
  21. package/tests/solver/bugreport03-fe4a17/__snapshots__/bugreport03-fe4a17.snap.svg +1 -1
  22. package/tests/solver/bugreport08-e3ec95/__snapshots__/bugreport08-e3ec95.snap.svg +1 -1
  23. package/tests/solver/bugreport09-618e09/__snapshots__/bugreport09-618e09.snap.svg +1 -1
  24. package/tests/solver/bugreport10-71239a/__snapshots__/bugreport10-71239a.snap.svg +1 -1
  25. package/tests/solver/bugreport11-b2de3c/__snapshots__/bugreport11-b2de3c.snap.svg +1 -1
  26. package/tests/solver/bugreport22-2a75ce/__snapshots__/bugreport22-2a75ce.snap.svg +1 -1
  27. package/tests/solver/bugreport24-05597c/__snapshots__/bugreport24-05597c.snap.svg +1 -1
  28. package/tests/solver/bugreport35-191db9/__snapshots__/bugreport35-191db9.snap.svg +1 -1
  29. package/tests/solver/bugreport36-bf8303/__snapshots__/bugreport36-bf8303.snap.svg +1 -1
  30. package/tests/solver/bugreport49-634662/__snapshots__/bugreport49-634662.snap.svg +44 -0
  31. package/tests/solver/bugreport49-634662/bugreport49-634662.test.ts +134 -0
package/dist/index.js CHANGED
@@ -202,6 +202,7 @@ var FindSegmentsWithAdjacentEmptySpaceSolver = class extends BaseSolver {
202
202
  }
203
203
  this.edgeSpatialIndex.finish();
204
204
  }
205
+ input;
205
206
  allEdges;
206
207
  unprocessedEdges = [];
207
208
  segmentsWithAdjacentEmptySpace = [];
@@ -361,6 +362,7 @@ var ExpandEdgesToEmptySpaceSolver = class extends BaseSolver2 {
361
362
  }))
362
363
  );
363
364
  }
365
+ input;
364
366
  unprocessedSegments = [];
365
367
  expandedSegments = [];
366
368
  lastSegment = null;
@@ -665,15 +667,98 @@ var GapFillSolverPipeline = class extends BasePipelineSolver {
665
667
  }
666
668
  };
667
669
 
668
- // lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts
669
- import {
670
- BasePipelineSolver as BasePipelineSolver2,
671
- definePipelineStep as definePipelineStep2
672
- } from "@tscircuit/solver-utils";
673
-
674
- // lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts
670
+ // lib/solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver.ts
675
671
  import { BaseSolver as BaseSolver3 } from "@tscircuit/solver-utils";
676
672
 
673
+ // lib/solvers/RectDiffSeedingSolver/layers.ts
674
+ function layerSortKey(n) {
675
+ const L = n.toLowerCase();
676
+ if (L === "top") return -1e6;
677
+ if (L === "bottom") return 1e6;
678
+ const m = /^inner(\d+)$/i.exec(L);
679
+ if (m) return parseInt(m[1], 10) || 0;
680
+ return 100 + L.charCodeAt(0);
681
+ }
682
+ function canonicalizeLayerOrder(names) {
683
+ return Array.from(new Set(names)).sort((a, b) => {
684
+ const ka = layerSortKey(a);
685
+ const kb = layerSortKey(b);
686
+ if (ka !== kb) return ka - kb;
687
+ return a.localeCompare(b);
688
+ });
689
+ }
690
+ function buildZIndexMap(params) {
691
+ const names = canonicalizeLayerOrder(
692
+ (params.obstacles ?? []).flatMap((o) => o.layers ?? [])
693
+ );
694
+ const declaredLayerCount = Math.max(1, params.layerCount || names.length || 1);
695
+ const fallback = Array.from(
696
+ { length: declaredLayerCount },
697
+ (_, i) => i === 0 ? "top" : i === declaredLayerCount - 1 ? "bottom" : `inner${i}`
698
+ );
699
+ const ordered = [];
700
+ const seen = /* @__PURE__ */ new Set();
701
+ const push = (n) => {
702
+ const key = n.toLowerCase();
703
+ if (seen.has(key)) return;
704
+ seen.add(key);
705
+ ordered.push(n);
706
+ };
707
+ fallback.forEach(push);
708
+ names.forEach(push);
709
+ const layerNames = ordered.slice(0, declaredLayerCount);
710
+ const clampIndex = (nameLower) => {
711
+ if (layerNames.length <= 1) return 0;
712
+ if (nameLower === "top") return 0;
713
+ if (nameLower === "bottom") return layerNames.length - 1;
714
+ const m = /^inner(\d+)$/i.exec(nameLower);
715
+ if (m) {
716
+ if (layerNames.length <= 2) return layerNames.length - 1;
717
+ const parsed = parseInt(m[1], 10);
718
+ const maxInner = layerNames.length - 2;
719
+ const clampedInner = Math.min(
720
+ maxInner,
721
+ Math.max(1, Number.isFinite(parsed) ? parsed : 1)
722
+ );
723
+ return clampedInner;
724
+ }
725
+ return 0;
726
+ };
727
+ const map = /* @__PURE__ */ new Map();
728
+ layerNames.forEach((n, i) => map.set(n.toLowerCase(), i));
729
+ ordered.slice(layerNames.length).forEach((n) => {
730
+ const key = n.toLowerCase();
731
+ map.set(key, clampIndex(key));
732
+ });
733
+ return { layerNames, zIndexByName: map };
734
+ }
735
+ function obstacleZs(ob, zIndexByName) {
736
+ if (ob.zLayers?.length)
737
+ return Array.from(new Set(ob.zLayers)).sort((a, b) => a - b);
738
+ const fromNames = (ob.layers ?? []).map((n) => zIndexByName.get(n.toLowerCase())).filter((v) => typeof v === "number");
739
+ return Array.from(new Set(fromNames)).sort((a, b) => a - b);
740
+ }
741
+ function obstacleToXYRect(ob) {
742
+ const w = ob.width;
743
+ const h = ob.height;
744
+ if (typeof w !== "number" || typeof h !== "number") return null;
745
+ return { x: ob.center.x - w / 2, y: ob.center.y - h / 2, width: w, height: h };
746
+ }
747
+
748
+ // lib/utils/getColorForZLayer.ts
749
+ var getColorForZLayer = (zLayers) => {
750
+ const minZ = Math.min(...zLayers);
751
+ const colors = [
752
+ { fill: "#dbeafe", stroke: "#3b82f6" },
753
+ { fill: "#fef3c7", stroke: "#f59e0b" },
754
+ { fill: "#d1fae5", stroke: "#10b981" },
755
+ { fill: "#e9d5ff", stroke: "#a855f7" },
756
+ { fill: "#fed7aa", stroke: "#f97316" },
757
+ { fill: "#fecaca", stroke: "#ef4444" }
758
+ ];
759
+ return colors[minZ % colors.length];
760
+ };
761
+
677
762
  // lib/utils/rectdiff-geometry.ts
678
763
  var EPS4 = 1e-9;
679
764
  var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
@@ -688,24 +773,18 @@ function containsPoint(r, p) {
688
773
  return p.x >= r.x - EPS4 && p.x <= r.x + r.width + EPS4 && p.y >= r.y - EPS4 && p.y <= r.y + r.height + EPS4;
689
774
  }
690
775
  function distancePointToRectEdges(p, r) {
691
- const edges = [
692
- [r.x, r.y, r.x + r.width, r.y],
693
- [r.x + r.width, r.y, r.x + r.width, r.y + r.height],
694
- [r.x + r.width, r.y + r.height, r.x, r.y + r.height],
695
- [r.x, r.y + r.height, r.x, r.y]
696
- ];
697
- let best = Infinity;
698
- for (const [x1, y1, x2, y2] of edges) {
699
- const A = p.x - x1, B = p.y - y1, C = x2 - x1, D = y2 - y1;
700
- const dot = A * C + B * D;
701
- const lenSq = C * C + D * D;
702
- let t = lenSq !== 0 ? dot / lenSq : 0;
703
- t = clamp(t, 0, 1);
704
- const xx = x1 + t * C;
705
- const yy = y1 + t * D;
706
- best = Math.min(best, Math.hypot(p.x - xx, p.y - yy));
707
- }
708
- return best;
776
+ const minX = r.x;
777
+ const maxX = r.x + r.width;
778
+ const minY = r.y;
779
+ const maxY = r.y + r.height;
780
+ if (p.x >= minX && p.x <= maxX && p.y >= minY && p.y <= maxY) {
781
+ return Math.min(p.x - minX, maxX - p.x, p.y - minY, maxY - p.y);
782
+ }
783
+ const dx = p.x < minX ? minX - p.x : p.x > maxX ? p.x - maxX : 0;
784
+ const dy = p.y < minY ? minY - p.y : p.y > maxY ? p.y - maxY : 0;
785
+ if (dx === 0) return dy;
786
+ if (dy === 0) return dx;
787
+ return Math.hypot(dx, dy);
709
788
  }
710
789
  function intersect1D(r1, r2) {
711
790
  const lo = Math.max(r1[0], r2[0]);
@@ -736,6 +815,235 @@ function subtractRect2D(A, B) {
736
815
  return out.filter((r) => r.width > EPS4 && r.height > EPS4);
737
816
  }
738
817
 
818
+ // lib/utils/padRect.ts
819
+ var padRect = (rect, clearance) => {
820
+ if (!clearance || clearance <= 0) return rect;
821
+ return {
822
+ x: rect.x - clearance,
823
+ y: rect.y - clearance,
824
+ width: rect.width + 2 * clearance,
825
+ height: rect.height + 2 * clearance
826
+ };
827
+ };
828
+
829
+ // lib/solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver.ts
830
+ var nodeToRect = (node) => ({
831
+ x: node.center.x - node.width / 2,
832
+ y: node.center.y - node.height / 2,
833
+ width: node.width,
834
+ height: node.height
835
+ });
836
+ var rectArea = (rect) => rect.width * rect.height;
837
+ var cloneNode = (node) => ({
838
+ ...node,
839
+ center: { ...node.center },
840
+ availableZ: [...node.availableZ]
841
+ });
842
+ var cloneNodeWithRect = (node, rect, capacityMeshNodeId) => ({
843
+ ...node,
844
+ capacityMeshNodeId,
845
+ center: {
846
+ x: rect.x + rect.width / 2,
847
+ y: rect.y + rect.height / 2
848
+ },
849
+ width: rect.width,
850
+ height: rect.height,
851
+ availableZ: [...node.availableZ],
852
+ layer: `z${node.availableZ.join(",")}`
853
+ });
854
+ var isFreeNode = (node) => !node._containsObstacle && !node._containsTarget;
855
+ var isSingletonOuterNode = (node, outerZ) => node.availableZ.length === 1 && node.availableZ[0] === outerZ;
856
+ var sameRect = (a, b) => Math.abs(a.x - b.x) <= EPS4 && Math.abs(a.y - b.y) <= EPS4 && Math.abs(a.width - b.width) <= EPS4 && Math.abs(a.height - b.height) <= EPS4;
857
+ var subtractRects = (target, cutters) => {
858
+ let remaining = [target];
859
+ for (const cutter of cutters) {
860
+ if (remaining.length === 0) return remaining;
861
+ const nextRemaining = [];
862
+ for (const piece of remaining) {
863
+ nextRemaining.push(...subtractRect2D(piece, cutter));
864
+ }
865
+ remaining = nextRemaining;
866
+ }
867
+ return remaining;
868
+ };
869
+ var isFullyCoveredByRects = (target, coveringRects) => {
870
+ return subtractRects(target, coveringRects).length === 0;
871
+ };
872
+ var OuterLayerContainmentMergeSolver = class extends BaseSolver3 {
873
+ constructor(input) {
874
+ super();
875
+ this.input = input;
876
+ }
877
+ input;
878
+ outputNodes = [];
879
+ promotedNodeIds = /* @__PURE__ */ new Set();
880
+ residualNodeIds = /* @__PURE__ */ new Set();
881
+ _setup() {
882
+ this.outputNodes = this.input.meshNodes.map(cloneNode);
883
+ this.promotedNodeIds.clear();
884
+ this.residualNodeIds.clear();
885
+ }
886
+ _step() {
887
+ this.outputNodes = this.processOuterLayerContainmentMerges();
888
+ this.solved = true;
889
+ }
890
+ processOuterLayerContainmentMerges() {
891
+ const srj = this.input.simpleRouteJson;
892
+ const layerCount = Math.max(1, srj.layerCount || 1);
893
+ if (layerCount < 3) {
894
+ return this.input.meshNodes.map(cloneNode);
895
+ }
896
+ const topZ = 0;
897
+ const bottomZ = layerCount - 1;
898
+ const viaMinSize = Math.max(srj.minViaDiameter ?? 0, srj.minTraceWidth || 0);
899
+ const originalNodes = this.input.meshNodes.map(cloneNode);
900
+ const obstaclesByLayer = this.buildObstaclesByLayer(layerCount);
901
+ const mutableOuterNodes = originalNodes.filter(
902
+ (node) => isFreeNode(node) && (isSingletonOuterNode(node, topZ) || isSingletonOuterNode(node, bottomZ))
903
+ );
904
+ const immutableNodes = originalNodes.filter(
905
+ (node) => !mutableOuterNodes.includes(node)
906
+ );
907
+ const freeSupportRectsByOuterLayer = /* @__PURE__ */ new Map();
908
+ freeSupportRectsByOuterLayer.set(
909
+ topZ,
910
+ originalNodes.filter((node) => isFreeNode(node) && node.availableZ.includes(topZ)).map(nodeToRect)
911
+ );
912
+ freeSupportRectsByOuterLayer.set(
913
+ bottomZ,
914
+ originalNodes.filter((node) => isFreeNode(node) && node.availableZ.includes(bottomZ)).map(nodeToRect)
915
+ );
916
+ const promotedNodes = [];
917
+ const promotedRects = [];
918
+ const candidateNodes = mutableOuterNodes.filter(
919
+ (node) => node.width + EPS4 >= viaMinSize && node.height + EPS4 >= viaMinSize
920
+ ).sort((a, b) => rectArea(nodeToRect(b)) - rectArea(nodeToRect(a)));
921
+ for (const candidate of candidateNodes) {
922
+ const candidateZ = candidate.availableZ[0];
923
+ const oppositeZ = candidateZ === topZ ? bottomZ : topZ;
924
+ const candidateRect = nodeToRect(candidate);
925
+ const oppositeSupportRects = freeSupportRectsByOuterLayer.get(oppositeZ) ?? [];
926
+ if (!this.isTransitCompatibleAcrossIntermediateLayers({
927
+ rect: candidateRect,
928
+ fromZ: candidateZ,
929
+ toZ: oppositeZ,
930
+ obstaclesByLayer
931
+ })) {
932
+ continue;
933
+ }
934
+ if (!isFullyCoveredByRects(candidateRect, oppositeSupportRects)) {
935
+ continue;
936
+ }
937
+ promotedNodes.push({
938
+ ...candidate,
939
+ availableZ: [topZ, bottomZ],
940
+ layer: `z${topZ},${bottomZ}`
941
+ });
942
+ promotedRects.push(candidateRect);
943
+ this.promotedNodeIds.add(candidate.capacityMeshNodeId);
944
+ }
945
+ let nextResidualId = 0;
946
+ const residualNodes = [];
947
+ for (const node of mutableOuterNodes) {
948
+ if (this.promotedNodeIds.has(node.capacityMeshNodeId)) {
949
+ continue;
950
+ }
951
+ const nodeRect = nodeToRect(node);
952
+ const remainingPieces = subtractRects(nodeRect, promotedRects);
953
+ if (remainingPieces.length === 1 && sameRect(remainingPieces[0], nodeRect)) {
954
+ residualNodes.push(node);
955
+ continue;
956
+ }
957
+ for (const piece of remainingPieces) {
958
+ const residualNode = cloneNodeWithRect(
959
+ node,
960
+ piece,
961
+ `${node.capacityMeshNodeId}-outer-merge-${nextResidualId++}`
962
+ );
963
+ residualNodes.push(residualNode);
964
+ this.residualNodeIds.add(residualNode.capacityMeshNodeId);
965
+ }
966
+ }
967
+ return [...immutableNodes, ...promotedNodes, ...residualNodes];
968
+ }
969
+ buildObstaclesByLayer(layerCount) {
970
+ const out = Array.from(
971
+ { length: layerCount },
972
+ () => []
973
+ );
974
+ for (const obstacle of this.input.simpleRouteJson.obstacles ?? []) {
975
+ const baseRect = obstacleToXYRect(obstacle);
976
+ if (!baseRect) continue;
977
+ const rect = padRect(baseRect, this.input.obstacleClearance ?? 0);
978
+ const zLayers = obstacleZs(obstacle, this.input.zIndexByName);
979
+ for (const z of zLayers) {
980
+ if (z < 0 || z >= layerCount) continue;
981
+ out[z].push({ obstacle, rect });
982
+ }
983
+ }
984
+ return out;
985
+ }
986
+ isTransitCompatibleAcrossIntermediateLayers(params) {
987
+ const { rect, fromZ, toZ, obstaclesByLayer } = params;
988
+ const lo = Math.min(fromZ, toZ);
989
+ const hi = Math.max(fromZ, toZ);
990
+ if (hi - lo < 2) return false;
991
+ for (let z = lo + 1; z < hi; z++) {
992
+ const overlapping = (obstaclesByLayer[z] ?? []).filter(
993
+ (entry) => overlaps(entry.rect, rect)
994
+ );
995
+ if (overlapping.length === 0) return false;
996
+ const nonCopperOverlap = overlapping.some(
997
+ (entry) => !entry.obstacle.isCopperPour
998
+ );
999
+ if (nonCopperOverlap) return false;
1000
+ const copperRects = overlapping.filter((entry) => entry.obstacle.isCopperPour).map((entry) => entry.rect);
1001
+ if (!isFullyCoveredByRects(rect, copperRects)) {
1002
+ return false;
1003
+ }
1004
+ }
1005
+ return true;
1006
+ }
1007
+ getOutput() {
1008
+ return { outputNodes: this.outputNodes };
1009
+ }
1010
+ visualize() {
1011
+ return {
1012
+ title: "OuterLayerContainmentMergeSolver",
1013
+ coordinateSystem: "cartesian",
1014
+ rects: this.outputNodes.map((node) => {
1015
+ const colors = getColorForZLayer(node.availableZ);
1016
+ const isPromoted = this.promotedNodeIds.has(node.capacityMeshNodeId);
1017
+ const isResidual = this.residualNodeIds.has(node.capacityMeshNodeId);
1018
+ return {
1019
+ center: node.center,
1020
+ width: node.width,
1021
+ height: node.height,
1022
+ stroke: isPromoted ? "rgba(22, 163, 74, 0.95)" : isResidual ? "rgba(37, 99, 235, 0.95)" : colors.stroke,
1023
+ fill: node._containsObstacle ? "rgba(239, 68, 68, 0.35)" : isPromoted ? "rgba(34, 197, 94, 0.28)" : isResidual ? "rgba(59, 130, 246, 0.18)" : colors.fill,
1024
+ layer: `z${node.availableZ.join(",")}`,
1025
+ label: [
1026
+ `node ${node.capacityMeshNodeId}`,
1027
+ `z:${node.availableZ.join(",")}`
1028
+ ].join("\n")
1029
+ };
1030
+ }),
1031
+ points: [],
1032
+ lines: [],
1033
+ texts: []
1034
+ };
1035
+ }
1036
+ };
1037
+
1038
+ // lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts
1039
+ import {
1040
+ BasePipelineSolver as BasePipelineSolver2,
1041
+ definePipelineStep as definePipelineStep2
1042
+ } from "@tscircuit/solver-utils";
1043
+
1044
+ // lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts
1045
+ import { BaseSolver as BaseSolver4 } from "@tscircuit/solver-utils";
1046
+
739
1047
  // lib/solvers/RectDiffSeedingSolver/isPointInPolygon.ts
740
1048
  function isPointInPolygon(p, polygon) {
741
1049
  let inside = false;
@@ -842,81 +1150,6 @@ function computeInverseRects(bounds, polygon) {
842
1150
  return mergedVertical;
843
1151
  }
844
1152
 
845
- // lib/solvers/RectDiffSeedingSolver/layers.ts
846
- function layerSortKey(n) {
847
- const L = n.toLowerCase();
848
- if (L === "top") return -1e6;
849
- if (L === "bottom") return 1e6;
850
- const m = /^inner(\d+)$/i.exec(L);
851
- if (m) return parseInt(m[1], 10) || 0;
852
- return 100 + L.charCodeAt(0);
853
- }
854
- function canonicalizeLayerOrder(names) {
855
- return Array.from(new Set(names)).sort((a, b) => {
856
- const ka = layerSortKey(a);
857
- const kb = layerSortKey(b);
858
- if (ka !== kb) return ka - kb;
859
- return a.localeCompare(b);
860
- });
861
- }
862
- function buildZIndexMap(params) {
863
- const names = canonicalizeLayerOrder(
864
- (params.obstacles ?? []).flatMap((o) => o.layers ?? [])
865
- );
866
- const declaredLayerCount = Math.max(1, params.layerCount || names.length || 1);
867
- const fallback = Array.from(
868
- { length: declaredLayerCount },
869
- (_, i) => i === 0 ? "top" : i === declaredLayerCount - 1 ? "bottom" : `inner${i}`
870
- );
871
- const ordered = [];
872
- const seen = /* @__PURE__ */ new Set();
873
- const push = (n) => {
874
- const key = n.toLowerCase();
875
- if (seen.has(key)) return;
876
- seen.add(key);
877
- ordered.push(n);
878
- };
879
- fallback.forEach(push);
880
- names.forEach(push);
881
- const layerNames = ordered.slice(0, declaredLayerCount);
882
- const clampIndex = (nameLower) => {
883
- if (layerNames.length <= 1) return 0;
884
- if (nameLower === "top") return 0;
885
- if (nameLower === "bottom") return layerNames.length - 1;
886
- const m = /^inner(\d+)$/i.exec(nameLower);
887
- if (m) {
888
- if (layerNames.length <= 2) return layerNames.length - 1;
889
- const parsed = parseInt(m[1], 10);
890
- const maxInner = layerNames.length - 2;
891
- const clampedInner = Math.min(
892
- maxInner,
893
- Math.max(1, Number.isFinite(parsed) ? parsed : 1)
894
- );
895
- return clampedInner;
896
- }
897
- return 0;
898
- };
899
- const map = /* @__PURE__ */ new Map();
900
- layerNames.forEach((n, i) => map.set(n.toLowerCase(), i));
901
- ordered.slice(layerNames.length).forEach((n) => {
902
- const key = n.toLowerCase();
903
- map.set(key, clampIndex(key));
904
- });
905
- return { layerNames, zIndexByName: map };
906
- }
907
- function obstacleZs(ob, zIndexByName) {
908
- if (ob.zLayers?.length)
909
- return Array.from(new Set(ob.zLayers)).sort((a, b) => a - b);
910
- const fromNames = (ob.layers ?? []).map((n) => zIndexByName.get(n.toLowerCase())).filter((v) => typeof v === "number");
911
- return Array.from(new Set(fromNames)).sort((a, b) => a - b);
912
- }
913
- function obstacleToXYRect(ob) {
914
- const w = ob.width;
915
- const h = ob.height;
916
- if (typeof w !== "number" || typeof h !== "number") return null;
917
- return { x: ob.center.x - w / 2, y: ob.center.y - h / 2, width: w, height: h };
918
- }
919
-
920
1153
  // lib/utils/isSelfRect.ts
921
1154
  var EPS5 = 1e-9;
922
1155
  var isSelfRect = (params) => Math.abs(params.rect.x + params.rect.width / 2 - params.startX) < EPS5 && Math.abs(params.rect.y + params.rect.height / 2 - params.startY) < EPS5 && Math.abs(params.rect.width - params.initialW) < EPS5 && Math.abs(params.rect.height - params.initialH) < EPS5;
@@ -1051,17 +1284,10 @@ function maxExpandUp(params) {
1051
1284
  }
1052
1285
  return Math.max(0, e);
1053
1286
  }
1054
- var toRect = (tree) => ({
1055
- x: tree.minX,
1056
- y: tree.minY,
1057
- width: tree.maxX - tree.minX,
1058
- height: tree.maxY - tree.minY
1059
- });
1060
1287
  var addBlocker = (params) => {
1061
1288
  const { rect, seen, blockers } = params;
1062
- const key = `${rect.x}|${rect.y}|${rect.width}|${rect.height}`;
1063
- if (seen.has(key)) return;
1064
- seen.add(key);
1289
+ if (seen.has(rect)) return;
1290
+ seen.add(rect);
1065
1291
  blockers.push(rect);
1066
1292
  };
1067
1293
  var toQueryRect = (params) => {
@@ -1098,23 +1324,22 @@ function expandRectFromSeed(params) {
1098
1324
  const blockersIndex = obsticalIndexByLayer[z];
1099
1325
  if (blockersIndex) {
1100
1326
  for (const entry of blockersIndex.search(query))
1101
- addBlocker({ rect: toRect(entry), seen, blockers });
1327
+ addBlocker({ rect: entry, seen, blockers });
1102
1328
  }
1103
1329
  const placedLayer = placedIndexByLayer[z];
1104
1330
  if (placedLayer) {
1105
1331
  for (const entry of placedLayer.search(query)) {
1106
1332
  const isFullStack = entry.zLayers.length >= totalLayers;
1107
1333
  if (!isFullStack) continue;
1108
- const rect = toRect(entry);
1109
1334
  if (isSelfRect({
1110
- rect,
1335
+ rect: entry,
1111
1336
  startX,
1112
1337
  startY,
1113
1338
  initialW,
1114
1339
  initialH
1115
1340
  }))
1116
1341
  continue;
1117
- addBlocker({ rect, seen, blockers });
1342
+ addBlocker({ rect: entry, seen, blockers });
1118
1343
  }
1119
1344
  }
1120
1345
  }
@@ -1205,9 +1430,9 @@ function isFullyOccupiedAtPoint(params) {
1205
1430
  };
1206
1431
  for (let z = 0; z < params.layerCount; z++) {
1207
1432
  const obstacleIdx = params.obstacleIndexByLayer[z];
1208
- const hasObstacle = !!obstacleIdx && obstacleIdx.search(query).length > 0;
1433
+ const hasObstacle = !!obstacleIdx && obstacleIdx.collides(query);
1209
1434
  const placedIdx = params.placedIndexByLayer[z];
1210
- const hasPlaced = !!placedIdx && placedIdx.search(query).length > 0;
1435
+ const hasPlaced = !!placedIdx && placedIdx.collides(query);
1211
1436
  if (!hasObstacle && !hasPlaced) return false;
1212
1437
  }
1213
1438
  return true;
@@ -1233,7 +1458,7 @@ function longestFreeSpanAroundZ(params) {
1233
1458
  maxY: y
1234
1459
  };
1235
1460
  const obstacleIdx = obstacleIndexByLayer[layer];
1236
- if (obstacleIdx && obstacleIdx.search(query).length > 0) return false;
1461
+ if (obstacleIdx && obstacleIdx.collides(query)) return false;
1237
1462
  const extras = additionalBlockersByLayer?.[layer] ?? [];
1238
1463
  return !extras.some((b) => containsPoint(b, { x, y }));
1239
1464
  };
@@ -1265,6 +1490,10 @@ function computeCandidates3D(params) {
1265
1490
  hardPlacedByLayer
1266
1491
  } = params;
1267
1492
  const out = /* @__PURE__ */ new Map();
1493
+ const hardRectsByLayer = Array.from({ length: layerCount }, (_, z) => [
1494
+ ...obstacleIndexByLayer[z]?.all() ?? [],
1495
+ ...hardPlacedByLayer[z] ?? []
1496
+ ]);
1268
1497
  for (let x = bounds.x; x < bounds.x + bounds.width; x += gridSize) {
1269
1498
  for (let y = bounds.y; y < bounds.y + bounds.height; y += gridSize) {
1270
1499
  if (Math.abs(x - bounds.x) < EPS4 || Math.abs(y - bounds.y) < EPS4 || x > bounds.x + bounds.width - gridSize - EPS4 || y > bounds.y + bounds.height - gridSize - EPS4) {
@@ -1296,14 +1525,11 @@ function computeCandidates3D(params) {
1296
1525
  }
1297
1526
  }
1298
1527
  const anchorZ = bestSpan.length ? bestSpan[Math.floor(bestSpan.length / 2)] : bestZ;
1299
- const hardAtZ = [
1300
- ...obstacleIndexByLayer[anchorZ]?.all() ?? [],
1301
- ...hardPlacedByLayer[anchorZ] ?? []
1302
- ];
1303
- const d = Math.min(
1304
- distancePointToRectEdges({ x, y }, bounds),
1305
- ...hardAtZ.length ? hardAtZ.map((b) => distancePointToRectEdges({ x, y }, b)) : [Infinity]
1306
- );
1528
+ const hardAtZ = hardRectsByLayer[anchorZ] ?? [];
1529
+ let d = distancePointToRectEdges({ x, y }, bounds);
1530
+ for (const blocker of hardAtZ) {
1531
+ d = Math.min(d, distancePointToRectEdges({ x, y }, blocker));
1532
+ }
1307
1533
  const distance = quantize2(d);
1308
1534
  const k = `${x.toFixed(6)}|${y.toFixed(6)}`;
1309
1535
  const cand = {
@@ -1328,6 +1554,12 @@ function computeCandidates3D(params) {
1328
1554
 
1329
1555
  // lib/solvers/RectDiffSeedingSolver/computeEdgeCandidates3D.ts
1330
1556
  var quantize3 = (value, precision = 1e-6) => Math.round(value / precision) * precision;
1557
+ var toRect = (rect) => "minX" in rect ? {
1558
+ x: rect.minX,
1559
+ y: rect.minY,
1560
+ width: rect.maxX - rect.minX,
1561
+ height: rect.maxY - rect.minY
1562
+ } : rect;
1331
1563
  function computeUncoveredSegments(params) {
1332
1564
  const { lineStart, lineEnd, coveringIntervals, minSegmentLength } = params;
1333
1565
  const lineStartQ = quantize3(lineStart);
@@ -1390,6 +1622,13 @@ function computeEdgeCandidates3D(params) {
1390
1622
  const out = [];
1391
1623
  const \u03B4 = Math.max(minSize * 0.15, EPS4 * 3);
1392
1624
  const dedup = /* @__PURE__ */ new Set();
1625
+ const hardRectsByLayer = Array.from(
1626
+ { length: layerCount },
1627
+ (_, z) => [
1628
+ ...obstacleIndexByLayer[z]?.all() ?? [],
1629
+ ...hardPlacedByLayer[z] ?? []
1630
+ ].map(toRect)
1631
+ );
1393
1632
  const key = (p) => `${p.z}|${p.x.toFixed(6)}|${p.y.toFixed(6)}`;
1394
1633
  function fullyOcc(p) {
1395
1634
  return isFullyOccupiedAtPoint({
@@ -1408,19 +1647,11 @@ function computeEdgeCandidates3D(params) {
1408
1647
  if (x < bounds.x + EPS4 || y < bounds.y + EPS4 || x > bounds.x + bounds.width - EPS4 || y > bounds.y + bounds.height - EPS4)
1409
1648
  return;
1410
1649
  if (fullyOcc({ x, y })) return;
1411
- const hard = [
1412
- ...obstacleIndexByLayer[z]?.all() ?? [],
1413
- ...hardPlacedByLayer[z] ?? []
1414
- ].map((b) => ({
1415
- x: quantize3(b.x),
1416
- y: quantize3(b.y),
1417
- width: quantize3(b.width),
1418
- height: quantize3(b.height)
1419
- }));
1420
- const d = Math.min(
1421
- distancePointToRectEdges({ x, y }, bounds),
1422
- ...hard.length ? hard.map((b) => distancePointToRectEdges({ x, y }, b)) : [Infinity]
1423
- );
1650
+ const hard = hardRectsByLayer[z] ?? [];
1651
+ let d = distancePointToRectEdges({ x, y }, bounds);
1652
+ for (const blocker of hard) {
1653
+ d = Math.min(d, distancePointToRectEdges({ x, y }, blocker));
1654
+ }
1424
1655
  const distance = quantize3(d);
1425
1656
  const k = key({ x, y, z });
1426
1657
  if (dedup.has(k)) return;
@@ -1445,10 +1676,7 @@ function computeEdgeCandidates3D(params) {
1445
1676
  });
1446
1677
  }
1447
1678
  for (let z = 0; z < layerCount; z++) {
1448
- const blockers = [
1449
- ...obstacleIndexByLayer[z]?.all() ?? [],
1450
- ...hardPlacedByLayer[z] ?? []
1451
- ].map((b) => ({
1679
+ const blockers = (hardRectsByLayer[z] ?? []).map((b) => ({
1452
1680
  x: quantize3(b.x),
1453
1681
  y: quantize3(b.y),
1454
1682
  width: quantize3(b.width),
@@ -1687,7 +1915,7 @@ function resizeSoftOverlaps(params, newIndex) {
1687
1915
  }
1688
1916
  }
1689
1917
  }
1690
- const sameRect = (a, b) => a.minX === b.minX && a.minY === b.minY && a.maxX === b.maxX && a.maxY === b.maxY;
1918
+ const sameRect2 = (a, b) => a.minX === b.minX && a.minY === b.minY && a.maxX === b.maxX && a.maxY === b.maxY;
1691
1919
  removeIdx.sort((a, b) => b - a).forEach((idx) => {
1692
1920
  const rem = params.placed.splice(idx, 1)[0];
1693
1921
  if (params.placedIndexByLayer) {
@@ -1696,7 +1924,7 @@ function resizeSoftOverlaps(params, newIndex) {
1696
1924
  if (tree)
1697
1925
  tree.remove(
1698
1926
  rectToTree(rem.rect, { zLayers: rem.zLayers }),
1699
- sameRect
1927
+ sameRect2
1700
1928
  );
1701
1929
  }
1702
1930
  }
@@ -1714,27 +1942,14 @@ function resizeSoftOverlaps(params, newIndex) {
1714
1942
  }
1715
1943
  }
1716
1944
 
1717
- // lib/utils/getColorForZLayer.ts
1718
- var getColorForZLayer = (zLayers) => {
1719
- const minZ = Math.min(...zLayers);
1720
- const colors = [
1721
- { fill: "#dbeafe", stroke: "#3b82f6" },
1722
- { fill: "#fef3c7", stroke: "#f59e0b" },
1723
- { fill: "#d1fae5", stroke: "#10b981" },
1724
- { fill: "#e9d5ff", stroke: "#a855f7" },
1725
- { fill: "#fed7aa", stroke: "#f97316" },
1726
- { fill: "#fecaca", stroke: "#ef4444" }
1727
- ];
1728
- return colors[minZ % colors.length];
1729
- };
1730
-
1731
1945
  // lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts
1732
1946
  import RBush3 from "rbush";
1733
- var RectDiffSeedingSolver = class extends BaseSolver3 {
1947
+ var RectDiffSeedingSolver = class extends BaseSolver4 {
1734
1948
  constructor(input) {
1735
1949
  super();
1736
1950
  this.input = input;
1737
1951
  }
1952
+ input;
1738
1953
  // Engine fields (mirrors initState / engine.ts)
1739
1954
  srj;
1740
1955
  layerNames;
@@ -1746,6 +1961,7 @@ var RectDiffSeedingSolver = class extends BaseSolver3 {
1746
1961
  candidates;
1747
1962
  placed;
1748
1963
  placedIndexByLayer;
1964
+ hardPlacedByLayer;
1749
1965
  expansionIndex;
1750
1966
  edgeAnalysisDone;
1751
1967
  totalSeedsThisGrid;
@@ -1801,6 +2017,7 @@ var RectDiffSeedingSolver = class extends BaseSolver3 {
1801
2017
  { length: layerCount },
1802
2018
  () => new RBush3()
1803
2019
  );
2020
+ this.hardPlacedByLayer = Array.from({ length: layerCount }, () => []);
1804
2021
  this.expansionIndex = 0;
1805
2022
  this.edgeAnalysisDone = false;
1806
2023
  this.totalSeedsThisGrid = 0;
@@ -1829,25 +2046,22 @@ var RectDiffSeedingSolver = class extends BaseSolver3 {
1829
2046
  maxMultiLayerSpan
1830
2047
  } = this.options;
1831
2048
  const grid = gridSizes[this.gridIndex];
1832
- const hardPlacedByLayer = allLayerNode({
1833
- layerCount: this.layerCount,
1834
- placed: this.placed
1835
- });
1836
2049
  if (this.candidates.length === 0 && this.consumedSeedsThisGrid === 0) {
1837
2050
  this.candidates = computeCandidates3D({
1838
2051
  bounds: this.bounds,
1839
2052
  gridSize: grid,
1840
2053
  layerCount: this.layerCount,
1841
- hardPlacedByLayer,
2054
+ hardPlacedByLayer: this.hardPlacedByLayer,
1842
2055
  obstacleIndexByLayer: this.input.obstacleIndexByLayer,
1843
2056
  placedIndexByLayer: this.placedIndexByLayer
1844
2057
  });
1845
2058
  this.totalSeedsThisGrid = this.candidates.length;
1846
2059
  this.consumedSeedsThisGrid = 0;
1847
2060
  }
1848
- if (this.candidates.length === 0) {
2061
+ if (this.consumedSeedsThisGrid >= this.candidates.length) {
1849
2062
  if (this.gridIndex + 1 < gridSizes.length) {
1850
2063
  this.gridIndex += 1;
2064
+ this.candidates = [];
1851
2065
  this.totalSeedsThisGrid = 0;
1852
2066
  this.consumedSeedsThisGrid = 0;
1853
2067
  return;
@@ -1860,20 +2074,28 @@ var RectDiffSeedingSolver = class extends BaseSolver3 {
1860
2074
  layerCount: this.layerCount,
1861
2075
  obstacleIndexByLayer: this.input.obstacleIndexByLayer,
1862
2076
  placedIndexByLayer: this.placedIndexByLayer,
1863
- hardPlacedByLayer
2077
+ hardPlacedByLayer: this.hardPlacedByLayer
1864
2078
  });
1865
2079
  this.edgeAnalysisDone = true;
1866
2080
  this.totalSeedsThisGrid = this.candidates.length;
1867
2081
  this.consumedSeedsThisGrid = 0;
1868
2082
  return;
1869
2083
  }
2084
+ this.candidates = [];
1870
2085
  this.solved = true;
1871
2086
  this.expansionIndex = 0;
1872
2087
  return;
1873
2088
  }
1874
2089
  }
1875
- const cand = this.candidates.shift();
1876
- this.consumedSeedsThisGrid += 1;
2090
+ const cand = this.candidates[this.consumedSeedsThisGrid++];
2091
+ if (isFullyOccupiedAtPoint({
2092
+ layerCount: this.layerCount,
2093
+ obstacleIndexByLayer: this.input.obstacleIndexByLayer,
2094
+ placedIndexByLayer: this.placedIndexByLayer,
2095
+ point: { x: cand.x, y: cand.y }
2096
+ })) {
2097
+ return;
2098
+ }
1877
2099
  const span = longestFreeSpanAroundZ({
1878
2100
  x: cand.x,
1879
2101
  y: cand.y,
@@ -1882,7 +2104,7 @@ var RectDiffSeedingSolver = class extends BaseSolver3 {
1882
2104
  minSpan: minMulti.minLayers,
1883
2105
  maxSpan: maxMultiLayerSpan,
1884
2106
  obstacleIndexByLayer: this.input.obstacleIndexByLayer,
1885
- additionalBlockersByLayer: hardPlacedByLayer
2107
+ additionalBlockersByLayer: this.hardPlacedByLayer
1886
2108
  });
1887
2109
  const attempts = [];
1888
2110
  if (span.length >= minMulti.minLayers) {
@@ -1929,14 +2151,10 @@ var RectDiffSeedingSolver = class extends BaseSolver3 {
1929
2151
  },
1930
2152
  newIndex
1931
2153
  );
1932
- this.candidates = this.candidates.filter(
1933
- (c) => !isFullyOccupiedAtPoint({
1934
- layerCount: this.layerCount,
1935
- obstacleIndexByLayer: this.input.obstacleIndexByLayer,
1936
- placedIndexByLayer: this.placedIndexByLayer,
1937
- point: { x: c.x, y: c.y }
1938
- })
1939
- );
2154
+ this.hardPlacedByLayer = allLayerNode({
2155
+ layerCount: this.layerCount,
2156
+ placed: this.placed
2157
+ });
1940
2158
  return;
1941
2159
  }
1942
2160
  }
@@ -2085,7 +2303,7 @@ z:${placement.zLayers.join(",")}`
2085
2303
  };
2086
2304
 
2087
2305
  // lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts
2088
- import { BaseSolver as BaseSolver4 } from "@tscircuit/solver-utils";
2306
+ import { BaseSolver as BaseSolver5 } from "@tscircuit/solver-utils";
2089
2307
 
2090
2308
  // lib/utils/finalizeRects.ts
2091
2309
  function finalizeRects(params) {
@@ -2157,11 +2375,12 @@ import RBush4 from "rbush";
2157
2375
  var sameTreeRect = (a, b) => a.minX === b.minX && a.minY === b.minY && a.maxX === b.maxX && a.maxY === b.maxY;
2158
2376
 
2159
2377
  // lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts
2160
- var RectDiffExpansionSolver = class extends BaseSolver4 {
2378
+ var RectDiffExpansionSolver = class extends BaseSolver5 {
2161
2379
  constructor(input) {
2162
2380
  super();
2163
2381
  this.input = input;
2164
2382
  }
2383
+ input;
2165
2384
  placedIndexByLayer = [];
2166
2385
  _meshNodes = [];
2167
2386
  _setup() {
@@ -2302,19 +2521,6 @@ import "rbush";
2302
2521
 
2303
2522
  // lib/solvers/RectDiffGridSolverPipeline/buildObstacleIndexes.ts
2304
2523
  import RBush5 from "rbush";
2305
-
2306
- // lib/utils/padRect.ts
2307
- var padRect = (rect, clearance) => {
2308
- if (!clearance || clearance <= 0) return rect;
2309
- return {
2310
- x: rect.x - clearance,
2311
- y: rect.y - clearance,
2312
- width: rect.width + 2 * clearance,
2313
- height: rect.height + 2 * clearance
2314
- };
2315
- };
2316
-
2317
- // lib/solvers/RectDiffGridSolverPipeline/buildObstacleIndexes.ts
2318
2524
  var buildObstacleIndexesByLayer = (params) => {
2319
2525
  const { srj, boardVoidRects, obstacleClearance } = params;
2320
2526
  const { layerNames, zIndexByName } = buildZIndexMap({
@@ -2658,6 +2864,7 @@ import { mergeGraphics as mergeGraphics2 } from "graphics-debug";
2658
2864
  var RectDiffPipeline = class extends BasePipelineSolver3 {
2659
2865
  rectDiffGridSolverPipeline;
2660
2866
  gapFillSolver;
2867
+ outerLayerContainmentMergeSolver;
2661
2868
  boardVoidRects;
2662
2869
  zIndexByName;
2663
2870
  layerNames;
@@ -2693,6 +2900,18 @@ var RectDiffPipeline = class extends BasePipelineSolver3 {
2693
2900
  }
2694
2901
  }
2695
2902
  ]
2903
+ ),
2904
+ definePipelineStep3(
2905
+ "outerLayerContainmentMergeSolver",
2906
+ OuterLayerContainmentMergeSolver,
2907
+ (rectDiffPipeline) => [
2908
+ {
2909
+ meshNodes: rectDiffPipeline.gapFillSolver?.getOutput().outputNodes ?? rectDiffPipeline.rectDiffGridSolverPipeline?.getOutput().meshNodes ?? [],
2910
+ simpleRouteJson: rectDiffPipeline.inputProblem.simpleRouteJson,
2911
+ zIndexByName: rectDiffPipeline.zIndexByName ?? /* @__PURE__ */ new Map(),
2912
+ obstacleClearance: rectDiffPipeline.inputProblem.obstacleClearance
2913
+ }
2914
+ ]
2696
2915
  )
2697
2916
  ];
2698
2917
  _setup() {
@@ -2718,6 +2937,10 @@ var RectDiffPipeline = class extends BasePipelineSolver3 {
2718
2937
  return [this.inputProblem];
2719
2938
  }
2720
2939
  getOutput() {
2940
+ const outerLayerMergeOutput = this.outerLayerContainmentMergeSolver?.getOutput();
2941
+ if (outerLayerMergeOutput) {
2942
+ return { meshNodes: outerLayerMergeOutput.outputNodes };
2943
+ }
2721
2944
  const gapFillOutput = this.gapFillSolver?.getOutput();
2722
2945
  if (gapFillOutput) {
2723
2946
  return { meshNodes: gapFillOutput.outputNodes };