diskpack 0.10.1__tar.gz → 0.10.2__tar.gz
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.
- diskpack-0.10.2/PKG-INFO +206 -0
- diskpack-0.10.2/README.md +195 -0
- {diskpack-0.10.1 → diskpack-0.10.2}/pyproject.toml +1 -1
- diskpack-0.10.2/src/diskpack/config.py +155 -0
- diskpack-0.10.2/src/diskpack.egg-info/PKG-INFO +206 -0
- diskpack-0.10.1/PKG-INFO +0 -222
- diskpack-0.10.1/README.md +0 -212
- diskpack-0.10.1/src/diskpack/config.py +0 -91
- diskpack-0.10.1/src/diskpack.egg-info/PKG-INFO +0 -222
- {diskpack-0.10.1 → diskpack-0.10.2}/LICENSE +0 -0
- {diskpack-0.10.1 → diskpack-0.10.2}/setup.cfg +0 -0
- {diskpack-0.10.1 → diskpack-0.10.2}/src/diskpack/__init__.py +0 -0
- {diskpack-0.10.1 → diskpack-0.10.2}/src/diskpack/geometry.py +0 -0
- {diskpack-0.10.1 → diskpack-0.10.2}/src/diskpack/packer.py +0 -0
- {diskpack-0.10.1 → diskpack-0.10.2}/src/diskpack.egg-info/SOURCES.txt +0 -0
- {diskpack-0.10.1 → diskpack-0.10.2}/src/diskpack.egg-info/dependency_links.txt +0 -0
- {diskpack-0.10.1 → diskpack-0.10.2}/src/diskpack.egg-info/requires.txt +0 -0
- {diskpack-0.10.1 → diskpack-0.10.2}/src/diskpack.egg-info/top_level.txt +0 -0
- {diskpack-0.10.1 → diskpack-0.10.2}/tests/tests.py +0 -0
diskpack-0.10.2/PKG-INFO
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: diskpack
|
|
3
|
+
Version: 0.10.2
|
|
4
|
+
Summary: A high-performance vectorized circle packer with spatial hashing.
|
|
5
|
+
Author-email: James Kelly <mrkellyjam@gmail.com>
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: numpy>=1.20
|
|
10
|
+
Dynamic: license-file
|
|
11
|
+
|
|
12
|
+
# diskpack
|
|
13
|
+
|
|
14
|
+
State-of-the-art circle packing for arbitrary polygons.
|
|
15
|
+
|
|
16
|
+

