pattapatta 0.3.0 → 0.4.1
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 +12 -0
- package/README.md +7 -0
- 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 +3 -4
- 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 +12 -13
- package/dist/index.js +1816 -1260
- 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 +1 -1
- package/dist/vec2-CWyXN1Wj.d.ts +0 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 170d279: Docs polish: Shiki syntax highlighting (Catppuccin), slate theme, sidebar logo, sponsor badge layout, and remove the sidebar collapse control.
|
|
8
|
+
|
|
9
|
+
## 0.4.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- e828df4: Add `createSponge`, `hilbertSort`, `findShortestTour`, and `arcDivision`, plus plotter hatch fills (`weave` / `stochastic` / `perpendicular` / `concentric`). Refresh contour docs examples with shapes that show skeletons and fields clearly.
|
|
14
|
+
|
|
3
15
|
## 0.3.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
# pattapatta
|
|
2
2
|
|
|
3
|
+
[](https://github.com/sponsors/MadOrkestra)
|
|
4
|
+
[](https://www.buymeacoffee.com/madorkestra)
|
|
5
|
+
|
|
3
6
|
Pen-plotter geometry library (inspired by [PGS](https://github.com/micycle1/PGS)): hatching, circle packing, triangulation / meshing, boolean path ops, Voronoi, tilings, and SVG I/O for Node and the browser.
|
|
4
7
|
|
|
5
8
|
**Stroke marks only** — solid area fills are not the goal.
|
|
6
9
|
|
|
10
|
+
> **Status:** Built entirely with AI as an experiment. **Not meant for public / production use yet.** Feedback welcome via [GitHub issues](https://github.com/MadOrkestra/pattapatta/issues) on the [repository](https://github.com/MadOrkestra/pattapatta).
|
|
11
|
+
|
|
7
12
|
[](https://www.npmjs.com/package/pattapatta)
|
|
8
13
|
|
|
9
14
|
## Install
|
|
@@ -61,3 +66,5 @@ pnpm docs:examples # regenerate SVG figures
|
|
|
61
66
|
## License
|
|
62
67
|
|
|
63
68
|
MIT — clean-room implementation (not a GPL line-port of PGS). See `docs/decisions/`.
|
|
69
|
+
|
|
70
|
+
Source: [github.com/MadOrkestra/pattapatta](https://github.com/MadOrkestra/pattapatta) · [Issues](https://github.com/MadOrkestra/pattapatta/issues) · [Sponsor](https://github.com/sponsors/MadOrkestra) · [Buy Me a Coffee](https://www.buymeacoffee.com/madorkestra)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { V as Vec2 } from '../
|
|
2
|
-
import { C as Circle } from '../circle-
|
|
3
|
-
import { P as Path } from '../path-0RjhYjJs.js';
|
|
1
|
+
import { P as Path, V as Vec2 } from '../path-Boeshk9L.js';
|
|
2
|
+
import { C as Circle } from '../circle-CauDMv8V.js';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Lattice inclusion mode.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { P as Path } from '../path-
|
|
2
|
-
import '../
|
|
1
|
+
import { P as Path } from '../path-Boeshk9L.js';
|
|
2
|
+
import { G as Group } from '../group-YVmSJuGo.js';
|
|
3
3
|
|
|
4
4
|
/** Regular n-gon approximating a circle. */
|
|
5
5
|
declare function createCircle(cx: number, cy: number, radius: number, segments?: number): Path;
|
|
@@ -13,6 +13,13 @@ declare function createArc(cx: number, cy: number, radius: number, startAngle: n
|
|
|
13
13
|
declare function createStar(cx: number, cy: number, outerRadius: number, innerRadius: number, points?: number, rotation?: number): Path;
|
|
14
14
|
/** Koch snowflake after `iterations` refinement (0 = equilateral triangle). */
|
|
15
15
|
declare function createKochSnowflake(cx: number, cy: number, radius: number, iterations?: number): Path;
|
|
16
|
+
/**
|
|
17
|
+
* Sponge-like porous structure (PGS `createSponge`).
|
|
18
|
+
* Voronoi cells are randomly class-merged, smoothed (Chaikin), optionally
|
|
19
|
+
* eroded for wall thickness, then subtracted from the bounding rectangle.
|
|
20
|
+
* Returns a group (may be multi-component).
|
|
21
|
+
*/
|
|
22
|
+
declare function createSponge(width: number, height: number, generators: number, thickness: number, smoothing: number, classes: number, seed?: number): Group;
|
|
16
23
|
declare const construction: {
|
|
17
24
|
createCircle: typeof createCircle;
|
|
18
25
|
createRect: typeof createRect;
|
|
@@ -21,6 +28,7 @@ declare const construction: {
|
|
|
21
28
|
createArc: typeof createArc;
|
|
22
29
|
createStar: typeof createStar;
|
|
23
30
|
createKochSnowflake: typeof createKochSnowflake;
|
|
31
|
+
createSponge: typeof createSponge;
|
|
24
32
|
};
|
|
25
33
|
|
|
26
|
-
export { construction, createArc, createCircle, createKochSnowflake, createRect, createRegularPolygon, createRing, createStar };
|
|
34
|
+
export { construction, createArc, createCircle, createKochSnowflake, createRect, createRegularPolygon, createRing, createSponge, createStar };
|
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
import { Delaunay } from 'd3-delaunay';
|
|
2
|
+
import { FillRule, JoinType, inflatePathsD, EndType, isPositiveD, differenceD, pointInPolygonD, PointInPolygonResult, unionD, areaD } from 'clipper2-ts';
|
|
3
|
+
|
|
1
4
|
// src/types/vec2.ts
|
|
5
|
+
function vec2(x, y) {
|
|
6
|
+
return { x, y };
|
|
7
|
+
}
|
|
2
8
|
function cloneVec2(v) {
|
|
3
9
|
return { x: v.x, y: v.y };
|
|
4
10
|
}
|
|
11
|
+
function equalsVec2(a, b, eps = 1e-9) {
|
|
12
|
+
return Math.abs(a.x - b.x) <= eps && Math.abs(a.y - b.y) <= eps;
|
|
13
|
+
}
|
|
5
14
|
|
|
6
15
|
// src/types/path.ts
|
|
7
16
|
function path(rings, closed = true) {
|
|
@@ -16,6 +25,379 @@ function polyline(points) {
|
|
|
16
25
|
function polygon(exterior, holes = []) {
|
|
17
26
|
return path([exterior, ...holes], true);
|
|
18
27
|
}
|
|
28
|
+
function normalizeRing(ring, eps = 1e-9) {
|
|
29
|
+
if (ring.length < 2) return ring.map(cloneVec2);
|
|
30
|
+
const first = ring[0];
|
|
31
|
+
const last = ring[ring.length - 1];
|
|
32
|
+
if (first && last && equalsVec2(first, last, eps)) {
|
|
33
|
+
return ring.slice(0, -1).map(cloneVec2);
|
|
34
|
+
}
|
|
35
|
+
return ring.map(cloneVec2);
|
|
36
|
+
}
|
|
37
|
+
function clonePath(p) {
|
|
38
|
+
return {
|
|
39
|
+
closed: p.closed,
|
|
40
|
+
rings: p.rings.map((ring) => ring.map(cloneVec2))
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/types/group.ts
|
|
45
|
+
function group(paths = []) {
|
|
46
|
+
return { paths: paths.map(clonePath) };
|
|
47
|
+
}
|
|
48
|
+
var DEFAULT_FILL_RULE = FillRule.NonZero;
|
|
49
|
+
function ringToPathD(ring) {
|
|
50
|
+
const pts = [];
|
|
51
|
+
for (const v of normalizeRing(ring)) {
|
|
52
|
+
pts.push({ x: v.x, y: v.y, z: 0 });
|
|
53
|
+
}
|
|
54
|
+
return pts;
|
|
55
|
+
}
|
|
56
|
+
function pathToPathsD(p) {
|
|
57
|
+
if (!p.closed || p.rings.length === 0) {
|
|
58
|
+
throw new Error("Boolean ops require a closed path with at least one ring");
|
|
59
|
+
}
|
|
60
|
+
return p.rings.map((ring) => ringToPathD(ring));
|
|
61
|
+
}
|
|
62
|
+
function pathsDToGroup(paths) {
|
|
63
|
+
if (paths.length === 0) return group([]);
|
|
64
|
+
const outers = [];
|
|
65
|
+
const holes = [];
|
|
66
|
+
for (const pd of paths) {
|
|
67
|
+
const ring = pathDToRing(pd);
|
|
68
|
+
if (ring.length < 3) continue;
|
|
69
|
+
if (isPositiveD(pd)) {
|
|
70
|
+
outers.push({ ring, holes: [] });
|
|
71
|
+
} else {
|
|
72
|
+
holes.push(ring);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for (const hole of holes) {
|
|
76
|
+
const sample = hole[0];
|
|
77
|
+
if (!sample) continue;
|
|
78
|
+
let assigned = false;
|
|
79
|
+
for (const outer of outers) {
|
|
80
|
+
if (pointInRing(sample, outer.ring)) {
|
|
81
|
+
outer.holes.push(hole);
|
|
82
|
+
assigned = true;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!assigned) {
|
|
87
|
+
outers.push({ ring: [...hole].reverse(), holes: [] });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return group(outers.map((o) => path([o.ring, ...o.holes], true)));
|
|
91
|
+
}
|
|
92
|
+
function pathDToRing(pd) {
|
|
93
|
+
return normalizeRing(pd.map((p) => vec2(p.x, p.y)));
|
|
94
|
+
}
|
|
95
|
+
function pointInRing(point, ring) {
|
|
96
|
+
const result = pointInPolygonD(
|
|
97
|
+
{ x: point.x, y: point.y, z: 0 },
|
|
98
|
+
ringToPathD(ring)
|
|
99
|
+
);
|
|
100
|
+
return result === PointInPolygonResult.IsInside || result === PointInPolygonResult.IsOn;
|
|
101
|
+
}
|
|
102
|
+
function clipUnion(a, b) {
|
|
103
|
+
return unionD(a, b, DEFAULT_FILL_RULE);
|
|
104
|
+
}
|
|
105
|
+
function clipDifference(a, b) {
|
|
106
|
+
return differenceD(a, b, DEFAULT_FILL_RULE);
|
|
107
|
+
}
|
|
108
|
+
function pathsArea(paths) {
|
|
109
|
+
let sum = 0;
|
|
110
|
+
for (const p of paths) {
|
|
111
|
+
sum += Math.abs(areaD(p));
|
|
112
|
+
}
|
|
113
|
+
return sum;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/predicates/index.ts
|
|
117
|
+
function ringArea(ring) {
|
|
118
|
+
if (ring.length < 3) return 0;
|
|
119
|
+
let sum = 0;
|
|
120
|
+
for (let i = 0; i < ring.length; i++) {
|
|
121
|
+
const a = ring[i];
|
|
122
|
+
const b = ring[(i + 1) % ring.length];
|
|
123
|
+
sum += a.x * b.y - b.x * a.y;
|
|
124
|
+
}
|
|
125
|
+
return sum / 2;
|
|
126
|
+
}
|
|
127
|
+
function area(p) {
|
|
128
|
+
if (!p.closed || p.rings.length === 0) return 0;
|
|
129
|
+
try {
|
|
130
|
+
return pathsArea(pathToPathsD(p));
|
|
131
|
+
} catch {
|
|
132
|
+
let total = 0;
|
|
133
|
+
for (let i = 0; i < p.rings.length; i++) {
|
|
134
|
+
const a = Math.abs(ringArea(p.rings[i]));
|
|
135
|
+
total += i === 0 ? a : -a;
|
|
136
|
+
}
|
|
137
|
+
return Math.max(0, total);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/shapeBoolean/index.ts
|
|
142
|
+
function ensureClosed(p) {
|
|
143
|
+
if (!p.closed) {
|
|
144
|
+
throw new Error("shapeBoolean requires closed paths");
|
|
145
|
+
}
|
|
146
|
+
return p;
|
|
147
|
+
}
|
|
148
|
+
function union(a, b) {
|
|
149
|
+
const result = clipUnion(pathToPathsD(ensureClosed(a)), pathToPathsD(ensureClosed(b)));
|
|
150
|
+
return pathsDToGroup(result);
|
|
151
|
+
}
|
|
152
|
+
function subtractAll(base, shapes) {
|
|
153
|
+
let acc = pathToPathsD(ensureClosed(base));
|
|
154
|
+
for (const s of shapes) {
|
|
155
|
+
acc = clipDifference(acc, pathToPathsD(ensureClosed(s)));
|
|
156
|
+
}
|
|
157
|
+
return pathsDToGroup(acc);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/voronoi/index.ts
|
|
161
|
+
function compoundVoronoi(sites, options = {}) {
|
|
162
|
+
if (sites.length === 0) return group([]);
|
|
163
|
+
if (sites.length === 1) {
|
|
164
|
+
const b2 = options.bounds ?? padBounds(sites, 1);
|
|
165
|
+
return group([
|
|
166
|
+
polygon([
|
|
167
|
+
{ x: b2.minX, y: b2.minY },
|
|
168
|
+
{ x: b2.maxX, y: b2.minY },
|
|
169
|
+
{ x: b2.maxX, y: b2.maxY },
|
|
170
|
+
{ x: b2.minX, y: b2.maxY }
|
|
171
|
+
])
|
|
172
|
+
]);
|
|
173
|
+
}
|
|
174
|
+
const b = options.bounds ?? padBounds(sites, 0.1);
|
|
175
|
+
const delaunay = Delaunay.from(
|
|
176
|
+
sites,
|
|
177
|
+
(d) => d.x,
|
|
178
|
+
(d) => d.y
|
|
179
|
+
);
|
|
180
|
+
const voronoi = delaunay.voronoi([b.minX, b.minY, b.maxX, b.maxY]);
|
|
181
|
+
const paths = [];
|
|
182
|
+
for (let i = 0; i < sites.length; i++) {
|
|
183
|
+
const poly = voronoi.cellPolygon(i);
|
|
184
|
+
if (!poly || poly.length < 3) continue;
|
|
185
|
+
const ring = poly.slice(0, -1).map(([x, y]) => ({ x, y }));
|
|
186
|
+
if (ring.length >= 3) paths.push(polygon(ring));
|
|
187
|
+
}
|
|
188
|
+
return group(paths);
|
|
189
|
+
}
|
|
190
|
+
function padBounds(sites, padRatio) {
|
|
191
|
+
let minX = Infinity;
|
|
192
|
+
let minY = Infinity;
|
|
193
|
+
let maxX = -Infinity;
|
|
194
|
+
let maxY = -Infinity;
|
|
195
|
+
for (const s of sites) {
|
|
196
|
+
minX = Math.min(minX, s.x);
|
|
197
|
+
minY = Math.min(minY, s.y);
|
|
198
|
+
maxX = Math.max(maxX, s.x);
|
|
199
|
+
maxY = Math.max(maxY, s.y);
|
|
200
|
+
}
|
|
201
|
+
const pad = Math.max(maxX - minX, maxY - minY, 1) * padRatio + 1e-6;
|
|
202
|
+
return {
|
|
203
|
+
minX: minX - pad,
|
|
204
|
+
minY: minY - pad,
|
|
205
|
+
maxX: maxX + pad,
|
|
206
|
+
maxY: maxY + pad
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function buffer(path2, delta, options = {}) {
|
|
210
|
+
const subject = pathToPathsD(path2);
|
|
211
|
+
const join = options.join === "miter" ? JoinType.Miter : options.join === "square" ? JoinType.Square : JoinType.Round;
|
|
212
|
+
const inflated = inflatePathsD(subject, delta, join, EndType.Polygon);
|
|
213
|
+
return pathsDToGroup(inflated);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/morphology/index.ts
|
|
217
|
+
function chaikinCut(p, iterations = 1, ratio = 0.25) {
|
|
218
|
+
const r = Math.min(0.49, Math.max(0.01, ratio));
|
|
219
|
+
let rings = p.rings;
|
|
220
|
+
for (let i = 0; i < Math.max(1, Math.floor(iterations)); i++) {
|
|
221
|
+
rings = rings.map((ring) => chaikinRing(ring, r, p.closed));
|
|
222
|
+
}
|
|
223
|
+
return path(rings, p.closed);
|
|
224
|
+
}
|
|
225
|
+
function smooth(p, iterations = 2) {
|
|
226
|
+
return chaikinCut(p, iterations);
|
|
227
|
+
}
|
|
228
|
+
function chaikinRing(ring, ratio, closed) {
|
|
229
|
+
if (ring.length < 2) return ring.map((v) => ({ ...v }));
|
|
230
|
+
const out = [];
|
|
231
|
+
const n = ring.length;
|
|
232
|
+
const edges = closed ? n : n - 1;
|
|
233
|
+
for (let i = 0; i < edges; i++) {
|
|
234
|
+
const a = ring[i];
|
|
235
|
+
const b = ring[(i + 1) % n];
|
|
236
|
+
out.push({
|
|
237
|
+
x: a.x * (1 - ratio) + b.x * ratio,
|
|
238
|
+
y: a.y * (1 - ratio) + b.y * ratio
|
|
239
|
+
});
|
|
240
|
+
out.push({
|
|
241
|
+
x: a.x * ratio + b.x * (1 - ratio),
|
|
242
|
+
y: a.y * ratio + b.y * (1 - ratio)
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
if (!closed) {
|
|
246
|
+
out.unshift({ ...ring[0] });
|
|
247
|
+
out.push({ ...ring[n - 1] });
|
|
248
|
+
}
|
|
249
|
+
return closed ? normalizeRing(out) : out;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/meshing/index.ts
|
|
253
|
+
function edgeKey(a, b) {
|
|
254
|
+
const aKey = `${a.x},${a.y}`;
|
|
255
|
+
const bKey = `${b.x},${b.y}`;
|
|
256
|
+
return aKey < bKey ? `${aKey}|${bKey}` : `${bKey}|${aKey}`;
|
|
257
|
+
}
|
|
258
|
+
function vertKey(v) {
|
|
259
|
+
return `${v.x},${v.y}`;
|
|
260
|
+
}
|
|
261
|
+
function unique(points) {
|
|
262
|
+
const seen = /* @__PURE__ */ new Set();
|
|
263
|
+
const out = [];
|
|
264
|
+
for (const p of points) {
|
|
265
|
+
const k = vertKey(p);
|
|
266
|
+
if (seen.has(k)) continue;
|
|
267
|
+
seen.add(k);
|
|
268
|
+
out.push({ ...p });
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
function cross(o, a, b) {
|
|
273
|
+
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
|
|
274
|
+
}
|
|
275
|
+
function ringOf(face) {
|
|
276
|
+
return face.rings[0] ?? [];
|
|
277
|
+
}
|
|
278
|
+
function buildEdgeMap(faces) {
|
|
279
|
+
const edges = /* @__PURE__ */ new Map();
|
|
280
|
+
faces.forEach((f, fi) => {
|
|
281
|
+
const ring = ringOf(f);
|
|
282
|
+
if (ring.length < 2) return;
|
|
283
|
+
const n = ring.length;
|
|
284
|
+
const closed = f.closed !== false && n >= 3;
|
|
285
|
+
const lim = closed ? n : n - 1;
|
|
286
|
+
for (let i = 0; i < lim; i++) {
|
|
287
|
+
const a = ring[i];
|
|
288
|
+
const b = ring[(i + 1) % n];
|
|
289
|
+
const key = edgeKey(a, b);
|
|
290
|
+
const rec = edges.get(key);
|
|
291
|
+
if (rec) rec.faceIds.push(fi);
|
|
292
|
+
else edges.set(key, { a: { ...a }, b: { ...b }, faceIds: [fi] });
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
return edges;
|
|
296
|
+
}
|
|
297
|
+
function faceAdjacency(faces) {
|
|
298
|
+
const edgeMap = buildEdgeMap(faces);
|
|
299
|
+
const adj = faces.map(() => []);
|
|
300
|
+
for (const e of edgeMap.values()) {
|
|
301
|
+
if (e.faceIds.length === 2) {
|
|
302
|
+
const [i, j] = e.faceIds;
|
|
303
|
+
adj[i].push(j);
|
|
304
|
+
adj[j].push(i);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return adj;
|
|
308
|
+
}
|
|
309
|
+
function stochasticMerge(faces, nClasses, seed = 1) {
|
|
310
|
+
if (faces.length === 0) return group([]);
|
|
311
|
+
const classes = Math.max(1, Math.floor(nClasses));
|
|
312
|
+
const rng = mulberry32(seed >>> 0);
|
|
313
|
+
const label = faces.map(() => Math.floor(rng() * classes));
|
|
314
|
+
const adj = faceAdjacency(faces);
|
|
315
|
+
const parent = faces.map((_, i) => i);
|
|
316
|
+
const find = (i) => {
|
|
317
|
+
let x = i;
|
|
318
|
+
while (parent[x] !== x) {
|
|
319
|
+
parent[x] = parent[parent[x]];
|
|
320
|
+
x = parent[x];
|
|
321
|
+
}
|
|
322
|
+
return x;
|
|
323
|
+
};
|
|
324
|
+
const unite = (a, b) => {
|
|
325
|
+
const ra = find(a);
|
|
326
|
+
const rb = find(b);
|
|
327
|
+
if (ra !== rb) parent[rb] = ra;
|
|
328
|
+
};
|
|
329
|
+
for (let i = 0; i < faces.length; i++) {
|
|
330
|
+
for (const j of adj[i]) {
|
|
331
|
+
if (j > i && label[i] === label[j]) unite(i, j);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const comps = /* @__PURE__ */ new Map();
|
|
335
|
+
for (let i = 0; i < faces.length; i++) {
|
|
336
|
+
const r = find(i);
|
|
337
|
+
const list = comps.get(r) ?? [];
|
|
338
|
+
list.push(i);
|
|
339
|
+
comps.set(r, list);
|
|
340
|
+
}
|
|
341
|
+
const out = [];
|
|
342
|
+
for (const members of comps.values()) {
|
|
343
|
+
if (members.length === 1) {
|
|
344
|
+
out.push(faces[members[0]]);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const merged = dissolveFaces(members.map((i) => faces[i]));
|
|
348
|
+
out.push(...merged);
|
|
349
|
+
}
|
|
350
|
+
return group(out);
|
|
351
|
+
}
|
|
352
|
+
function dissolveFaces(faces) {
|
|
353
|
+
if (faces.length === 0) return [];
|
|
354
|
+
if (faces.length === 1) return [faces[0]];
|
|
355
|
+
try {
|
|
356
|
+
let acc = faces[0];
|
|
357
|
+
for (let i = 1; i < faces.length; i++) {
|
|
358
|
+
const u = union(acc, faces[i]);
|
|
359
|
+
if (u.paths.length === 0) continue;
|
|
360
|
+
acc = u.paths.reduce(
|
|
361
|
+
(best, p) => area(p) > area(best) ? p : best
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
return [acc];
|
|
365
|
+
} catch {
|
|
366
|
+
return [convexHullOf(faces.flatMap((f) => ringOf(f)))];
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function convexHullOf(points) {
|
|
370
|
+
const pts = unique(points);
|
|
371
|
+
if (pts.length < 3) return polygon(pts);
|
|
372
|
+
pts.sort((a, b) => a.x === b.x ? a.y - b.y : a.x - b.x);
|
|
373
|
+
const lower = [];
|
|
374
|
+
for (const p of pts) {
|
|
375
|
+
while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) {
|
|
376
|
+
lower.pop();
|
|
377
|
+
}
|
|
378
|
+
lower.push(p);
|
|
379
|
+
}
|
|
380
|
+
const upper = [];
|
|
381
|
+
for (let i = pts.length - 1; i >= 0; i--) {
|
|
382
|
+
const p = pts[i];
|
|
383
|
+
while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) {
|
|
384
|
+
upper.pop();
|
|
385
|
+
}
|
|
386
|
+
upper.push(p);
|
|
387
|
+
}
|
|
388
|
+
lower.pop();
|
|
389
|
+
upper.pop();
|
|
390
|
+
return polygon([...lower, ...upper]);
|
|
391
|
+
}
|
|
392
|
+
function mulberry32(a) {
|
|
393
|
+
return () => {
|
|
394
|
+
a |= 0;
|
|
395
|
+
a = a + 1831565813 | 0;
|
|
396
|
+
let t = Math.imul(a ^ a >>> 15, 1 | a);
|
|
397
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
398
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
399
|
+
};
|
|
400
|
+
}
|
|
19
401
|
|
|
20
402
|
// src/construction/index.ts
|
|
21
403
|
function createCircle(cx, cy, radius, segments = 64) {
|
|
@@ -71,6 +453,50 @@ function createKochSnowflake(cx, cy, radius, iterations = 3) {
|
|
|
71
453
|
}
|
|
72
454
|
return polygon(ring);
|
|
73
455
|
}
|
|
456
|
+
function createSponge(width, height, generators, thickness, smoothing, classes, seed = 1) {
|
|
457
|
+
const w = Math.max(width, 1e-9);
|
|
458
|
+
const h = Math.max(height, 1e-9);
|
|
459
|
+
const nGen = Math.max(6, Math.floor(generators));
|
|
460
|
+
const nClasses = Math.max(1, Math.floor(classes));
|
|
461
|
+
const smoothIters = Math.max(0, Math.floor(smoothing));
|
|
462
|
+
const wall = Math.max(0, thickness);
|
|
463
|
+
const sites = randomInBox(nGen, 0, 0, w, h, seed);
|
|
464
|
+
const cells = compoundVoronoi(sites, {
|
|
465
|
+
bounds: { minX: 0, minY: 0, maxX: w, maxY: h }
|
|
466
|
+
});
|
|
467
|
+
const merged = stochasticMerge(cells.paths, nClasses, seed + 1);
|
|
468
|
+
const pores = [];
|
|
469
|
+
for (let p of merged.paths) {
|
|
470
|
+
if (smoothIters > 0) p = smooth(p, smoothIters);
|
|
471
|
+
if (wall > 1e-12) {
|
|
472
|
+
pores.push(...buffer(p, -wall * 0.5).paths);
|
|
473
|
+
} else {
|
|
474
|
+
pores.push(p);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
const frame = createRect(0, 0, w, h);
|
|
478
|
+
if (pores.length === 0) return group([frame]);
|
|
479
|
+
return subtractAll(frame, pores);
|
|
480
|
+
}
|
|
481
|
+
function randomInBox(count, minX, minY, maxX, maxY, seed) {
|
|
482
|
+
const rng = mulberry322(seed >>> 0);
|
|
483
|
+
const out = [];
|
|
484
|
+
for (let i = 0; i < count; i++) {
|
|
485
|
+
out.push({
|
|
486
|
+
x: minX + rng() * (maxX - minX),
|
|
487
|
+
y: minY + rng() * (maxY - minY)
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
return out;
|
|
491
|
+
}
|
|
492
|
+
function mulberry322(a) {
|
|
493
|
+
return function() {
|
|
494
|
+
let t = a += 1831565813;
|
|
495
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
496
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
497
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
498
|
+
};
|
|
499
|
+
}
|
|
74
500
|
function kochRefine(ring) {
|
|
75
501
|
const out = [];
|
|
76
502
|
const n = ring.length;
|
|
@@ -94,9 +520,10 @@ var construction = {
|
|
|
94
520
|
createRing,
|
|
95
521
|
createArc,
|
|
96
522
|
createStar,
|
|
97
|
-
createKochSnowflake
|
|
523
|
+
createKochSnowflake,
|
|
524
|
+
createSponge
|
|
98
525
|
};
|
|
99
526
|
|
|
100
|
-
export { construction, createArc, createCircle, createKochSnowflake, createRect, createRegularPolygon, createRing, createStar };
|
|
527
|
+
export { construction, createArc, createCircle, createKochSnowflake, createRect, createRegularPolygon, createRing, createSponge, createStar };
|
|
101
528
|
//# sourceMappingURL=index.js.map
|
|
102
529
|
//# sourceMappingURL=index.js.map
|