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/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,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
|