@tscircuit/rectdiff 0.0.25 → 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.
- package/.github/workflows/bun-pver-release.yml +45 -24
- package/dist/index.d.ts +25 -0
- package/dist/index.js +345 -114
- package/lib/RectDiffPipeline.ts +23 -0
- package/lib/solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver.ts +311 -0
- package/lib/types/srj-types.ts +1 -0
- package/package.json +2 -1
- package/pages/pour.page.tsx +18 -0
- package/test-assets/bugreport49-634662.json +412 -0
- package/tests/solver/bugreport08-e3ec95/__snapshots__/bugreport08-e3ec95.snap.svg +1 -1
- package/tests/solver/bugreport22-2a75ce/__snapshots__/bugreport22-2a75ce.snap.svg +1 -1
- package/tests/solver/bugreport24-05597c/__snapshots__/bugreport24-05597c.snap.svg +1 -1
- package/tests/solver/bugreport49-634662/__snapshots__/bugreport49-634662.snap.svg +44 -0
- 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/
|
|
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));
|
|
@@ -730,6 +815,235 @@ function subtractRect2D(A, B) {
|
|
|
730
815
|
return out.filter((r) => r.width > EPS4 && r.height > EPS4);
|
|
731
816
|
}
|
|
732
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
|
+
|
|
733
1047
|
// lib/solvers/RectDiffSeedingSolver/isPointInPolygon.ts
|
|
734
1048
|
function isPointInPolygon(p, polygon) {
|
|
735
1049
|
let inside = false;
|
|
@@ -836,81 +1150,6 @@ function computeInverseRects(bounds, polygon) {
|
|
|
836
1150
|
return mergedVertical;
|
|
837
1151
|
}
|
|
838
1152
|
|
|
839
|
-
// lib/solvers/RectDiffSeedingSolver/layers.ts
|
|
840
|
-
function layerSortKey(n) {
|
|
841
|
-
const L = n.toLowerCase();
|
|
842
|
-
if (L === "top") return -1e6;
|
|
843
|
-
if (L === "bottom") return 1e6;
|
|
844
|
-
const m = /^inner(\d+)$/i.exec(L);
|
|
845
|
-
if (m) return parseInt(m[1], 10) || 0;
|
|
846
|
-
return 100 + L.charCodeAt(0);
|
|
847
|
-
}
|
|
848
|
-
function canonicalizeLayerOrder(names) {
|
|
849
|
-
return Array.from(new Set(names)).sort((a, b) => {
|
|
850
|
-
const ka = layerSortKey(a);
|
|
851
|
-
const kb = layerSortKey(b);
|
|
852
|
-
if (ka !== kb) return ka - kb;
|
|
853
|
-
return a.localeCompare(b);
|
|
854
|
-
});
|
|
855
|
-
}
|
|
856
|
-
function buildZIndexMap(params) {
|
|
857
|
-
const names = canonicalizeLayerOrder(
|
|
858
|
-
(params.obstacles ?? []).flatMap((o) => o.layers ?? [])
|
|
859
|
-
);
|
|
860
|
-
const declaredLayerCount = Math.max(1, params.layerCount || names.length || 1);
|
|
861
|
-
const fallback = Array.from(
|
|
862
|
-
{ length: declaredLayerCount },
|
|
863
|
-
(_, i) => i === 0 ? "top" : i === declaredLayerCount - 1 ? "bottom" : `inner${i}`
|
|
864
|
-
);
|
|
865
|
-
const ordered = [];
|
|
866
|
-
const seen = /* @__PURE__ */ new Set();
|
|
867
|
-
const push = (n) => {
|
|
868
|
-
const key = n.toLowerCase();
|
|
869
|
-
if (seen.has(key)) return;
|
|
870
|
-
seen.add(key);
|
|
871
|
-
ordered.push(n);
|
|
872
|
-
};
|
|
873
|
-
fallback.forEach(push);
|
|
874
|
-
names.forEach(push);
|
|
875
|
-
const layerNames = ordered.slice(0, declaredLayerCount);
|
|
876
|
-
const clampIndex = (nameLower) => {
|
|
877
|
-
if (layerNames.length <= 1) return 0;
|
|
878
|
-
if (nameLower === "top") return 0;
|
|
879
|
-
if (nameLower === "bottom") return layerNames.length - 1;
|
|
880
|
-
const m = /^inner(\d+)$/i.exec(nameLower);
|
|
881
|
-
if (m) {
|
|
882
|
-
if (layerNames.length <= 2) return layerNames.length - 1;
|
|
883
|
-
const parsed = parseInt(m[1], 10);
|
|
884
|
-
const maxInner = layerNames.length - 2;
|
|
885
|
-
const clampedInner = Math.min(
|
|
886
|
-
maxInner,
|
|
887
|
-
Math.max(1, Number.isFinite(parsed) ? parsed : 1)
|
|
888
|
-
);
|
|
889
|
-
return clampedInner;
|
|
890
|
-
}
|
|
891
|
-
return 0;
|
|
892
|
-
};
|
|
893
|
-
const map = /* @__PURE__ */ new Map();
|
|
894
|
-
layerNames.forEach((n, i) => map.set(n.toLowerCase(), i));
|
|
895
|
-
ordered.slice(layerNames.length).forEach((n) => {
|
|
896
|
-
const key = n.toLowerCase();
|
|
897
|
-
map.set(key, clampIndex(key));
|
|
898
|
-
});
|
|
899
|
-
return { layerNames, zIndexByName: map };
|
|
900
|
-
}
|
|
901
|
-
function obstacleZs(ob, zIndexByName) {
|
|
902
|
-
if (ob.zLayers?.length)
|
|
903
|
-
return Array.from(new Set(ob.zLayers)).sort((a, b) => a - b);
|
|
904
|
-
const fromNames = (ob.layers ?? []).map((n) => zIndexByName.get(n.toLowerCase())).filter((v) => typeof v === "number");
|
|
905
|
-
return Array.from(new Set(fromNames)).sort((a, b) => a - b);
|
|
906
|
-
}
|
|
907
|
-
function obstacleToXYRect(ob) {
|
|
908
|
-
const w = ob.width;
|
|
909
|
-
const h = ob.height;
|
|
910
|
-
if (typeof w !== "number" || typeof h !== "number") return null;
|
|
911
|
-
return { x: ob.center.x - w / 2, y: ob.center.y - h / 2, width: w, height: h };
|
|
912
|
-
}
|
|
913
|
-
|
|
914
1153
|
// lib/utils/isSelfRect.ts
|
|
915
1154
|
var EPS5 = 1e-9;
|
|
916
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;
|
|
@@ -1676,7 +1915,7 @@ function resizeSoftOverlaps(params, newIndex) {
|
|
|
1676
1915
|
}
|
|
1677
1916
|
}
|
|
1678
1917
|
}
|
|
1679
|
-
const
|
|
1918
|
+
const sameRect2 = (a, b) => a.minX === b.minX && a.minY === b.minY && a.maxX === b.maxX && a.maxY === b.maxY;
|
|
1680
1919
|
removeIdx.sort((a, b) => b - a).forEach((idx) => {
|
|
1681
1920
|
const rem = params.placed.splice(idx, 1)[0];
|
|
1682
1921
|
if (params.placedIndexByLayer) {
|
|
@@ -1685,7 +1924,7 @@ function resizeSoftOverlaps(params, newIndex) {
|
|
|
1685
1924
|
if (tree)
|
|
1686
1925
|
tree.remove(
|
|
1687
1926
|
rectToTree(rem.rect, { zLayers: rem.zLayers }),
|
|
1688
|
-
|
|
1927
|
+
sameRect2
|
|
1689
1928
|
);
|
|
1690
1929
|
}
|
|
1691
1930
|
}
|
|
@@ -1703,27 +1942,14 @@ function resizeSoftOverlaps(params, newIndex) {
|
|
|
1703
1942
|
}
|
|
1704
1943
|
}
|
|
1705
1944
|
|
|
1706
|
-
// lib/utils/getColorForZLayer.ts
|
|
1707
|
-
var getColorForZLayer = (zLayers) => {
|
|
1708
|
-
const minZ = Math.min(...zLayers);
|
|
1709
|
-
const colors = [
|
|
1710
|
-
{ fill: "#dbeafe", stroke: "#3b82f6" },
|
|
1711
|
-
{ fill: "#fef3c7", stroke: "#f59e0b" },
|
|
1712
|
-
{ fill: "#d1fae5", stroke: "#10b981" },
|
|
1713
|
-
{ fill: "#e9d5ff", stroke: "#a855f7" },
|
|
1714
|
-
{ fill: "#fed7aa", stroke: "#f97316" },
|
|
1715
|
-
{ fill: "#fecaca", stroke: "#ef4444" }
|
|
1716
|
-
];
|
|
1717
|
-
return colors[minZ % colors.length];
|
|
1718
|
-
};
|
|
1719
|
-
|
|
1720
1945
|
// lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts
|
|
1721
1946
|
import RBush3 from "rbush";
|
|
1722
|
-
var RectDiffSeedingSolver = class extends
|
|
1947
|
+
var RectDiffSeedingSolver = class extends BaseSolver4 {
|
|
1723
1948
|
constructor(input) {
|
|
1724
1949
|
super();
|
|
1725
1950
|
this.input = input;
|
|
1726
1951
|
}
|
|
1952
|
+
input;
|
|
1727
1953
|
// Engine fields (mirrors initState / engine.ts)
|
|
1728
1954
|
srj;
|
|
1729
1955
|
layerNames;
|
|
@@ -2077,7 +2303,7 @@ z:${placement.zLayers.join(",")}`
|
|
|
2077
2303
|
};
|
|
2078
2304
|
|
|
2079
2305
|
// lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts
|
|
2080
|
-
import { BaseSolver as
|
|
2306
|
+
import { BaseSolver as BaseSolver5 } from "@tscircuit/solver-utils";
|
|
2081
2307
|
|
|
2082
2308
|
// lib/utils/finalizeRects.ts
|
|
2083
2309
|
function finalizeRects(params) {
|
|
@@ -2149,11 +2375,12 @@ import RBush4 from "rbush";
|
|
|
2149
2375
|
var sameTreeRect = (a, b) => a.minX === b.minX && a.minY === b.minY && a.maxX === b.maxX && a.maxY === b.maxY;
|
|
2150
2376
|
|
|
2151
2377
|
// lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts
|
|
2152
|
-
var RectDiffExpansionSolver = class extends
|
|
2378
|
+
var RectDiffExpansionSolver = class extends BaseSolver5 {
|
|
2153
2379
|
constructor(input) {
|
|
2154
2380
|
super();
|
|
2155
2381
|
this.input = input;
|
|
2156
2382
|
}
|
|
2383
|
+
input;
|
|
2157
2384
|
placedIndexByLayer = [];
|
|
2158
2385
|
_meshNodes = [];
|
|
2159
2386
|
_setup() {
|
|
@@ -2294,19 +2521,6 @@ import "rbush";
|
|
|
2294
2521
|
|
|
2295
2522
|
// lib/solvers/RectDiffGridSolverPipeline/buildObstacleIndexes.ts
|
|
2296
2523
|
import RBush5 from "rbush";
|
|
2297
|
-
|
|
2298
|
-
// lib/utils/padRect.ts
|
|
2299
|
-
var padRect = (rect, clearance) => {
|
|
2300
|
-
if (!clearance || clearance <= 0) return rect;
|
|
2301
|
-
return {
|
|
2302
|
-
x: rect.x - clearance,
|
|
2303
|
-
y: rect.y - clearance,
|
|
2304
|
-
width: rect.width + 2 * clearance,
|
|
2305
|
-
height: rect.height + 2 * clearance
|
|
2306
|
-
};
|
|
2307
|
-
};
|
|
2308
|
-
|
|
2309
|
-
// lib/solvers/RectDiffGridSolverPipeline/buildObstacleIndexes.ts
|
|
2310
2524
|
var buildObstacleIndexesByLayer = (params) => {
|
|
2311
2525
|
const { srj, boardVoidRects, obstacleClearance } = params;
|
|
2312
2526
|
const { layerNames, zIndexByName } = buildZIndexMap({
|
|
@@ -2650,6 +2864,7 @@ import { mergeGraphics as mergeGraphics2 } from "graphics-debug";
|
|
|
2650
2864
|
var RectDiffPipeline = class extends BasePipelineSolver3 {
|
|
2651
2865
|
rectDiffGridSolverPipeline;
|
|
2652
2866
|
gapFillSolver;
|
|
2867
|
+
outerLayerContainmentMergeSolver;
|
|
2653
2868
|
boardVoidRects;
|
|
2654
2869
|
zIndexByName;
|
|
2655
2870
|
layerNames;
|
|
@@ -2685,6 +2900,18 @@ var RectDiffPipeline = class extends BasePipelineSolver3 {
|
|
|
2685
2900
|
}
|
|
2686
2901
|
}
|
|
2687
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
|
+
]
|
|
2688
2915
|
)
|
|
2689
2916
|
];
|
|
2690
2917
|
_setup() {
|
|
@@ -2710,6 +2937,10 @@ var RectDiffPipeline = class extends BasePipelineSolver3 {
|
|
|
2710
2937
|
return [this.inputProblem];
|
|
2711
2938
|
}
|
|
2712
2939
|
getOutput() {
|
|
2940
|
+
const outerLayerMergeOutput = this.outerLayerContainmentMergeSolver?.getOutput();
|
|
2941
|
+
if (outerLayerMergeOutput) {
|
|
2942
|
+
return { meshNodes: outerLayerMergeOutput.outputNodes };
|
|
2943
|
+
}
|
|
2713
2944
|
const gapFillOutput = this.gapFillSolver?.getOutput();
|
|
2714
2945
|
if (gapFillOutput) {
|
|
2715
2946
|
return { meshNodes: gapFillOutput.outputNodes };
|
package/lib/RectDiffPipeline.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { GridFill3DOptions, XYRect } from "./rectdiff-types"
|
|
|
8
8
|
import type { CapacityMeshNode } from "./types/capacity-mesh-types"
|
|
9
9
|
import type { GraphicsObject } from "graphics-debug"
|
|
10
10
|
import { GapFillSolverPipeline } from "./solvers/GapFillSolver/GapFillSolverPipeline"
|
|
11
|
+
import { OuterLayerContainmentMergeSolver } from "./solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver"
|
|
11
12
|
import { RectDiffGridSolverPipeline } from "./solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline"
|
|
12
13
|
import { createBaseVisualization } from "./rectdiff-visualization"
|
|
13
14
|
import { buildFinalRectDiffVisualization } from "./buildFinalRectDiffVisualization"
|
|
@@ -25,6 +26,7 @@ export interface RectDiffPipelineInput {
|
|
|
25
26
|
export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput> {
|
|
26
27
|
rectDiffGridSolverPipeline?: RectDiffGridSolverPipeline
|
|
27
28
|
gapFillSolver?: GapFillSolverPipeline
|
|
29
|
+
outerLayerContainmentMergeSolver?: OuterLayerContainmentMergeSolver
|
|
28
30
|
boardVoidRects: XYRect[] | undefined
|
|
29
31
|
zIndexByName?: Map<string, number>
|
|
30
32
|
layerNames?: string[]
|
|
@@ -69,6 +71,22 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
|
|
|
69
71
|
},
|
|
70
72
|
],
|
|
71
73
|
),
|
|
74
|
+
definePipelineStep(
|
|
75
|
+
"outerLayerContainmentMergeSolver",
|
|
76
|
+
OuterLayerContainmentMergeSolver,
|
|
77
|
+
(rectDiffPipeline: RectDiffPipeline) => [
|
|
78
|
+
{
|
|
79
|
+
meshNodes:
|
|
80
|
+
rectDiffPipeline.gapFillSolver?.getOutput().outputNodes ??
|
|
81
|
+
rectDiffPipeline.rectDiffGridSolverPipeline?.getOutput()
|
|
82
|
+
.meshNodes ??
|
|
83
|
+
[],
|
|
84
|
+
simpleRouteJson: rectDiffPipeline.inputProblem.simpleRouteJson,
|
|
85
|
+
zIndexByName: rectDiffPipeline.zIndexByName ?? new Map(),
|
|
86
|
+
obstacleClearance: rectDiffPipeline.inputProblem.obstacleClearance,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
),
|
|
72
90
|
]
|
|
73
91
|
|
|
74
92
|
override _setup(): void {
|
|
@@ -100,6 +118,11 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
|
|
|
100
118
|
}
|
|
101
119
|
|
|
102
120
|
override getOutput(): { meshNodes: CapacityMeshNode[] } {
|
|
121
|
+
const outerLayerMergeOutput =
|
|
122
|
+
this.outerLayerContainmentMergeSolver?.getOutput()
|
|
123
|
+
if (outerLayerMergeOutput) {
|
|
124
|
+
return { meshNodes: outerLayerMergeOutput.outputNodes }
|
|
125
|
+
}
|
|
103
126
|
const gapFillOutput = this.gapFillSolver?.getOutput()
|
|
104
127
|
if (gapFillOutput) {
|
|
105
128
|
return { meshNodes: gapFillOutput.outputNodes }
|