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.
Files changed (46) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +0 -43
  3. package/dist/{circle-BDbKfrdR.d.ts → circle-CauDMv8V.d.ts} +1 -1
  4. package/dist/circlePacking/index.d.ts +2 -3
  5. package/dist/construction/index.d.ts +11 -3
  6. package/dist/construction/index.js +429 -2
  7. package/dist/construction/index.js.map +1 -1
  8. package/dist/contour/index.d.ts +90 -4
  9. package/dist/contour/index.js +1326 -3
  10. package/dist/contour/index.js.map +1 -1
  11. package/dist/conversion/index.d.ts +2 -3
  12. package/dist/{group-BNjVVYSQ.d.ts → group-YVmSJuGo.d.ts} +1 -1
  13. package/dist/hatch/index.d.ts +70 -4
  14. package/dist/hatch/index.js +414 -3
  15. package/dist/hatch/index.js.map +1 -1
  16. package/dist/hull/index.d.ts +1 -2
  17. package/dist/index.d.ts +13 -14
  18. package/dist/index.js +2228 -632
  19. package/dist/index.js.map +1 -1
  20. package/dist/meshing/index.d.ts +3 -4
  21. package/dist/morphology/index.d.ts +2 -3
  22. package/dist/optimisation/index.d.ts +2 -3
  23. package/dist/{path-0RjhYjJs.d.ts → path-Boeshk9L.d.ts} +9 -2
  24. package/dist/pointSet/index.d.ts +15 -2
  25. package/dist/pointSet/index.js +148 -2
  26. package/dist/pointSet/index.js.map +1 -1
  27. package/dist/polygonisation/index.d.ts +1 -2
  28. package/dist/polygonisation/index.js +54 -50
  29. package/dist/polygonisation/index.js.map +1 -1
  30. package/dist/predicates/index.d.ts +2 -3
  31. package/dist/processing/index.d.ts +3 -4
  32. package/dist/{segment-BKuIFMKD.d.ts → segment-CpLILSd7.d.ts} +1 -1
  33. package/dist/segmentSet/index.d.ts +41 -3
  34. package/dist/segmentSet/index.js +261 -1
  35. package/dist/segmentSet/index.js.map +1 -1
  36. package/dist/shapeBoolean/index.d.ts +2 -3
  37. package/dist/svg/index.d.ts +2 -3
  38. package/dist/tiling/index.d.ts +10 -4
  39. package/dist/tiling/index.js +148 -13
  40. package/dist/tiling/index.js.map +1 -1
  41. package/dist/transformation/index.d.ts +1 -2
  42. package/dist/triangulation/index.d.ts +1 -2
  43. package/dist/triangulation/index.js.map +1 -1
  44. package/dist/voronoi/index.d.ts +2 -3
  45. package/package.json +2 -2
  46. package/dist/vec2-CWyXN1Wj.d.ts +0 -10
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 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.
8
+
9
+ ## 0.3.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 1c203c2: Add contour skeletons and fields: medialAxis (with pruning), chordalAxis, straightSkeleton, centerLine, distanceField, contrastField, distanceTree, and isoline helpers.
14
+
15
+ ## 0.2.2
16
+
17
+ ### Patch Changes
18
+
19
+ - Add docs site favicon, OG/Twitter preview image, and social meta tags.
20
+
3
21
  ## 0.2.1
4
22
 
5
23
  ### Patch Changes
package/README.md CHANGED
@@ -58,49 +58,6 @@ pnpm docs:examples # regenerate SVG figures
58
58
  | `pattapatta/voronoi` | Voronoi cells |
59
59
  | … | See `website/` API pages |
60
60
 
61
- ## Releasing
62
-
63
- This repo uses [Changesets](https://github.com/changesets/changesets). Only the root `pattapatta` package is published; `website` is ignored. CI publishes via [npm trusted publishing (OIDC)](https://docs.npmjs.com/trusted-publishers) — no long-lived `NPM_TOKEN` (2FA-bypass tokens are being deprecated for direct publish; see [npm changelog](https://github.blog/changelog/2026-07-08-npm-install-time-security-and-gat-bypass2fa-deprecation/)).
64
-
65
- ### One-time npm setup
66
-
67
- 1. If the package is not on npm yet, publish once locally (trusted publishers can only be configured on an existing package):
68
- ```bash
69
- pnpm build && npm publish --access public --otp=<code>
70
- ```
71
- 2. On [npmjs.com](https://www.npmjs.com/) → **pattapatta** → **Settings** → **Trusted Publisher** → **GitHub Actions**:
72
- - **Organization or user:** `MadOrkestra`
73
- - **Repository:** `pattapatta`
74
- - **Workflow filename:** `release.yml` (filename only)
75
- - **Allowed actions:** include **`npm publish`** (not stage-only — Changesets publishes directly)
76
- 3. Optionally under **Publishing access**, require 2FA and disallow tokens once OIDC works.
77
- 4. Remove any repo `NPM_TOKEN` secret if it was only used for publish.
78
-
79
- ### Routine releases
80
-
81
- 1. In a PR that changes the library, run `pnpm changeset`, commit the file under `.changeset/`, and merge to `main`.
82
- 2. The **Release** workflow opens or updates a **Version Packages** PR (bumps version, updates `CHANGELOG.md`).
83
- 3. Merging that PR publishes to npm (OIDC + provenance) and creates a GitHub Release.
84
- Publish must use **`npm publish`** (not `pnpm publish`) — OIDC only works with the npm CLI ([docs](https://docs.npmjs.com/trusted-publishers)).
85
-
86
- Current package version is in `package.json` (e.g. `0.2.0`). If publish failed after a version bump, fix the trusted-publisher config (workflow name must be exactly `release.yml`) and re-run **Release** — Changesets will publish any version not yet on npm.
87
-
88
- If CI fails with **`ENEEDAUTH`** or **`E404 Not Found - PUT …/pattapatta`**, treat it as auth failure (npm hides 401 as 404). Check:
89
-
90
- 1. Trusted Publisher on npmjs.com matches exactly: `MadOrkestra` / `pattapatta` / `release.yml`, and **`npm publish` is allowed** (new configs default to stage-only).
91
- 2. Release uses `scripts/publish-oidc.sh` → `npm publish` (Changesets would otherwise pick `pnpm publish`, which cannot OIDC).
92
- 3. Job has `id-token: write` and runs on a **GitHub-hosted** runner.
93
- 4. `package.json` `repository.url` matches `https://github.com/MadOrkestra/pattapatta.git`.
94
-
95
- Local checks used by CI:
96
-
97
- ```bash
98
- pnpm install --frozen-lockfile
99
- pnpm test
100
- pnpm typecheck
101
- pnpm build
102
- ```
103
-
104
61
  ## License
105
62
 
106
63
  MIT — clean-room implementation (not a GPL line-port of PGS). See `docs/decisions/`.
@@ -1,4 +1,4 @@
1
- import { V as Vec2 } from './vec2-CWyXN1Wj.js';
1
+ import { V as Vec2 } from './path-Boeshk9L.js';
2
2
 
3
3
  /** Circle with center (x, y) and radius r (PGS often stored radius in PVector.z). */
4
4
  type Circle = {
@@ -1,6 +1,5 @@
1
- import { V as Vec2 } from '../vec2-CWyXN1Wj.js';
2
- import { C as Circle } from '../circle-BDbKfrdR.js';
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-0RjhYjJs.js';
2
- import '../vec2-CWyXN1Wj.js';
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