pattapatta 0.2.1 → 0.4.0
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/CHANGELOG.md +18 -0
- package/README.md +0 -43
- package/dist/{circle-BDbKfrdR.d.ts → circle-CauDMv8V.d.ts} +1 -1
- package/dist/circlePacking/index.d.ts +2 -3
- package/dist/construction/index.d.ts +11 -3
- package/dist/construction/index.js +429 -2
- package/dist/construction/index.js.map +1 -1
- package/dist/contour/index.d.ts +90 -4
- package/dist/contour/index.js +1326 -3
- package/dist/contour/index.js.map +1 -1
- package/dist/conversion/index.d.ts +2 -3
- package/dist/{group-BNjVVYSQ.d.ts → group-YVmSJuGo.d.ts} +1 -1
- package/dist/hatch/index.d.ts +70 -4
- package/dist/hatch/index.js +414 -3
- package/dist/hatch/index.js.map +1 -1
- package/dist/hull/index.d.ts +1 -2
- package/dist/index.d.ts +13 -14
- package/dist/index.js +2228 -632
- package/dist/index.js.map +1 -1
- package/dist/meshing/index.d.ts +3 -4
- package/dist/morphology/index.d.ts +2 -3
- package/dist/optimisation/index.d.ts +2 -3
- package/dist/{path-0RjhYjJs.d.ts → path-Boeshk9L.d.ts} +9 -2
- package/dist/pointSet/index.d.ts +15 -2
- package/dist/pointSet/index.js +148 -2
- package/dist/pointSet/index.js.map +1 -1
- package/dist/polygonisation/index.d.ts +1 -2
- package/dist/polygonisation/index.js +54 -50
- package/dist/polygonisation/index.js.map +1 -1
- package/dist/predicates/index.d.ts +2 -3
- package/dist/processing/index.d.ts +3 -4
- package/dist/{segment-BKuIFMKD.d.ts → segment-CpLILSd7.d.ts} +1 -1
- package/dist/segmentSet/index.d.ts +41 -3
- package/dist/segmentSet/index.js +261 -1
- package/dist/segmentSet/index.js.map +1 -1
- package/dist/shapeBoolean/index.d.ts +2 -3
- package/dist/svg/index.d.ts +2 -3
- package/dist/tiling/index.d.ts +10 -4
- package/dist/tiling/index.js +148 -13
- package/dist/tiling/index.js.map +1 -1
- package/dist/transformation/index.d.ts +1 -2
- package/dist/triangulation/index.d.ts +1 -2
- package/dist/triangulation/index.js.map +1 -1
- package/dist/voronoi/index.d.ts +2 -3
- package/package.json +2 -2
- package/dist/vec2-CWyXN1Wj.d.ts +0 -10
package/dist/index.js
CHANGED
|
@@ -765,6 +765,193 @@ function parallelSegments(centerX, centerY, length, spacing, angle, n) {
|
|
|
765
765
|
}
|
|
766
766
|
return edges;
|
|
767
767
|
}
|
|
768
|
+
function stochasticSegments(width2, height2, n, minLength, maxLength, seed = 1, originX = 0, originY = 0) {
|
|
769
|
+
const minL = minLength ?? 1;
|
|
770
|
+
const maxL = maxLength ?? Math.min(width2, height2);
|
|
771
|
+
if (n < 1 || width2 <= 0 || height2 <= 0) return [];
|
|
772
|
+
const monoLength = minL === maxL;
|
|
773
|
+
const rng = mulberry32(seed >>> 0);
|
|
774
|
+
const edges = [];
|
|
775
|
+
const maxAttempts = Math.max(1e4, n * 500);
|
|
776
|
+
let attempts = 0;
|
|
777
|
+
while (edges.length < n && attempts < maxAttempts) {
|
|
778
|
+
attempts++;
|
|
779
|
+
const length = monoLength ? minL : minL + rng() * (maxL - minL);
|
|
780
|
+
if (!(length > 0) || length * 2 >= width2 || length * 2 >= height2) {
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
const ax = originX + rng() * (width2 - length * 2) + length;
|
|
784
|
+
const ay = originY + rng() * (height2 - length * 2) + length;
|
|
785
|
+
const theta = rng() * Math.PI * 2;
|
|
786
|
+
const bx = ax + Math.cos(theta) * length;
|
|
787
|
+
const by = ay + Math.sin(theta) * length;
|
|
788
|
+
const a = vec2(ax, ay);
|
|
789
|
+
const b = vec2(bx, by);
|
|
790
|
+
let hits = false;
|
|
791
|
+
for (const s of edges) {
|
|
792
|
+
if (segmentsIntersect(a, b, s.a, s.b)) {
|
|
793
|
+
hits = true;
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
if (!hits) edges.push(segment(a, b));
|
|
798
|
+
}
|
|
799
|
+
return edges;
|
|
800
|
+
}
|
|
801
|
+
function weaveSegments(width2, height2, cellSize, A = 1, B = 1, C = 1, options = {}) {
|
|
802
|
+
if (!(cellSize > 0)) {
|
|
803
|
+
throw new Error("weaveSegments: cellSize must be > 0");
|
|
804
|
+
}
|
|
805
|
+
if (A <= 0 || B <= 0) {
|
|
806
|
+
throw new Error("weaveSegments: A and B must be > 0");
|
|
807
|
+
}
|
|
808
|
+
const {
|
|
809
|
+
originX = 0,
|
|
810
|
+
originY = 0,
|
|
811
|
+
swapColors = false,
|
|
812
|
+
cellFraction = 1,
|
|
813
|
+
extendSingletonsToEdge = true
|
|
814
|
+
} = options;
|
|
815
|
+
if (!(cellFraction > 0)) {
|
|
816
|
+
throw new Error("weaveSegments: cellFraction must be > 0");
|
|
817
|
+
}
|
|
818
|
+
const P = A + B;
|
|
819
|
+
const f = Math.max(0, Math.min(1, cellFraction));
|
|
820
|
+
const cols = Math.max(1, Math.floor(width2 / cellSize));
|
|
821
|
+
const rows = Math.max(1, Math.floor(height2 / cellSize));
|
|
822
|
+
const gridW = cols * cellSize;
|
|
823
|
+
const gridH = rows * cellSize;
|
|
824
|
+
const dx = originX + (width2 - gridW) * 0.5;
|
|
825
|
+
const dy = originY + (height2 - gridH) * 0.5;
|
|
826
|
+
const gridLeft = dx;
|
|
827
|
+
const gridRight = dx + gridW;
|
|
828
|
+
const gridBottom = dy;
|
|
829
|
+
const gridTop = dy + gridH;
|
|
830
|
+
const weft = Array.from(
|
|
831
|
+
{ length: rows },
|
|
832
|
+
() => Array.from({ length: cols }, () => false)
|
|
833
|
+
);
|
|
834
|
+
for (let r = 0; r < rows; r++) {
|
|
835
|
+
const shift = floorMod(r * C, P);
|
|
836
|
+
for (let c = 0; c < cols; c++) {
|
|
837
|
+
const idx = floorMod(c + shift, P);
|
|
838
|
+
let isWeft = idx < A;
|
|
839
|
+
if (swapColors) isWeft = !isWeft;
|
|
840
|
+
weft[r][c] = isWeft;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
const segs = [];
|
|
844
|
+
for (let r = 0; r < rows; r++) {
|
|
845
|
+
let c = 0;
|
|
846
|
+
while (c < cols) {
|
|
847
|
+
if (!weft[r][c]) {
|
|
848
|
+
c++;
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
const start = c;
|
|
852
|
+
while (c + 1 < cols && weft[r][c + 1]) c++;
|
|
853
|
+
const end = c;
|
|
854
|
+
const runLen = end - start + 1;
|
|
855
|
+
const y = dy + (r + 0.5) * cellSize;
|
|
856
|
+
let x0 = dx + (start + (1 - f)) * cellSize;
|
|
857
|
+
let x1 = dx + (end + f) * cellSize;
|
|
858
|
+
if (extendSingletonsToEdge && runLen === 1) {
|
|
859
|
+
const cx = dx + (start + 0.5) * cellSize;
|
|
860
|
+
if (start === 0) {
|
|
861
|
+
x0 = gridLeft;
|
|
862
|
+
x1 = cx;
|
|
863
|
+
} else if (end === cols - 1) {
|
|
864
|
+
x0 = cx;
|
|
865
|
+
x1 = gridRight;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
x0 = clamp(x0, gridLeft, gridRight);
|
|
869
|
+
x1 = clamp(x1, gridLeft, gridRight);
|
|
870
|
+
segs.push(segment(vec2(x0, y), vec2(x1, y)));
|
|
871
|
+
c++;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
for (let c = 0; c < cols; c++) {
|
|
875
|
+
let r = 0;
|
|
876
|
+
while (r < rows) {
|
|
877
|
+
if (weft[r][c]) {
|
|
878
|
+
r++;
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
const start = r;
|
|
882
|
+
while (r + 1 < rows && !weft[r + 1][c]) r++;
|
|
883
|
+
const end = r;
|
|
884
|
+
const runLen = end - start + 1;
|
|
885
|
+
const x = dx + (c + 0.5) * cellSize;
|
|
886
|
+
let y0 = dy + (start + (1 - f)) * cellSize;
|
|
887
|
+
let y1 = dy + (end + f) * cellSize;
|
|
888
|
+
if (extendSingletonsToEdge && runLen === 1) {
|
|
889
|
+
const cy = dy + (start + 0.5) * cellSize;
|
|
890
|
+
if (start === 0) {
|
|
891
|
+
y0 = gridBottom;
|
|
892
|
+
y1 = cy;
|
|
893
|
+
} else if (end === rows - 1) {
|
|
894
|
+
y0 = cy;
|
|
895
|
+
y1 = gridTop;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
y0 = clamp(y0, gridBottom, gridTop);
|
|
899
|
+
y1 = clamp(y1, gridBottom, gridTop);
|
|
900
|
+
segs.push(segment(vec2(x, y0), vec2(x, y1)));
|
|
901
|
+
r++;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
return segs;
|
|
905
|
+
}
|
|
906
|
+
function perpendicularPathSegments(path3, interSegmentDistance, length, startOffset = 0) {
|
|
907
|
+
if (!(interSegmentDistance > 0)) return [];
|
|
908
|
+
const lengthFn = typeof length === "function" ? length : () => length;
|
|
909
|
+
const startNorm = (startOffset % 1 + 1) % 1;
|
|
910
|
+
const edges = [];
|
|
911
|
+
for (const ring2 of path3.rings) {
|
|
912
|
+
if (ring2.length < 2) continue;
|
|
913
|
+
const closed = path3.closed;
|
|
914
|
+
let pts = ring2.map((v) => vec2(v.x, v.y));
|
|
915
|
+
if (closed && shoelace(pts) > 0) {
|
|
916
|
+
pts = pts.slice().reverse();
|
|
917
|
+
}
|
|
918
|
+
const indexed = lengthIndex(pts, closed);
|
|
919
|
+
const end = indexed.total;
|
|
920
|
+
if (!(end > 0)) continue;
|
|
921
|
+
const count = Math.max(1, Math.round(end / interSegmentDistance));
|
|
922
|
+
const increment = 1 / count;
|
|
923
|
+
const eps = Math.max(
|
|
924
|
+
end * 1e-4,
|
|
925
|
+
Math.min(interSegmentDistance * 0.35, end * 0.01)
|
|
926
|
+
);
|
|
927
|
+
for (let i = 0; i < count; i++) {
|
|
928
|
+
const posFrac = (startNorm + i * increment) % 1;
|
|
929
|
+
const idx = posFrac * end;
|
|
930
|
+
const idxM = closed ? wrapIndex(idx - eps, end) : clamp(idx - eps, 0, end);
|
|
931
|
+
const idxP = closed ? wrapIndex(idx + eps, end) : clamp(idx + eps, 0, end);
|
|
932
|
+
const pm = indexed.pointAt(idxM);
|
|
933
|
+
const pp = indexed.pointAt(idxP);
|
|
934
|
+
const pc = indexed.pointAt(idx);
|
|
935
|
+
const tx = pp.x - pm.x;
|
|
936
|
+
const ty = pp.y - pm.y;
|
|
937
|
+
const tlen = Math.hypot(tx, ty);
|
|
938
|
+
if (tlen === 0) continue;
|
|
939
|
+
const nx = -ty / tlen;
|
|
940
|
+
const ny = tx / tlen;
|
|
941
|
+
const normalAngle = Math.atan2(ny, nx);
|
|
942
|
+
const L = lengthFn(pc.x, pc.y, posFrac, normalAngle);
|
|
943
|
+
if (!(L > 0)) continue;
|
|
944
|
+
const half = L * 0.5;
|
|
945
|
+
edges.push(
|
|
946
|
+
segment(
|
|
947
|
+
vec2(pc.x - nx * half, pc.y - ny * half),
|
|
948
|
+
vec2(pc.x + nx * half, pc.y + ny * half)
|
|
949
|
+
)
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return edges;
|
|
954
|
+
}
|
|
768
955
|
function filterByMinLength(segments, minLength) {
|
|
769
956
|
return segments.filter((s) => segmentLength(s) >= minLength);
|
|
770
957
|
}
|
|
@@ -790,11 +977,84 @@ function segmentsToOpenPaths(segments) {
|
|
|
790
977
|
}
|
|
791
978
|
var segmentSet = {
|
|
792
979
|
parallelSegments,
|
|
980
|
+
stochasticSegments,
|
|
981
|
+
weaveSegments,
|
|
982
|
+
perpendicularPathSegments,
|
|
793
983
|
filterByMinLength,
|
|
794
984
|
filterAxisAligned,
|
|
795
985
|
segmentLength,
|
|
796
986
|
segmentsToOpenPaths
|
|
797
987
|
};
|
|
988
|
+
function floorMod(n, m) {
|
|
989
|
+
return (n % m + m) % m;
|
|
990
|
+
}
|
|
991
|
+
function clamp(v, lo, hi) {
|
|
992
|
+
return Math.max(lo, Math.min(hi, v));
|
|
993
|
+
}
|
|
994
|
+
function wrapIndex(idx, end) {
|
|
995
|
+
if (end <= 0) return 0;
|
|
996
|
+
let x = idx % end;
|
|
997
|
+
if (x < 0) x += end;
|
|
998
|
+
return x;
|
|
999
|
+
}
|
|
1000
|
+
function shoelace(ring2) {
|
|
1001
|
+
let sum = 0;
|
|
1002
|
+
for (let i = 0; i < ring2.length; i++) {
|
|
1003
|
+
const a = ring2[i];
|
|
1004
|
+
const b = ring2[(i + 1) % ring2.length];
|
|
1005
|
+
sum += a.x * b.y - b.x * a.y;
|
|
1006
|
+
}
|
|
1007
|
+
return sum / 2;
|
|
1008
|
+
}
|
|
1009
|
+
function segmentsIntersect(a, b, c, d) {
|
|
1010
|
+
return ccw(a, c, d) !== ccw(b, c, d) && ccw(a, b, c) !== ccw(a, b, d);
|
|
1011
|
+
}
|
|
1012
|
+
function ccw(a, b, c) {
|
|
1013
|
+
return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
|
|
1014
|
+
}
|
|
1015
|
+
function mulberry32(seed) {
|
|
1016
|
+
return function() {
|
|
1017
|
+
let t = seed += 1831565813;
|
|
1018
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
1019
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
1020
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
function lengthIndex(ring2, closed) {
|
|
1024
|
+
const n = ring2.length;
|
|
1025
|
+
const edgeCount = closed ? n : n - 1;
|
|
1026
|
+
const lengths = [];
|
|
1027
|
+
let total = 0;
|
|
1028
|
+
for (let i = 0; i < edgeCount; i++) {
|
|
1029
|
+
const a = ring2[i];
|
|
1030
|
+
const b = ring2[(i + 1) % n];
|
|
1031
|
+
const L = Math.hypot(b.x - a.x, b.y - a.y);
|
|
1032
|
+
lengths.push(L);
|
|
1033
|
+
total += L;
|
|
1034
|
+
}
|
|
1035
|
+
return {
|
|
1036
|
+
total,
|
|
1037
|
+
pointAt(dist2) {
|
|
1038
|
+
let remain = dist2;
|
|
1039
|
+
if (remain <= 0) return { ...ring2[0] };
|
|
1040
|
+
if (remain >= total) return { ...ring2[closed ? 0 : n - 1] };
|
|
1041
|
+
for (let i = 0; i < lengths.length; i++) {
|
|
1042
|
+
const L = lengths[i];
|
|
1043
|
+
if (remain <= L || i === lengths.length - 1) {
|
|
1044
|
+
const t = L < 1e-12 ? 0 : remain / L;
|
|
1045
|
+
const a = ring2[i];
|
|
1046
|
+
const b = ring2[(i + 1) % n];
|
|
1047
|
+
return {
|
|
1048
|
+
x: a.x + (b.x - a.x) * t,
|
|
1049
|
+
y: a.y + (b.y - a.y) * t
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
remain -= L;
|
|
1053
|
+
}
|
|
1054
|
+
return { ...ring2[0] };
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
798
1058
|
|
|
799
1059
|
// src/segmentSet/clip.ts
|
|
800
1060
|
function clipSegmentToPath(seg, clip) {
|
|
@@ -825,8 +1085,8 @@ function clipSegmentToPath(seg, clip) {
|
|
|
825
1085
|
const h0 = hits[i];
|
|
826
1086
|
const h1 = hits[i + 1];
|
|
827
1087
|
if (h1.t - h0.t < 1e-9) continue;
|
|
828
|
-
const
|
|
829
|
-
if (containsPoint(clip,
|
|
1088
|
+
const mid5 = vec2((h0.p.x + h1.p.x) / 2, (h0.p.y + h1.p.y) / 2);
|
|
1089
|
+
if (containsPoint(clip, mid5)) {
|
|
830
1090
|
out.push(segment(h0.p, h1.p));
|
|
831
1091
|
}
|
|
832
1092
|
}
|
|
@@ -854,6 +1114,34 @@ function segmentIntersection(a, b, c, d) {
|
|
|
854
1114
|
p: vec2(a.x + t * rX, a.y + t * rY)
|
|
855
1115
|
};
|
|
856
1116
|
}
|
|
1117
|
+
function buffer(path3, delta, options = {}) {
|
|
1118
|
+
const subject = pathToPathsD(path3);
|
|
1119
|
+
const join = options.join === "miter" ? JoinType.Miter : options.join === "square" ? JoinType.Square : JoinType.Round;
|
|
1120
|
+
const inflated = inflatePathsD(subject, delta, join, EndType.Polygon);
|
|
1121
|
+
return pathsDToGroup(inflated);
|
|
1122
|
+
}
|
|
1123
|
+
function erosionDilation(p, amount) {
|
|
1124
|
+
const a = Math.abs(amount);
|
|
1125
|
+
if (a < 1e-12) return group([p]);
|
|
1126
|
+
const eroded = buffer(p, -a);
|
|
1127
|
+
if (eroded.paths.length === 0) return eroded;
|
|
1128
|
+
const dilated = [];
|
|
1129
|
+
for (const q of eroded.paths) {
|
|
1130
|
+
dilated.push(...buffer(q, a).paths);
|
|
1131
|
+
}
|
|
1132
|
+
return group(dilated);
|
|
1133
|
+
}
|
|
1134
|
+
function dilationErosion(p, amount) {
|
|
1135
|
+
const a = Math.abs(amount);
|
|
1136
|
+
if (a < 1e-12) return group([p]);
|
|
1137
|
+
const dilated = buffer(p, a);
|
|
1138
|
+
if (dilated.paths.length === 0) return dilated;
|
|
1139
|
+
const eroded = [];
|
|
1140
|
+
for (const q of dilated.paths) {
|
|
1141
|
+
eroded.push(...buffer(q, -a).paths);
|
|
1142
|
+
}
|
|
1143
|
+
return group(eroded);
|
|
1144
|
+
}
|
|
857
1145
|
|
|
858
1146
|
// src/hatch/index.ts
|
|
859
1147
|
function parallel(path3, options = {}) {
|
|
@@ -886,6 +1174,94 @@ function cross(path3, options = {}) {
|
|
|
886
1174
|
const b = parallel(path3, { ...options, angle: angle + Math.PI / 2 });
|
|
887
1175
|
return [...a, ...b];
|
|
888
1176
|
}
|
|
1177
|
+
function weave(path3, options = {}) {
|
|
1178
|
+
if (!path3.closed || !path3.rings[0]) {
|
|
1179
|
+
throw new Error("hatch.weave requires a closed path");
|
|
1180
|
+
}
|
|
1181
|
+
const bounds2 = pathBounds(path3);
|
|
1182
|
+
const width2 = Math.max(bounds2.maxX - bounds2.minX, 1e-9);
|
|
1183
|
+
const height2 = Math.max(bounds2.maxY - bounds2.minY, 1e-9);
|
|
1184
|
+
const cellSize = options.cellSize ?? Math.max(Math.min(width2, height2) / 12, 1e-6);
|
|
1185
|
+
const weaveOpts = {
|
|
1186
|
+
originX: bounds2.minX,
|
|
1187
|
+
originY: bounds2.minY
|
|
1188
|
+
};
|
|
1189
|
+
if (options.swapColors !== void 0) weaveOpts.swapColors = options.swapColors;
|
|
1190
|
+
if (options.cellFraction !== void 0) {
|
|
1191
|
+
weaveOpts.cellFraction = options.cellFraction;
|
|
1192
|
+
}
|
|
1193
|
+
if (options.extendSingletonsToEdge !== void 0) {
|
|
1194
|
+
weaveOpts.extendSingletonsToEdge = options.extendSingletonsToEdge;
|
|
1195
|
+
}
|
|
1196
|
+
const raw = weaveSegments(
|
|
1197
|
+
width2,
|
|
1198
|
+
height2,
|
|
1199
|
+
cellSize,
|
|
1200
|
+
options.A ?? 1,
|
|
1201
|
+
options.B ?? 1,
|
|
1202
|
+
options.C ?? 1,
|
|
1203
|
+
weaveOpts
|
|
1204
|
+
);
|
|
1205
|
+
return clipSegmentsToPath(raw, path3);
|
|
1206
|
+
}
|
|
1207
|
+
function stochastic(path3, options = {}) {
|
|
1208
|
+
if (!path3.closed || !path3.rings[0]) {
|
|
1209
|
+
throw new Error("hatch.stochastic requires a closed path");
|
|
1210
|
+
}
|
|
1211
|
+
const bounds2 = pathBounds(path3);
|
|
1212
|
+
const width2 = Math.max(bounds2.maxX - bounds2.minX, 1e-9);
|
|
1213
|
+
const height2 = Math.max(bounds2.maxY - bounds2.minY, 1e-9);
|
|
1214
|
+
const count = options.count ?? 40;
|
|
1215
|
+
const minLength = options.length ?? options.minLength ?? 1;
|
|
1216
|
+
const maxLength = options.length ?? options.maxLength ?? Math.min(width2, height2);
|
|
1217
|
+
const raw = stochasticSegments(
|
|
1218
|
+
width2,
|
|
1219
|
+
height2,
|
|
1220
|
+
count,
|
|
1221
|
+
minLength,
|
|
1222
|
+
maxLength,
|
|
1223
|
+
options.seed ?? 1,
|
|
1224
|
+
bounds2.minX,
|
|
1225
|
+
bounds2.minY
|
|
1226
|
+
);
|
|
1227
|
+
return clipSegmentsToPath(raw, path3);
|
|
1228
|
+
}
|
|
1229
|
+
function perpendicular(path3, options = {}) {
|
|
1230
|
+
if (!path3.rings[0]) {
|
|
1231
|
+
throw new Error("hatch.perpendicular requires a path with at least one ring");
|
|
1232
|
+
}
|
|
1233
|
+
const spacing = Math.max(options.spacing ?? 0.15, 1e-9);
|
|
1234
|
+
const length = options.length ?? 0.2;
|
|
1235
|
+
return perpendicularPathSegments(
|
|
1236
|
+
path3,
|
|
1237
|
+
spacing,
|
|
1238
|
+
length,
|
|
1239
|
+
options.startOffset ?? 0
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
function concentric(path3, options = {}) {
|
|
1243
|
+
if (!path3.closed || !path3.rings[0]) {
|
|
1244
|
+
throw new Error("hatch.concentric requires a closed path");
|
|
1245
|
+
}
|
|
1246
|
+
const spacing = Math.max(options.spacing ?? 0.15, 1e-9);
|
|
1247
|
+
const bounds2 = pathBounds(path3);
|
|
1248
|
+
const diag = Math.hypot(bounds2.maxX - bounds2.minX, bounds2.maxY - bounds2.minY);
|
|
1249
|
+
const maxCount = options.count ?? Math.max(1, Math.floor(diag / (2 * spacing)));
|
|
1250
|
+
const out = [];
|
|
1251
|
+
for (let i = 1; i <= maxCount; i++) {
|
|
1252
|
+
const shell = buffer(path3, -spacing * i);
|
|
1253
|
+
if (shell.paths.length === 0) break;
|
|
1254
|
+
for (const p of shell.paths) {
|
|
1255
|
+
out.push(
|
|
1256
|
+
path(
|
|
1257
|
+
p.rings.map((r) => r.map((v) => ({ x: v.x, y: v.y }))),
|
|
1258
|
+
true
|
|
1259
|
+
)
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
return out;
|
|
1264
|
+
}
|
|
889
1265
|
function hatchLineCount(bounds2, spacing, angle) {
|
|
890
1266
|
const nx = Math.cos(angle + Math.PI / 2);
|
|
891
1267
|
const ny = Math.sin(angle + Math.PI / 2);
|
|
@@ -922,7 +1298,11 @@ function pathBounds(path3) {
|
|
|
922
1298
|
}
|
|
923
1299
|
var hatch = {
|
|
924
1300
|
parallel,
|
|
925
|
-
cross
|
|
1301
|
+
cross,
|
|
1302
|
+
weave,
|
|
1303
|
+
stochastic,
|
|
1304
|
+
perpendicular,
|
|
1305
|
+
concentric
|
|
926
1306
|
};
|
|
927
1307
|
|
|
928
1308
|
// src/circlePacking/index.ts
|
|
@@ -988,7 +1368,7 @@ function centeredLatticeGrid(b, stepX, stepY, inset, endPad) {
|
|
|
988
1368
|
return { ox, oy, nX, nY };
|
|
989
1369
|
}
|
|
990
1370
|
function stochasticPack(path3, points, minRadius, seed = 1) {
|
|
991
|
-
const rng =
|
|
1371
|
+
const rng = mulberry322(seed >>> 0);
|
|
992
1372
|
const b = bounds(path3);
|
|
993
1373
|
const packing = [];
|
|
994
1374
|
for (let attempt = 0; attempt < points; attempt++) {
|
|
@@ -1029,7 +1409,7 @@ function obstaclePack(path3, obstacles, n, tolerance = 1) {
|
|
|
1029
1409
|
function frontChainPack(path3, radiusMin, radiusMax, seed = 1) {
|
|
1030
1410
|
let rMin = Math.max(1e-6, Math.min(radiusMin, radiusMax));
|
|
1031
1411
|
let rMax = Math.max(rMin, Math.max(radiusMin, radiusMax));
|
|
1032
|
-
const rng =
|
|
1412
|
+
const rng = mulberry322(seed >>> 0);
|
|
1033
1413
|
const b = bounds(path3);
|
|
1034
1414
|
const packing = [];
|
|
1035
1415
|
const area2 = Math.max(1e-6, (b.maxX - b.minX) * (b.maxY - b.minY));
|
|
@@ -1088,7 +1468,7 @@ function frontChainPack(path3, radiusMin, radiusMax, seed = 1) {
|
|
|
1088
1468
|
function repulsionPack(path3, radiusMin, radiusMax, seed = 1, iterations = 80) {
|
|
1089
1469
|
const rMin = Math.max(1e-6, Math.min(radiusMin, radiusMax));
|
|
1090
1470
|
const rMax = Math.max(rMin, Math.max(radiusMin, radiusMax));
|
|
1091
|
-
const rng =
|
|
1471
|
+
const rng = mulberry322(seed >>> 0);
|
|
1092
1472
|
const b = bounds(path3);
|
|
1093
1473
|
const area2 = Math.max(1e-6, (b.maxX - b.minX) * (b.maxY - b.minY));
|
|
1094
1474
|
const avgR = (rMin + rMax) / 2;
|
|
@@ -1212,7 +1592,7 @@ function vecInBounds(b, rng) {
|
|
|
1212
1592
|
y: b.minY + rng() * (b.maxY - b.minY)
|
|
1213
1593
|
};
|
|
1214
1594
|
}
|
|
1215
|
-
function
|
|
1595
|
+
function mulberry322(a) {
|
|
1216
1596
|
return function() {
|
|
1217
1597
|
let t = a += 1831565813;
|
|
1218
1598
|
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
@@ -1232,36 +1612,6 @@ var circlePacking = {
|
|
|
1232
1612
|
circleOverlapsPath,
|
|
1233
1613
|
circleContainedInPath
|
|
1234
1614
|
};
|
|
1235
|
-
function buffer(path3, delta, options = {}) {
|
|
1236
|
-
const subject = pathToPathsD(path3);
|
|
1237
|
-
const join = options.join === "miter" ? JoinType.Miter : options.join === "square" ? JoinType.Square : JoinType.Round;
|
|
1238
|
-
const inflated = inflatePathsD(subject, delta, join, EndType.Polygon);
|
|
1239
|
-
return pathsDToGroup(inflated);
|
|
1240
|
-
}
|
|
1241
|
-
function erosionDilation(p, amount) {
|
|
1242
|
-
const a = Math.abs(amount);
|
|
1243
|
-
if (a < 1e-12) return group([p]);
|
|
1244
|
-
const eroded = buffer(p, -a);
|
|
1245
|
-
if (eroded.paths.length === 0) return eroded;
|
|
1246
|
-
const dilated = [];
|
|
1247
|
-
for (const q of eroded.paths) {
|
|
1248
|
-
dilated.push(...buffer(q, a).paths);
|
|
1249
|
-
}
|
|
1250
|
-
return group(dilated);
|
|
1251
|
-
}
|
|
1252
|
-
function dilationErosion(p, amount) {
|
|
1253
|
-
const a = Math.abs(amount);
|
|
1254
|
-
if (a < 1e-12) return group([p]);
|
|
1255
|
-
const dilated = buffer(p, a);
|
|
1256
|
-
if (dilated.paths.length === 0) return dilated;
|
|
1257
|
-
const eroded = [];
|
|
1258
|
-
for (const q of dilated.paths) {
|
|
1259
|
-
eroded.push(...buffer(q, -a).paths);
|
|
1260
|
-
}
|
|
1261
|
-
return group(eroded);
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
// src/morphology/index.ts
|
|
1265
1615
|
function simplify(p, epsilon) {
|
|
1266
1616
|
if (epsilon <= 0 || p.rings.length === 0) return p;
|
|
1267
1617
|
const rings = [];
|
|
@@ -1508,13 +1858,13 @@ function removeSmallHoles(p, minArea2) {
|
|
|
1508
1858
|
if (p.rings.length <= 1) return p;
|
|
1509
1859
|
const exterior = p.rings[0];
|
|
1510
1860
|
const kept = p.rings.slice(1).filter((hole) => {
|
|
1511
|
-
const a = Math.abs(
|
|
1861
|
+
const a = Math.abs(shoelace2(hole));
|
|
1512
1862
|
return a >= minArea2;
|
|
1513
1863
|
});
|
|
1514
1864
|
return path([exterior, ...kept], p.closed);
|
|
1515
1865
|
}
|
|
1516
1866
|
function generateRandomPoints(p, count, seed = 1) {
|
|
1517
|
-
const rng =
|
|
1867
|
+
const rng = mulberry323(seed >>> 0);
|
|
1518
1868
|
const b = bounds(p);
|
|
1519
1869
|
const out = [];
|
|
1520
1870
|
let attempts = 0;
|
|
@@ -1569,10 +1919,10 @@ function segmentsOnExterior(p) {
|
|
|
1569
1919
|
function slice(p, a, b) {
|
|
1570
1920
|
const dx = b.x - a.x;
|
|
1571
1921
|
const dy = b.y - a.y;
|
|
1572
|
-
const
|
|
1573
|
-
if (
|
|
1574
|
-
const nx = -dy /
|
|
1575
|
-
const ny = dx /
|
|
1922
|
+
const len2 = Math.hypot(dx, dy);
|
|
1923
|
+
if (len2 < 1e-12) return group([p]);
|
|
1924
|
+
const nx = -dy / len2;
|
|
1925
|
+
const ny = dx / len2;
|
|
1576
1926
|
const bb = bounds(p);
|
|
1577
1927
|
const span = Math.hypot(bb.maxX - bb.minX, bb.maxY - bb.minY) * 4 + 10;
|
|
1578
1928
|
const left = halfPlanePoly(a, nx, ny, span);
|
|
@@ -1601,9 +1951,9 @@ function intersectionPoints(a, b) {
|
|
|
1601
1951
|
for (const sb of segsB) {
|
|
1602
1952
|
const hit = segmentIntersection2(sa.a, sa.b, sb.a, sb.b);
|
|
1603
1953
|
if (!hit) continue;
|
|
1604
|
-
const
|
|
1605
|
-
if (seen.has(
|
|
1606
|
-
seen.add(
|
|
1954
|
+
const key4 = `${hit.x.toFixed(9)},${hit.y.toFixed(9)}`;
|
|
1955
|
+
if (seen.has(key4)) continue;
|
|
1956
|
+
seen.add(key4);
|
|
1607
1957
|
out.push(hit);
|
|
1608
1958
|
}
|
|
1609
1959
|
}
|
|
@@ -1684,7 +2034,7 @@ function pointAtArcLength(ring2, lengths, target) {
|
|
|
1684
2034
|
}
|
|
1685
2035
|
return { ...ring2[0] };
|
|
1686
2036
|
}
|
|
1687
|
-
function
|
|
2037
|
+
function shoelace2(ring2) {
|
|
1688
2038
|
let sum = 0;
|
|
1689
2039
|
for (let i = 0; i < ring2.length; i++) {
|
|
1690
2040
|
const a = ring2[i];
|
|
@@ -1693,7 +2043,7 @@ function shoelace(ring2) {
|
|
|
1693
2043
|
}
|
|
1694
2044
|
return sum / 2;
|
|
1695
2045
|
}
|
|
1696
|
-
function
|
|
2046
|
+
function mulberry323(a) {
|
|
1697
2047
|
return function() {
|
|
1698
2048
|
let t = a += 1831565813;
|
|
1699
2049
|
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
@@ -1762,79 +2112,62 @@ var conversion = {
|
|
|
1762
2112
|
group
|
|
1763
2113
|
};
|
|
1764
2114
|
|
|
1765
|
-
// src/
|
|
1766
|
-
function
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
};
|
|
1776
|
-
|
|
1777
|
-
// src/hull/index.ts
|
|
1778
|
-
function convexHull(points) {
|
|
1779
|
-
const pts = uniquePoints(points);
|
|
1780
|
-
if (pts.length < 3) return polygon(pts);
|
|
1781
|
-
pts.sort((a, b) => a.x === b.x ? a.y - b.y : a.x - b.x);
|
|
1782
|
-
const lower = [];
|
|
1783
|
-
for (const p of pts) {
|
|
1784
|
-
while (lower.length >= 2 && cross2(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) {
|
|
1785
|
-
lower.pop();
|
|
1786
|
-
}
|
|
1787
|
-
lower.push(p);
|
|
2115
|
+
// src/pointSet/hilbert.ts
|
|
2116
|
+
function xy2d(order, x, y) {
|
|
2117
|
+
let rx;
|
|
2118
|
+
let ry;
|
|
2119
|
+
let s;
|
|
2120
|
+
let d = 0;
|
|
2121
|
+
for (s = 1 << order - 1; s > 0; s >>= 1) {
|
|
2122
|
+
rx = (x & s) > 0 ? 1 : 0;
|
|
2123
|
+
ry = (y & s) > 0 ? 1 : 0;
|
|
2124
|
+
d += s * s * (3 * rx ^ ry);
|
|
2125
|
+
({ x, y } = rot(s, x, y, rx, ry));
|
|
1788
2126
|
}
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
2127
|
+
return d;
|
|
2128
|
+
}
|
|
2129
|
+
function rot(n, x, y, rx, ry) {
|
|
2130
|
+
if (ry === 0) {
|
|
2131
|
+
if (rx === 1) {
|
|
2132
|
+
x = n - 1 - x;
|
|
2133
|
+
y = n - 1 - y;
|
|
1794
2134
|
}
|
|
1795
|
-
|
|
2135
|
+
return { x: y, y: x };
|
|
1796
2136
|
}
|
|
1797
|
-
|
|
1798
|
-
upper.pop();
|
|
1799
|
-
return polygon([...lower, ...upper]);
|
|
1800
|
-
}
|
|
1801
|
-
function convexHullPath(p) {
|
|
1802
|
-
const pts = [];
|
|
1803
|
-
for (const ring2 of p.rings) pts.push(...ring2);
|
|
1804
|
-
return convexHull(pts);
|
|
1805
|
-
}
|
|
1806
|
-
function boundingBox(p) {
|
|
1807
|
-
const b = bounds(p);
|
|
1808
|
-
return polygon([
|
|
1809
|
-
{ x: b.minX, y: b.minY },
|
|
1810
|
-
{ x: b.maxX, y: b.minY },
|
|
1811
|
-
{ x: b.maxX, y: b.maxY },
|
|
1812
|
-
{ x: b.minX, y: b.maxY }
|
|
1813
|
-
]);
|
|
1814
|
-
}
|
|
1815
|
-
function cross2(o, a, b) {
|
|
1816
|
-
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
|
|
2137
|
+
return { x, y };
|
|
1817
2138
|
}
|
|
1818
|
-
function
|
|
1819
|
-
|
|
1820
|
-
|
|
2139
|
+
function hilbertSort(points, order = 10) {
|
|
2140
|
+
if (points.length === 0) return [];
|
|
2141
|
+
let minX = Infinity;
|
|
2142
|
+
let minY = Infinity;
|
|
2143
|
+
let maxX = -Infinity;
|
|
2144
|
+
let maxY = -Infinity;
|
|
1821
2145
|
for (const p of points) {
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
2146
|
+
minX = Math.min(minX, p.x);
|
|
2147
|
+
minY = Math.min(minY, p.y);
|
|
2148
|
+
maxX = Math.max(maxX, p.x);
|
|
2149
|
+
maxY = Math.max(maxY, p.y);
|
|
1826
2150
|
}
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
2151
|
+
const span = Math.max(maxX - minX, maxY - minY, 1e-9);
|
|
2152
|
+
const n = 1 << order;
|
|
2153
|
+
const scored = points.map((p) => {
|
|
2154
|
+
const x = Math.min(
|
|
2155
|
+
n - 1,
|
|
2156
|
+
Math.max(0, Math.floor((p.x - minX) / span * (n - 1)))
|
|
2157
|
+
);
|
|
2158
|
+
const y = Math.min(
|
|
2159
|
+
n - 1,
|
|
2160
|
+
Math.max(0, Math.floor((p.y - minY) / span * (n - 1)))
|
|
2161
|
+
);
|
|
2162
|
+
return { p, h: xy2d(order, x, y) };
|
|
2163
|
+
});
|
|
2164
|
+
scored.sort((a, b) => a.h - b.h);
|
|
2165
|
+
return scored.map((s) => s.p);
|
|
2166
|
+
}
|
|
2167
|
+
|
|
1835
2168
|
// src/pointSet/index.ts
|
|
1836
2169
|
function random(count, minX, minY, maxX, maxY, seed = 1) {
|
|
1837
|
-
const rng =
|
|
2170
|
+
const rng = mulberry324(seed >>> 0);
|
|
1838
2171
|
const out = [];
|
|
1839
2172
|
for (let i = 0; i < count; i++) {
|
|
1840
2173
|
out.push({
|
|
@@ -1879,7 +2212,7 @@ function ring(count, center, radius) {
|
|
|
1879
2212
|
}
|
|
1880
2213
|
return out;
|
|
1881
2214
|
}
|
|
1882
|
-
function
|
|
2215
|
+
function mulberry324(a) {
|
|
1883
2216
|
return function() {
|
|
1884
2217
|
let t = a += 1831565813;
|
|
1885
2218
|
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
@@ -1888,7 +2221,7 @@ function mulberry323(a) {
|
|
|
1888
2221
|
};
|
|
1889
2222
|
}
|
|
1890
2223
|
function poisson(minDistance, minX, minY, maxX, maxY, seed = 1, maxAttempts = 30) {
|
|
1891
|
-
const rng =
|
|
2224
|
+
const rng = mulberry324(seed >>> 0);
|
|
1892
2225
|
const r = Math.max(minDistance, 1e-9);
|
|
1893
2226
|
const cell = r / Math.SQRT2;
|
|
1894
2227
|
const w = maxX - minX;
|
|
@@ -1959,13 +2292,87 @@ function prunePointsWithinDistance(points, minDistance) {
|
|
|
1959
2292
|
}
|
|
1960
2293
|
return out;
|
|
1961
2294
|
}
|
|
2295
|
+
function findShortestTour(points) {
|
|
2296
|
+
if (points.length === 0) return polygon([]);
|
|
2297
|
+
if (points.length === 1) return polygon([{ ...points[0] }]);
|
|
2298
|
+
if (points.length === 2) {
|
|
2299
|
+
return polyline([
|
|
2300
|
+
{ ...points[0] },
|
|
2301
|
+
{ ...points[1] },
|
|
2302
|
+
{ ...points[0] }
|
|
2303
|
+
]);
|
|
2304
|
+
}
|
|
2305
|
+
const tour = nearestNeighborTour(points);
|
|
2306
|
+
twoOpt(tour);
|
|
2307
|
+
return polygon(tour);
|
|
2308
|
+
}
|
|
2309
|
+
function nearestNeighborTour(points) {
|
|
2310
|
+
const remaining = points.map((p) => ({ ...p }));
|
|
2311
|
+
let startI = 0;
|
|
2312
|
+
for (let i = 1; i < remaining.length; i++) {
|
|
2313
|
+
const a = remaining[i];
|
|
2314
|
+
const b = remaining[startI];
|
|
2315
|
+
if (a.x < b.x || a.x === b.x && a.y < b.y) startI = i;
|
|
2316
|
+
}
|
|
2317
|
+
const ordered = [remaining.splice(startI, 1)[0]];
|
|
2318
|
+
while (remaining.length) {
|
|
2319
|
+
const last = ordered[ordered.length - 1];
|
|
2320
|
+
let bestI = 0;
|
|
2321
|
+
let bestD = Infinity;
|
|
2322
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
2323
|
+
const d = Math.hypot(
|
|
2324
|
+
remaining[i].x - last.x,
|
|
2325
|
+
remaining[i].y - last.y
|
|
2326
|
+
);
|
|
2327
|
+
if (d < bestD) {
|
|
2328
|
+
bestD = d;
|
|
2329
|
+
bestI = i;
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
ordered.push(remaining.splice(bestI, 1)[0]);
|
|
2333
|
+
}
|
|
2334
|
+
return ordered;
|
|
2335
|
+
}
|
|
2336
|
+
function twoOpt(tour) {
|
|
2337
|
+
const n = tour.length;
|
|
2338
|
+
if (n < 4) return;
|
|
2339
|
+
let improved = true;
|
|
2340
|
+
while (improved) {
|
|
2341
|
+
improved = false;
|
|
2342
|
+
for (let i = 0; i < n - 1; i++) {
|
|
2343
|
+
for (let j = i + 2; j < n; j++) {
|
|
2344
|
+
if (i === 0 && j === n - 1) continue;
|
|
2345
|
+
const a = tour[i];
|
|
2346
|
+
const b = tour[(i + 1) % n];
|
|
2347
|
+
const c = tour[j];
|
|
2348
|
+
const d = tour[(j + 1) % n];
|
|
2349
|
+
const before = Math.hypot(a.x - b.x, a.y - b.y) + Math.hypot(c.x - d.x, c.y - d.y);
|
|
2350
|
+
const after = Math.hypot(a.x - c.x, a.y - c.y) + Math.hypot(b.x - d.x, b.y - d.y);
|
|
2351
|
+
if (after + 1e-12 < before) {
|
|
2352
|
+
let lo = i + 1;
|
|
2353
|
+
let hi = j;
|
|
2354
|
+
while (lo < hi) {
|
|
2355
|
+
const tmp = tour[lo];
|
|
2356
|
+
tour[lo] = tour[hi];
|
|
2357
|
+
tour[hi] = tmp;
|
|
2358
|
+
lo++;
|
|
2359
|
+
hi--;
|
|
2360
|
+
}
|
|
2361
|
+
improved = true;
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
1962
2367
|
var pointSet = {
|
|
1963
2368
|
random,
|
|
1964
2369
|
squareGrid,
|
|
1965
2370
|
hexGrid,
|
|
1966
2371
|
ring,
|
|
1967
2372
|
poisson,
|
|
1968
|
-
prunePointsWithinDistance
|
|
2373
|
+
prunePointsWithinDistance,
|
|
2374
|
+
hilbertSort,
|
|
2375
|
+
findShortestTour
|
|
1969
2376
|
};
|
|
1970
2377
|
|
|
1971
2378
|
// src/triangulation/index.ts
|
|
@@ -2121,9 +2528,9 @@ function longestEdgeMidpoint(a, b, c) {
|
|
|
2121
2528
|
let best = edges[0];
|
|
2122
2529
|
let bestLen = -1;
|
|
2123
2530
|
for (const e of edges) {
|
|
2124
|
-
const
|
|
2125
|
-
if (
|
|
2126
|
-
bestLen =
|
|
2531
|
+
const len2 = Math.hypot(e[0].x - e[1].x, e[0].y - e[1].y);
|
|
2532
|
+
if (len2 > bestLen) {
|
|
2533
|
+
bestLen = len2;
|
|
2127
2534
|
best = e;
|
|
2128
2535
|
}
|
|
2129
2536
|
}
|
|
@@ -2138,6 +2545,205 @@ var triangulation = {
|
|
|
2138
2545
|
refine
|
|
2139
2546
|
};
|
|
2140
2547
|
|
|
2548
|
+
// src/contour/dissolve.ts
|
|
2549
|
+
function key(v, digits = 9) {
|
|
2550
|
+
return `${v.x.toFixed(digits)},${v.y.toFixed(digits)}`;
|
|
2551
|
+
}
|
|
2552
|
+
function same(a, b, eps = 1e-9) {
|
|
2553
|
+
return Math.abs(a.x - b.x) <= eps && Math.abs(a.y - b.y) <= eps;
|
|
2554
|
+
}
|
|
2555
|
+
function dissolveSegments(segments) {
|
|
2556
|
+
const segs = segments.filter((s) => !same(s.a, s.b));
|
|
2557
|
+
if (segs.length === 0) return group([]);
|
|
2558
|
+
const adj = /* @__PURE__ */ new Map();
|
|
2559
|
+
const pts = /* @__PURE__ */ new Map();
|
|
2560
|
+
const add = (a, b, segIdx) => {
|
|
2561
|
+
const ka = key(a);
|
|
2562
|
+
const kb = key(b);
|
|
2563
|
+
pts.set(ka, a);
|
|
2564
|
+
pts.set(kb, b);
|
|
2565
|
+
if (!adj.has(ka)) adj.set(ka, []);
|
|
2566
|
+
if (!adj.has(kb)) adj.set(kb, []);
|
|
2567
|
+
adj.get(ka).push({ segIdx, other: kb });
|
|
2568
|
+
adj.get(kb).push({ segIdx, other: ka });
|
|
2569
|
+
};
|
|
2570
|
+
for (let i = 0; i < segs.length; i++) {
|
|
2571
|
+
add(segs[i].a, segs[i].b, i);
|
|
2572
|
+
}
|
|
2573
|
+
const used = /* @__PURE__ */ new Set();
|
|
2574
|
+
const paths = [];
|
|
2575
|
+
const unusedDegree = (k) => (adj.get(k) ?? []).filter((e) => !used.has(e.segIdx)).length;
|
|
2576
|
+
const nextUnused = (k) => (adj.get(k) ?? []).find((e) => !used.has(e.segIdx));
|
|
2577
|
+
const starts = [...adj.keys()].sort(
|
|
2578
|
+
(a, b) => unusedDegree(a) - unusedDegree(b)
|
|
2579
|
+
);
|
|
2580
|
+
for (const start of starts) {
|
|
2581
|
+
let link = nextUnused(start);
|
|
2582
|
+
while (link) {
|
|
2583
|
+
const chain = [pts.get(start)];
|
|
2584
|
+
used.add(link.segIdx);
|
|
2585
|
+
let next = link.other;
|
|
2586
|
+
chain.push(pts.get(next));
|
|
2587
|
+
while (unusedDegree(next) === 1) {
|
|
2588
|
+
const e = nextUnused(next);
|
|
2589
|
+
if (!e) break;
|
|
2590
|
+
used.add(e.segIdx);
|
|
2591
|
+
next = e.other;
|
|
2592
|
+
chain.push(pts.get(next));
|
|
2593
|
+
if (next === start) break;
|
|
2594
|
+
}
|
|
2595
|
+
if (chain.length >= 2) paths.push(polyline(chain));
|
|
2596
|
+
link = nextUnused(start);
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
for (let i = 0; i < segs.length; i++) {
|
|
2600
|
+
if (used.has(i)) continue;
|
|
2601
|
+
const s = segs[i];
|
|
2602
|
+
paths.push(polyline([s.a, s.b]));
|
|
2603
|
+
}
|
|
2604
|
+
return group(paths);
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
// src/contour/chordalAxis.ts
|
|
2608
|
+
function mid(a, b) {
|
|
2609
|
+
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
2610
|
+
}
|
|
2611
|
+
function len(a, b) {
|
|
2612
|
+
return Math.hypot(b.x - a.x, b.y - a.y);
|
|
2613
|
+
}
|
|
2614
|
+
function edgeKey(a, b) {
|
|
2615
|
+
const ka = `${a.x.toFixed(8)},${a.y.toFixed(8)}`;
|
|
2616
|
+
const kb = `${b.x.toFixed(8)},${b.y.toFixed(8)}`;
|
|
2617
|
+
return ka < kb ? `${ka}|${kb}` : `${kb}|${ka}`;
|
|
2618
|
+
}
|
|
2619
|
+
function chordalAxis(shape) {
|
|
2620
|
+
if (!shape.closed || shape.rings.length === 0) return dissolveSegments([]);
|
|
2621
|
+
const b = boundsSpan(shape);
|
|
2622
|
+
const densified = densify(shape, Math.max(b / 40, 1e-3));
|
|
2623
|
+
const tris = delaunayTriangulation(densified);
|
|
2624
|
+
if (tris.length === 0) return dissolveSegments([]);
|
|
2625
|
+
const edgeCount = /* @__PURE__ */ new Map();
|
|
2626
|
+
const triEdges = [];
|
|
2627
|
+
for (const t of tris) {
|
|
2628
|
+
const r = t.rings[0];
|
|
2629
|
+
if (!r || r.length < 3) continue;
|
|
2630
|
+
const a = r[0];
|
|
2631
|
+
const b0 = r[1];
|
|
2632
|
+
const c = r[2];
|
|
2633
|
+
triEdges.push([a, b0, c]);
|
|
2634
|
+
for (const [u, v] of [
|
|
2635
|
+
[a, b0],
|
|
2636
|
+
[b0, c],
|
|
2637
|
+
[c, a]
|
|
2638
|
+
]) {
|
|
2639
|
+
const k = edgeKey(u, v);
|
|
2640
|
+
edgeCount.set(k, (edgeCount.get(k) ?? 0) + 1);
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
const isBoundary = (u, v) => (edgeCount.get(edgeKey(u, v)) ?? 0) <= 1;
|
|
2644
|
+
const segs = [];
|
|
2645
|
+
for (const [a, b0, c] of triEdges) {
|
|
2646
|
+
const edges = [
|
|
2647
|
+
[a, b0],
|
|
2648
|
+
[b0, c],
|
|
2649
|
+
[c, a]
|
|
2650
|
+
];
|
|
2651
|
+
const interior = edges.filter(([u, v]) => !isBoundary(u, v));
|
|
2652
|
+
const degree = interior.length;
|
|
2653
|
+
if (degree === 1) {
|
|
2654
|
+
const [u, v] = interior[0];
|
|
2655
|
+
const cen = centroid({ rings: [[a, b0, c]]});
|
|
2656
|
+
segs.push(segment(cen, mid(u, v)));
|
|
2657
|
+
} else if (degree === 2) {
|
|
2658
|
+
const m0 = mid(interior[0][0], interior[0][1]);
|
|
2659
|
+
const m1 = mid(interior[1][0], interior[1][1]);
|
|
2660
|
+
segs.push(segment(m0, m1));
|
|
2661
|
+
} else if (degree === 3) {
|
|
2662
|
+
const ranked = edges.map(([u, v]) => ({ u, v, l: len(u, v) })).sort((x, y) => y.l - x.l);
|
|
2663
|
+
const longest = ranked[0];
|
|
2664
|
+
const shortA = ranked[1];
|
|
2665
|
+
const shortB = ranked[2];
|
|
2666
|
+
const mL = mid(longest.u, longest.v);
|
|
2667
|
+
segs.push(segment(mid(shortA.u, shortA.v), mL));
|
|
2668
|
+
segs.push(segment(mid(shortB.u, shortB.v), mL));
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
return dissolveSegments(segs);
|
|
2672
|
+
}
|
|
2673
|
+
function boundsSpan(p) {
|
|
2674
|
+
let minX = Infinity;
|
|
2675
|
+
let minY = Infinity;
|
|
2676
|
+
let maxX = -Infinity;
|
|
2677
|
+
let maxY = -Infinity;
|
|
2678
|
+
for (const ring2 of p.rings) {
|
|
2679
|
+
for (const v of ring2) {
|
|
2680
|
+
minX = Math.min(minX, v.x);
|
|
2681
|
+
minY = Math.min(minY, v.y);
|
|
2682
|
+
maxX = Math.max(maxX, v.x);
|
|
2683
|
+
maxY = Math.max(maxY, v.y);
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
return Math.max(maxX - minX, maxY - minY, 1);
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
// src/hull/index.ts
|
|
2690
|
+
function convexHull(points) {
|
|
2691
|
+
const pts = uniquePoints(points);
|
|
2692
|
+
if (pts.length < 3) return polygon(pts);
|
|
2693
|
+
pts.sort((a, b) => a.x === b.x ? a.y - b.y : a.x - b.x);
|
|
2694
|
+
const lower = [];
|
|
2695
|
+
for (const p of pts) {
|
|
2696
|
+
while (lower.length >= 2 && cross2(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) {
|
|
2697
|
+
lower.pop();
|
|
2698
|
+
}
|
|
2699
|
+
lower.push(p);
|
|
2700
|
+
}
|
|
2701
|
+
const upper = [];
|
|
2702
|
+
for (let i = pts.length - 1; i >= 0; i--) {
|
|
2703
|
+
const p = pts[i];
|
|
2704
|
+
while (upper.length >= 2 && cross2(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) {
|
|
2705
|
+
upper.pop();
|
|
2706
|
+
}
|
|
2707
|
+
upper.push(p);
|
|
2708
|
+
}
|
|
2709
|
+
lower.pop();
|
|
2710
|
+
upper.pop();
|
|
2711
|
+
return polygon([...lower, ...upper]);
|
|
2712
|
+
}
|
|
2713
|
+
function convexHullPath(p) {
|
|
2714
|
+
const pts = [];
|
|
2715
|
+
for (const ring2 of p.rings) pts.push(...ring2);
|
|
2716
|
+
return convexHull(pts);
|
|
2717
|
+
}
|
|
2718
|
+
function boundingBox(p) {
|
|
2719
|
+
const b = bounds(p);
|
|
2720
|
+
return polygon([
|
|
2721
|
+
{ x: b.minX, y: b.minY },
|
|
2722
|
+
{ x: b.maxX, y: b.minY },
|
|
2723
|
+
{ x: b.maxX, y: b.maxY },
|
|
2724
|
+
{ x: b.minX, y: b.maxY }
|
|
2725
|
+
]);
|
|
2726
|
+
}
|
|
2727
|
+
function cross2(o, a, b) {
|
|
2728
|
+
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
|
|
2729
|
+
}
|
|
2730
|
+
function uniquePoints(points) {
|
|
2731
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2732
|
+
const out = [];
|
|
2733
|
+
for (const p of points) {
|
|
2734
|
+
const key4 = `${p.x},${p.y}`;
|
|
2735
|
+
if (seen.has(key4)) continue;
|
|
2736
|
+
seen.add(key4);
|
|
2737
|
+
out.push({ x: p.x, y: p.y });
|
|
2738
|
+
}
|
|
2739
|
+
return out;
|
|
2740
|
+
}
|
|
2741
|
+
var hull = {
|
|
2742
|
+
convexHull,
|
|
2743
|
+
convexHullPath,
|
|
2744
|
+
boundingBox
|
|
2745
|
+
};
|
|
2746
|
+
|
|
2141
2747
|
// src/optimisation/index.ts
|
|
2142
2748
|
function envelope(p) {
|
|
2143
2749
|
return boundingBox(p);
|
|
@@ -2170,12 +2776,12 @@ function maximumInscribedAARectangle(p) {
|
|
|
2170
2776
|
let hi = Math.min(w, h) / 2;
|
|
2171
2777
|
let best = hi;
|
|
2172
2778
|
for (let i = 0; i < 40; i++) {
|
|
2173
|
-
const
|
|
2174
|
-
if (fits(
|
|
2175
|
-
best =
|
|
2176
|
-
hi =
|
|
2779
|
+
const mid5 = (lo + hi) / 2;
|
|
2780
|
+
if (fits(mid5)) {
|
|
2781
|
+
best = mid5;
|
|
2782
|
+
hi = mid5;
|
|
2177
2783
|
} else {
|
|
2178
|
-
lo =
|
|
2784
|
+
lo = mid5;
|
|
2179
2785
|
}
|
|
2180
2786
|
}
|
|
2181
2787
|
const m = best;
|
|
@@ -2320,399 +2926,649 @@ var optimisation = {
|
|
|
2320
2926
|
farthestPointPair,
|
|
2321
2927
|
minimumBoundingCircle
|
|
2322
2928
|
};
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
]
|
|
2334
|
-
|
|
2929
|
+
|
|
2930
|
+
// src/contour/boundaryDistance.ts
|
|
2931
|
+
function distanceToBoundary2(path3, p) {
|
|
2932
|
+
let best = Infinity;
|
|
2933
|
+
for (const ring2 of path3.rings) {
|
|
2934
|
+
if (ring2.length < 2) continue;
|
|
2935
|
+
const n = ring2.length;
|
|
2936
|
+
const closed = path3.closed;
|
|
2937
|
+
const limit = closed ? n : n - 1;
|
|
2938
|
+
for (let i = 0; i < limit; i++) {
|
|
2939
|
+
const a = ring2[i];
|
|
2940
|
+
const b = ring2[(i + 1) % n];
|
|
2941
|
+
best = Math.min(best, distPointSeg(p, a, b));
|
|
2942
|
+
}
|
|
2335
2943
|
}
|
|
2336
|
-
|
|
2944
|
+
return best;
|
|
2945
|
+
}
|
|
2946
|
+
function sampleBoundary(path3, maxSegLen) {
|
|
2947
|
+
const pts = [];
|
|
2948
|
+
const lim = Math.max(maxSegLen, 1e-9);
|
|
2949
|
+
for (const ring2 of path3.rings) {
|
|
2950
|
+
if (ring2.length < 2) continue;
|
|
2951
|
+
const n = ring2.length;
|
|
2952
|
+
const limit = path3.closed ? n : n - 1;
|
|
2953
|
+
for (let i = 0; i < limit; i++) {
|
|
2954
|
+
const a = ring2[i];
|
|
2955
|
+
const b = ring2[(i + 1) % n];
|
|
2956
|
+
const len2 = Math.hypot(b.x - a.x, b.y - a.y);
|
|
2957
|
+
const steps = Math.max(1, Math.ceil(len2 / lim));
|
|
2958
|
+
for (let s = 0; s < steps; s++) {
|
|
2959
|
+
const t = s / steps;
|
|
2960
|
+
pts.push({ x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t });
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
return pts;
|
|
2965
|
+
}
|
|
2966
|
+
function distPointSeg(p, a, b) {
|
|
2967
|
+
const dx = b.x - a.x;
|
|
2968
|
+
const dy = b.y - a.y;
|
|
2969
|
+
const len2 = dx * dx + dy * dy;
|
|
2970
|
+
if (len2 < 1e-18) return Math.hypot(p.x - a.x, p.y - a.y);
|
|
2971
|
+
let t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2;
|
|
2972
|
+
t = Math.max(0, Math.min(1, t));
|
|
2973
|
+
return Math.hypot(p.x - (a.x + t * dx), p.y - (a.y + t * dy));
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
// src/contour/medialAxis.ts
|
|
2977
|
+
function medialAxis(shape, axialThreshold = 0, distanceThreshold = 0, areaThreshold = 0) {
|
|
2978
|
+
const g = buildMedialAxisGraph(shape);
|
|
2979
|
+
if (!g) return dissolveSegments([]);
|
|
2980
|
+
const kept = pruneEdges(g, axialThreshold, distanceThreshold, areaThreshold);
|
|
2981
|
+
const segs = kept.map((e) => {
|
|
2982
|
+
const a = g.nodes[e.head].position;
|
|
2983
|
+
const b = g.nodes[e.tail].position;
|
|
2984
|
+
return segment(a, b);
|
|
2985
|
+
});
|
|
2986
|
+
return dissolveSegments(segs);
|
|
2987
|
+
}
|
|
2988
|
+
function buildMedialAxisGraph(shape) {
|
|
2989
|
+
if (!shape.closed || shape.rings.length === 0) return null;
|
|
2990
|
+
const b = bounds(shape);
|
|
2991
|
+
const span = Math.max(b.maxX - b.minX, b.maxY - b.minY, 1);
|
|
2992
|
+
const densified = densify(shape, Math.max(span / 60, 1e-3));
|
|
2993
|
+
const samples = sampleBoundary(densified, Math.max(span / 80, 1e-3));
|
|
2994
|
+
if (samples.length < 3) return null;
|
|
2995
|
+
const uniq = [];
|
|
2996
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2997
|
+
for (const p of samples) {
|
|
2998
|
+
const k = `${p.x.toFixed(6)},${p.y.toFixed(6)}`;
|
|
2999
|
+
if (seen.has(k)) continue;
|
|
3000
|
+
seen.add(k);
|
|
3001
|
+
uniq.push(p);
|
|
3002
|
+
}
|
|
3003
|
+
if (uniq.length < 3) return null;
|
|
2337
3004
|
const delaunay = Delaunay.from(
|
|
2338
|
-
|
|
3005
|
+
uniq,
|
|
2339
3006
|
(d) => d.x,
|
|
2340
3007
|
(d) => d.y
|
|
2341
3008
|
);
|
|
2342
|
-
const
|
|
2343
|
-
const
|
|
2344
|
-
|
|
3009
|
+
const pad = span * 0.5;
|
|
3010
|
+
const voronoi2 = delaunay.voronoi([
|
|
3011
|
+
b.minX - pad,
|
|
3012
|
+
b.minY - pad,
|
|
3013
|
+
b.maxX + pad,
|
|
3014
|
+
b.maxY + pad
|
|
3015
|
+
]);
|
|
3016
|
+
const raw = [];
|
|
3017
|
+
const edgeSeen = /* @__PURE__ */ new Set();
|
|
3018
|
+
for (let i = 0; i < uniq.length; i++) {
|
|
2345
3019
|
const poly = voronoi2.cellPolygon(i);
|
|
2346
|
-
if (!poly || poly.length <
|
|
2347
|
-
|
|
2348
|
-
|
|
3020
|
+
if (!poly || poly.length < 2) continue;
|
|
3021
|
+
for (let j = 0; j < poly.length - 1; j++) {
|
|
3022
|
+
const p0 = poly[j];
|
|
3023
|
+
const p1 = poly[j + 1];
|
|
3024
|
+
const a = { x: p0[0], y: p0[1] };
|
|
3025
|
+
const bb = { x: p1[0], y: p1[1] };
|
|
3026
|
+
if (!containsPoint(shape, a) || !containsPoint(shape, bb)) continue;
|
|
3027
|
+
const ek = edgeKey2(a, bb);
|
|
3028
|
+
if (edgeSeen.has(ek)) continue;
|
|
3029
|
+
edgeSeen.add(ek);
|
|
3030
|
+
const mid5 = { x: (a.x + bb.x) / 2, y: (a.y + bb.y) / 2 };
|
|
3031
|
+
const site = uniq[i];
|
|
3032
|
+
const radius = Math.hypot(mid5.x - site.x, mid5.y - site.y);
|
|
3033
|
+
raw.push({ a, b: bb, radius });
|
|
3034
|
+
}
|
|
2349
3035
|
}
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
const
|
|
2354
|
-
|
|
3036
|
+
if (raw.length === 0) return null;
|
|
3037
|
+
const nodeIndex = /* @__PURE__ */ new Map();
|
|
3038
|
+
const nodes = [];
|
|
3039
|
+
const ensure = (p, radius) => {
|
|
3040
|
+
const k = key2(p);
|
|
3041
|
+
let id = nodeIndex.get(k);
|
|
3042
|
+
if (id !== void 0) {
|
|
3043
|
+
nodes[id].radius = Math.max(nodes[id].radius, radius);
|
|
3044
|
+
return id;
|
|
3045
|
+
}
|
|
3046
|
+
id = nodes.length;
|
|
3047
|
+
nodeIndex.set(k, id);
|
|
3048
|
+
nodes.push({
|
|
3049
|
+
id,
|
|
3050
|
+
position: p,
|
|
3051
|
+
radius,
|
|
3052
|
+
parent: null,
|
|
3053
|
+
children: [],
|
|
3054
|
+
rootDist: 0,
|
|
3055
|
+
featureArea: Math.PI * radius * radius
|
|
3056
|
+
});
|
|
3057
|
+
return id;
|
|
2355
3058
|
};
|
|
2356
|
-
const
|
|
2357
|
-
const
|
|
2358
|
-
|
|
2359
|
-
|
|
3059
|
+
const undirected = [];
|
|
3060
|
+
for (const e of raw) {
|
|
3061
|
+
const midR = e.radius;
|
|
3062
|
+
const u = ensure(e.a, midR);
|
|
3063
|
+
const v = ensure(e.b, midR);
|
|
3064
|
+
if (u === v) continue;
|
|
3065
|
+
const length = Math.hypot(
|
|
3066
|
+
nodes[u].position.x - nodes[v].position.x,
|
|
3067
|
+
nodes[u].position.y - nodes[v].position.y
|
|
3068
|
+
);
|
|
3069
|
+
const axial = length > 1e-12 ? Math.abs(nodes[u].radius - nodes[v].radius) / length : 0;
|
|
3070
|
+
undirected.push({ u, v, axial, length });
|
|
2360
3071
|
}
|
|
2361
|
-
|
|
2362
|
-
}
|
|
2363
|
-
|
|
2364
|
-
|
|
3072
|
+
const mic = maximumInscribedCircle(shape, Math.max(span * 0.01, 0.1));
|
|
3073
|
+
const rootPos = mic ? { x: mic.x, y: mic.y } : {
|
|
3074
|
+
x: (b.minX + b.maxX) / 2,
|
|
3075
|
+
y: (b.minY + b.maxY) / 2
|
|
3076
|
+
};
|
|
3077
|
+
let root = 0;
|
|
3078
|
+
let bestD = Infinity;
|
|
3079
|
+
for (const n of nodes) {
|
|
3080
|
+
const d = Math.hypot(n.position.x - rootPos.x, n.position.y - rootPos.y);
|
|
3081
|
+
if (d < bestD) {
|
|
3082
|
+
bestD = d;
|
|
3083
|
+
root = n.id;
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
const adj = /* @__PURE__ */ new Map();
|
|
3087
|
+
for (const e of undirected) {
|
|
3088
|
+
if (!adj.has(e.u)) adj.set(e.u, []);
|
|
3089
|
+
if (!adj.has(e.v)) adj.set(e.v, []);
|
|
3090
|
+
adj.get(e.u).push({ to: e.v, axial: e.axial, length: e.length });
|
|
3091
|
+
adj.get(e.v).push({ to: e.u, axial: e.axial, length: e.length });
|
|
3092
|
+
}
|
|
3093
|
+
const edges = [];
|
|
3094
|
+
const visited = /* @__PURE__ */ new Set([root]);
|
|
3095
|
+
const queue = [root];
|
|
3096
|
+
nodes[root].rootDist = 0;
|
|
3097
|
+
while (queue.length) {
|
|
3098
|
+
const u = queue.shift();
|
|
3099
|
+
for (const link of adj.get(u) ?? []) {
|
|
3100
|
+
if (visited.has(link.to)) continue;
|
|
3101
|
+
visited.add(link.to);
|
|
3102
|
+
nodes[link.to].parent = u;
|
|
3103
|
+
nodes[u].children.push(link.to);
|
|
3104
|
+
nodes[link.to].rootDist = nodes[u].rootDist + link.length;
|
|
3105
|
+
edges.push({
|
|
3106
|
+
head: u,
|
|
3107
|
+
tail: link.to,
|
|
3108
|
+
axial: link.axial,
|
|
3109
|
+
length: link.length
|
|
3110
|
+
});
|
|
3111
|
+
queue.push(link.to);
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
const post = [];
|
|
3115
|
+
const stack = [root];
|
|
3116
|
+
const seenN = /* @__PURE__ */ new Set();
|
|
3117
|
+
while (stack.length) {
|
|
3118
|
+
const u = stack.pop();
|
|
3119
|
+
if (seenN.has(u)) {
|
|
3120
|
+
post.push(u);
|
|
3121
|
+
continue;
|
|
3122
|
+
}
|
|
3123
|
+
seenN.add(u);
|
|
3124
|
+
stack.push(u);
|
|
3125
|
+
for (const c of nodes[u].children) stack.push(c);
|
|
3126
|
+
}
|
|
3127
|
+
for (const u of post) {
|
|
3128
|
+
let area2 = nodes[u].featureArea;
|
|
3129
|
+
for (const c of nodes[u].children) area2 += nodes[c].featureArea;
|
|
3130
|
+
nodes[u].featureArea = area2;
|
|
3131
|
+
}
|
|
3132
|
+
return { nodes, edges, root };
|
|
3133
|
+
}
|
|
3134
|
+
function pruneEdges(g, axialT, distT, areaT) {
|
|
3135
|
+
const maxDist = Math.max(...g.nodes.map((n) => n.rootDist), 1e-9);
|
|
3136
|
+
const maxArea2 = Math.max(...g.nodes.map((n) => n.featureArea), 1e-9);
|
|
3137
|
+
const maxAxial = Math.max(...g.edges.map((e) => e.axial), 1e-9);
|
|
3138
|
+
const aT = clamp01(axialT);
|
|
3139
|
+
const dT = clamp01(distT);
|
|
3140
|
+
const arT = clamp01(areaT);
|
|
3141
|
+
return g.edges.filter((e) => {
|
|
3142
|
+
const tail = g.nodes[e.tail];
|
|
3143
|
+
if (aT > 0 && e.axial / maxAxial < aT * 0.15 && aT > 0.05) {
|
|
3144
|
+
if (tail.children.length === 0 && e.axial / maxAxial < aT) return false;
|
|
3145
|
+
}
|
|
3146
|
+
if (dT > 0) {
|
|
3147
|
+
const nd = tail.rootDist / maxDist;
|
|
3148
|
+
if (tail.children.length === 0 && nd > 1 - dT && dT > 0) ;
|
|
3149
|
+
if (tail.children.length === 0 && 1 - nd < dT * 0.5 && dT >= 0.5) {
|
|
3150
|
+
return false;
|
|
3151
|
+
}
|
|
3152
|
+
if (tail.children.length === 0 && nd * dT > 0.85) return false;
|
|
3153
|
+
}
|
|
3154
|
+
if (arT > 0) {
|
|
3155
|
+
const na = tail.featureArea / maxArea2;
|
|
3156
|
+
if (na < arT) return false;
|
|
3157
|
+
}
|
|
3158
|
+
if (aT > 0 && e.axial / maxAxial > 1 - aT * 0.5 && tail.children.length === 0) {
|
|
3159
|
+
return false;
|
|
3160
|
+
}
|
|
3161
|
+
return true;
|
|
3162
|
+
});
|
|
2365
3163
|
}
|
|
2366
|
-
function
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
let maxY = -Infinity;
|
|
2371
|
-
for (const s of sites) {
|
|
2372
|
-
minX = Math.min(minX, s.x);
|
|
2373
|
-
minY = Math.min(minY, s.y);
|
|
2374
|
-
maxX = Math.max(maxX, s.x);
|
|
2375
|
-
maxY = Math.max(maxY, s.y);
|
|
3164
|
+
function centerLine(shape, straightnessWeighting = 0.7, smoothing = 50) {
|
|
3165
|
+
const g = buildMedialAxisGraph(shape);
|
|
3166
|
+
if (!g || g.nodes.length < 2) {
|
|
3167
|
+
return polyline([]);
|
|
2376
3168
|
}
|
|
2377
|
-
const
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
3169
|
+
const root = g.nodes[g.root];
|
|
3170
|
+
const childRoots = root.children;
|
|
3171
|
+
const leavesOf = (start) => {
|
|
3172
|
+
const out = [];
|
|
3173
|
+
const stack = [start];
|
|
3174
|
+
while (stack.length) {
|
|
3175
|
+
const u = stack.pop();
|
|
3176
|
+
const n = g.nodes[u];
|
|
3177
|
+
if (n.children.length === 0) out.push(n);
|
|
3178
|
+
else stack.push(...n.children);
|
|
3179
|
+
}
|
|
3180
|
+
return out;
|
|
2383
3181
|
};
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
3182
|
+
let bestPath = [];
|
|
3183
|
+
if (childRoots.length <= 1) {
|
|
3184
|
+
bestPath = longestPathNodes(g);
|
|
3185
|
+
} else if (childRoots.length === 2) {
|
|
3186
|
+
bestPath = pathBetweenLeaves(
|
|
3187
|
+
g,
|
|
3188
|
+
leavesOf(childRoots[0]),
|
|
3189
|
+
leavesOf(childRoots[1]),
|
|
3190
|
+
root,
|
|
3191
|
+
straightnessWeighting
|
|
3192
|
+
);
|
|
3193
|
+
} else {
|
|
3194
|
+
const groups = childRoots.map(leavesOf);
|
|
3195
|
+
let bestW = -Infinity;
|
|
3196
|
+
for (let i = 0; i < groups.length; i++) {
|
|
3197
|
+
for (let j = i + 1; j < groups.length; j++) {
|
|
3198
|
+
const { path: p, weight } = bestLeafPair(
|
|
3199
|
+
g,
|
|
3200
|
+
groups[i],
|
|
3201
|
+
groups[j],
|
|
3202
|
+
root,
|
|
3203
|
+
straightnessWeighting
|
|
3204
|
+
);
|
|
3205
|
+
if (weight > bestW) {
|
|
3206
|
+
bestW = weight;
|
|
3207
|
+
bestPath = p;
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
2409
3211
|
}
|
|
2410
|
-
|
|
3212
|
+
if (bestPath.length < 2) bestPath = longestPathNodes(g);
|
|
3213
|
+
const pts = bestPath.map((id) => g.nodes[id].position);
|
|
3214
|
+
return polyline(gaussianSmooth(pts, smoothing));
|
|
3215
|
+
}
|
|
3216
|
+
function bestLeafPair(g, a, b, root, straightness) {
|
|
3217
|
+
let bestW = -Infinity;
|
|
3218
|
+
let best = [];
|
|
3219
|
+
for (const d1 of a) {
|
|
3220
|
+
for (const d2 of b) {
|
|
3221
|
+
const angle = angleBetween(d1.position, root.position, d2.position);
|
|
3222
|
+
const aw = Math.pow(1 + angle, straightness);
|
|
3223
|
+
const weight = (d1.rootDist + d2.rootDist) * Math.max(aw - 1, 1);
|
|
3224
|
+
if (weight > bestW) {
|
|
3225
|
+
bestW = weight;
|
|
3226
|
+
best = [...pathToRoot(g, d1.id), ...pathToRoot(g, d2.id).reverse().slice(1)];
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
return { path: best, weight: bestW };
|
|
2411
3231
|
}
|
|
2412
|
-
function
|
|
2413
|
-
|
|
2414
|
-
const inner = createCircle(cx, cy, Math.min(innerRadius, outerRadius), segments).rings[0];
|
|
2415
|
-
return path([outer, [...inner].reverse()], true);
|
|
3232
|
+
function pathBetweenLeaves(g, a, b, root, straightness) {
|
|
3233
|
+
return bestLeafPair(g, a, b, root, straightness).path;
|
|
2416
3234
|
}
|
|
2417
|
-
function
|
|
2418
|
-
const
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
3235
|
+
function pathToRoot(g, id) {
|
|
3236
|
+
const path3 = [id];
|
|
3237
|
+
let cur = id;
|
|
3238
|
+
while (g.nodes[cur].parent !== null) {
|
|
3239
|
+
cur = g.nodes[cur].parent;
|
|
3240
|
+
path3.push(cur);
|
|
2423
3241
|
}
|
|
2424
|
-
return
|
|
3242
|
+
return path3;
|
|
2425
3243
|
}
|
|
2426
|
-
function
|
|
2427
|
-
const
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
3244
|
+
function longestPathNodes(g) {
|
|
3245
|
+
const leaves = g.nodes.filter((n) => n.children.length === 0 && n.id !== g.root);
|
|
3246
|
+
if (leaves.length === 0) return g.nodes.map((n) => n.id);
|
|
3247
|
+
let farthest = leaves[0];
|
|
3248
|
+
let best = -1;
|
|
3249
|
+
for (const L of leaves) {
|
|
3250
|
+
if (L.rootDist > best) {
|
|
3251
|
+
best = L.rootDist;
|
|
3252
|
+
farthest = L;
|
|
3253
|
+
}
|
|
2433
3254
|
}
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
3255
|
+
let bestPath = pathToRoot(g, farthest.id);
|
|
3256
|
+
let bestLen = farthest.rootDist;
|
|
3257
|
+
for (const L of leaves) {
|
|
3258
|
+
if (L.id === farthest.id) continue;
|
|
3259
|
+
const p = [
|
|
3260
|
+
...pathToRoot(g, farthest.id),
|
|
3261
|
+
...pathToRoot(g, L.id).reverse().slice(1)
|
|
3262
|
+
];
|
|
3263
|
+
const len2 = farthest.rootDist + L.rootDist;
|
|
3264
|
+
if (len2 > bestLen) {
|
|
3265
|
+
bestLen = len2;
|
|
3266
|
+
bestPath = p;
|
|
3267
|
+
}
|
|
2442
3268
|
}
|
|
2443
|
-
return
|
|
3269
|
+
return bestPath;
|
|
2444
3270
|
}
|
|
2445
|
-
function
|
|
3271
|
+
function angleBetween(a, apex, b) {
|
|
3272
|
+
const ax = a.x - apex.x;
|
|
3273
|
+
const ay = a.y - apex.y;
|
|
3274
|
+
const bx = b.x - apex.x;
|
|
3275
|
+
const by = b.y - apex.y;
|
|
3276
|
+
const la = Math.hypot(ax, ay) || 1;
|
|
3277
|
+
const lb = Math.hypot(bx, by) || 1;
|
|
3278
|
+
const cos = Math.max(-1, Math.min(1, (ax * bx + ay * by) / (la * lb)));
|
|
3279
|
+
return Math.acos(cos);
|
|
3280
|
+
}
|
|
3281
|
+
function gaussianSmooth(pts, sigma) {
|
|
3282
|
+
if (pts.length < 3 || sigma <= 0) return pts;
|
|
3283
|
+
const radius = Math.max(1, Math.round(Math.min(pts.length / 4, sigma / 10)));
|
|
2446
3284
|
const out = [];
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
3285
|
+
for (let i = 0; i < pts.length; i++) {
|
|
3286
|
+
let sx = 0;
|
|
3287
|
+
let sy = 0;
|
|
3288
|
+
let w = 0;
|
|
3289
|
+
for (let k = -radius; k <= radius; k++) {
|
|
3290
|
+
const j = Math.max(0, Math.min(pts.length - 1, i + k));
|
|
3291
|
+
const wk = Math.exp(-(k * k) / (2 * (radius * 0.5) ** 2 + 1e-9));
|
|
3292
|
+
sx += pts[j].x * wk;
|
|
3293
|
+
sy += pts[j].y * wk;
|
|
3294
|
+
w += wk;
|
|
3295
|
+
}
|
|
3296
|
+
out.push({ x: sx / w, y: sy / w });
|
|
2458
3297
|
}
|
|
3298
|
+
out[0] = pts[0];
|
|
3299
|
+
out[out.length - 1] = pts[pts.length - 1];
|
|
2459
3300
|
return out;
|
|
2460
3301
|
}
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
3302
|
+
function key2(p) {
|
|
3303
|
+
return `${p.x.toFixed(7)},${p.y.toFixed(7)}`;
|
|
3304
|
+
}
|
|
3305
|
+
function edgeKey2(a, b) {
|
|
3306
|
+
const ka = key2(a);
|
|
3307
|
+
const kb = key2(b);
|
|
3308
|
+
return ka < kb ? `${ka}|${kb}` : `${kb}|${ka}`;
|
|
3309
|
+
}
|
|
3310
|
+
function clamp01(v) {
|
|
3311
|
+
return Math.max(0, Math.min(1, v));
|
|
3312
|
+
}
|
|
2470
3313
|
|
|
2471
|
-
// src/
|
|
2472
|
-
function
|
|
2473
|
-
const
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
3314
|
+
// src/contour/straightSkeleton.ts
|
|
3315
|
+
function straightSkeleton(shape) {
|
|
3316
|
+
const parts = straightSkeletonParts(shape);
|
|
3317
|
+
return group([
|
|
3318
|
+
...parts.faces.paths,
|
|
3319
|
+
...parts.branches.paths,
|
|
3320
|
+
...parts.bones.paths
|
|
3321
|
+
]);
|
|
3322
|
+
}
|
|
3323
|
+
function straightSkeletonParts(shape) {
|
|
3324
|
+
const empty = {
|
|
3325
|
+
faces: group([]),
|
|
3326
|
+
branches: group([]),
|
|
3327
|
+
bones: group([])
|
|
3328
|
+
};
|
|
3329
|
+
if (!shape.closed || !shape.rings[0] || shape.rings[0].length < 3) {
|
|
3330
|
+
return empty;
|
|
3331
|
+
}
|
|
3332
|
+
const b = bounds(shape);
|
|
3333
|
+
const span = Math.max(b.maxX - b.minX, b.maxY - b.minY, 1);
|
|
3334
|
+
const delta = Math.max(span / 40, 0.5);
|
|
3335
|
+
const densified = densify(shape, Math.max(span / 40, 1e-3));
|
|
3336
|
+
const levels = [densified];
|
|
3337
|
+
let current = densified;
|
|
3338
|
+
for (let i = 0; i < 40; i++) {
|
|
3339
|
+
const next = buffer(current, -delta);
|
|
3340
|
+
if (next.paths.length === 0) break;
|
|
3341
|
+
let best = next.paths[0];
|
|
3342
|
+
let bestA = -1;
|
|
3343
|
+
for (const p of next.paths) {
|
|
3344
|
+
const bb = bounds(p);
|
|
3345
|
+
const a = (bb.maxX - bb.minX) * (bb.maxY - bb.minY);
|
|
3346
|
+
if (a > bestA) {
|
|
3347
|
+
bestA = a;
|
|
3348
|
+
best = p;
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
if (!best.rings[0] || best.rings[0].length < 3) break;
|
|
3352
|
+
levels.push(best);
|
|
3353
|
+
current = best;
|
|
3354
|
+
}
|
|
3355
|
+
const boneSegs = [];
|
|
3356
|
+
for (let i = 0; i < levels.length - 1; i++) {
|
|
3357
|
+
const a = levels[i];
|
|
3358
|
+
const bb = levels[i + 1];
|
|
3359
|
+
const ca = centroid(a);
|
|
3360
|
+
const cb = centroid(bb);
|
|
3361
|
+
if (containsPoint(shape, mid2(ca, cb))) {
|
|
3362
|
+
boneSegs.push(segment(ca, cb));
|
|
3363
|
+
}
|
|
3364
|
+
for (const ring2 of a.rings) {
|
|
3365
|
+
for (const p of ring2) {
|
|
3366
|
+
const q = nearestOnPath(bb, p);
|
|
3367
|
+
if (q && containsPoint(shape, mid2(p, q))) {
|
|
3368
|
+
boneSegs.push(segment(p, q));
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
2480
3371
|
}
|
|
2481
3372
|
}
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
3373
|
+
const branchSegs = [];
|
|
3374
|
+
const outer = densified.rings[0];
|
|
3375
|
+
const target = levels[1] ?? levels[0];
|
|
3376
|
+
for (let i = 0; i < outer.length; i++) {
|
|
3377
|
+
const a = outer[i];
|
|
3378
|
+
const bb = outer[(i + 1) % outer.length];
|
|
3379
|
+
const m = mid2(a, bb);
|
|
3380
|
+
const q = nearestOnPath(target, m);
|
|
3381
|
+
if (q) branchSegs.push(segment(m, q));
|
|
3382
|
+
}
|
|
3383
|
+
const faces = [];
|
|
3384
|
+
if (levels.length >= 2) {
|
|
3385
|
+
const inner = levels[1].rings[0] ?? [];
|
|
3386
|
+
for (let i = 0; i < outer.length; i++) {
|
|
3387
|
+
const a = outer[i];
|
|
3388
|
+
const bb = outer[(i + 1) % outer.length];
|
|
3389
|
+
const ia = nearestOnRing(inner, a);
|
|
3390
|
+
const ib = nearestOnRing(inner, bb);
|
|
3391
|
+
if (ia && ib) {
|
|
3392
|
+
const face = [a, bb, ib, ia];
|
|
3393
|
+
const c = {
|
|
3394
|
+
x: (a.x + bb.x + ib.x + ia.x) / 4,
|
|
3395
|
+
y: (a.y + bb.y + ib.y + ia.y) / 4
|
|
3396
|
+
};
|
|
3397
|
+
if (containsPoint(shape, c)) faces.push(polygon(face));
|
|
3398
|
+
}
|
|
2495
3399
|
}
|
|
2496
3400
|
}
|
|
2497
|
-
return
|
|
3401
|
+
return {
|
|
3402
|
+
faces: group(faces),
|
|
3403
|
+
branches: dissolveSegments(branchSegs),
|
|
3404
|
+
bones: dissolveSegments(boneSegs)
|
|
3405
|
+
};
|
|
2498
3406
|
}
|
|
2499
|
-
function
|
|
2500
|
-
|
|
2501
|
-
for (let i = 0; i < 6; i++) {
|
|
2502
|
-
const t = Math.PI / 180 * (60 * i - 30);
|
|
2503
|
-
ring2.push({ x: cx + Math.cos(t) * r, y: cy + Math.sin(t) * r });
|
|
2504
|
-
}
|
|
2505
|
-
return polygon(ring2);
|
|
3407
|
+
function mid2(a, b) {
|
|
3408
|
+
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
2506
3409
|
}
|
|
2507
|
-
function
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
const
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
b.minX + i * dw,
|
|
2518
|
-
b.minY + j * dh,
|
|
2519
|
-
dw,
|
|
2520
|
-
dh
|
|
2521
|
-
);
|
|
2522
|
-
paths.push(...intersect(p, cell).paths);
|
|
3410
|
+
function nearestOnPath(p, q) {
|
|
3411
|
+
let best = null;
|
|
3412
|
+
let bestD = Infinity;
|
|
3413
|
+
for (const ring2 of p.rings) {
|
|
3414
|
+
for (const v of ring2) {
|
|
3415
|
+
const d = Math.hypot(v.x - q.x, v.y - q.y);
|
|
3416
|
+
if (d < bestD) {
|
|
3417
|
+
bestD = d;
|
|
3418
|
+
best = v;
|
|
3419
|
+
}
|
|
2523
3420
|
}
|
|
2524
3421
|
}
|
|
2525
|
-
return
|
|
3422
|
+
return best;
|
|
2526
3423
|
}
|
|
2527
|
-
function
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
3424
|
+
function nearestOnRing(ring2, q) {
|
|
3425
|
+
if (ring2.length === 0) return null;
|
|
3426
|
+
let best = ring2[0];
|
|
3427
|
+
let bestD = Infinity;
|
|
3428
|
+
for (const v of ring2) {
|
|
3429
|
+
const d = Math.hypot(v.x - q.x, v.y - q.y);
|
|
3430
|
+
if (d < bestD) {
|
|
3431
|
+
bestD = d;
|
|
3432
|
+
best = v;
|
|
2534
3433
|
}
|
|
2535
|
-
cur = next;
|
|
2536
3434
|
}
|
|
2537
|
-
return
|
|
2538
|
-
}
|
|
2539
|
-
function subdivideQuad(p) {
|
|
2540
|
-
const ring2 = p.rings[0];
|
|
2541
|
-
if (!ring2 || ring2.length < 4) return [p];
|
|
2542
|
-
const [a, b, c, d] = ring2;
|
|
2543
|
-
if (!a || !b || !c || !d) return [p];
|
|
2544
|
-
const mab = mid(a, b);
|
|
2545
|
-
const mbc = mid(b, c);
|
|
2546
|
-
const mcd = mid(c, d);
|
|
2547
|
-
const mda = mid(d, a);
|
|
2548
|
-
const cen = mid(mid(a, c), mid(b, d));
|
|
2549
|
-
return [
|
|
2550
|
-
polygon([a, mab, cen, mda]),
|
|
2551
|
-
polygon([mab, b, mbc, cen]),
|
|
2552
|
-
polygon([cen, mbc, c, mcd]),
|
|
2553
|
-
polygon([mda, cen, mcd, d])
|
|
2554
|
-
];
|
|
3435
|
+
return best;
|
|
2555
3436
|
}
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
3437
|
+
|
|
3438
|
+
// src/contour/marchingSquares.ts
|
|
3439
|
+
function isolinesFromFunction(bounds2, sampleSpacing, contourInterval, fn, isolineMin = Number.NaN, isolineMax = Number.NaN) {
|
|
3440
|
+
const [xmin, ymin, xmax, ymax] = bounds2;
|
|
3441
|
+
const dx = Math.max(sampleSpacing, 1e-9);
|
|
3442
|
+
const dy = dx;
|
|
3443
|
+
const nx = Math.max(2, Math.ceil((xmax - xmin) / dx) + 1);
|
|
3444
|
+
const ny = Math.max(2, Math.ceil((ymax - ymin) / dy) + 1);
|
|
3445
|
+
const grid = [];
|
|
3446
|
+
let gmin = Infinity;
|
|
3447
|
+
let gmax = -Infinity;
|
|
3448
|
+
for (let j = 0; j < ny; j++) {
|
|
3449
|
+
const row = [];
|
|
3450
|
+
const y = ymin + j * ((ymax - ymin) / (ny - 1));
|
|
3451
|
+
for (let i = 0; i < nx; i++) {
|
|
3452
|
+
const x = xmin + i * ((xmax - xmin) / (nx - 1));
|
|
3453
|
+
let v = fn(x, y);
|
|
3454
|
+
if (!Number.isFinite(v)) v = Number.NEGATIVE_INFINITY;
|
|
3455
|
+
row.push(v);
|
|
3456
|
+
if (Number.isFinite(v)) {
|
|
3457
|
+
gmin = Math.min(gmin, v);
|
|
3458
|
+
gmax = Math.max(gmax, v);
|
|
3459
|
+
}
|
|
2563
3460
|
}
|
|
2564
|
-
|
|
3461
|
+
grid.push(row);
|
|
2565
3462
|
}
|
|
2566
|
-
|
|
3463
|
+
const z0 = Number.isFinite(isolineMin) ? isolineMin : gmin;
|
|
3464
|
+
const z1 = Number.isFinite(isolineMax) ? isolineMax : gmax;
|
|
3465
|
+
if (!Number.isFinite(z0) || !Number.isFinite(z1) || contourInterval <= 0) {
|
|
3466
|
+
return group([]);
|
|
3467
|
+
}
|
|
3468
|
+
const levels = [];
|
|
3469
|
+
const start = Math.ceil(z0 / contourInterval) * contourInterval;
|
|
3470
|
+
for (let z = start; z <= z1 + 1e-12; z += contourInterval) {
|
|
3471
|
+
levels.push(z);
|
|
3472
|
+
}
|
|
3473
|
+
const segs = [];
|
|
3474
|
+
for (const level of levels) {
|
|
3475
|
+
segs.push(...marchLevel(grid, xmin, ymin, xmax, ymax, nx, ny, level));
|
|
3476
|
+
}
|
|
3477
|
+
return dissolveSegments(segs);
|
|
2567
3478
|
}
|
|
2568
|
-
function
|
|
2569
|
-
|
|
2570
|
-
if (!ring2 || ring2.length < 3) return [p];
|
|
2571
|
-
const a = ring2[0];
|
|
2572
|
-
const b = ring2[1];
|
|
2573
|
-
const c = ring2[2];
|
|
2574
|
-
const mab = mid(a, b);
|
|
2575
|
-
const mbc = mid(b, c);
|
|
2576
|
-
const mca = mid(c, a);
|
|
2577
|
-
return [
|
|
2578
|
-
polygon([a, mab, mca]),
|
|
2579
|
-
polygon([mab, b, mbc]),
|
|
2580
|
-
polygon([mca, mbc, c]),
|
|
2581
|
-
polygon([mab, mbc, mca])
|
|
2582
|
-
];
|
|
3479
|
+
function isolineZeroFromFunction(bounds2, sampleSpacing, fn) {
|
|
3480
|
+
return isolinesFromFunction(bounds2, sampleSpacing, 1, fn, 0, 0);
|
|
2583
3481
|
}
|
|
2584
|
-
function
|
|
2585
|
-
const
|
|
2586
|
-
const
|
|
2587
|
-
const
|
|
2588
|
-
const
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
const
|
|
2601
|
-
|
|
3482
|
+
function marchLevel(grid, xmin, ymin, xmax, ymax, nx, ny, level) {
|
|
3483
|
+
const segs = [];
|
|
3484
|
+
const xAt = (i) => xmin + i / (nx - 1) * (xmax - xmin);
|
|
3485
|
+
const yAt = (j) => ymin + j / (ny - 1) * (ymax - ymin);
|
|
3486
|
+
const lerp2 = (x0, y0, v0, x1, y1, v1) => {
|
|
3487
|
+
const t = (level - v0) / (v1 - v0 || 1e-15);
|
|
3488
|
+
return { x: x0 + t * (x1 - x0), y: y0 + t * (y1 - y0) };
|
|
3489
|
+
};
|
|
3490
|
+
for (let j = 0; j < ny - 1; j++) {
|
|
3491
|
+
for (let i = 0; i < nx - 1; i++) {
|
|
3492
|
+
const v00 = grid[j][i];
|
|
3493
|
+
const v10 = grid[j][i + 1];
|
|
3494
|
+
const v11 = grid[j + 1][i + 1];
|
|
3495
|
+
const v01 = grid[j + 1][i];
|
|
3496
|
+
const x0 = xAt(i);
|
|
3497
|
+
const x1 = xAt(i + 1);
|
|
3498
|
+
const y0 = yAt(j);
|
|
3499
|
+
const y1 = yAt(j + 1);
|
|
3500
|
+
let code = 0;
|
|
3501
|
+
if (v00 >= level) code |= 1;
|
|
3502
|
+
if (v10 >= level) code |= 2;
|
|
3503
|
+
if (v11 >= level) code |= 4;
|
|
3504
|
+
if (v01 >= level) code |= 8;
|
|
3505
|
+
if (code === 0 || code === 15) continue;
|
|
3506
|
+
const bottom = () => lerp2(x0, y0, v00, x1, y0, v10);
|
|
3507
|
+
const right = () => lerp2(x1, y0, v10, x1, y1, v11);
|
|
3508
|
+
const top = () => lerp2(x0, y1, v01, x1, y1, v11);
|
|
3509
|
+
const left = () => lerp2(x0, y0, v00, x0, y1, v01);
|
|
3510
|
+
const add = (a, b) => segs.push(segment(a, b));
|
|
3511
|
+
switch (code) {
|
|
3512
|
+
case 1:
|
|
3513
|
+
case 14:
|
|
3514
|
+
add(left(), bottom());
|
|
3515
|
+
break;
|
|
3516
|
+
case 2:
|
|
3517
|
+
case 13:
|
|
3518
|
+
add(bottom(), right());
|
|
3519
|
+
break;
|
|
3520
|
+
case 3:
|
|
3521
|
+
case 12:
|
|
3522
|
+
add(left(), right());
|
|
3523
|
+
break;
|
|
3524
|
+
case 4:
|
|
3525
|
+
case 11:
|
|
3526
|
+
add(right(), top());
|
|
3527
|
+
break;
|
|
3528
|
+
case 6:
|
|
3529
|
+
case 9:
|
|
3530
|
+
add(bottom(), top());
|
|
3531
|
+
break;
|
|
3532
|
+
case 7:
|
|
3533
|
+
case 8:
|
|
3534
|
+
add(left(), top());
|
|
3535
|
+
break;
|
|
3536
|
+
case 5: {
|
|
3537
|
+
const avg = (v00 + v10 + v11 + v01) / 4;
|
|
3538
|
+
if (avg >= level) {
|
|
3539
|
+
add(left(), top());
|
|
3540
|
+
add(bottom(), right());
|
|
3541
|
+
} else {
|
|
3542
|
+
add(left(), bottom());
|
|
3543
|
+
add(right(), top());
|
|
3544
|
+
}
|
|
3545
|
+
break;
|
|
3546
|
+
}
|
|
3547
|
+
case 10: {
|
|
3548
|
+
const avg = (v00 + v10 + v11 + v01) / 4;
|
|
3549
|
+
if (avg >= level) {
|
|
3550
|
+
add(left(), bottom());
|
|
3551
|
+
add(right(), top());
|
|
3552
|
+
} else {
|
|
3553
|
+
add(left(), top());
|
|
3554
|
+
add(bottom(), right());
|
|
3555
|
+
}
|
|
3556
|
+
break;
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
2602
3559
|
}
|
|
2603
|
-
parts.length = 0;
|
|
2604
|
-
parts.push(...next);
|
|
2605
3560
|
}
|
|
2606
|
-
return
|
|
2607
|
-
}
|
|
2608
|
-
function hatchSubdivision(p, spacing, angle = 0) {
|
|
2609
|
-
return sliceDivision(p, spacing, angle);
|
|
2610
|
-
}
|
|
2611
|
-
function mid(a, b) {
|
|
2612
|
-
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
3561
|
+
return segs;
|
|
2613
3562
|
}
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
hatchSubdivision
|
|
2622
|
-
};
|
|
2623
|
-
|
|
2624
|
-
// src/polygonisation/index.ts
|
|
2625
|
-
function maxArea(points) {
|
|
2626
|
-
return convexHull(points);
|
|
2627
|
-
}
|
|
2628
|
-
function minArea(points) {
|
|
2629
|
-
const hull2 = convexHull(points).rings[0] ?? [];
|
|
2630
|
-
if (hull2.length < 3) return convexHull(points);
|
|
2631
|
-
if (hull2.length === 3) return polygon(hull2);
|
|
2632
|
-
let best = null;
|
|
2633
|
-
let bestA = Infinity;
|
|
2634
|
-
const h = hull2;
|
|
2635
|
-
const n = h.length;
|
|
2636
|
-
for (let i = 0; i < n; i++) {
|
|
2637
|
-
for (let j = i + 1; j < n; j++) {
|
|
2638
|
-
for (let k = j + 1; k < n; k++) {
|
|
2639
|
-
const tri = polygon([h[i], h[j], h[k]]);
|
|
2640
|
-
if (!coversPoints(tri, points)) continue;
|
|
2641
|
-
const a = area(tri);
|
|
2642
|
-
if (a < bestA) {
|
|
2643
|
-
bestA = a;
|
|
2644
|
-
best = tri;
|
|
2645
|
-
}
|
|
2646
|
-
}
|
|
2647
|
-
}
|
|
2648
|
-
}
|
|
2649
|
-
return best ?? convexHull(points);
|
|
2650
|
-
}
|
|
2651
|
-
function minPerimeter(points) {
|
|
2652
|
-
if (points.length === 0) return polygon([]);
|
|
2653
|
-
if (points.length < 3) return polygon(points);
|
|
2654
|
-
const remaining = points.map((p) => ({ ...p }));
|
|
2655
|
-
const ordered = [remaining.shift()];
|
|
2656
|
-
while (remaining.length) {
|
|
2657
|
-
const last = ordered[ordered.length - 1];
|
|
2658
|
-
let bestI = 0;
|
|
2659
|
-
let bestD = Infinity;
|
|
2660
|
-
for (let i = 0; i < remaining.length; i++) {
|
|
2661
|
-
const d = Math.hypot(
|
|
2662
|
-
remaining[i].x - last.x,
|
|
2663
|
-
remaining[i].y - last.y
|
|
2664
|
-
);
|
|
2665
|
-
if (d < bestD) {
|
|
2666
|
-
bestD = d;
|
|
2667
|
-
bestI = i;
|
|
2668
|
-
}
|
|
2669
|
-
}
|
|
2670
|
-
ordered.push(remaining.splice(bestI, 1)[0]);
|
|
2671
|
-
}
|
|
2672
|
-
return polygon(ordered);
|
|
2673
|
-
}
|
|
2674
|
-
function horizontal(points) {
|
|
2675
|
-
const pts = [...points].sort(
|
|
2676
|
-
(a, b) => a.y === b.y ? a.x - b.x : a.y - b.y
|
|
2677
|
-
);
|
|
2678
|
-
return polygon(pts);
|
|
2679
|
-
}
|
|
2680
|
-
function vertical(points) {
|
|
2681
|
-
const pts = [...points].sort(
|
|
2682
|
-
(a, b) => a.x === b.x ? a.y - b.y : a.x - b.x
|
|
2683
|
-
);
|
|
2684
|
-
return polygon(pts);
|
|
2685
|
-
}
|
|
2686
|
-
function angular(points) {
|
|
2687
|
-
if (points.length === 0) return polygon([]);
|
|
2688
|
-
const c = centroidOf(points);
|
|
2689
|
-
const pts = [...points].sort(
|
|
2690
|
-
(a, b) => Math.atan2(a.y - c.y, a.x - c.x) - Math.atan2(b.y - c.y, b.x - c.x)
|
|
2691
|
-
);
|
|
2692
|
-
return polygon(pts);
|
|
2693
|
-
}
|
|
2694
|
-
function circular(points) {
|
|
2695
|
-
return angular(points);
|
|
2696
|
-
}
|
|
2697
|
-
function onion(points) {
|
|
2698
|
-
const layers = onionLayers(points);
|
|
2699
|
-
return layers[0] ?? polygon([]);
|
|
2700
|
-
}
|
|
2701
|
-
function onionLayers(points) {
|
|
2702
|
-
let remaining = points.map((p) => ({ ...p }));
|
|
2703
|
-
const layers = [];
|
|
2704
|
-
while (remaining.length >= 3) {
|
|
2705
|
-
const hull2 = convexHull(remaining);
|
|
2706
|
-
const ring2 = hull2.rings[0] ?? [];
|
|
2707
|
-
if (ring2.length < 3) break;
|
|
2708
|
-
layers.push(hull2);
|
|
2709
|
-
const onHull = new Set(ring2.map((v) => `${v.x},${v.y}`));
|
|
2710
|
-
remaining = remaining.filter((p) => !onHull.has(`${p.x},${p.y}`));
|
|
3563
|
+
function isolinesFromPoints(points, values, intervals, _smoothing = 0) {
|
|
3564
|
+
if (points.length < 3 || intervals < 1) return group([]);
|
|
3565
|
+
let minZ = Infinity;
|
|
3566
|
+
let maxZ = -Infinity;
|
|
3567
|
+
for (const v of values) {
|
|
3568
|
+
minZ = Math.min(minZ, v);
|
|
3569
|
+
maxZ = Math.max(maxZ, v);
|
|
2711
3570
|
}
|
|
2712
|
-
return
|
|
2713
|
-
}
|
|
2714
|
-
function hilbert(points) {
|
|
2715
|
-
if (points.length === 0) return polygon([]);
|
|
3571
|
+
if (!Number.isFinite(minZ) || maxZ - minZ < 1e-15) return group([]);
|
|
2716
3572
|
let minX = Infinity;
|
|
2717
3573
|
let minY = Infinity;
|
|
2718
3574
|
let maxX = -Infinity;
|
|
@@ -2723,135 +3579,371 @@ function hilbert(points) {
|
|
|
2723
3579
|
maxX = Math.max(maxX, p.x);
|
|
2724
3580
|
maxY = Math.max(maxY, p.y);
|
|
2725
3581
|
}
|
|
2726
|
-
const span = Math.max(maxX - minX, maxY - minY,
|
|
2727
|
-
const
|
|
2728
|
-
const
|
|
2729
|
-
const
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
return
|
|
3582
|
+
const span = Math.max(maxX - minX, maxY - minY, 1);
|
|
3583
|
+
const sampleSpacing = span / Math.max(20, Math.sqrt(points.length));
|
|
3584
|
+
const interval = (maxZ - minZ) / intervals;
|
|
3585
|
+
const fn = (x, y) => {
|
|
3586
|
+
let num2 = 0;
|
|
3587
|
+
let den = 0;
|
|
3588
|
+
for (let i = 0; i < points.length; i++) {
|
|
3589
|
+
const p = points[i];
|
|
3590
|
+
const d2 = (x - p.x) ** 2 + (y - p.y) ** 2;
|
|
3591
|
+
const w = 1 / Math.max(d2, 1e-12);
|
|
3592
|
+
num2 += w * values[i];
|
|
3593
|
+
den += w;
|
|
3594
|
+
}
|
|
3595
|
+
return num2 / den;
|
|
3596
|
+
};
|
|
3597
|
+
return isolinesFromFunction(
|
|
3598
|
+
[minX, minY, maxX, maxY],
|
|
3599
|
+
sampleSpacing,
|
|
3600
|
+
interval,
|
|
3601
|
+
fn,
|
|
3602
|
+
minZ,
|
|
3603
|
+
maxZ
|
|
3604
|
+
);
|
|
2742
3605
|
}
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
const b =
|
|
2748
|
-
const
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
3606
|
+
|
|
3607
|
+
// src/contour/fields.ts
|
|
3608
|
+
function distanceField(shape, spacing, pole) {
|
|
3609
|
+
if (!shape.closed || shape.rings.length === 0 || spacing <= 0) return group([]);
|
|
3610
|
+
const b = bounds(shape);
|
|
3611
|
+
const span = Math.max(b.maxX - b.minX, b.maxY - b.minY, 1);
|
|
3612
|
+
let polePt = pole;
|
|
3613
|
+
if (!polePt) {
|
|
3614
|
+
const mic = maximumInscribedCircle(shape, Math.max(span * 0.01, 0.1));
|
|
3615
|
+
polePt = mic ? { x: mic.x, y: mic.y } : { x: (b.minX + b.maxX) / 2, y: (b.minY + b.maxY) / 2 };
|
|
3616
|
+
}
|
|
3617
|
+
const pad = Math.max(spacing, 1);
|
|
3618
|
+
const box = [
|
|
3619
|
+
b.minX - pad,
|
|
3620
|
+
b.minY - pad,
|
|
3621
|
+
b.maxX + pad,
|
|
3622
|
+
b.maxY + pad
|
|
3623
|
+
];
|
|
3624
|
+
const sampleSpacing = Math.max(spacing / 10, span / 80);
|
|
3625
|
+
const fn = (x, y) => {
|
|
3626
|
+
const p = { x, y };
|
|
3627
|
+
if (!insideSafe(shape, p)) return Number.NEGATIVE_INFINITY;
|
|
3628
|
+
const dGeo = distanceToBoundary2(shape, p);
|
|
3629
|
+
const dPoint = Math.hypot(x - polePt.x, y - polePt.y);
|
|
3630
|
+
return dGeo - dPoint;
|
|
3631
|
+
};
|
|
3632
|
+
const raw = isolinesFromFunction(box, sampleSpacing, spacing, fn);
|
|
3633
|
+
return clipPolylinesToPath(raw, shape);
|
|
2753
3634
|
}
|
|
2754
|
-
function
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
const
|
|
2759
|
-
const
|
|
2760
|
-
const
|
|
2761
|
-
const
|
|
2762
|
-
const
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
3635
|
+
function contrastField(shape, intervals, reference) {
|
|
3636
|
+
if (!shape.closed || shape.rings.length === 0 || intervals < 1) {
|
|
3637
|
+
return group([]);
|
|
3638
|
+
}
|
|
3639
|
+
const b = bounds(shape);
|
|
3640
|
+
const areaApprox = (b.maxX - b.minX) * (b.maxY - b.minY);
|
|
3641
|
+
const count = Math.max(100, Math.floor(areaApprox / 100));
|
|
3642
|
+
const minDist = Math.sqrt(areaApprox / (count * 1.5));
|
|
3643
|
+
const samples = poisson(
|
|
3644
|
+
Math.max(minDist, 1e-3),
|
|
3645
|
+
b.minX,
|
|
3646
|
+
b.minY,
|
|
3647
|
+
b.maxX,
|
|
3648
|
+
b.maxY,
|
|
3649
|
+
1337
|
|
3650
|
+
).filter((p) => containsPoint(shape, p));
|
|
3651
|
+
if (samples.length < 3) return group([]);
|
|
3652
|
+
const values = samples.map((p) => {
|
|
3653
|
+
const dB = distanceToBoundary2(shape, p);
|
|
3654
|
+
const dR = Math.hypot(p.x - reference.x, p.y - reference.y);
|
|
3655
|
+
return Math.abs(dB - dR);
|
|
3656
|
+
});
|
|
3657
|
+
const lines = isolinesFromPoints(samples, values, Math.max(1, intervals), 0);
|
|
3658
|
+
return clipPolylinesToPath(lines, shape);
|
|
3659
|
+
}
|
|
3660
|
+
function isolines(shape, highPoint, intervalSpacing) {
|
|
3661
|
+
if (!shape.closed || intervalSpacing <= 0) return group([]);
|
|
3662
|
+
const b = bounds(shape);
|
|
3663
|
+
const span = Math.max(b.maxX - b.minX, b.maxY - b.minY, 1);
|
|
3664
|
+
const pad = intervalSpacing;
|
|
3665
|
+
const box = [
|
|
3666
|
+
b.minX - pad,
|
|
3667
|
+
b.minY - pad,
|
|
3668
|
+
b.maxX + pad,
|
|
3669
|
+
b.maxY + pad
|
|
3670
|
+
];
|
|
3671
|
+
const fn = (x, y) => {
|
|
3672
|
+
if (!containsPoint(shape, { x, y })) return -1;
|
|
3673
|
+
return Math.hypot(x - highPoint.x, y - highPoint.y);
|
|
3674
|
+
};
|
|
3675
|
+
const raw = isolinesFromFunction(
|
|
3676
|
+
box,
|
|
3677
|
+
Math.max(intervalSpacing / 8, span / 100),
|
|
3678
|
+
intervalSpacing,
|
|
3679
|
+
fn,
|
|
3680
|
+
0,
|
|
3681
|
+
span
|
|
3682
|
+
);
|
|
3683
|
+
return clipPolylinesToPath(raw, shape);
|
|
2770
3684
|
}
|
|
2771
|
-
function
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
3685
|
+
function clipPolylinesToPath(g, shape) {
|
|
3686
|
+
const paths = [];
|
|
3687
|
+
for (const p of g.paths) {
|
|
3688
|
+
const ring2 = p.rings[0];
|
|
3689
|
+
if (!ring2 || ring2.length < 2) continue;
|
|
3690
|
+
let current = [];
|
|
3691
|
+
const flush = () => {
|
|
3692
|
+
if (current.length >= 2) {
|
|
3693
|
+
paths.push({ rings: [current], closed: false });
|
|
3694
|
+
}
|
|
3695
|
+
current = [];
|
|
3696
|
+
};
|
|
3697
|
+
for (let i = 0; i < ring2.length - 1; i++) {
|
|
3698
|
+
const a = ring2[i];
|
|
3699
|
+
const b = ring2[i + 1];
|
|
3700
|
+
if (!finitePt(a) || !finitePt(b)) {
|
|
3701
|
+
flush();
|
|
3702
|
+
continue;
|
|
3703
|
+
}
|
|
3704
|
+
const mid5 = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
3705
|
+
if (insideSafe(shape, mid5)) {
|
|
3706
|
+
if (current.length === 0) current.push(a);
|
|
3707
|
+
current.push(b);
|
|
3708
|
+
} else {
|
|
3709
|
+
flush();
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
flush();
|
|
2777
3713
|
}
|
|
2778
|
-
return
|
|
3714
|
+
return group(paths);
|
|
2779
3715
|
}
|
|
2780
|
-
function
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
({ x, y } = rot(s, x, y, rx, ry));
|
|
3716
|
+
function finitePt(p) {
|
|
3717
|
+
return Number.isFinite(p.x) && Number.isFinite(p.y);
|
|
3718
|
+
}
|
|
3719
|
+
function insideSafe(shape, point) {
|
|
3720
|
+
if (!finitePt(point)) return false;
|
|
3721
|
+
try {
|
|
3722
|
+
return containsPoint(shape, point);
|
|
3723
|
+
} catch {
|
|
3724
|
+
return pointInPoly2(point, shape.rings[0]) && shape.rings.slice(1).every((h) => !pointInPoly2(point, h));
|
|
2790
3725
|
}
|
|
2791
|
-
return d;
|
|
2792
3726
|
}
|
|
2793
|
-
function
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
3727
|
+
function pointInPoly2(point, ring2) {
|
|
3728
|
+
let inside = false;
|
|
3729
|
+
for (let i = 0, j = ring2.length - 1; i < ring2.length; j = i++) {
|
|
3730
|
+
const pi = ring2[i];
|
|
3731
|
+
const pj = ring2[j];
|
|
3732
|
+
if (pi.y > point.y !== pj.y > point.y && point.x < (pj.x - pi.x) * (point.y - pi.y) / (pj.y - pi.y + 1e-15) + pi.x) {
|
|
3733
|
+
inside = !inside;
|
|
2798
3734
|
}
|
|
2799
|
-
return { x: y, y: x };
|
|
2800
3735
|
}
|
|
2801
|
-
return
|
|
3736
|
+
return inside;
|
|
2802
3737
|
}
|
|
2803
|
-
var polygonisation = {
|
|
2804
|
-
maxArea,
|
|
2805
|
-
minArea,
|
|
2806
|
-
minPerimeter,
|
|
2807
|
-
horizontal,
|
|
2808
|
-
vertical,
|
|
2809
|
-
angular,
|
|
2810
|
-
circular,
|
|
2811
|
-
onion,
|
|
2812
|
-
onionLayers,
|
|
2813
|
-
hilbert
|
|
2814
|
-
};
|
|
2815
3738
|
|
|
2816
|
-
// src/
|
|
2817
|
-
function
|
|
2818
|
-
|
|
2819
|
-
const bKey = `${b.x},${b.y}`;
|
|
2820
|
-
return aKey < bKey ? `${aKey}|${bKey}` : `${bKey}|${aKey}`;
|
|
2821
|
-
}
|
|
2822
|
-
function vertKey(v) {
|
|
2823
|
-
return `${v.x},${v.y}`;
|
|
2824
|
-
}
|
|
2825
|
-
function same(a, b, eps = 1e-12) {
|
|
2826
|
-
return Math.abs(a.x - b.x) < eps && Math.abs(a.y - b.y) < eps;
|
|
2827
|
-
}
|
|
2828
|
-
function dist(a, b) {
|
|
2829
|
-
return Math.hypot(a.x - b.x, a.y - b.y);
|
|
2830
|
-
}
|
|
2831
|
-
function mid2(a, b) {
|
|
2832
|
-
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
2833
|
-
}
|
|
2834
|
-
function lerp(a, b, t) {
|
|
2835
|
-
return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
|
|
3739
|
+
// src/contour/distanceTree.ts
|
|
3740
|
+
function key3(v) {
|
|
3741
|
+
return `${v.x.toFixed(8)},${v.y.toFixed(8)}`;
|
|
2836
3742
|
}
|
|
2837
|
-
function
|
|
2838
|
-
const
|
|
2839
|
-
const
|
|
2840
|
-
|
|
2841
|
-
const
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
3743
|
+
function distanceTree(mesh, source, flatten2) {
|
|
3744
|
+
const adj = /* @__PURE__ */ new Map();
|
|
3745
|
+
const pts = /* @__PURE__ */ new Map();
|
|
3746
|
+
const addEdge = (a, b) => {
|
|
3747
|
+
const ka = key3(a);
|
|
3748
|
+
const kb = key3(b);
|
|
3749
|
+
if (ka === kb) return;
|
|
3750
|
+
pts.set(ka, a);
|
|
3751
|
+
pts.set(kb, b);
|
|
3752
|
+
if (!adj.has(ka)) adj.set(ka, []);
|
|
3753
|
+
if (!adj.has(kb)) adj.set(kb, []);
|
|
3754
|
+
adj.get(ka).push({ to: kb, pt: b });
|
|
3755
|
+
adj.get(kb).push({ to: ka, pt: a });
|
|
3756
|
+
};
|
|
3757
|
+
for (const face of mesh.paths) {
|
|
3758
|
+
for (const ring2 of face.rings) {
|
|
3759
|
+
if (ring2.length < 2) continue;
|
|
3760
|
+
const n = ring2.length;
|
|
3761
|
+
const closed = face.closed;
|
|
3762
|
+
const limit = closed ? n : n - 1;
|
|
3763
|
+
for (let i = 0; i < limit; i++) {
|
|
3764
|
+
addEdge(ring2[i], ring2[(i + 1) % n]);
|
|
3765
|
+
}
|
|
3766
|
+
}
|
|
2845
3767
|
}
|
|
2846
|
-
return
|
|
3768
|
+
if (pts.size === 0) return group([]);
|
|
3769
|
+
let sourceKey = "";
|
|
3770
|
+
let best = Infinity;
|
|
3771
|
+
for (const [k, p] of pts) {
|
|
3772
|
+
const d = Math.hypot(p.x - source.x, p.y - source.y);
|
|
3773
|
+
if (d < best) {
|
|
3774
|
+
best = d;
|
|
3775
|
+
sourceKey = k;
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3778
|
+
const parent = /* @__PURE__ */ new Map();
|
|
3779
|
+
parent.set(sourceKey, null);
|
|
3780
|
+
const q = [sourceKey];
|
|
3781
|
+
while (q.length) {
|
|
3782
|
+
const u = q.shift();
|
|
3783
|
+
for (const { to } of adj.get(u) ?? []) {
|
|
3784
|
+
if (parent.has(to)) continue;
|
|
3785
|
+
parent.set(to, u);
|
|
3786
|
+
q.push(to);
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
if (flatten2) {
|
|
3790
|
+
const segs = [];
|
|
3791
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3792
|
+
for (const [v, p] of parent) {
|
|
3793
|
+
if (p === null) continue;
|
|
3794
|
+
const ek = v < p ? `${v}|${p}` : `${p}|${v}`;
|
|
3795
|
+
if (seen.has(ek)) continue;
|
|
3796
|
+
seen.add(ek);
|
|
3797
|
+
segs.push(segment(pts.get(p), pts.get(v)));
|
|
3798
|
+
}
|
|
3799
|
+
return dissolveSegments(segs);
|
|
3800
|
+
}
|
|
3801
|
+
const paths = [];
|
|
3802
|
+
for (const [v] of parent) {
|
|
3803
|
+
if (v === sourceKey) continue;
|
|
3804
|
+
const chain = [];
|
|
3805
|
+
let cur = v;
|
|
3806
|
+
while (cur !== null) {
|
|
3807
|
+
chain.push(pts.get(cur));
|
|
3808
|
+
cur = parent.get(cur) ?? null;
|
|
3809
|
+
}
|
|
3810
|
+
chain.reverse();
|
|
3811
|
+
if (chain.length >= 2) paths.push(polyline(chain));
|
|
3812
|
+
}
|
|
3813
|
+
return group(paths);
|
|
2847
3814
|
}
|
|
2848
|
-
|
|
2849
|
-
|
|
3815
|
+
|
|
3816
|
+
// src/contour/index.ts
|
|
3817
|
+
function offsetCurvesOutward(p, distance) {
|
|
3818
|
+
return buffer(p, Math.abs(distance));
|
|
2850
3819
|
}
|
|
2851
|
-
function
|
|
2852
|
-
return
|
|
3820
|
+
function offsetCurvesInward(p, distance) {
|
|
3821
|
+
return buffer(p, -Math.abs(distance));
|
|
2853
3822
|
}
|
|
2854
|
-
|
|
3823
|
+
var contour = {
|
|
3824
|
+
offsetCurvesOutward,
|
|
3825
|
+
offsetCurvesInward,
|
|
3826
|
+
medialAxis,
|
|
3827
|
+
chordalAxis,
|
|
3828
|
+
straightSkeleton,
|
|
3829
|
+
straightSkeletonParts,
|
|
3830
|
+
centerLine,
|
|
3831
|
+
distanceField,
|
|
3832
|
+
contrastField,
|
|
3833
|
+
distanceTree,
|
|
3834
|
+
isolines,
|
|
3835
|
+
isolinesFromFunction,
|
|
3836
|
+
isolineZeroFromFunction,
|
|
3837
|
+
isolinesFromPoints,
|
|
3838
|
+
dissolveSegments
|
|
3839
|
+
};
|
|
3840
|
+
function compoundVoronoi(sites, options = {}) {
|
|
3841
|
+
if (sites.length === 0) return group([]);
|
|
3842
|
+
if (sites.length === 1) {
|
|
3843
|
+
const b2 = options.bounds ?? padBounds(sites, 1);
|
|
3844
|
+
return group([
|
|
3845
|
+
polygon([
|
|
3846
|
+
{ x: b2.minX, y: b2.minY },
|
|
3847
|
+
{ x: b2.maxX, y: b2.minY },
|
|
3848
|
+
{ x: b2.maxX, y: b2.maxY },
|
|
3849
|
+
{ x: b2.minX, y: b2.maxY }
|
|
3850
|
+
])
|
|
3851
|
+
]);
|
|
3852
|
+
}
|
|
3853
|
+
const b = options.bounds ?? padBounds(sites, 0.1);
|
|
3854
|
+
const delaunay = Delaunay.from(
|
|
3855
|
+
sites,
|
|
3856
|
+
(d) => d.x,
|
|
3857
|
+
(d) => d.y
|
|
3858
|
+
);
|
|
3859
|
+
const voronoi2 = delaunay.voronoi([b.minX, b.minY, b.maxX, b.maxY]);
|
|
3860
|
+
const paths = [];
|
|
3861
|
+
for (let i = 0; i < sites.length; i++) {
|
|
3862
|
+
const poly = voronoi2.cellPolygon(i);
|
|
3863
|
+
if (!poly || poly.length < 3) continue;
|
|
3864
|
+
const ring2 = poly.slice(0, -1).map(([x, y]) => ({ x, y }));
|
|
3865
|
+
if (ring2.length >= 3) paths.push(polygon(ring2));
|
|
3866
|
+
}
|
|
3867
|
+
return group(paths);
|
|
3868
|
+
}
|
|
3869
|
+
function innerVoronoi(sites, container, options = {}) {
|
|
3870
|
+
const b = options.bounds ?? {
|
|
3871
|
+
...bounds(container)
|
|
3872
|
+
};
|
|
3873
|
+
const cells = compoundVoronoi(sites, { bounds: b });
|
|
3874
|
+
const clipped = [];
|
|
3875
|
+
for (const cell of cells.paths) {
|
|
3876
|
+
clipped.push(...intersect(cell, container).paths);
|
|
3877
|
+
}
|
|
3878
|
+
return group(clipped);
|
|
3879
|
+
}
|
|
3880
|
+
function innerVoronoiRaw(sites, options = {}) {
|
|
3881
|
+
return compoundVoronoi(sites, options);
|
|
3882
|
+
}
|
|
3883
|
+
function padBounds(sites, padRatio) {
|
|
3884
|
+
let minX = Infinity;
|
|
3885
|
+
let minY = Infinity;
|
|
3886
|
+
let maxX = -Infinity;
|
|
3887
|
+
let maxY = -Infinity;
|
|
3888
|
+
for (const s of sites) {
|
|
3889
|
+
minX = Math.min(minX, s.x);
|
|
3890
|
+
minY = Math.min(minY, s.y);
|
|
3891
|
+
maxX = Math.max(maxX, s.x);
|
|
3892
|
+
maxY = Math.max(maxY, s.y);
|
|
3893
|
+
}
|
|
3894
|
+
const pad = Math.max(maxX - minX, maxY - minY, 1) * padRatio + 1e-6;
|
|
3895
|
+
return {
|
|
3896
|
+
minX: minX - pad,
|
|
3897
|
+
minY: minY - pad,
|
|
3898
|
+
maxX: maxX + pad,
|
|
3899
|
+
maxY: maxY + pad
|
|
3900
|
+
};
|
|
3901
|
+
}
|
|
3902
|
+
var voronoi = {
|
|
3903
|
+
compoundVoronoi,
|
|
3904
|
+
innerVoronoi,
|
|
3905
|
+
innerVoronoiRaw
|
|
3906
|
+
};
|
|
3907
|
+
|
|
3908
|
+
// src/meshing/index.ts
|
|
3909
|
+
function edgeKey3(a, b) {
|
|
3910
|
+
const aKey = `${a.x},${a.y}`;
|
|
3911
|
+
const bKey = `${b.x},${b.y}`;
|
|
3912
|
+
return aKey < bKey ? `${aKey}|${bKey}` : `${bKey}|${aKey}`;
|
|
3913
|
+
}
|
|
3914
|
+
function vertKey(v) {
|
|
3915
|
+
return `${v.x},${v.y}`;
|
|
3916
|
+
}
|
|
3917
|
+
function same2(a, b, eps = 1e-12) {
|
|
3918
|
+
return Math.abs(a.x - b.x) < eps && Math.abs(a.y - b.y) < eps;
|
|
3919
|
+
}
|
|
3920
|
+
function dist(a, b) {
|
|
3921
|
+
return Math.hypot(a.x - b.x, a.y - b.y);
|
|
3922
|
+
}
|
|
3923
|
+
function mid3(a, b) {
|
|
3924
|
+
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
3925
|
+
}
|
|
3926
|
+
function lerp(a, b, t) {
|
|
3927
|
+
return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
|
|
3928
|
+
}
|
|
3929
|
+
function unique(points) {
|
|
3930
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3931
|
+
const out = [];
|
|
3932
|
+
for (const p of points) {
|
|
3933
|
+
const k = vertKey(p);
|
|
3934
|
+
if (seen.has(k)) continue;
|
|
3935
|
+
seen.add(k);
|
|
3936
|
+
out.push({ ...p });
|
|
3937
|
+
}
|
|
3938
|
+
return out;
|
|
3939
|
+
}
|
|
3940
|
+
function cross3(o, a, b) {
|
|
3941
|
+
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
|
|
3942
|
+
}
|
|
3943
|
+
function ringOf(face) {
|
|
3944
|
+
return face.rings[0] ?? [];
|
|
3945
|
+
}
|
|
3946
|
+
function buildEdgeMap(faces) {
|
|
2855
3947
|
const edges = /* @__PURE__ */ new Map();
|
|
2856
3948
|
faces.forEach((f, fi) => {
|
|
2857
3949
|
const ring2 = ringOf(f);
|
|
@@ -2862,10 +3954,10 @@ function buildEdgeMap(faces) {
|
|
|
2862
3954
|
for (let i = 0; i < lim; i++) {
|
|
2863
3955
|
const a = ring2[i];
|
|
2864
3956
|
const b = ring2[(i + 1) % n];
|
|
2865
|
-
const
|
|
2866
|
-
const rec = edges.get(
|
|
3957
|
+
const key4 = edgeKey3(a, b);
|
|
3958
|
+
const rec = edges.get(key4);
|
|
2867
3959
|
if (rec) rec.faceIds.push(fi);
|
|
2868
|
-
else edges.set(
|
|
3960
|
+
else edges.set(key4, { a: { ...a }, b: { ...b }, faceIds: [fi] });
|
|
2869
3961
|
}
|
|
2870
3962
|
});
|
|
2871
3963
|
return edges;
|
|
@@ -2909,8 +4001,8 @@ function facesFromKeptEdges(tris, kept) {
|
|
|
2909
4001
|
if (ra !== rb) parent[rb] = ra;
|
|
2910
4002
|
};
|
|
2911
4003
|
const edgeMap = buildEdgeMap(tris);
|
|
2912
|
-
for (const [
|
|
2913
|
-
if (e.faceIds.length === 2 && !kept.has(
|
|
4004
|
+
for (const [key4, e] of edgeMap) {
|
|
4005
|
+
if (e.faceIds.length === 2 && !kept.has(key4)) {
|
|
2914
4006
|
unite(e.faceIds[0], e.faceIds[1]);
|
|
2915
4007
|
}
|
|
2916
4008
|
}
|
|
@@ -2936,10 +4028,10 @@ function boundaryRingOfComponent(tris, members) {
|
|
|
2936
4028
|
for (let i = 0; i < ring2.length; i++) {
|
|
2937
4029
|
const a = ring2[i];
|
|
2938
4030
|
const b = ring2[(i + 1) % ring2.length];
|
|
2939
|
-
const
|
|
2940
|
-
const rec = edgeCount.get(
|
|
4031
|
+
const key4 = edgeKey3(a, b);
|
|
4032
|
+
const rec = edgeCount.get(key4);
|
|
2941
4033
|
if (rec) rec.n++;
|
|
2942
|
-
else edgeCount.set(
|
|
4034
|
+
else edgeCount.set(key4, { a: { ...a }, b: { ...b }, n: 1 });
|
|
2943
4035
|
}
|
|
2944
4036
|
}
|
|
2945
4037
|
const boundary = [];
|
|
@@ -2969,14 +4061,14 @@ function orderEdgesIntoRing(edges) {
|
|
|
2969
4061
|
const maxSteps = edges.length + 2;
|
|
2970
4062
|
for (let step = 0; step < maxSteps; step++) {
|
|
2971
4063
|
ring2.push({ ...curr });
|
|
2972
|
-
if (
|
|
4064
|
+
if (same2(curr, start) && ring2.length > 2) {
|
|
2973
4065
|
ring2.pop();
|
|
2974
4066
|
return ring2;
|
|
2975
4067
|
}
|
|
2976
4068
|
const nbrs = adj.get(vertKey(curr)) ?? [];
|
|
2977
4069
|
let next = null;
|
|
2978
4070
|
for (const n of nbrs) {
|
|
2979
|
-
if (!
|
|
4071
|
+
if (!same2(n, prev)) {
|
|
2980
4072
|
next = n;
|
|
2981
4073
|
break;
|
|
2982
4074
|
}
|
|
@@ -2991,13 +4083,13 @@ function applyEdgeFilter(tris, shouldKeep, preservePerimeter) {
|
|
|
2991
4083
|
const edgeMap = buildEdgeMap(tris);
|
|
2992
4084
|
const peri = perimeterKeys(edgeMap);
|
|
2993
4085
|
const kept = /* @__PURE__ */ new Set();
|
|
2994
|
-
for (const [
|
|
2995
|
-
const isPeri = peri.has(
|
|
4086
|
+
for (const [key4, e] of edgeMap) {
|
|
4087
|
+
const isPeri = peri.has(key4);
|
|
2996
4088
|
if (isPeri && preservePerimeter) {
|
|
2997
|
-
kept.add(
|
|
4089
|
+
kept.add(key4);
|
|
2998
4090
|
continue;
|
|
2999
4091
|
}
|
|
3000
|
-
if (shouldKeep(e.a, e.b, isPeri)) kept.add(
|
|
4092
|
+
if (shouldKeep(e.a, e.b, isPeri)) kept.add(key4);
|
|
3001
4093
|
}
|
|
3002
4094
|
return facesFromKeptEdges(tris, kept);
|
|
3003
4095
|
}
|
|
@@ -3025,8 +4117,8 @@ function extractInnerVertices(faces) {
|
|
|
3025
4117
|
const edgeMap = buildEdgeMap(faces);
|
|
3026
4118
|
const peri = perimeterKeys(edgeMap);
|
|
3027
4119
|
const periVerts = /* @__PURE__ */ new Set();
|
|
3028
|
-
for (const
|
|
3029
|
-
const e = edgeMap.get(
|
|
4120
|
+
for (const key4 of peri) {
|
|
4121
|
+
const e = edgeMap.get(key4);
|
|
3030
4122
|
periVerts.add(vertKey(e.a));
|
|
3031
4123
|
periVerts.add(vertKey(e.b));
|
|
3032
4124
|
}
|
|
@@ -3076,10 +4168,10 @@ function densifyRing2(ring2, maxLen) {
|
|
|
3076
4168
|
return out;
|
|
3077
4169
|
}
|
|
3078
4170
|
function isGabriel(a, b, points) {
|
|
3079
|
-
const m =
|
|
4171
|
+
const m = mid3(a, b);
|
|
3080
4172
|
const r = dist(a, b) / 2;
|
|
3081
4173
|
for (const p of points) {
|
|
3082
|
-
if (
|
|
4174
|
+
if (same2(p, a) || same2(p, b)) continue;
|
|
3083
4175
|
if (dist(p, m) < r - 1e-9) return false;
|
|
3084
4176
|
}
|
|
3085
4177
|
return true;
|
|
@@ -3087,7 +4179,7 @@ function isGabriel(a, b, points) {
|
|
|
3087
4179
|
function isRelativeNeighbor(a, b, points) {
|
|
3088
4180
|
const dab = dist(a, b);
|
|
3089
4181
|
for (const p of points) {
|
|
3090
|
-
if (
|
|
4182
|
+
if (same2(p, a) || same2(p, b)) continue;
|
|
3091
4183
|
if (Math.max(dist(p, a), dist(p, b)) < dab - 1e-9) return false;
|
|
3092
4184
|
}
|
|
3093
4185
|
return true;
|
|
@@ -3108,7 +4200,7 @@ function urquhartFaces(input, preservePerimeter = false) {
|
|
|
3108
4200
|
const d = dist(a, b);
|
|
3109
4201
|
if (d > longestLen) {
|
|
3110
4202
|
longestLen = d;
|
|
3111
|
-
longestKey =
|
|
4203
|
+
longestKey = edgeKey3(a, b);
|
|
3112
4204
|
}
|
|
3113
4205
|
}
|
|
3114
4206
|
if (longestKey && !(preservePerimeter && peri.has(longestKey))) {
|
|
@@ -3116,8 +4208,8 @@ function urquhartFaces(input, preservePerimeter = false) {
|
|
|
3116
4208
|
}
|
|
3117
4209
|
}
|
|
3118
4210
|
const kept = /* @__PURE__ */ new Set();
|
|
3119
|
-
for (const
|
|
3120
|
-
if (!removed.has(
|
|
4211
|
+
for (const key4 of edgeMap.keys()) {
|
|
4212
|
+
if (!removed.has(key4)) kept.add(key4);
|
|
3121
4213
|
}
|
|
3122
4214
|
return group(facesFromKeptEdges(tris, kept));
|
|
3123
4215
|
}
|
|
@@ -3148,12 +4240,12 @@ function spannerFaces(input, k = 1, preservePerimeter = false) {
|
|
|
3148
4240
|
const edgeMap = buildEdgeMap(tris);
|
|
3149
4241
|
const peri = perimeterKeys(edgeMap);
|
|
3150
4242
|
const stretch = Math.max(1, k);
|
|
3151
|
-
const edges = [...edgeMap.entries()].map(([
|
|
3152
|
-
key,
|
|
4243
|
+
const edges = [...edgeMap.entries()].map(([key4, e]) => ({
|
|
4244
|
+
key: key4,
|
|
3153
4245
|
a: e.a,
|
|
3154
4246
|
b: e.b,
|
|
3155
4247
|
len: dist(e.a, e.b),
|
|
3156
|
-
peri: peri.has(
|
|
4248
|
+
peri: peri.has(key4)
|
|
3157
4249
|
}));
|
|
3158
4250
|
edges.sort((a, b) => a.len - b.len);
|
|
3159
4251
|
const adj = /* @__PURE__ */ new Map();
|
|
@@ -3208,8 +4300,8 @@ function dualFaces(faces) {
|
|
|
3208
4300
|
const edgeMap = buildEdgeMap(faces);
|
|
3209
4301
|
const peri = perimeterKeys(edgeMap);
|
|
3210
4302
|
const periVerts = /* @__PURE__ */ new Set();
|
|
3211
|
-
for (const
|
|
3212
|
-
const e = edgeMap.get(
|
|
4303
|
+
for (const key4 of peri) {
|
|
4304
|
+
const e = edgeMap.get(key4);
|
|
3213
4305
|
periVerts.add(vertKey(e.a));
|
|
3214
4306
|
periVerts.add(vertKey(e.b));
|
|
3215
4307
|
}
|
|
@@ -3245,8 +4337,8 @@ function triVerts(t) {
|
|
|
3245
4337
|
function midpointMap(faces) {
|
|
3246
4338
|
const mids = /* @__PURE__ */ new Map();
|
|
3247
4339
|
const edgeMap = buildEdgeMap(faces);
|
|
3248
|
-
for (const [
|
|
3249
|
-
mids.set(
|
|
4340
|
+
for (const [key4, e] of edgeMap) {
|
|
4341
|
+
mids.set(key4, mid3(e.a, e.b));
|
|
3250
4342
|
}
|
|
3251
4343
|
return mids;
|
|
3252
4344
|
}
|
|
@@ -3256,7 +4348,7 @@ function centroidQuadrangulation(input, preservePerimeter = true) {
|
|
|
3256
4348
|
const centers = tris.map((t) => centroid(t));
|
|
3257
4349
|
const out = [];
|
|
3258
4350
|
const peri = perimeterKeys(edgeMap);
|
|
3259
|
-
for (const [
|
|
4351
|
+
for (const [key4, e] of edgeMap) {
|
|
3260
4352
|
const ids = e.faceIds;
|
|
3261
4353
|
if (ids.length === 2) {
|
|
3262
4354
|
const c0 = centers[ids[0]];
|
|
@@ -3265,7 +4357,7 @@ function centroidQuadrangulation(input, preservePerimeter = true) {
|
|
|
3265
4357
|
} else if (ids.length === 1 && preservePerimeter) {
|
|
3266
4358
|
const c0 = centers[ids[0]];
|
|
3267
4359
|
out.push(polygon([e.a, e.b, c0]));
|
|
3268
|
-
} else if (ids.length === 1 && !preservePerimeter && peri.has(
|
|
4360
|
+
} else if (ids.length === 1 && !preservePerimeter && peri.has(key4)) ;
|
|
3269
4361
|
}
|
|
3270
4362
|
return group(out);
|
|
3271
4363
|
}
|
|
@@ -3278,9 +4370,9 @@ function splitQuadrangulation(input) {
|
|
|
3278
4370
|
if (!v) continue;
|
|
3279
4371
|
const [a, b, c] = v;
|
|
3280
4372
|
const g = centroid(t);
|
|
3281
|
-
const mab = mids.get(
|
|
3282
|
-
const mbc = mids.get(
|
|
3283
|
-
const mca = mids.get(
|
|
4373
|
+
const mab = mids.get(edgeKey3(a, b)) ?? mid3(a, b);
|
|
4374
|
+
const mbc = mids.get(edgeKey3(b, c)) ?? mid3(b, c);
|
|
4375
|
+
const mca = mids.get(edgeKey3(c, a)) ?? mid3(c, a);
|
|
3284
4376
|
out.push(polygon([a, mab, g, mca]));
|
|
3285
4377
|
out.push(polygon([b, mbc, g, mab]));
|
|
3286
4378
|
out.push(polygon([c, mca, g, mbc]));
|
|
@@ -3291,13 +4383,13 @@ function edgeCollapseQuadrangulation(input, preservePerimeter = true) {
|
|
|
3291
4383
|
const tris = asTriFaces(input);
|
|
3292
4384
|
const edgeMap = buildEdgeMap(tris);
|
|
3293
4385
|
const candidates = [];
|
|
3294
|
-
for (const [
|
|
4386
|
+
for (const [key4, e] of edgeMap) {
|
|
3295
4387
|
if (e.faceIds.length !== 2) continue;
|
|
3296
4388
|
const [i, j] = e.faceIds;
|
|
3297
4389
|
const quad = mergeTriPair(tris[i], tris[j], e.a, e.b);
|
|
3298
4390
|
if (!quad) continue;
|
|
3299
4391
|
candidates.push({
|
|
3300
|
-
key,
|
|
4392
|
+
key: key4,
|
|
3301
4393
|
a: e.a,
|
|
3302
4394
|
b: e.b,
|
|
3303
4395
|
i,
|
|
@@ -3333,7 +4425,7 @@ function mergeTriPair(t0, t1, ea, eb) {
|
|
|
3333
4425
|
}
|
|
3334
4426
|
function oppositeVertex(t, ea, eb) {
|
|
3335
4427
|
for (const v of ringOf(t)) {
|
|
3336
|
-
if (!
|
|
4428
|
+
if (!same2(v, ea) && !same2(v, eb)) return v;
|
|
3337
4429
|
}
|
|
3338
4430
|
return null;
|
|
3339
4431
|
}
|
|
@@ -3395,8 +4487,8 @@ function vertexNeighborhood(faces) {
|
|
|
3395
4487
|
const edgeMap = buildEdgeMap(faces);
|
|
3396
4488
|
const peri = perimeterKeys(edgeMap);
|
|
3397
4489
|
const perimeter = /* @__PURE__ */ new Set();
|
|
3398
|
-
for (const
|
|
3399
|
-
const e = edgeMap.get(
|
|
4490
|
+
for (const key4 of peri) {
|
|
4491
|
+
const e = edgeMap.get(key4);
|
|
3400
4492
|
perimeter.add(vertKey(e.a));
|
|
3401
4493
|
perimeter.add(vertKey(e.b));
|
|
3402
4494
|
}
|
|
@@ -3519,8 +4611,8 @@ function simplifyMesh(faces, tolerance, preservePerimeter = true) {
|
|
|
3519
4611
|
};
|
|
3520
4612
|
const periVerts = /* @__PURE__ */ new Set();
|
|
3521
4613
|
if (preservePerimeter) {
|
|
3522
|
-
for (const
|
|
3523
|
-
const e = edgeMap.get(
|
|
4614
|
+
for (const key4 of peri) {
|
|
4615
|
+
const e = edgeMap.get(key4);
|
|
3524
4616
|
periVerts.add(vertKey(e.a));
|
|
3525
4617
|
periVerts.add(vertKey(e.b));
|
|
3526
4618
|
}
|
|
@@ -3564,7 +4656,7 @@ function clusterSnap(points, tol) {
|
|
|
3564
4656
|
function stochasticMerge(faces, nClasses, seed = 1) {
|
|
3565
4657
|
if (faces.length === 0) return group([]);
|
|
3566
4658
|
const classes = Math.max(1, Math.floor(nClasses));
|
|
3567
|
-
const rng =
|
|
4659
|
+
const rng = mulberry325(seed >>> 0);
|
|
3568
4660
|
const label = faces.map(() => Math.floor(rng() * classes));
|
|
3569
4661
|
const adj = faceAdjacency(faces);
|
|
3570
4662
|
const parent = faces.map((_, i) => i);
|
|
@@ -3704,7 +4796,7 @@ function convexHullOf(points) {
|
|
|
3704
4796
|
upper.pop();
|
|
3705
4797
|
return polygon([...lower, ...upper]);
|
|
3706
4798
|
}
|
|
3707
|
-
function
|
|
4799
|
+
function mulberry325(a) {
|
|
3708
4800
|
return () => {
|
|
3709
4801
|
a |= 0;
|
|
3710
4802
|
a = a + 1831565813 | 0;
|
|
@@ -3741,8 +4833,8 @@ function findBreaks(faces) {
|
|
|
3741
4833
|
function nearDuplicateEdge(a, b, tol) {
|
|
3742
4834
|
const d1 = dist(a.a, b.a) + dist(a.b, b.b);
|
|
3743
4835
|
const d2 = dist(a.a, b.b) + dist(a.b, b.a);
|
|
3744
|
-
const
|
|
3745
|
-
return Math.min(d1, d2) < tol * 2 +
|
|
4836
|
+
const len2 = dist(a.a, a.b);
|
|
4837
|
+
return Math.min(d1, d2) < tol * 2 + len2 * 0.05 && Math.min(d1, d2) > 1e-9;
|
|
3746
4838
|
}
|
|
3747
4839
|
function fixBreaks(faces, maxGapWidth) {
|
|
3748
4840
|
if (faces.length === 0) return group([]);
|
|
@@ -3851,7 +4943,7 @@ function fixBrokenFaces(coverage, tolerance, polygonise = true) {
|
|
|
3851
4943
|
out.push(p);
|
|
3852
4944
|
continue;
|
|
3853
4945
|
}
|
|
3854
|
-
if (ring2.length >= 3 &&
|
|
4946
|
+
if (ring2.length >= 3 && same2(ring2[0], ring2[ring2.length - 1], tol * 2)) {
|
|
3855
4947
|
out.push(polygon(ring2.slice(0, -1)));
|
|
3856
4948
|
} else if (ring2.length >= 3 && dist(ring2[0], ring2[ring2.length - 1]) <= tol * 2) {
|
|
3857
4949
|
out.push(polygon(ring2));
|
|
@@ -3928,6 +5020,510 @@ var meshing = {
|
|
|
3928
5020
|
findIslands
|
|
3929
5021
|
};
|
|
3930
5022
|
|
|
3931
|
-
|
|
5023
|
+
// src/construction/index.ts
|
|
5024
|
+
function createCircle(cx, cy, radius, segments = 64) {
|
|
5025
|
+
return createRegularPolygon(cx, cy, radius, Math.max(3, Math.floor(segments)));
|
|
5026
|
+
}
|
|
5027
|
+
function createRect(x, y, width2, height2) {
|
|
5028
|
+
return polygon([
|
|
5029
|
+
{ x, y },
|
|
5030
|
+
{ x: x + width2, y },
|
|
5031
|
+
{ x: x + width2, y: y + height2 },
|
|
5032
|
+
{ x, y: y + height2 }
|
|
5033
|
+
]);
|
|
5034
|
+
}
|
|
5035
|
+
function createRegularPolygon(cx, cy, radius, sides, rotation = -Math.PI / 2) {
|
|
5036
|
+
const n = Math.max(3, Math.floor(sides));
|
|
5037
|
+
const ring2 = [];
|
|
5038
|
+
for (let i = 0; i < n; i++) {
|
|
5039
|
+
const t = rotation + i / n * Math.PI * 2;
|
|
5040
|
+
ring2.push({ x: cx + Math.cos(t) * radius, y: cy + Math.sin(t) * radius });
|
|
5041
|
+
}
|
|
5042
|
+
return polygon(ring2);
|
|
5043
|
+
}
|
|
5044
|
+
function createRing(cx, cy, outerRadius, innerRadius, segments = 64) {
|
|
5045
|
+
const outer = createCircle(cx, cy, outerRadius, segments).rings[0];
|
|
5046
|
+
const inner = createCircle(cx, cy, Math.min(innerRadius, outerRadius), segments).rings[0];
|
|
5047
|
+
return path([outer, [...inner].reverse()], true);
|
|
5048
|
+
}
|
|
5049
|
+
function createArc(cx, cy, radius, startAngle, endAngle, segments = 32) {
|
|
5050
|
+
const n = Math.max(2, Math.floor(segments));
|
|
5051
|
+
const pts = [];
|
|
5052
|
+
for (let i = 0; i <= n; i++) {
|
|
5053
|
+
const t = startAngle + (endAngle - startAngle) * i / n;
|
|
5054
|
+
pts.push({ x: cx + Math.cos(t) * radius, y: cy + Math.sin(t) * radius });
|
|
5055
|
+
}
|
|
5056
|
+
return polyline(pts);
|
|
5057
|
+
}
|
|
5058
|
+
function createStar(cx, cy, outerRadius, innerRadius, points = 5, rotation = -Math.PI / 2) {
|
|
5059
|
+
const n = Math.max(3, Math.floor(points));
|
|
5060
|
+
const ring2 = [];
|
|
5061
|
+
for (let i = 0; i < n * 2; i++) {
|
|
5062
|
+
const r = i % 2 === 0 ? outerRadius : innerRadius;
|
|
5063
|
+
const t = rotation + i / (n * 2) * Math.PI * 2;
|
|
5064
|
+
ring2.push({ x: cx + Math.cos(t) * r, y: cy + Math.sin(t) * r });
|
|
5065
|
+
}
|
|
5066
|
+
return polygon(ring2);
|
|
5067
|
+
}
|
|
5068
|
+
function createKochSnowflake(cx, cy, radius, iterations = 3) {
|
|
5069
|
+
const tri = createRegularPolygon(cx, cy, radius, 3);
|
|
5070
|
+
let ring2 = tri.rings[0];
|
|
5071
|
+
const iters = Math.max(0, Math.min(6, Math.floor(iterations)));
|
|
5072
|
+
for (let i = 0; i < iters; i++) {
|
|
5073
|
+
ring2 = kochRefine(ring2);
|
|
5074
|
+
}
|
|
5075
|
+
return polygon(ring2);
|
|
5076
|
+
}
|
|
5077
|
+
function createSponge(width2, height2, generators, thickness, smoothing, classes, seed = 1) {
|
|
5078
|
+
const w = Math.max(width2, 1e-9);
|
|
5079
|
+
const h = Math.max(height2, 1e-9);
|
|
5080
|
+
const nGen = Math.max(6, Math.floor(generators));
|
|
5081
|
+
const nClasses = Math.max(1, Math.floor(classes));
|
|
5082
|
+
const smoothIters = Math.max(0, Math.floor(smoothing));
|
|
5083
|
+
const wall = Math.max(0, thickness);
|
|
5084
|
+
const sites = randomInBox(nGen, 0, 0, w, h, seed);
|
|
5085
|
+
const cells = compoundVoronoi(sites, {
|
|
5086
|
+
bounds: { minX: 0, minY: 0, maxX: w, maxY: h }
|
|
5087
|
+
});
|
|
5088
|
+
const merged = stochasticMerge(cells.paths, nClasses, seed + 1);
|
|
5089
|
+
const pores = [];
|
|
5090
|
+
for (let p of merged.paths) {
|
|
5091
|
+
if (smoothIters > 0) p = smooth(p, smoothIters);
|
|
5092
|
+
if (wall > 1e-12) {
|
|
5093
|
+
pores.push(...buffer(p, -wall * 0.5).paths);
|
|
5094
|
+
} else {
|
|
5095
|
+
pores.push(p);
|
|
5096
|
+
}
|
|
5097
|
+
}
|
|
5098
|
+
const frame = createRect(0, 0, w, h);
|
|
5099
|
+
if (pores.length === 0) return group([frame]);
|
|
5100
|
+
return subtractAll(frame, pores);
|
|
5101
|
+
}
|
|
5102
|
+
function randomInBox(count, minX, minY, maxX, maxY, seed) {
|
|
5103
|
+
const rng = mulberry326(seed >>> 0);
|
|
5104
|
+
const out = [];
|
|
5105
|
+
for (let i = 0; i < count; i++) {
|
|
5106
|
+
out.push({
|
|
5107
|
+
x: minX + rng() * (maxX - minX),
|
|
5108
|
+
y: minY + rng() * (maxY - minY)
|
|
5109
|
+
});
|
|
5110
|
+
}
|
|
5111
|
+
return out;
|
|
5112
|
+
}
|
|
5113
|
+
function mulberry326(a) {
|
|
5114
|
+
return function() {
|
|
5115
|
+
let t = a += 1831565813;
|
|
5116
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
5117
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
5118
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
5119
|
+
};
|
|
5120
|
+
}
|
|
5121
|
+
function kochRefine(ring2) {
|
|
5122
|
+
const out = [];
|
|
5123
|
+
const n = ring2.length;
|
|
5124
|
+
for (let i = 0; i < n; i++) {
|
|
5125
|
+
const a = ring2[i];
|
|
5126
|
+
const b = ring2[(i + 1) % n];
|
|
5127
|
+
const dx = b.x - a.x;
|
|
5128
|
+
const dy = b.y - a.y;
|
|
5129
|
+
const p1 = { x: a.x + dx / 3, y: a.y + dy / 3 };
|
|
5130
|
+
const p2 = { x: a.x + 2 * dx / 3, y: a.y + 2 * dy / 3 };
|
|
5131
|
+
const px = a.x + dx / 2 - dy * (Math.sqrt(3) / 6);
|
|
5132
|
+
const py = a.y + dy / 2 + dx * (Math.sqrt(3) / 6);
|
|
5133
|
+
out.push(a, p1, { x: px, y: py }, p2);
|
|
5134
|
+
}
|
|
5135
|
+
return out;
|
|
5136
|
+
}
|
|
5137
|
+
var construction = {
|
|
5138
|
+
createCircle,
|
|
5139
|
+
createRect,
|
|
5140
|
+
createRegularPolygon,
|
|
5141
|
+
createRing,
|
|
5142
|
+
createArc,
|
|
5143
|
+
createStar,
|
|
5144
|
+
createKochSnowflake,
|
|
5145
|
+
createSponge
|
|
5146
|
+
};
|
|
5147
|
+
|
|
5148
|
+
// src/tiling/index.ts
|
|
5149
|
+
function squareGrid2(spacing, minX, minY, maxX, maxY) {
|
|
5150
|
+
const s = Math.max(spacing, 1e-9);
|
|
5151
|
+
const paths = [];
|
|
5152
|
+
for (let x = minX; x < maxX - 1e-12; x += s) {
|
|
5153
|
+
for (let y = minY; y < maxY - 1e-12; y += s) {
|
|
5154
|
+
const w = Math.min(s, maxX - x);
|
|
5155
|
+
const h = Math.min(s, maxY - y);
|
|
5156
|
+
if (w > 1e-12 && h > 1e-12) paths.push(createRect(x, y, w, h));
|
|
5157
|
+
}
|
|
5158
|
+
}
|
|
5159
|
+
return group(paths);
|
|
5160
|
+
}
|
|
5161
|
+
function hexTiling(spacing, minX, minY, maxX, maxY) {
|
|
5162
|
+
const s = Math.max(spacing, 1e-9);
|
|
5163
|
+
const r = s / 2;
|
|
5164
|
+
const dx = s * 0.75;
|
|
5165
|
+
const dy = s * Math.sqrt(3) * 0.5;
|
|
5166
|
+
const paths = [];
|
|
5167
|
+
let row = 0;
|
|
5168
|
+
for (let y = minY + r; y <= maxY + r; y += dy, row++) {
|
|
5169
|
+
const x0 = minX + r + (row % 2 === 0 ? 0 : dx * 0.5);
|
|
5170
|
+
for (let x = x0; x <= maxX + r; x += dx) {
|
|
5171
|
+
paths.push(hexagon(x, y, r));
|
|
5172
|
+
}
|
|
5173
|
+
}
|
|
5174
|
+
return group(paths);
|
|
5175
|
+
}
|
|
5176
|
+
function hexagon(cx, cy, r) {
|
|
5177
|
+
const ring2 = [];
|
|
5178
|
+
for (let i = 0; i < 6; i++) {
|
|
5179
|
+
const t = Math.PI / 180 * (60 * i - 30);
|
|
5180
|
+
ring2.push({ x: cx + Math.cos(t) * r, y: cy + Math.sin(t) * r });
|
|
5181
|
+
}
|
|
5182
|
+
return polygon(ring2);
|
|
5183
|
+
}
|
|
5184
|
+
function rectSubdivision(p, nx, ny) {
|
|
5185
|
+
const b = bounds(p);
|
|
5186
|
+
const cols = Math.max(1, Math.floor(nx));
|
|
5187
|
+
const rows = Math.max(1, Math.floor(ny));
|
|
5188
|
+
const dw = (b.maxX - b.minX) / cols;
|
|
5189
|
+
const dh = (b.maxY - b.minY) / rows;
|
|
5190
|
+
const paths = [];
|
|
5191
|
+
for (let i = 0; i < cols; i++) {
|
|
5192
|
+
for (let j = 0; j < rows; j++) {
|
|
5193
|
+
const cell = createRect(
|
|
5194
|
+
b.minX + i * dw,
|
|
5195
|
+
b.minY + j * dh,
|
|
5196
|
+
dw,
|
|
5197
|
+
dh
|
|
5198
|
+
);
|
|
5199
|
+
paths.push(...intersect(p, cell).paths);
|
|
5200
|
+
}
|
|
5201
|
+
}
|
|
5202
|
+
return group(paths);
|
|
5203
|
+
}
|
|
5204
|
+
function quadSubdivision(faces, depth = 1) {
|
|
5205
|
+
let cur = faces;
|
|
5206
|
+
const d = Math.max(0, Math.floor(depth));
|
|
5207
|
+
for (let i = 0; i < d; i++) {
|
|
5208
|
+
const next = [];
|
|
5209
|
+
for (const f of cur) {
|
|
5210
|
+
next.push(...subdivideQuad(f));
|
|
5211
|
+
}
|
|
5212
|
+
cur = next;
|
|
5213
|
+
}
|
|
5214
|
+
return group(cur);
|
|
5215
|
+
}
|
|
5216
|
+
function subdivideQuad(p) {
|
|
5217
|
+
const ring2 = p.rings[0];
|
|
5218
|
+
if (!ring2 || ring2.length < 4) return [p];
|
|
5219
|
+
const [a, b, c, d] = ring2;
|
|
5220
|
+
if (!a || !b || !c || !d) return [p];
|
|
5221
|
+
const mab = mid4(a, b);
|
|
5222
|
+
const mbc = mid4(b, c);
|
|
5223
|
+
const mcd = mid4(c, d);
|
|
5224
|
+
const mda = mid4(d, a);
|
|
5225
|
+
const cen = mid4(mid4(a, c), mid4(b, d));
|
|
5226
|
+
return [
|
|
5227
|
+
polygon([a, mab, cen, mda]),
|
|
5228
|
+
polygon([mab, b, mbc, cen]),
|
|
5229
|
+
polygon([cen, mbc, c, mcd]),
|
|
5230
|
+
polygon([mda, cen, mcd, d])
|
|
5231
|
+
];
|
|
5232
|
+
}
|
|
5233
|
+
function triangleSubdivision(faces, depth = 1) {
|
|
5234
|
+
let cur = faces;
|
|
5235
|
+
const d = Math.max(0, Math.floor(depth));
|
|
5236
|
+
for (let i = 0; i < d; i++) {
|
|
5237
|
+
const next = [];
|
|
5238
|
+
for (const f of cur) {
|
|
5239
|
+
next.push(...subdivideTriangle(f));
|
|
5240
|
+
}
|
|
5241
|
+
cur = next;
|
|
5242
|
+
}
|
|
5243
|
+
return group(cur);
|
|
5244
|
+
}
|
|
5245
|
+
function subdivideTriangle(p) {
|
|
5246
|
+
const ring2 = p.rings[0];
|
|
5247
|
+
if (!ring2 || ring2.length < 3) return [p];
|
|
5248
|
+
const a = ring2[0];
|
|
5249
|
+
const b = ring2[1];
|
|
5250
|
+
const c = ring2[2];
|
|
5251
|
+
const mab = mid4(a, b);
|
|
5252
|
+
const mbc = mid4(b, c);
|
|
5253
|
+
const mca = mid4(c, a);
|
|
5254
|
+
return [
|
|
5255
|
+
polygon([a, mab, mca]),
|
|
5256
|
+
polygon([mab, b, mbc]),
|
|
5257
|
+
polygon([mca, mbc, c]),
|
|
5258
|
+
polygon([mab, mbc, mca])
|
|
5259
|
+
];
|
|
5260
|
+
}
|
|
5261
|
+
function sliceDivision(p, spacing, angle = 0) {
|
|
5262
|
+
const s = Math.max(spacing, 1e-9);
|
|
5263
|
+
const b = bounds(p);
|
|
5264
|
+
const cx = (b.minX + b.maxX) / 2;
|
|
5265
|
+
const cy = (b.minY + b.maxY) / 2;
|
|
5266
|
+
const diag = Math.hypot(b.maxX - b.minX, b.maxY - b.minY) + s;
|
|
5267
|
+
const ux = Math.cos(angle);
|
|
5268
|
+
const uy = Math.sin(angle);
|
|
5269
|
+
const nx = -uy;
|
|
5270
|
+
const ny = ux;
|
|
5271
|
+
const parts = [p];
|
|
5272
|
+
for (let t = -diag; t <= diag; t += s) {
|
|
5273
|
+
const a = { x: cx + nx * t - ux * diag, y: cy + ny * t - uy * diag };
|
|
5274
|
+
const bb = { x: cx + nx * t + ux * diag, y: cy + ny * t + uy * diag };
|
|
5275
|
+
const next = [];
|
|
5276
|
+
for (const piece of parts) {
|
|
5277
|
+
const g = slice(piece, a, bb);
|
|
5278
|
+
next.push(...g.paths.length ? g.paths : [piece]);
|
|
5279
|
+
}
|
|
5280
|
+
parts.length = 0;
|
|
5281
|
+
parts.push(...next);
|
|
5282
|
+
}
|
|
5283
|
+
return group(parts);
|
|
5284
|
+
}
|
|
5285
|
+
function hatchSubdivision(p, spacing, angle = 0) {
|
|
5286
|
+
return sliceDivision(p, spacing, angle);
|
|
5287
|
+
}
|
|
5288
|
+
function arcDivision(width2, height2, arcs, seed = 1, circlePoints = 64) {
|
|
5289
|
+
const w = Math.max(width2, 1e-9);
|
|
5290
|
+
const h = Math.max(height2, 1e-9);
|
|
5291
|
+
const n = Math.max(0, Math.floor(arcs));
|
|
5292
|
+
const segs = Math.max(8, Math.floor(circlePoints));
|
|
5293
|
+
const rect = createRect(0, 0, w, h);
|
|
5294
|
+
if (n === 0) return group([rect]);
|
|
5295
|
+
const rng = mulberry327(seed >>> 0);
|
|
5296
|
+
let faces = [rect];
|
|
5297
|
+
const minArea2 = w * h * 1e-8;
|
|
5298
|
+
for (let i = 0; i < n; i++) {
|
|
5299
|
+
const { cx, cy, r } = sampleBoundaryCircle(w, h, rng);
|
|
5300
|
+
const disk = createCircle(cx, cy, r, segs);
|
|
5301
|
+
const next = [];
|
|
5302
|
+
for (const face of faces) {
|
|
5303
|
+
for (const piece of intersect(face, disk).paths) {
|
|
5304
|
+
if (area(piece) > minArea2) next.push(piece);
|
|
5305
|
+
}
|
|
5306
|
+
for (const piece of subtract(face, disk).paths) {
|
|
5307
|
+
if (area(piece) > minArea2) next.push(piece);
|
|
5308
|
+
}
|
|
5309
|
+
}
|
|
5310
|
+
if (next.length > 0) faces = next;
|
|
5311
|
+
}
|
|
5312
|
+
return group(faces);
|
|
5313
|
+
}
|
|
5314
|
+
function sampleBoundaryCircle(w, h, rng) {
|
|
5315
|
+
const peri = 2 * (w + h);
|
|
5316
|
+
let t = rng() * peri;
|
|
5317
|
+
let cx;
|
|
5318
|
+
let cy;
|
|
5319
|
+
let side;
|
|
5320
|
+
if (t < w) {
|
|
5321
|
+
side = 0;
|
|
5322
|
+
cx = t;
|
|
5323
|
+
cy = 0;
|
|
5324
|
+
} else {
|
|
5325
|
+
t -= w;
|
|
5326
|
+
if (t < h) {
|
|
5327
|
+
side = 1;
|
|
5328
|
+
cx = w;
|
|
5329
|
+
cy = t;
|
|
5330
|
+
} else {
|
|
5331
|
+
t -= h;
|
|
5332
|
+
if (t < w) {
|
|
5333
|
+
side = 2;
|
|
5334
|
+
cx = w - t;
|
|
5335
|
+
cy = h;
|
|
5336
|
+
} else {
|
|
5337
|
+
side = 3;
|
|
5338
|
+
cx = 0;
|
|
5339
|
+
cy = h - (t - w);
|
|
5340
|
+
}
|
|
5341
|
+
}
|
|
5342
|
+
}
|
|
5343
|
+
const distOther = [
|
|
5344
|
+
cy,
|
|
5345
|
+
// to top
|
|
5346
|
+
w - cx,
|
|
5347
|
+
// to right
|
|
5348
|
+
h - cy,
|
|
5349
|
+
// to bottom
|
|
5350
|
+
cx
|
|
5351
|
+
// to left
|
|
5352
|
+
];
|
|
5353
|
+
let minSecond = Infinity;
|
|
5354
|
+
for (let s = 0; s < 4; s++) {
|
|
5355
|
+
if (s === side) continue;
|
|
5356
|
+
minSecond = Math.min(minSecond, distOther[s]);
|
|
5357
|
+
}
|
|
5358
|
+
const maxR = Math.hypot(w, h);
|
|
5359
|
+
const r = minSecond + rng() * Math.max(0, maxR - minSecond);
|
|
5360
|
+
return { cx, cy, r: Math.max(r, minSecond + 1e-6) };
|
|
5361
|
+
}
|
|
5362
|
+
function mulberry327(a) {
|
|
5363
|
+
return function() {
|
|
5364
|
+
let t = a += 1831565813;
|
|
5365
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
5366
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
5367
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
5368
|
+
};
|
|
5369
|
+
}
|
|
5370
|
+
function mid4(a, b) {
|
|
5371
|
+
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
5372
|
+
}
|
|
5373
|
+
var tiling = {
|
|
5374
|
+
squareGrid: squareGrid2,
|
|
5375
|
+
hexTiling,
|
|
5376
|
+
rectSubdivision,
|
|
5377
|
+
quadSubdivision,
|
|
5378
|
+
triangleSubdivision,
|
|
5379
|
+
sliceDivision,
|
|
5380
|
+
hatchSubdivision,
|
|
5381
|
+
arcDivision
|
|
5382
|
+
};
|
|
5383
|
+
|
|
5384
|
+
// src/polygonisation/index.ts
|
|
5385
|
+
function maxArea(points) {
|
|
5386
|
+
return convexHull(points);
|
|
5387
|
+
}
|
|
5388
|
+
function minArea(points) {
|
|
5389
|
+
const hull2 = convexHull(points).rings[0] ?? [];
|
|
5390
|
+
if (hull2.length < 3) return convexHull(points);
|
|
5391
|
+
if (hull2.length === 3) return polygon(hull2);
|
|
5392
|
+
let best = null;
|
|
5393
|
+
let bestA = Infinity;
|
|
5394
|
+
const h = hull2;
|
|
5395
|
+
const n = h.length;
|
|
5396
|
+
for (let i = 0; i < n; i++) {
|
|
5397
|
+
for (let j = i + 1; j < n; j++) {
|
|
5398
|
+
for (let k = j + 1; k < n; k++) {
|
|
5399
|
+
const tri = polygon([h[i], h[j], h[k]]);
|
|
5400
|
+
if (!coversPoints(tri, points)) continue;
|
|
5401
|
+
const a = area(tri);
|
|
5402
|
+
if (a < bestA) {
|
|
5403
|
+
bestA = a;
|
|
5404
|
+
best = tri;
|
|
5405
|
+
}
|
|
5406
|
+
}
|
|
5407
|
+
}
|
|
5408
|
+
}
|
|
5409
|
+
return best ?? convexHull(points);
|
|
5410
|
+
}
|
|
5411
|
+
function minPerimeter(points) {
|
|
5412
|
+
if (points.length === 0) return polygon([]);
|
|
5413
|
+
if (points.length < 3) return polygon(points);
|
|
5414
|
+
const remaining = points.map((p) => ({ ...p }));
|
|
5415
|
+
const ordered = [remaining.shift()];
|
|
5416
|
+
while (remaining.length) {
|
|
5417
|
+
const last = ordered[ordered.length - 1];
|
|
5418
|
+
let bestI = 0;
|
|
5419
|
+
let bestD = Infinity;
|
|
5420
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
5421
|
+
const d = Math.hypot(
|
|
5422
|
+
remaining[i].x - last.x,
|
|
5423
|
+
remaining[i].y - last.y
|
|
5424
|
+
);
|
|
5425
|
+
if (d < bestD) {
|
|
5426
|
+
bestD = d;
|
|
5427
|
+
bestI = i;
|
|
5428
|
+
}
|
|
5429
|
+
}
|
|
5430
|
+
ordered.push(remaining.splice(bestI, 1)[0]);
|
|
5431
|
+
}
|
|
5432
|
+
return polygon(ordered);
|
|
5433
|
+
}
|
|
5434
|
+
function horizontal(points) {
|
|
5435
|
+
const pts = [...points].sort(
|
|
5436
|
+
(a, b) => a.y === b.y ? a.x - b.x : a.y - b.y
|
|
5437
|
+
);
|
|
5438
|
+
return polygon(pts);
|
|
5439
|
+
}
|
|
5440
|
+
function vertical(points) {
|
|
5441
|
+
const pts = [...points].sort(
|
|
5442
|
+
(a, b) => a.x === b.x ? a.y - b.y : a.x - b.x
|
|
5443
|
+
);
|
|
5444
|
+
return polygon(pts);
|
|
5445
|
+
}
|
|
5446
|
+
function angular(points) {
|
|
5447
|
+
if (points.length === 0) return polygon([]);
|
|
5448
|
+
const c = centroidOf(points);
|
|
5449
|
+
const pts = [...points].sort(
|
|
5450
|
+
(a, b) => Math.atan2(a.y - c.y, a.x - c.x) - Math.atan2(b.y - c.y, b.x - c.x)
|
|
5451
|
+
);
|
|
5452
|
+
return polygon(pts);
|
|
5453
|
+
}
|
|
5454
|
+
function circular(points) {
|
|
5455
|
+
return angular(points);
|
|
5456
|
+
}
|
|
5457
|
+
function onion(points) {
|
|
5458
|
+
const layers = onionLayers(points);
|
|
5459
|
+
return layers[0] ?? polygon([]);
|
|
5460
|
+
}
|
|
5461
|
+
function onionLayers(points) {
|
|
5462
|
+
let remaining = points.map((p) => ({ ...p }));
|
|
5463
|
+
const layers = [];
|
|
5464
|
+
while (remaining.length >= 3) {
|
|
5465
|
+
const hull2 = convexHull(remaining);
|
|
5466
|
+
const ring2 = hull2.rings[0] ?? [];
|
|
5467
|
+
if (ring2.length < 3) break;
|
|
5468
|
+
layers.push(hull2);
|
|
5469
|
+
const onHull = new Set(ring2.map((v) => `${v.x},${v.y}`));
|
|
5470
|
+
remaining = remaining.filter((p) => !onHull.has(`${p.x},${p.y}`));
|
|
5471
|
+
}
|
|
5472
|
+
return layers;
|
|
5473
|
+
}
|
|
5474
|
+
function hilbert(points) {
|
|
5475
|
+
return polygon(hilbertSort(points));
|
|
5476
|
+
}
|
|
5477
|
+
function coversPoints(poly, points) {
|
|
5478
|
+
const r = poly.rings[0];
|
|
5479
|
+
if (!r || r.length < 3) return false;
|
|
5480
|
+
const a = r[0];
|
|
5481
|
+
const b = r[1];
|
|
5482
|
+
const c = r[2];
|
|
5483
|
+
for (const p of points) {
|
|
5484
|
+
if (!pointInTriangle(p, a, b, c)) return false;
|
|
5485
|
+
}
|
|
5486
|
+
return true;
|
|
5487
|
+
}
|
|
5488
|
+
function pointInTriangle(p, a, b, c) {
|
|
5489
|
+
const v0x = c.x - a.x;
|
|
5490
|
+
const v0y = c.y - a.y;
|
|
5491
|
+
const v1x = b.x - a.x;
|
|
5492
|
+
const v1y = b.y - a.y;
|
|
5493
|
+
const v2x = p.x - a.x;
|
|
5494
|
+
const v2y = p.y - a.y;
|
|
5495
|
+
const dot00 = v0x * v0x + v0y * v0y;
|
|
5496
|
+
const dot01 = v0x * v1x + v0y * v1y;
|
|
5497
|
+
const dot02 = v0x * v2x + v0y * v2y;
|
|
5498
|
+
const dot11 = v1x * v1x + v1y * v1y;
|
|
5499
|
+
const dot12 = v1x * v2x + v1y * v2y;
|
|
5500
|
+
const inv = 1 / (dot00 * dot11 - dot01 * dot01 || 1e-18);
|
|
5501
|
+
const u = (dot11 * dot02 - dot01 * dot12) * inv;
|
|
5502
|
+
const v = (dot00 * dot12 - dot01 * dot02) * inv;
|
|
5503
|
+
return u >= -1e-9 && v >= -1e-9 && u + v <= 1 + 1e-9;
|
|
5504
|
+
}
|
|
5505
|
+
function centroidOf(points) {
|
|
5506
|
+
let sx = 0;
|
|
5507
|
+
let sy = 0;
|
|
5508
|
+
for (const p of points) {
|
|
5509
|
+
sx += p.x;
|
|
5510
|
+
sy += p.y;
|
|
5511
|
+
}
|
|
5512
|
+
return { x: sx / points.length, y: sy / points.length };
|
|
5513
|
+
}
|
|
5514
|
+
var polygonisation = {
|
|
5515
|
+
maxArea,
|
|
5516
|
+
minArea,
|
|
5517
|
+
minPerimeter,
|
|
5518
|
+
horizontal,
|
|
5519
|
+
vertical,
|
|
5520
|
+
angular,
|
|
5521
|
+
circular,
|
|
5522
|
+
onion,
|
|
5523
|
+
onionLayers,
|
|
5524
|
+
hilbert
|
|
5525
|
+
};
|
|
5526
|
+
|
|
5527
|
+
export { angular, arcDivision, area, areaGroup, areaMerge, areaSimilarity, assertAreaClose, boundingBox, bounds, buffer, centerLine, centroid, centroidQuadrangulation, centroidSortFaces, centroidSplit, chaikinCut, chordalAxis, circle, circleCenter, circleContainedInPath, circleOverlapsPath, circlePacking, circular, clipSegmentToPath, clipSegmentsToPath, cloneGroup, clonePath, cloneVec2, closestPoint, closestPointPair, closestVertex, complement, compoundVoronoi, construction, containmentAgreement, containsPoint, contour, contrastField, conversion, convexHull, convexHullPath, copy, createArc, createCircle, createKochSnowflake, createRect, createRegularPolygon, createRing, createSponge, createStar, delaunayTriangulation, delaunayTriangulationPoints, densify, dilationErosion, dissolve, dissolveSegments, distanceField, distanceTree, dualFaces, earCutTriangulation, edgeCollapseQuadrangulation, envelope, equalsVec2, erosionDilation, extractBoundary, extractHoles, extractInnerEdges, extractInnerVertices, extractPerimeter, farthestPointPair, filterAxisAligned, filterByMinLength, findBreaks, findContainingFace, findIslands, findShortestTour, fixBreaks, fixBrokenFaces, flatten, flipHorizontal, flipVertical, fromArray, fromContours, frontChainPack, gabrielFaces, generateRandomGridPoints, generateRandomPoints, group, groupFromPaths, hatch, concentric as hatchConcentric, cross as hatchCross, parallel as hatchParallel, perpendicular as hatchPerpendicular, stochastic as hatchStochastic, hatchSubdivision, weave as hatchWeave, height, hexGrid, hexLatticePack, hexTiling, hilbert as hilbertPolygonise, hilbertSort, hull, innerVoronoi, innerVoronoiRaw, intersect, intersectionPoints, isolineZeroFromFunction, isolines, isolinesFromFunction, isolinesFromPoints, largestEmptyCircle, largestEmptyCircles, matchingQuadrangulation, maxArea, maximumInscribedAARectangle, maximumInscribedCircle, maximumInscribedPack, maximumInscribedPackUntil, medialAxis, meshing, minArea, minPerimeter, minimumBoundingCircle, minkDifference, minkSum, morphology, normalizeRing, obstaclePack, occlusionSubtract, offsetCurvesInward, offsetCurvesOutward, onion, onionLayers, optimisation, oraclePathsToGroup, originScale, overlapRegions, parallelSegments, parsePathData, parseSvg, path, perpendicularPathSegments, ring as pointRing, pointSet, pointsOnExterior, poisson, poissonTriangulation, poissonTriangulationPoints, polygon, polygonisation, horizontal as polygoniseHorizontal, vertical as polygoniseVertical, polyline, predicates, processing, prunePointsWithinDistance, quadSubdivision, radialSortFaces, radialWarp, random as randomPoints, rectSubdivision, reducePrecision, refine, relativeAreaError, relativeNeighborFaces, removeSmallHoles, repulsionPack, resizeByHeight, resizeByWidth, ringArea, ringsToPath, rotate, rotateAroundCenter, roundVertexCoords, scale, segment, segmentLength, segmentSet, segmentsOnExterior, segmentsToOpenPaths, serializePathData, shapeBoolean, shear, simplify, simplifyMesh, sineWarp, slice, sliceDivision, smooth, smoothMesh, spannerFaces, spiralQuadrangulation, splitEdges, splitQuadrangulation, squareGrid, squareLatticePack, squareGrid2 as squareTiling, stochasticMerge, stochasticPack, stochasticSegments, straightSkeleton, straightSkeletonParts, subdivideMesh, subtract, subtractAll, symDifference, tiling, toArray, toContours, toSvg, transformation, translate, translateCentroidTo, translateCornerTo, translateToOrigin, triangleSubdivision, triangulation, union, unionAll, urquhartFaces, vec2, voronoi, weaveSegments, width };
|
|
3932
5528
|
//# sourceMappingURL=index.js.map
|
|
3933
5529
|
//# sourceMappingURL=index.js.map
|