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.
- package/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +98 -0
- package/dist/circle-BDbKfrdR.d.ts +12 -0
- package/dist/circlePacking/index.d.ts +78 -0
- package/dist/circlePacking/index.js +381 -0
- package/dist/circlePacking/index.js.map +1 -0
- package/dist/cli.js +365 -0
- package/dist/cli.js.map +1 -0
- package/dist/construction/index.d.ts +26 -0
- package/dist/construction/index.js +102 -0
- package/dist/construction/index.js.map +1 -0
- package/dist/contour/index.d.ts +20 -0
- package/dist/contour/index.js +118 -0
- package/dist/contour/index.js.map +1 -0
- package/dist/conversion/index.d.ts +31 -0
- package/dist/conversion/index.js +99 -0
- package/dist/conversion/index.js.map +1 -0
- package/dist/group-BNjVVYSQ.d.ts +13 -0
- package/dist/hatch/index.d.ts +41 -0
- package/dist/hatch/index.js +253 -0
- package/dist/hatch/index.js.map +1 -0
- package/dist/hull/index.d.ts +16 -0
- package/dist/hull/index.js +97 -0
- package/dist/hull/index.js.map +1 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +3933 -0
- package/dist/index.js.map +1 -0
- package/dist/meshing/index.d.ts +125 -0
- package/dist/meshing/index.js +1333 -0
- package/dist/meshing/index.js.map +1 -0
- package/dist/morphology/index.d.ts +58 -0
- package/dist/morphology/index.js +268 -0
- package/dist/morphology/index.js.map +1 -0
- package/dist/optimisation/index.d.ts +43 -0
- package/dist/optimisation/index.js +400 -0
- package/dist/optimisation/index.js.map +1 -0
- package/dist/path-0RjhYjJs.d.ts +21 -0
- package/dist/pointSet/index.d.ts +24 -0
- package/dist/pointSet/index.js +139 -0
- package/dist/pointSet/index.js.map +1 -0
- package/dist/polygonisation/index.d.ts +44 -0
- package/dist/polygonisation/index.js +310 -0
- package/dist/polygonisation/index.js.map +1 -0
- package/dist/predicates/index.d.ts +35 -0
- package/dist/predicates/index.js +159 -0
- package/dist/predicates/index.js.map +1 -0
- package/dist/processing/index.d.ts +54 -0
- package/dist/processing/index.js +457 -0
- package/dist/processing/index.js.map +1 -0
- package/dist/segment-BKuIFMKD.d.ts +10 -0
- package/dist/segmentSet/index.d.ts +25 -0
- package/dist/segmentSet/index.js +109 -0
- package/dist/segmentSet/index.js.map +1 -0
- package/dist/shapeBoolean/index.d.ts +41 -0
- package/dist/shapeBoolean/index.js +211 -0
- package/dist/shapeBoolean/index.js.map +1 -0
- package/dist/svg/index.d.ts +38 -0
- package/dist/svg/index.js +317 -0
- package/dist/svg/index.js.map +1 -0
- package/dist/tiling/index.d.ts +32 -0
- package/dist/tiling/index.js +344 -0
- package/dist/tiling/index.js.map +1 -0
- package/dist/transformation/index.d.ts +37 -0
- package/dist/transformation/index.js +184 -0
- package/dist/transformation/index.js.map +1 -0
- package/dist/triangulation/index.d.ts +48 -0
- package/dist/triangulation/index.js +334 -0
- package/dist/triangulation/index.js.map +1 -0
- package/dist/vec2-CWyXN1Wj.d.ts +10 -0
- package/dist/voronoi/index.d.ts +31 -0
- package/dist/voronoi/index.js +211 -0
- package/dist/voronoi/index.js.map +1 -0
- 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
|
+
[](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
|