pattapatta 0.2.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 (74) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +98 -0
  4. package/dist/circle-BDbKfrdR.d.ts +12 -0
  5. package/dist/circlePacking/index.d.ts +78 -0
  6. package/dist/circlePacking/index.js +381 -0
  7. package/dist/circlePacking/index.js.map +1 -0
  8. package/dist/cli.js +365 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/construction/index.d.ts +26 -0
  11. package/dist/construction/index.js +102 -0
  12. package/dist/construction/index.js.map +1 -0
  13. package/dist/contour/index.d.ts +20 -0
  14. package/dist/contour/index.js +118 -0
  15. package/dist/contour/index.js.map +1 -0
  16. package/dist/conversion/index.d.ts +31 -0
  17. package/dist/conversion/index.js +99 -0
  18. package/dist/conversion/index.js.map +1 -0
  19. package/dist/group-BNjVVYSQ.d.ts +13 -0
  20. package/dist/hatch/index.d.ts +41 -0
  21. package/dist/hatch/index.js +253 -0
  22. package/dist/hatch/index.js.map +1 -0
  23. package/dist/hull/index.d.ts +16 -0
  24. package/dist/hull/index.js +97 -0
  25. package/dist/hull/index.js.map +1 -0
  26. package/dist/index.d.ts +67 -0
  27. package/dist/index.js +3933 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/meshing/index.d.ts +125 -0
  30. package/dist/meshing/index.js +1333 -0
  31. package/dist/meshing/index.js.map +1 -0
  32. package/dist/morphology/index.d.ts +58 -0
  33. package/dist/morphology/index.js +268 -0
  34. package/dist/morphology/index.js.map +1 -0
  35. package/dist/optimisation/index.d.ts +43 -0
  36. package/dist/optimisation/index.js +400 -0
  37. package/dist/optimisation/index.js.map +1 -0
  38. package/dist/path-0RjhYjJs.d.ts +21 -0
  39. package/dist/pointSet/index.d.ts +24 -0
  40. package/dist/pointSet/index.js +139 -0
  41. package/dist/pointSet/index.js.map +1 -0
  42. package/dist/polygonisation/index.d.ts +44 -0
  43. package/dist/polygonisation/index.js +310 -0
  44. package/dist/polygonisation/index.js.map +1 -0
  45. package/dist/predicates/index.d.ts +35 -0
  46. package/dist/predicates/index.js +159 -0
  47. package/dist/predicates/index.js.map +1 -0
  48. package/dist/processing/index.d.ts +54 -0
  49. package/dist/processing/index.js +457 -0
  50. package/dist/processing/index.js.map +1 -0
  51. package/dist/segment-BKuIFMKD.d.ts +10 -0
  52. package/dist/segmentSet/index.d.ts +25 -0
  53. package/dist/segmentSet/index.js +109 -0
  54. package/dist/segmentSet/index.js.map +1 -0
  55. package/dist/shapeBoolean/index.d.ts +41 -0
  56. package/dist/shapeBoolean/index.js +211 -0
  57. package/dist/shapeBoolean/index.js.map +1 -0
  58. package/dist/svg/index.d.ts +38 -0
  59. package/dist/svg/index.js +317 -0
  60. package/dist/svg/index.js.map +1 -0
  61. package/dist/tiling/index.d.ts +32 -0
  62. package/dist/tiling/index.js +344 -0
  63. package/dist/tiling/index.js.map +1 -0
  64. package/dist/transformation/index.d.ts +37 -0
  65. package/dist/transformation/index.js +184 -0
  66. package/dist/transformation/index.js.map +1 -0
  67. package/dist/triangulation/index.d.ts +48 -0
  68. package/dist/triangulation/index.js +334 -0
  69. package/dist/triangulation/index.js.map +1 -0
  70. package/dist/vec2-CWyXN1Wj.d.ts +10 -0
  71. package/dist/voronoi/index.d.ts +31 -0
  72. package/dist/voronoi/index.js +211 -0
  73. package/dist/voronoi/index.js.map +1 -0
  74. package/package.json +165 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e272ce5: Centered lattice packing with clipped overlap fills, hatch counts that cover the full path, poisson/refine triangulation, and expanded meshing (Urquhart/spanner graphs, quadrangulation, smooth/subdivide/simplify, merge and topology fixes).