|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install diskpack
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from diskpack import CirclePacker, PackingConfig
|
|
29
|
+
import numpy as np
|
|
30
|
+
|
|
31
|
+
# Define a polygon (list of vertices)
|
|
32
|
+
square = [(0, 0), (100, 0), (100, 100), (0, 100)]
|
|
33
|
+
|
|
34
|
+
# Pack circles
|
|
35
|
+
packer = CirclePacker([np.array(square)])
|
|
36
|
+
circles = packer.pack()
|
|
37
|
+
|
|
38
|
+
# Each circle is (x, y, radius)
|
|
39
|
+
for x, y, r in circles:
|
|
40
|
+
print(f"Circle at ({x:.1f}, {y:.1f}) with radius {r:.1f}")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Choosing the Right Algorithm
|
|
44
|
+
|
|
45
|
+
diskpack offers multiple packing strategies optimized for different use cases:
|
|
46
|
+
|
|
47
|
+
| Shape Type | Best Algorithm | Config |
|
|
48
|
+
|------------|----------------|--------|
|
|
49
|
+
| Simple convex (square, rectangle) | Random sampling | `PackingConfig()` |
|
|
50
|
+
| Complex/concave (star, L-shape, letters) | Hybrid | `PackingConfig(use_hybrid_packing=True)` |
|
|
51
|
+
| Fixed radius, need speed | Hex grid | `PackingConfig(fixed_radius=5.0)` |
|
|
52
|
+
| Fixed radius, need density | Hybrid | `PackingConfig(fixed_radius=5.0, use_hybrid_packing=True)` |
|
|
53
|
+
| Artistic/organic look | Random | `PackingConfig(use_hex_grid=False)` |
|
|
54
|
+
|
|
55
|
+
### Simple Convex Shapes
|
|
56
|
+
|
|
57
|
+
For squares, rectangles, and other simple convex polygons, the default random sampling achieves the best density:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
config = PackingConfig(
|
|
61
|
+
padding=0.5,
|
|
62
|
+
min_radius=1.0,
|
|
63
|
+
)
|
|
64
|
+
packer = CirclePacker([np.array(square)], config)
|
|
65
|
+
circles = packer.pack() # ~86% density
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Complex/Concave Shapes
|
|
69
|
+
|
|
70
|
+
For stars, L-shapes, letters, and other complex polygons, hybrid mode fills corners better:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
star = [
|
|
74
|
+
(50, 0), (61, 35), (98, 35), (68, 57), (79, 91),
|
|
75
|
+
(50, 70), (21, 91), (32, 57), (2, 35), (39, 35)
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
config = PackingConfig(
|
|
79
|
+
use_hybrid_packing=True,
|
|
80
|
+
verbose=True, # See progress
|
|
81
|
+
)
|
|
82
|
+
packer = CirclePacker([np.array(star)], config)
|
|
83
|
+
circles = packer.pack() # ~69% density (vs ~64% for random)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Fixed Radius Packing
|
|
87
|
+
|
|
88
|
+
When all circles must have the same radius:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
# Fastest (hex grid pattern)
|
|
92
|
+
config = PackingConfig(fixed_radius=3.0)
|
|
93
|
+
|
|
94
|
+
# Densest (fills corners)
|
|
95
|
+
config = PackingConfig(fixed_radius=3.0, use_hybrid_packing=True)
|
|
96
|
+
|
|
97
|
+
# Organic look (random placement)
|
|
98
|
+
config = PackingConfig(fixed_radius=3.0, use_hex_grid=False)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Tuning Hybrid Mode
|
|
102
|
+
|
|
103
|
+
Hybrid mode works in three phases:
|
|
104
|
+
|
|
105
|
+
1. **Phase 1 (Large)**: Place circles ≥ 50% of max possible radius
|
|
106
|
+
2. **Phase 2 (Medium)**: Place circles ≥ 25% of max possible radius
|
|
107
|
+
3. **Phase 3 (Small)**: Fill remaining gaps with random sampling
|
|
108
|
+
|
|
109
|
+
You can tune the thresholds:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
# For complex shapes with tight corners (default)
|
|
113
|
+
config = PackingConfig(
|
|
114
|
+
use_hybrid_packing=True,
|
|
115
|
+
hybrid_large_threshold=0.5, # Phase 1: >= 50% of max
|
|
116
|
+
hybrid_medium_threshold=0.25, # Phase 2: >= 25% of max
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# For simpler shapes (more circles in Phases 1-2)
|
|
120
|
+
config = PackingConfig(
|
|
121
|
+
use_hybrid_packing=True,
|
|
122
|
+
hybrid_large_threshold=0.3, # Phase 1: >= 30% of max
|
|
123
|
+
hybrid_medium_threshold=0.1, # Phase 2: >= 10% of max
|
|
124
|
+
)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Performance Tuning
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
config = PackingConfig(
|
|
131
|
+
# Stop after N consecutive failed attempts (higher = more circles, slower)
|
|
132
|
+
max_failed_attempts=200,
|
|
133
|
+
|
|
134
|
+
# Points sampled per iteration (higher = better placements, more memory)
|
|
135
|
+
sample_batch_size=50,
|
|
136
|
+
|
|
137
|
+
# Minimum gap between circles
|
|
138
|
+
padding=1.5,
|
|
139
|
+
|
|
140
|
+
# Smallest circle to place
|
|
141
|
+
min_radius=1.0,
|
|
142
|
+
)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## API Reference
|
|
146
|
+
|
|
147
|
+
### CirclePacker
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
CirclePacker(polygons: List[np.ndarray], config: PackingConfig = None)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- `polygons`: List of polygon vertices. Each polygon is an Nx2 numpy array.
|
|
154
|
+
- `config`: Optional configuration. Uses defaults if not provided.
|
|
155
|
+
|
|
156
|
+
**Methods:**
|
|
157
|
+
|
|
158
|
+
- `pack() -> List[Tuple[float, float, float]]`: Pack circles and return as list
|
|
159
|
+
- `generate() -> Iterator[Tuple[float, float, float]]`: Generate circles lazily
|
|
160
|
+
|
|
161
|
+
### PackingConfig
|
|
162
|
+
|
|
163
|
+
See the docstring for full parameter documentation:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from diskpack import PackingConfig
|
|
167
|
+
help(PackingConfig)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Benchmark Results
|
|
171
|
+
|
|
172
|
+
Tested on 100×100 unit shapes:
|
|
173
|
+
|
|
174
|
+
| Shape | Algorithm | Time | Circles | Density |
|
|
175
|
+
|-------|-----------|------|---------|---------|
|
|
176
|
+
| Square | Random | 0.31s | 49 | **86.4%** |
|
|
177
|
+
| Square | Hybrid | **0.15s** | 55 | 85.8% |
|
|
178
|
+
| L-Shape | Random | 0.68s | 50 | 76.9% |
|
|
179
|
+
| L-Shape | Hybrid | **0.30s** | 36 | **79.3%** |
|
|
180
|
+
| Star | Random | 0.32s | 39 | 64.4% |
|
|
181
|
+
| Star | Hybrid | **0.25s** | 27 | **68.8%** |
|
|
182
|
+
|
|
183
|
+
Fixed radius (r=3.0) on Star shape:
|
|
184
|
+
|
|
185
|
+
| Algorithm | Time | Circles | Density |
|
|
186
|
+
|-----------|------|---------|---------|
|
|
187
|
+
| Hex Grid | **0.007s** | 40 | 40.0% |
|
|
188
|
+
| Hybrid | 0.08s | **51** | **51.0%** |
|
|
189
|
+
|
|
190
|
+
## Comparison with Shapely
|
|
191
|
+
|
|
192
|
+
diskpack achieves higher density and faster packing compared to the Python Shapely library. Where Shapely creates Python objects for each point-in-polygon and distance check, diskpack uses vectorized NumPy operations with precomputed edge geometry and batched candidate evaluation — sampling many points per iteration and greedily placing the largest valid circle. A grid-based spatial index keeps collision detection O(1) as circle count grows. The batched Shapely method can approach similar density by also picking the best of many candidates, but at ~30x the runtime due to per-point object overhead.
|
|
193
|
+
|
|
194
|
+

|
|
195
|
+
|
|
196
|
+
| Method | Circles | Density | Time |
|
|
197
|
+
|--------|---------|---------|------|
|
|
198
|
+
| **diskpack** | 47 | **86.7%** | **0.679s** |
|
|
199
|
+
| shapely (naive) | 109 | 73.4% | 1.424s |
|
|
200
|
+
| shapely (batched) | 60 | 85.3% | 19.569s |
|
|
201
|
+
|
|
202
|
+
See [`demo/diskpack_comparisons.ipynb`](demo/diskpack_comparisons.ipynb) for the full benchmark notebook.
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
MIT
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# diskpack
|
|
2
|
+
|
|
3
|
+
State-of-the-art circle packing for arbitrary polygons.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install diskpack
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from diskpack import CirclePacker, PackingConfig
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
# Define a polygon (list of vertices)
|
|
21
|
+
square = [(0, 0), (100, 0), (100, 100), (0, 100)]
|
|
22
|
+
|
|
23
|
+
# Pack circles
|
|
24
|
+
packer = CirclePacker([np.array(square)])
|
|
25
|
+
circles = packer.pack()
|
|
26
|
+
|
|
27
|
+
# Each circle is (x, y, radius)
|
|
28
|
+
for x, y, r in circles:
|
|
29
|
+
print(f"Circle at ({x:.1f}, {y:.1f}) with radius {r:.1f}")
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Choosing the Right Algorithm
|
|
33
|
+
|
|
34
|
+
diskpack offers multiple packing strategies optimized for different use cases:
|
|
35
|
+
|
|
36
|
+
| Shape Type | Best Algorithm | Config |
|
|
37
|
+
|------------|----------------|--------|
|
|
38
|
+
| Simple convex (square, rectangle) | Random sampling | `PackingConfig()` |
|
|
39
|
+
| Complex/concave (star, L-shape, letters) | Hybrid | `PackingConfig(use_hybrid_packing=True)` |
|
|
40
|
+
| Fixed radius, need speed | Hex grid | `PackingConfig(fixed_radius=5.0)` |
|
|
41
|
+
| Fixed radius, need density | Hybrid | `PackingConfig(fixed_radius=5.0, use_hybrid_packing=True)` |
|
|
42
|
+
| Artistic/organic look | Random | `PackingConfig(use_hex_grid=False)` |
|
|
43
|
+
|
|
44
|
+
### Simple Convex Shapes
|
|
45
|
+
|
|
46
|
+
For squares, rectangles, and other simple convex polygons, the default random sampling achieves the best density:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
config = PackingConfig(
|
|
50
|
+
padding=0.5,
|
|
51
|
+
min_radius=1.0,
|
|
52
|
+
)
|
|
53
|
+
packer = CirclePacker([np.array(square)], config)
|
|
54
|
+
circles = packer.pack() # ~86% density
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Complex/Concave Shapes
|
|
58
|
+
|
|
59
|
+
For stars, L-shapes, letters, and other complex polygons, hybrid mode fills corners better:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
star = [
|
|
63
|
+
(50, 0), (61, 35), (98, 35), (68, 57), (79, 91),
|
|
64
|
+
(50, 70), (21, 91), (32, 57), (2, 35), (39, 35)
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
config = PackingConfig(
|
|
68
|
+
use_hybrid_packing=True,
|
|
69
|
+
verbose=True, # See progress
|
|
70
|
+
)
|
|
71
|
+
packer = CirclePacker([np.array(star)], config)
|
|
72
|
+
circles = packer.pack() # ~69% density (vs ~64% for random)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Fixed Radius Packing
|
|
76
|
+
|
|
77
|
+
When all circles must have the same radius:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
# Fastest (hex grid pattern)
|
|
81
|
+
config = PackingConfig(fixed_radius=3.0)
|
|
82
|
+
|
|
83
|
+
# Densest (fills corners)
|
|
84
|
+
config = PackingConfig(fixed_radius=3.0, use_hybrid_packing=True)
|
|
85
|
+
|
|
86
|
+
# Organic look (random placement)
|
|
87
|
+
config = PackingConfig(fixed_radius=3.0, use_hex_grid=False)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Tuning Hybrid Mode
|
|
91
|
+
|
|
92
|
+
Hybrid mode works in three phases:
|
|
93
|
+
|
|
94
|
+
1. **Phase 1 (Large)**: Place circles ≥ 50% of max possible radius
|
|
95
|
+
2. **Phase 2 (Medium)**: Place circles ≥ 25% of max possible radius
|
|
96
|
+
3. **Phase 3 (Small)**: Fill remaining gaps with random sampling
|
|
97
|
+
|
|
98
|
+
You can tune the thresholds:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
# For complex shapes with tight corners (default)
|
|
102
|
+
config = PackingConfig(
|
|
103
|
+
use_hybrid_packing=True,
|
|
104
|
+
hybrid_large_threshold=0.5, # Phase 1: >= 50% of max
|
|
105
|
+
hybrid_medium_threshold=0.25, # Phase 2: >= 25% of max
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# For simpler shapes (more circles in Phases 1-2)
|
|
109
|
+
config = PackingConfig(
|
|
110
|
+
use_hybrid_packing=True,
|
|
111
|
+
hybrid_large_threshold=0.3, # Phase 1: >= 30% of max
|
|
112
|
+
hybrid_medium_threshold=0.1, # Phase 2: >= 10% of max
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Performance Tuning
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
config = PackingConfig(
|
|
120
|
+
# Stop after N consecutive failed attempts (higher = more circles, slower)
|
|
121
|
+
max_failed_attempts=200,
|
|
122
|
+
|
|
123
|
+
# Points sampled per iteration (higher = better placements, more memory)
|
|
124
|
+
sample_batch_size=50,
|
|
125
|
+
|
|
126
|
+
# Minimum gap between circles
|
|
127
|
+
padding=1.5,
|
|
128
|
+
|
|
129
|
+
# Smallest circle to place
|
|
130
|
+
min_radius=1.0,
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## API Reference
|
|
135
|
+
|
|
136
|
+
### CirclePacker
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
CirclePacker(polygons: List[np.ndarray], config: PackingConfig = None)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
- `polygons`: List of polygon vertices. Each polygon is an Nx2 numpy array.
|
|
143
|
+
- `config`: Optional configuration. Uses defaults if not provided.
|
|
144
|
+
|
|
145
|
+
**Methods:**
|
|
146
|
+
|
|
147
|
+
- `pack() -> List[Tuple[float, float, float]]`: Pack circles and return as list
|
|
148
|
+
- `generate() -> Iterator[Tuple[float, float, float]]`: Generate circles lazily
|
|
149
|
+
|
|
150
|
+
### PackingConfig
|
|
151
|
+
|
|
152
|
+
See the docstring for full parameter documentation:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from diskpack import PackingConfig
|
|
156
|
+
help(PackingConfig)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Benchmark Results
|
|
160
|
+
|
|
161
|
+
Tested on 100×100 unit shapes:
|
|
162
|
+
|
|
163
|
+
| Shape | Algorithm | Time | Circles | Density |
|
|
164
|
+
|-------|-----------|------|---------|---------|
|
|
165
|
+
| Square | Random | 0.31s | 49 | **86.4%** |
|
|
166
|
+
| Square | Hybrid | **0.15s** | 55 | 85.8% |
|
|
167
|
+
| L-Shape | Random | 0.68s | 50 | 76.9% |
|
|
168
|
+
| L-Shape | Hybrid | **0.30s** | 36 | **79.3%** |
|
|
169
|
+
| Star | Random | 0.32s | 39 | 64.4% |
|
|
170
|
+
| Star | Hybrid | **0.25s** | 27 | **68.8%** |
|
|
171
|
+
|
|
172
|
+
Fixed radius (r=3.0) on Star shape:
|
|
173
|
+
|
|
174
|
+
| Algorithm | Time | Circles | Density |
|
|
175
|
+
|-----------|------|---------|---------|
|
|
176
|
+
| Hex Grid | **0.007s** | 40 | 40.0% |
|
|
177
|
+
| Hybrid | 0.08s | **51** | **51.0%** |
|
|
178
|
+
|
|
179
|
+
## Comparison with Shapely
|
|
180
|
+
|
|
181
|
+
diskpack achieves higher density and faster packing compared to the Python Shapely library. Where Shapely creates Python objects for each point-in-polygon and distance check, diskpack uses vectorized NumPy operations with precomputed edge geometry and batched candidate evaluation — sampling many points per iteration and greedily placing the largest valid circle. A grid-based spatial index keeps collision detection O(1) as circle count grows. The batched Shapely method can approach similar density by also picking the best of many candidates, but at ~30x the runtime due to per-point object overhead.
|
|
182
|
+
|
|
183
|
+

|
|
184
|
+
|
|
185
|
+
| Method | Circles | Density | Time |
|
|
186
|
+
|--------|---------|---------|------|
|
|
187
|
+
| **diskpack** | 47 | **86.7%** | **0.679s** |
|
|
188
|
+
| shapely (naive) | 109 | 73.4% | 1.424s |
|
|
189
|
+
| shapely (batched) | 60 | 85.3% | 19.569s |
|
|
190
|
+
|
|
191
|
+
See [`demo/diskpack_comparisons.ipynb`](demo/diskpack_comparisons.ipynb) for the full benchmark notebook.
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "diskpack"
|
|
7
|
-
version = "0.10.
|
|
7
|
+
version = "0.10.2"
|
|
8
8
|
authors = [{ name="James Kelly", email="mrkellyjam@gmail.com" }]
|
|
9
9
|
description = "A high-performance vectorized circle packer with spatial hashing."
|
|
10
10
|
readme = "README.md"
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration and type definitions for circle packing.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Optional, Tuple
|
|
8
|
+
from enum import Enum
|
|
9
|
+
|
|
10
|
+
# Type aliases
|
|
11
|
+
Polygon = np.ndarray
|
|
12
|
+
Point = np.ndarray
|
|
13
|
+
GridKey = Tuple[int, int]
|
|
14
|
+
Circle = Tuple[float, float, float] # (x, y, radius)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PackingMode(Enum):
|
|
18
|
+
"""Available packing strategies."""
|
|
19
|
+
RANDOM = "random"
|
|
20
|
+
HEX_GRID = "hex_grid"
|
|
21
|
+
FRONT = "front"
|
|
22
|
+
HYBRID = "hybrid"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class PackingConfig:
|
|
27
|
+
"""
|
|
28
|
+
Configuration parameters for the circle packing algorithm.
|
|
29
|
+
|
|
30
|
+
Quick Start
|
|
31
|
+
-----------
|
|
32
|
+
# Best density for simple convex shapes (squares, rectangles, circles):
|
|
33
|
+
config = PackingConfig() # Uses random sampling (default)
|
|
34
|
+
|
|
35
|
+
# Best density for complex/concave shapes (stars, L-shapes, letters):
|
|
36
|
+
config = PackingConfig(use_hybrid_packing=True)
|
|
37
|
+
|
|
38
|
+
# Fixed radius circles - fastest:
|
|
39
|
+
config = PackingConfig(fixed_radius=5.0)
|
|
40
|
+
|
|
41
|
+
# Fixed radius circles - best density on complex shapes:
|
|
42
|
+
config = PackingConfig(fixed_radius=5.0, use_hybrid_packing=True)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
Algorithm Selection Guide
|
|
46
|
+
-------------------------
|
|
47
|
+
| Shape Type | Best Algorithm | Config |
|
|
48
|
+
|---------------------|-----------------------------|---------------------------------|
|
|
49
|
+
| Simple convex | Random sampling (default) | PackingConfig() |
|
|
50
|
+
| Complex/concave | Hybrid | use_hybrid_packing=True |
|
|
51
|
+
| Fixed radius, speed | Hex grid (default for fixed)| fixed_radius=X |
|
|
52
|
+
| Fixed radius, dense | Hybrid | fixed_radius=X, use_hybrid_packing=True |
|
|
53
|
+
| Artistic/organic | Random sampling | use_hex_grid=False |
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
Tuning Hybrid Mode
|
|
57
|
+
------------------
|
|
58
|
+
Hybrid mode works in phases:
|
|
59
|
+
Phase 1: Place large circles (>= hybrid_large_threshold * max_radius)
|
|
60
|
+
Phase 2: Place medium circles (>= hybrid_medium_threshold * max_radius)
|
|
61
|
+
Phase 3: Fill remaining space with random sampling
|
|
62
|
+
|
|
63
|
+
For complex shapes with tight corners (stars, letters):
|
|
64
|
+
- Use higher thresholds: hybrid_large_threshold=0.5, hybrid_medium_threshold=0.25
|
|
65
|
+
- This prioritizes filling corners with appropriately-sized circles
|
|
66
|
+
|
|
67
|
+
For simpler shapes where you want hybrid's speed:
|
|
68
|
+
- Use lower thresholds: hybrid_large_threshold=0.3, hybrid_medium_threshold=0.1
|
|
69
|
+
- This lets the front-based algorithm place more circles before random cleanup
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
padding : float, default=1.5
|
|
75
|
+
Minimum gap between circles and between circles and polygon edges.
|
|
76
|
+
|
|
77
|
+
min_radius : float, default=1.0
|
|
78
|
+
Smallest circle that will be placed. Circles below this size are skipped.
|
|
79
|
+
|
|
80
|
+
fixed_radius : float, optional
|
|
81
|
+
If set, all circles will have exactly this radius. Enables hex grid mode
|
|
82
|
+
by default (override with use_hex_grid=False for organic placement).
|
|
83
|
+
|
|
84
|
+
use_hex_grid : bool, default=True
|
|
85
|
+
Use hexagonal grid for fixed radius packing. Fastest option but creates
|
|
86
|
+
a regular pattern. Set to False for organic/random placement.
|
|
87
|
+
|
|
88
|
+
use_front_packing : bool, default=False
|
|
89
|
+
Use front-based algorithm. Good for filling corners but may produce
|
|
90
|
+
many small circles. Usually better to use use_hybrid_packing instead.
|
|
91
|
+
|
|
92
|
+
use_hybrid_packing : bool, default=False
|
|
93
|
+
Use state-of-the-art multi-phase algorithm. Best for complex shapes.
|
|
94
|
+
Combines front-based (for corners) with random sampling (for gaps).
|
|
95
|
+
|
|
96
|
+
max_failed_attempts : int, default=200
|
|
97
|
+
Stop after this many consecutive failed placement attempts.
|
|
98
|
+
Higher values may find more circles but take longer.
|
|
99
|
+
|
|
100
|
+
sample_batch_size : int, default=50
|
|
101
|
+
Number of random points sampled per iteration. Higher values find
|
|
102
|
+
better placements per iteration but use more memory.
|
|
103
|
+
|
|
104
|
+
hybrid_large_threshold : float, default=0.5
|
|
105
|
+
Phase 1 minimum radius as fraction of max possible radius.
|
|
106
|
+
Only circles >= this fraction are placed in Phase 1.
|
|
107
|
+
|
|
108
|
+
hybrid_medium_threshold : float, default=0.25
|
|
109
|
+
Phase 2 minimum radius as fraction of max possible radius.
|
|
110
|
+
Only circles >= this fraction are placed in Phase 2.
|
|
111
|
+
|
|
112
|
+
verbose : bool, default=False
|
|
113
|
+
Print progress information during packing.
|
|
114
|
+
"""
|
|
115
|
+
# Basic parameters
|
|
116
|
+
padding: float = 1.5
|
|
117
|
+
min_radius: float = 1.0
|
|
118
|
+
fixed_radius: Optional[float] = None
|
|
119
|
+
|
|
120
|
+
# Algorithm selection
|
|
121
|
+
use_hex_grid: bool = True
|
|
122
|
+
use_front_packing: bool = False
|
|
123
|
+
use_hybrid_packing: bool = False
|
|
124
|
+
|
|
125
|
+
# Performance tuning
|
|
126
|
+
max_failed_attempts: int = 200
|
|
127
|
+
sample_batch_size: int = 50
|
|
128
|
+
grid_resolution_divisor: float = 25
|
|
129
|
+
mega_circle_threshold: float = 0.5
|
|
130
|
+
ray_cast_epsilon: float = 1e-10
|
|
131
|
+
|
|
132
|
+
# Hybrid mode parameters
|
|
133
|
+
hybrid_large_threshold: float = 0.5
|
|
134
|
+
hybrid_medium_threshold: float = 0.25
|
|
135
|
+
|
|
136
|
+
# Output
|
|
137
|
+
verbose: bool = False
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass
|
|
141
|
+
class PackingProgress:
|
|
142
|
+
"""Tracks the current state of the packing algorithm."""
|
|
143
|
+
circles_placed: int = 0
|
|
144
|
+
failed_attempts: int = 0
|
|
145
|
+
max_failed_attempts: int = 200
|
|
146
|
+
phase: str = ""
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def progress_ratio(self) -> float:
|
|
150
|
+
"""How close to stopping (0.0 = just started, 1.0 = done)."""
|
|
151
|
+
return self.failed_attempts / self.max_failed_attempts if self.max_failed_attempts > 0 else 0
|
|
152
|
+
|
|
153
|
+
def __str__(self) -> str:
|
|
154
|
+
phase_str = f"[{self.phase}] " if self.phase else ""
|
|
155
|
+
return f"{phase_str}Placed: {self.circles_placed} | Failed: {self.failed_attempts}/{self.max_failed_attempts} ({self.progress_ratio:.0%})"
|