8
+
9
+ All notable changes to this project will be documented in this file.
10
+
11
+ The format is maintained by [Changesets](https://github.com/changesets/changesets).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pattapatta contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # pattapatta
2
+
3
+ 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
+
5
+ **Stroke marks only** — solid area fills are not the goal.
6
+
7
+ [![npm](https://img.shields.io/npm/v/pattapatta.svg)](https://www.npmjs.com/package/pattapatta)
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pnpm add pattapatta
13
+ ```
14
+
15
+ Requires **Node ≥ 18** (ESM). Works in modern browsers via bundlers.
16
+
17
+ ## Quick example
18
+
19
+ ```ts
20
+ import { createRect, union, hatchParallel, toSvg, group, segmentsToOpenPaths } from 'pattapatta'
21
+
22
+ const a = createRect(0, 0, 2, 1)
23
+ const b = createRect(1, 0, 2, 1)
24
+ const merged = union(a, b)
25
+ const strokes = hatchParallel(merged.paths[0]!, { spacing: 0.08, count: 40 })
26
+ console.log(toSvg(group(segmentsToOpenPaths(strokes))))
27
+ ```
28
+
29
+ ```bash
30
+ pnpm dlx pattapatta --help
31
+ pnpm dlx pattapatta svg:roundtrip in.svg -o out.svg
32
+ ```
33
+
34
+ ## Docs site
35
+
36
+ Live: [pattapatta.madorkestra.com](https://pattapatta.madorkestra.com) (GitHub Pages; deploys on push to `main`).
37
+
38
+ Local documentation PWA (SvelteKit + shadcn):
39
+
40
+ ```bash
41
+ pnpm install
42
+ pnpm docs:dev
43
+ pnpm docs:build
44
+ pnpm docs:examples # regenerate SVG figures
45
+ ```
46
+
47
+ ## Modules
48
+
49
+ | Import | Role |
50
+ |--------|------|
51
+ | `pattapatta` | Types, SVG, all facades |
52
+ | `pattapatta/shapeBoolean` | Union / intersect / subtract / occlusion |
53
+ | `pattapatta/hatch` | Parallel / cross hatch fills |
54
+ | `pattapatta/circlePacking` | Lattices (`overlap` \| `contained`), LEC, stochastic |
55
+ | `pattapatta/triangulation` | Earcut, Delaunay, Poisson Steiner, refine |
56
+ | `pattapatta/morphology` | Buffer, simplify, warps |
57
+ | `pattapatta/meshing` | Graph faces, quadrangulation, mesh process / repair |
58
+ | `pattapatta/voronoi` | Voronoi cells |
59
+ | … | See `website/` API pages |
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
+
85
+ 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.
86
+
87
+ Local checks used by CI:
88
+
89
+ ```bash
90
+ pnpm install --frozen-lockfile
91
+ pnpm test
92
+ pnpm typecheck
93
+ pnpm build
94
+ ```
95
+
96
+ ## License
97
+
98
+ MIT — clean-room implementation (not a GPL line-port of PGS). See `docs/decisions/`.
@@ -0,0 +1,12 @@
1
+ import { V as Vec2 } from './vec2-CWyXN1Wj.js';
2
+
3
+ /** Circle with center (x, y) and radius r (PGS often stored radius in PVector.z). */
4
+ type Circle = {
5
+ x: number;
6
+ y: number;
7
+ r: number;
8
+ };
9
+ declare function circle(x: number, y: number, r: number): Circle;
10
+ declare function circleCenter(c: Circle): Vec2;
11
+
12
+ export { type Circle as C, circleCenter as a, circle as c };
@@ -0,0 +1,78 @@
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';
4
+
5
+ /**
6
+ * Lattice inclusion mode.
7
+ * - `overlap` (default): full-path fill — keep disks that overlap the path;
8
+ * center the lattice so opposite-edge cutoffs match. Clip in SVG to cut at
9
+ * the boundary (including holes).
10
+ * - `contained`: keep only disks fully inside the path (when you must stroke
11
+ * complete circles); lattice centered with equal leftover margins.
12
+ */
13
+ type LatticePackMode = 'overlap' | 'contained';
14
+ /**
15
+ * Square lattice of equal circles.
16
+ * @see docs/decisions/0003-deferred-circle-packing.md
17
+ */
18
+ declare function squareLatticePack(path: Path, diameter: number, mode?: LatticePackMode): Circle[];
19
+ /**
20
+ * Hexagonal lattice of equal circles.
21
+ */
22
+ declare function hexLatticePack(path: Path, diameter: number, mode?: LatticePackMode): Circle[];
23
+ /**
24
+ * Seeded stochastic packing: sample random points, grow max non-overlapping
25
+ * radius (tangent to nearest circle or limited by distance to boundary).
26
+ * Triangulation Steiner mode deferred.
27
+ */
28
+ declare function stochasticPack(path: Path, points: number, minRadius: number, seed?: number): Circle[];
29
+ /**
30
+ * Pack up to `n` maximum inscribed circles via iterative largest-empty-circle
31
+ * search (grid + local refine). Circles are fully contained and non-overlapping.
32
+ * `tolerance` controls coarse grid step as a fraction of the bbox diagonal
33
+ * (PGS uses a related accuracy knob; values ~0.5–1 are reasonable).
34
+ *
35
+ * Deferred: `tangencyPack`, `trinscribedPack` — see ADR 0003.
36
+ */
37
+ declare function maximumInscribedPack(path: Path, n: number, tolerance?: number): Circle[];
38
+ /**
39
+ * Pack inscribed circles while radius stays ≥ `minRadius`.
40
+ */
41
+ declare function maximumInscribedPackUntil(path: Path, minRadius: number, tolerance?: number): Circle[];
42
+ /**
43
+ * Successive LECs inside `path`, treating `obstacles` as already-placed disks.
44
+ * Simpler than full PGS obstacle packing; sufficient for “avoid these circles.”
45
+ */
46
+ declare function obstaclePack(path: Path, obstacles: Circle[], n: number, tolerance?: number): Circle[];
47
+ /**
48
+ * Front-chain-inspired packing of (possibly varying) radii in the path envelope,
49
+ * then keep circles that overlap the path. Seeded for determinism.
50
+ * Grows until placement stalls (region effectively full); no fixed circle count.
51
+ * Not a line-by-line port of PGS FrontChainPacker — same role, approximate layout.
52
+ */
53
+ declare function frontChainPack(path: Path, radiusMin: number, radiusMax: number, seed?: number): Circle[];
54
+ /**
55
+ * Seed random circles in the envelope, then iteratively repulse overlaps;
56
+ * keep those that overlap the path (PGS repulsionPack role).
57
+ */
58
+ declare function repulsionPack(path: Path, radiusMin: number, radiusMax: number, seed?: number, iterations?: number): Circle[];
59
+ /** True if disk overlaps the filled path (including partial exterior overlap). */
60
+ declare function circleOverlapsPath(c: Circle, path: Path): boolean;
61
+ /** True if the disk lies fully inside the path. */
62
+ declare function circleContainedInPath(c: Circle, path: Path): boolean;
63
+ /** Distance from point to polygon: 0 if inside, else distance to boundary. */
64
+ declare function distanceToPolygon(p: Vec2, path: Path): number;
65
+ declare const circlePacking: {
66
+ squareLatticePack: typeof squareLatticePack;
67
+ hexLatticePack: typeof hexLatticePack;
68
+ stochasticPack: typeof stochasticPack;
69
+ maximumInscribedPack: typeof maximumInscribedPack;
70
+ maximumInscribedPackUntil: typeof maximumInscribedPackUntil;
71
+ obstaclePack: typeof obstaclePack;
72
+ frontChainPack: typeof frontChainPack;
73
+ repulsionPack: typeof repulsionPack;
74
+ circleOverlapsPath: typeof circleOverlapsPath;
75
+ circleContainedInPath: typeof circleContainedInPath;
76
+ };
77
+
78
+ export { type LatticePackMode, circleContainedInPath, circleOverlapsPath, circlePacking, distanceToPolygon, frontChainPack, hexLatticePack, maximumInscribedPack, maximumInscribedPackUntil, obstaclePack, repulsionPack, squareLatticePack, stochasticPack };
@@ -0,0 +1,381 @@
1
+ import { FillRule, pointInPolygonD, PointInPolygonResult } from 'clipper2-ts';
2
+
3
+ // src/types/vec2.ts
4
+ function cloneVec2(v) {
5
+ return { x: v.x, y: v.y };
6
+ }
7
+ function equalsVec2(a, b, eps = 1e-9) {
8
+ return Math.abs(a.x - b.x) <= eps && Math.abs(a.y - b.y) <= eps;
9
+ }
10
+
11
+ // src/types/circle.ts
12
+ function circle(x, y, r) {
13
+ return { x, y, r };
14
+ }
15
+
16
+ // src/types/path.ts
17
+ function normalizeRing(ring, eps = 1e-9) {
18
+ if (ring.length < 2) return ring.map(cloneVec2);
19
+ const first = ring[0];
20
+ const last = ring[ring.length - 1];
21
+ if (first && last && equalsVec2(first, last, eps)) {
22
+ return ring.slice(0, -1).map(cloneVec2);
23
+ }
24
+ return ring.map(cloneVec2);
25
+ }
26
+ FillRule.NonZero;
27
+ function ringToPathD(ring) {
28
+ const pts = [];
29
+ for (const v of normalizeRing(ring)) {
30
+ pts.push({ x: v.x, y: v.y, z: 0 });
31
+ }
32
+ return pts;
33
+ }
34
+ function pointInRing(point, ring) {
35
+ const result = pointInPolygonD(
36
+ { x: point.x, y: point.y, z: 0 },
37
+ ringToPathD(ring)
38
+ );
39
+ return result === PointInPolygonResult.IsInside || result === PointInPolygonResult.IsOn;
40
+ }
41
+
42
+ // src/predicates/index.ts
43
+ function containsPoint(p, point) {
44
+ if (!p.closed || !p.rings[0]) return false;
45
+ if (!pointInRing(point, p.rings[0])) return false;
46
+ for (let i = 1; i < p.rings.length; i++) {
47
+ const hole = p.rings[i];
48
+ if (hole && pointInRing(point, hole)) return false;
49
+ }
50
+ return true;
51
+ }
52
+ function bounds(p) {
53
+ let minX = Infinity;
54
+ let minY = Infinity;
55
+ let maxX = -Infinity;
56
+ let maxY = -Infinity;
57
+ for (const ring of p.rings) {
58
+ for (const v of ring) {
59
+ minX = Math.min(minX, v.x);
60
+ minY = Math.min(minY, v.y);
61
+ maxX = Math.max(maxX, v.x);
62
+ maxY = Math.max(maxY, v.y);
63
+ }
64
+ }
65
+ if (!Number.isFinite(minX)) {
66
+ return { minX: 0, minY: 0, maxX: 0, maxY: 0 };
67
+ }
68
+ return { minX, minY, maxX, maxY };
69
+ }
70
+
71
+ // src/circlePacking/index.ts
72
+ function squareLatticePack(path2, diameter, mode = "overlap") {
73
+ const d = Math.max(diameter, 0.1);
74
+ const r = d / 2;
75
+ const b = bounds(path2);
76
+ const out = [];
77
+ const accept = mode === "contained" ? circleContainedInPath : circleOverlapsPath;
78
+ const inset = mode === "contained" ? r : 0;
79
+ const { ox, oy, nX, nY } = centeredLatticeGrid(b, d, d, inset, d);
80
+ if (nX < 1 || nY < 1) return out;
81
+ for (let i = 0; i < nX; i++) {
82
+ for (let j = 0; j < nY; j++) {
83
+ const c = circle(ox + i * d, oy + j * d, r);
84
+ if (accept(c, path2)) out.push(c);
85
+ }
86
+ }
87
+ return out;
88
+ }
89
+ function hexLatticePack(path2, diameter, mode = "overlap") {
90
+ const d = Math.max(diameter, 0.1);
91
+ const r = d / 2;
92
+ const b = bounds(path2);
93
+ const z = r * Math.sqrt(3);
94
+ const out = [];
95
+ const accept = mode === "contained" ? circleContainedInPath : circleOverlapsPath;
96
+ const inset = mode === "contained" ? r : 0;
97
+ const { ox, nX, nY } = centeredLatticeGrid(b, z, d, inset, d);
98
+ if (nX < 1 || nY < 1) return out;
99
+ const innerH = b.maxY - b.minY - 2 * inset;
100
+ const extentY = (nY - 1) * d + r;
101
+ const oy = b.minY + inset + (innerH - extentY) / 2 + r;
102
+ let offset = 0;
103
+ for (let i = 0; i < nX; i++) {
104
+ offset = offset === r ? 0 : r;
105
+ for (let j = 0; j < nY; j++) {
106
+ const c = circle(ox + i * z, oy + j * d - offset, r);
107
+ if (accept(c, path2)) out.push(c);
108
+ }
109
+ }
110
+ return out;
111
+ }
112
+ function centeredLatticeGrid(b, stepX, stepY, inset, endPad) {
113
+ const innerW = b.maxX - b.minX - 2 * inset;
114
+ const innerH = b.maxY - b.minY - 2 * inset;
115
+ if (innerW < -1e-9 || innerH < -1e-9) {
116
+ return { ox: 0, oy: 0, nX: 0, nY: 0 };
117
+ }
118
+ let nX;
119
+ let nY;
120
+ if (inset > 0) {
121
+ nX = Math.floor(innerW / stepX + 1e-9) + 1;
122
+ nY = Math.floor(innerH / stepY + 1e-9) + 1;
123
+ } else {
124
+ nX = Math.floor((innerW + endPad) / stepX - 1e-12) + 1;
125
+ nY = Math.floor((innerH + endPad) / stepY - 1e-12) + 1;
126
+ }
127
+ const spanX = (nX - 1) * stepX;
128
+ const spanY = (nY - 1) * stepY;
129
+ const ox = b.minX + inset + (innerW - spanX) / 2;
130
+ const oy = b.minY + inset + (innerH - spanY) / 2;
131
+ return { ox, oy, nX, nY };
132
+ }
133
+ function stochasticPack(path2, points, minRadius, seed = 1) {
134
+ const rng = mulberry32(seed >>> 0);
135
+ const b = bounds(path2);
136
+ const packing = [];
137
+ for (let attempt = 0; attempt < points; attempt++) {
138
+ const p = vecInBounds(b, rng);
139
+ if (!containsPoint(path2, p)) continue;
140
+ const maxR = clearanceRadius(p, path2, packing);
141
+ if (maxR < minRadius) continue;
142
+ packing.push(circle(p.x, p.y, maxR));
143
+ }
144
+ return packing;
145
+ }
146
+ function maximumInscribedPack(path2, n, tolerance = 1) {
147
+ return obstaclePack(path2, [], n, tolerance);
148
+ }
149
+ function maximumInscribedPackUntil(path2, minRadius, tolerance = 1) {
150
+ const packing = [];
151
+ const tol = Math.max(0.01, tolerance);
152
+ const minR = Math.max(0.01, minRadius);
153
+ for (; ; ) {
154
+ const next = findLargestEmptyCircle(path2, packing, tol);
155
+ if (!next || next.r < minR) break;
156
+ packing.push(next);
157
+ }
158
+ return packing;
159
+ }
160
+ function obstaclePack(path2, obstacles, n, tolerance = 1) {
161
+ const packing = obstacles.map((c) => ({ ...c }));
162
+ const placed = [];
163
+ const tol = Math.max(0.01, tolerance);
164
+ for (let i = 0; i < n; i++) {
165
+ const next = findLargestEmptyCircle(path2, packing, tol);
166
+ if (!next || next.r <= 1e-9) break;
167
+ packing.push(next);
168
+ placed.push(next);
169
+ }
170
+ return placed;
171
+ }
172
+ function frontChainPack(path2, radiusMin, radiusMax, seed = 1) {
173
+ let rMin = Math.max(1e-6, Math.min(radiusMin, radiusMax));
174
+ let rMax = Math.max(rMin, Math.max(radiusMin, radiusMax));
175
+ const rng = mulberry32(seed >>> 0);
176
+ const b = bounds(path2);
177
+ const packing = [];
178
+ const area = Math.max(1e-6, (b.maxX - b.minX) * (b.maxY - b.minY));
179
+ const capacity = Math.max(
180
+ 4,
181
+ Math.ceil(area / (Math.PI * rMin * rMin) * 2)
182
+ );
183
+ const trySeed = () => {
184
+ const p = vecInBounds(b, rng);
185
+ const r = rMin + rng() * (rMax - rMin);
186
+ const c = circle(p.x, p.y, r);
187
+ if (!overlapsAny(c, packing) && circleOverlapsPath(c, path2)) {
188
+ packing.push(c);
189
+ return true;
190
+ }
191
+ return false;
192
+ };
193
+ for (let i = 0; i < 8; i++) trySeed();
194
+ let fails = 0;
195
+ while (packing.length < capacity) {
196
+ const stallLimit = Math.max(500, packing.length * 12);
197
+ if (fails >= stallLimit) break;
198
+ if (packing.length === 0) {
199
+ if (trySeed()) fails = 0;
200
+ else fails++;
201
+ continue;
202
+ }
203
+ if (fails > 0 && fails % 50 === 0) {
204
+ if (trySeed()) {
205
+ fails = 0;
206
+ continue;
207
+ }
208
+ }
209
+ const base = packing[Math.floor(rng() * packing.length)];
210
+ const ang = rng() * Math.PI * 2;
211
+ const r = rMin + rng() * (rMax - rMin);
212
+ const dist = base.r + r;
213
+ const c = circle(
214
+ base.x + Math.cos(ang) * dist,
215
+ base.y + Math.sin(ang) * dist,
216
+ r
217
+ );
218
+ if (c.x < b.minX - rMax || c.x > b.maxX + rMax || c.y < b.minY - rMax || c.y > b.maxY + rMax) {
219
+ fails++;
220
+ continue;
221
+ }
222
+ if (!overlapsAny(c, packing) && circleOverlapsPath(c, path2)) {
223
+ packing.push(c);
224
+ fails = 0;
225
+ } else {
226
+ fails++;
227
+ }
228
+ }
229
+ return packing.filter((c) => circleOverlapsPath(c, path2));
230
+ }
231
+ function repulsionPack(path2, radiusMin, radiusMax, seed = 1, iterations = 80) {
232
+ const rMin = Math.max(1e-6, Math.min(radiusMin, radiusMax));
233
+ const rMax = Math.max(rMin, Math.max(radiusMin, radiusMax));
234
+ const rng = mulberry32(seed >>> 0);
235
+ const b = bounds(path2);
236
+ const area = Math.max(1e-6, (b.maxX - b.minX) * (b.maxY - b.minY));
237
+ const avgR = (rMin + rMax) / 2;
238
+ const target = Math.max(
239
+ 4,
240
+ Math.floor(area / (Math.PI * avgR * avgR) * 0.6)
241
+ );
242
+ const circles = [];
243
+ for (let i = 0; i < target; i++) {
244
+ const p = vecInBounds(b, rng);
245
+ circles.push(circle(p.x, p.y, rMin + rng() * (rMax - rMin)));
246
+ }
247
+ for (let iter = 0; iter < iterations; iter++) {
248
+ for (let i = 0; i < circles.length; i++) {
249
+ for (let j = i + 1; j < circles.length; j++) {
250
+ const a = circles[i];
251
+ const bC = circles[j];
252
+ const dx = bC.x - a.x;
253
+ const dy = bC.y - a.y;
254
+ const dist = Math.hypot(dx, dy) || 1e-9;
255
+ const minDist = a.r + bC.r;
256
+ if (dist >= minDist) continue;
257
+ const push = (minDist - dist) / 2 * 0.5;
258
+ const ux = dx / dist;
259
+ const uy = dy / dist;
260
+ a.x -= ux * push;
261
+ a.y -= uy * push;
262
+ bC.x += ux * push;
263
+ bC.y += uy * push;
264
+ }
265
+ }
266
+ for (const c of circles) {
267
+ c.x = Math.min(b.maxX + c.r, Math.max(b.minX - c.r, c.x));
268
+ c.y = Math.min(b.maxY + c.r, Math.max(b.minY - c.r, c.y));
269
+ }
270
+ }
271
+ return circles.filter((c) => circleOverlapsPath(c, path2));
272
+ }
273
+ function circleOverlapsPath(c, path2) {
274
+ const center = { x: c.x, y: c.y };
275
+ const dist = distanceToPolygon(center, path2);
276
+ return dist <= c.r * 0.95;
277
+ }
278
+ function circleContainedInPath(c, path2) {
279
+ const center = { x: c.x, y: c.y };
280
+ if (!containsPoint(path2, center)) return false;
281
+ return distanceToBoundary(center, path2) + 1e-9 >= c.r;
282
+ }
283
+ function distanceToPolygon(p, path2) {
284
+ if (containsPoint(path2, p)) return 0;
285
+ return distanceToBoundary(p, path2);
286
+ }
287
+ function findLargestEmptyCircle(path2, obstacles, tolerance) {
288
+ const b = bounds(path2);
289
+ const diag = Math.hypot(b.maxX - b.minX, b.maxY - b.minY) || 1;
290
+ const step = Math.max(diag * 0.02 * Math.sqrt(tolerance), diag * 0.01);
291
+ let best = null;
292
+ for (let x = b.minX; x <= b.maxX; x += step) {
293
+ for (let y = b.minY; y <= b.maxY; y += step) {
294
+ const p = { x, y };
295
+ if (!containsPoint(path2, p)) continue;
296
+ const r = clearanceRadius(p, path2, obstacles);
297
+ if (!best || r > best.r) best = circle(p.x, p.y, r);
298
+ }
299
+ }
300
+ if (!best) return null;
301
+ let cur = best;
302
+ let span = step;
303
+ for (let iter = 0; iter < 8; iter++) {
304
+ let improved = cur;
305
+ for (let dx = -1; dx <= 1; dx++) {
306
+ for (let dy = -1; dy <= 1; dy++) {
307
+ if (dx === 0 && dy === 0) continue;
308
+ const p = { x: cur.x + dx * span, y: cur.y + dy * span };
309
+ if (!containsPoint(path2, p)) continue;
310
+ const r = clearanceRadius(p, path2, obstacles);
311
+ if (r > improved.r) improved = circle(p.x, p.y, r);
312
+ }
313
+ }
314
+ cur = improved;
315
+ span *= 0.5;
316
+ }
317
+ return cur;
318
+ }
319
+ function clearanceRadius(p, path2, obstacles) {
320
+ let r = distanceToBoundary(p, path2);
321
+ for (const c of obstacles) {
322
+ r = Math.min(r, Math.hypot(p.x - c.x, p.y - c.y) - c.r);
323
+ }
324
+ return Math.max(0, r);
325
+ }
326
+ function overlapsAny(c, packing) {
327
+ for (const o of packing) {
328
+ if (Math.hypot(c.x - o.x, c.y - o.y) < c.r + o.r - 1e-9) return true;
329
+ }
330
+ return false;
331
+ }
332
+ function distanceToBoundary(p, path2) {
333
+ let min = Infinity;
334
+ for (const ring of path2.rings) {
335
+ for (let i = 0; i < ring.length; i++) {
336
+ const a = ring[i];
337
+ const b = ring[(i + 1) % ring.length];
338
+ min = Math.min(min, pointSegmentDistance(p, a, b));
339
+ }
340
+ }
341
+ return min;
342
+ }
343
+ function pointSegmentDistance(p, a, b) {
344
+ const abx = b.x - a.x;
345
+ const aby = b.y - a.y;
346
+ const len2 = abx * abx + aby * aby;
347
+ if (len2 < 1e-18) return Math.hypot(p.x - a.x, p.y - a.y);
348
+ let t = ((p.x - a.x) * abx + (p.y - a.y) * aby) / len2;
349
+ t = Math.max(0, Math.min(1, t));
350
+ return Math.hypot(p.x - (a.x + t * abx), p.y - (a.y + t * aby));
351
+ }
352
+ function vecInBounds(b, rng) {
353
+ return {
354
+ x: b.minX + rng() * (b.maxX - b.minX),
355
+ y: b.minY + rng() * (b.maxY - b.minY)
356
+ };
357
+ }
358
+ function mulberry32(a) {
359
+ return function() {
360
+ let t = a += 1831565813;
361
+ t = Math.imul(t ^ t >>> 15, t | 1);
362
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
363
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
364
+ };
365
+ }
366
+ var circlePacking = {
367
+ squareLatticePack,
368
+ hexLatticePack,
369
+ stochasticPack,
370
+ maximumInscribedPack,
371
+ maximumInscribedPackUntil,
372
+ obstaclePack,
373
+ frontChainPack,
374
+ repulsionPack,
375
+ circleOverlapsPath,
376
+ circleContainedInPath
377
+ };
378
+
379
+ export { circleContainedInPath, circleOverlapsPath, circlePacking, distanceToPolygon, frontChainPack, hexLatticePack, maximumInscribedPack, maximumInscribedPackUntil, obstaclePack, repulsionPack, squareLatticePack, stochasticPack };
380
+ //# sourceMappingURL=index.js.map
381
+ //# sourceMappingURL=index.js.map