diskpack 0.2.0__tar.gz → 0.4.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: diskpack
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: A high-performance vectorized circle packer with spatial hashing.
5
5
  Author-email: James Kelly <mrkellyjam@gmail.com>
6
6
  Requires-Python: >=3.8
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "diskpack"
7
- version = "0.2.0"
7
+ version = "0.4.0"
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,375 @@
1
+ import numpy as np
2
+ from dataclasses import dataclass, field
3
+ from typing import List, Optional, Iterator, Tuple, Dict
4
+
5
+ # Type aliases
6
+ Polygon = np.ndarray
7
+ Point = np.ndarray
8
+ GridKey = Tuple[int, int]
9
+ Circle = Tuple[float, float, float]
10
+
11
+
12
+ @dataclass
13
+ class PackingConfig:
14
+ """Configuration parameters for the circle packing algorithm."""
15
+ padding: float = 1.5
16
+ min_radius: float = 1.0
17
+ grid_resolution_divisor: float = 25
18
+ max_failed_attempts: int = 200
19
+ mega_circle_threshold: float = 0.5
20
+ ray_cast_epsilon: float = 1e-10
21
+ sample_batch_size: int = 50
22
+ fixed_radius: Optional[float] = None
23
+ verbose: bool = False
24
+
25
+
26
+ @dataclass
27
+ class PackingProgress:
28
+ """Tracks the current state of the packing algorithm."""
29
+ circles_placed: int = 0
30
+ failed_attempts: int = 0
31
+ max_failed_attempts: int = 200
32
+
33
+ @property
34
+ def progress_ratio(self) -> float:
35
+ """How close we are to stopping (0.0 = just started, 1.0 = about to stop)."""
36
+ return self.failed_attempts / self.max_failed_attempts
37
+
38
+ def __str__(self) -> str:
39
+ return f"Placed: {self.circles_placed} | Failed attempts: {self.failed_attempts}/{self.max_failed_attempts} ({self.progress_ratio:.0%})"
40
+
41
+
42
+ class PolygonGeometry:
43
+ """Handles geometric calculations for polygon boundaries."""
44
+
45
+ def __init__(self, polygons: List[Polygon], epsilon: float = 1e-10):
46
+ self.polygons = [np.array(p, dtype=float) for p in polygons]
47
+ self.epsilon = epsilon
48
+ self._compute_bounds()
49
+ self._precompute_edges()
50
+
51
+ def _compute_bounds(self) -> None:
52
+ all_vertices = np.vstack(self.polygons)
53
+ self.min_coords = np.min(all_vertices, axis=0)
54
+ self.max_coords = np.max(all_vertices, axis=0)
55
+
56
+ def _precompute_edges(self) -> None:
57
+ """Precompute edge data for vectorized distance calculations."""
58
+ all_p1 = []
59
+ all_p2 = []
60
+ for poly in self.polygons:
61
+ n = len(poly)
62
+ for i in range(n):
63
+ all_p1.append(poly[i])
64
+ all_p2.append(poly[(i + 1) % n])
65
+
66
+ self.edge_starts = np.array(all_p1)
67
+ self.edge_ends = np.array(all_p2)
68
+ self.edge_vecs = self.edge_ends - self.edge_starts
69
+ self.edge_lengths_sq = np.sum(self.edge_vecs ** 2, axis=1)
70
+
71
+ def contains_points(self, points: np.ndarray) -> np.ndarray:
72
+ """Even-Odd Rule for interior detection, supports holes."""
73
+ x, y = points[:, 0], points[:, 1]
74
+ inside = np.zeros(len(points), dtype=bool)
75
+
76
+ for poly in self.polygons:
77
+ n = len(poly)
78
+ for i in range(n):
79
+ p1, p2 = poly[i], poly[(i + 1) % n]
80
+ crosses_edge = (p1[1] > y) != (p2[1] > y)
81
+ dy = p2[1] - p1[1] + self.epsilon
82
+ x_intercept = (p2[0] - p1[0]) * (y - p1[1]) / dy + p1[0]
83
+ inside ^= crosses_edge & (x < x_intercept)
84
+
85
+ return inside
86
+
87
+ def distance_to_boundary(self, point: Point) -> float:
88
+ """Vectorized distance to nearest polygon edge."""
89
+ to_point = point - self.edge_starts
90
+
91
+ dots = np.sum(to_point * self.edge_vecs, axis=1)
92
+
93
+ with np.errstate(divide='ignore', invalid='ignore'):
94
+ t = np.clip(dots / self.edge_lengths_sq, 0, 1)
95
+ t = np.where(self.edge_lengths_sq == 0, 0, t)
96
+
97
+ projections = self.edge_starts + t[:, np.newaxis] * self.edge_vecs
98
+ distances = np.linalg.norm(point - projections, axis=1)
99
+
100
+ return float(np.min(distances))
101
+
102
+ def distances_to_boundary_batch(self, points: np.ndarray) -> np.ndarray:
103
+ """
104
+ Vectorized distance calculation for multiple points at once.
105
+ """
106
+ n_points = len(points)
107
+ n_edges = len(self.edge_starts)
108
+
109
+ to_point = points[:, np.newaxis, :] - self.edge_starts[np.newaxis, :, :]
110
+ dots = np.sum(to_point * self.edge_vecs[np.newaxis, :, :], axis=2)
111
+
112
+ with np.errstate(divide='ignore', invalid='ignore'):
113
+ t = np.clip(dots / self.edge_lengths_sq[np.newaxis, :], 0, 1)
114
+ t = np.where(self.edge_lengths_sq[np.newaxis, :] == 0, 0, t)
115
+
116
+ projections = (
117
+ self.edge_starts[np.newaxis, :, :] +
118
+ t[:, :, np.newaxis] * self.edge_vecs[np.newaxis, :, :]
119
+ )
120
+
121
+ distances = np.linalg.norm(points[:, np.newaxis, :] - projections, axis=2)
122
+
123
+ return np.min(distances, axis=1)
124
+
125
+
126
+ @dataclass
127
+ class SpatialIndex:
128
+ """Grid-based spatial index for efficient collision detection."""
129
+ cell_size: float
130
+ origin: np.ndarray
131
+ mega_threshold: float
132
+ grid: Dict[GridKey, List[int]] = field(default_factory=dict)
133
+ mega_circles: List[int] = field(default_factory=list)
134
+
135
+ _centers: np.ndarray = field(default_factory=lambda: np.empty((0, 2)))
136
+ _radii: np.ndarray = field(default_factory=lambda: np.empty(0))
137
+
138
+ def add_circle(self, index: int, center: Point, radius: float) -> None:
139
+ self._centers = np.vstack([self._centers, center]) if len(self._centers) > 0 else center.reshape(1, 2)
140
+ self._radii = np.append(self._radii, radius)
141
+
142
+ if radius > self.cell_size * self.mega_threshold:
143
+ self.mega_circles.append(index)
144
+ else:
145
+ key = self._get_cell_key(center)
146
+ self.grid.setdefault(key, []).append(index)
147
+
148
+ def get_nearby_indices(self, point: Point) -> Iterator[int]:
149
+ yield from self.mega_circles
150
+ center_key = self._get_cell_key(point)
151
+ for dx in range(-1, 2):
152
+ for dy in range(-1, 2):
153
+ neighbor_key = (center_key[0] + dx, center_key[1] + dy)
154
+ if neighbor_key in self.grid:
155
+ yield from self.grid[neighbor_key]
156
+
157
+ def distance_to_circles(self, point: Point) -> float:
158
+ """Get minimum distance from point to any existing circle's edge."""
159
+ if len(self._centers) == 0:
160
+ return float('inf')
161
+
162
+ indices = list(self.get_nearby_indices(point))
163
+ if not indices:
164
+ return float('inf')
165
+
166
+ centers = self._centers[indices]
167
+ radii = self._radii[indices]
168
+
169
+ distances = np.linalg.norm(centers - point, axis=1) - radii
170
+ return float(np.min(distances))
171
+
172
+ def _get_cell_key(self, point: Point) -> GridKey:
173
+ cell_coords = ((point - self.origin) // self.cell_size).astype(int)
174
+ return (int(cell_coords[0]), int(cell_coords[1]))
175
+
176
+
177
+ class CirclePacker:
178
+ """Packs circles within polygon boundaries using random sampling."""
179
+
180
+ def __init__(self, polygons: List[Polygon], config: Optional[PackingConfig] = None):
181
+ self.config = config or PackingConfig()
182
+ self.geometry = PolygonGeometry(polygons, self.config.ray_cast_epsilon)
183
+ self.centers: List[Point] = []
184
+ self.radii: List[float] = []
185
+ self.progress = PackingProgress(max_failed_attempts=self.config.max_failed_attempts)
186
+
187
+ extent = max(self.geometry.max_coords - self.geometry.min_coords)
188
+ cell_size = extent / self.config.grid_resolution_divisor
189
+ self.spatial_index = SpatialIndex(
190
+ cell_size=cell_size,
191
+ origin=self.geometry.min_coords,
192
+ mega_threshold=self.config.mega_circle_threshold
193
+ )
194
+
195
+ def _sample_candidate_points(self, count: int) -> np.ndarray:
196
+ points = np.random.uniform(
197
+ self.geometry.min_coords,
198
+ self.geometry.max_coords,
199
+ size=(count, 2)
200
+ )
201
+ return points[self.geometry.contains_points(points)]
202
+
203
+ def _compute_max_radius(self, point: Point) -> float:
204
+ """Compute max radius for a single point."""
205
+ max_radius = self.geometry.distance_to_boundary(point)
206
+ circle_dist = self.spatial_index.distance_to_circles(point)
207
+ max_radius = min(max_radius, circle_dist)
208
+ return max_radius - self.config.padding
209
+
210
+ def _compute_max_radii_batch(self, points: np.ndarray) -> np.ndarray:
211
+ """Vectorized max radius computation for multiple points."""
212
+ if len(points) == 0:
213
+ return np.array([])
214
+
215
+ max_radii = self.geometry.distances_to_boundary_batch(points)
216
+
217
+ if len(self.centers) > 0:
218
+ centers_arr = np.array(self.centers)
219
+ radii_arr = np.array(self.radii)
220
+
221
+ for i, point in enumerate(points):
222
+ indices = list(self.spatial_index.get_nearby_indices(point))
223
+ if indices:
224
+ nearby_centers = centers_arr[indices]
225
+ nearby_radii = radii_arr[indices]
226
+ distances = np.linalg.norm(nearby_centers - point, axis=1) - nearby_radii
227
+ max_radii[i] = min(max_radii[i], np.min(distances))
228
+
229
+ return max_radii - self.config.padding
230
+
231
+ def _find_best_placement(self, candidates: np.ndarray) -> Optional[Tuple[Point, float]]:
232
+ if len(candidates) == 0:
233
+ return None
234
+
235
+ radii = self._compute_max_radii_batch(candidates)
236
+ fixed = self.config.fixed_radius
237
+
238
+ if fixed is not None:
239
+ valid_mask = radii >= fixed
240
+ if not np.any(valid_mask):
241
+ return None
242
+ valid_indices = np.where(valid_mask)[0]
243
+ best_idx = valid_indices[0]
244
+ return candidates[best_idx], fixed
245
+ else:
246
+ best_idx = np.argmax(radii)
247
+ best_radius = radii[best_idx]
248
+
249
+ if best_radius >= self.config.min_radius:
250
+ return candidates[best_idx], best_radius
251
+ return None
252
+
253
+ def _place_circle(self, center: Point, radius: float) -> None:
254
+ idx = len(self.centers)
255
+ self.centers.append(center)
256
+ self.radii.append(radius)
257
+ self.spatial_index.add_circle(idx, center, radius)
258
+
259
+ def _generate_hex_grid(self, radius: float) -> np.ndarray:
260
+ """
261
+ Generate a hexagonal grid of points within the bounding box.
262
+ Hex grid is the optimal packing arrangement for equal circles.
263
+ """
264
+ spacing = (radius + self.config.padding) * 2
265
+ dy = spacing * np.sqrt(3) / 2
266
+
267
+ min_x, min_y = self.geometry.min_coords
268
+ max_x, max_y = self.geometry.max_coords
269
+
270
+ # Add margin to ensure coverage
271
+ min_x -= spacing
272
+ min_y -= spacing
273
+ max_x += spacing
274
+ max_y += spacing
275
+
276
+ points = []
277
+ row = 0
278
+ y = min_y
279
+
280
+ while y <= max_y:
281
+ # Offset every other row by half spacing
282
+ x_offset = (spacing / 2) if row % 2 else 0
283
+ x = min_x + x_offset
284
+
285
+ while x <= max_x:
286
+ points.append([x, y])
287
+ x += spacing
288
+
289
+ y += dy
290
+ row += 1
291
+
292
+ return np.array(points) if points else np.empty((0, 2))
293
+
294
+ def _pack_hex_grid(self) -> List[Circle]:
295
+ """
296
+ Pack circles using hexagonal grid placement.
297
+ Much faster and denser than random sampling for fixed radius.
298
+ """
299
+ radius = self.config.fixed_radius
300
+ circles = []
301
+
302
+ # Generate hex grid
303
+ grid_points = self._generate_hex_grid(radius)
304
+
305
+ if len(grid_points) == 0:
306
+ return circles
307
+
308
+ # Filter to points inside polygon
309
+ inside_mask = self.geometry.contains_points(grid_points)
310
+ interior_points = grid_points[inside_mask]
311
+
312
+ # Filter to points with enough clearance from boundary
313
+ min_clearance = radius + self.config.padding
314
+ boundary_distances = self.geometry.distances_to_boundary_batch(interior_points)
315
+ valid_mask = boundary_distances >= min_clearance
316
+
317
+ valid_points = interior_points[valid_mask]
318
+
319
+ if self.config.verbose:
320
+ print(f"Hex grid: {len(grid_points)} total -> {len(interior_points)} inside -> {len(valid_points)} valid")
321
+
322
+ # All valid points become circles (no collision check needed - hex grid guarantees no overlap)
323
+ for point in valid_points:
324
+ self._place_circle(point, radius)
325
+ circles.append((float(point[0]), float(point[1]), float(radius)))
326
+
327
+ if self.config.verbose:
328
+ print(f"Done! Placed {len(circles)} circles")
329
+
330
+ return circles
331
+
332
+ def generate(self) -> Iterator[Circle]:
333
+ """
334
+ Generate circles until no more can be placed.
335
+
336
+ For fixed_radius mode, uses optimized hex grid placement.
337
+ For variable radius mode, uses random sampling with best-fit selection.
338
+
339
+ Yields:
340
+ Tuples of (x, y, radius) for each placed circle.
341
+ """
342
+ # Use hex grid for fixed radius - much faster and denser
343
+ if self.config.fixed_radius is not None:
344
+ yield from self._pack_hex_grid()
345
+ return
346
+
347
+ # Variable radius mode - use random sampling
348
+ self.progress = PackingProgress(max_failed_attempts=self.config.max_failed_attempts)
349
+
350
+ while self.progress.failed_attempts < self.config.max_failed_attempts:
351
+ candidates = self._sample_candidate_points(self.config.sample_batch_size)
352
+ result = self._find_best_placement(candidates)
353
+
354
+ if result is not None:
355
+ center, radius = result
356
+ self._place_circle(center, radius)
357
+ self.progress.circles_placed += 1
358
+ self.progress.failed_attempts = 0
359
+
360
+ if self.config.verbose and self.progress.circles_placed % 25 == 0:
361
+ print(self.progress)
362
+
363
+ yield (float(center[0]), float(center[1]), float(radius))
364
+ else:
365
+ self.progress.failed_attempts += 1
366
+
367
+ if self.config.verbose and self.progress.failed_attempts % 50 == 0:
368
+ print(self.progress)
369
+
370
+ if self.config.verbose:
371
+ print(f"Done! {self.progress}")
372
+
373
+ def pack(self) -> List[Circle]:
374
+ """Pack circles and return them as a list."""
375
+ return list(self.generate())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: diskpack
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: A high-performance vectorized circle packer with spatial hashing.
5
5
  Author-email: James Kelly <mrkellyjam@gmail.com>
6
6
  Requires-Python: >=3.8
@@ -1,208 +0,0 @@
1
- import numpy as np
2
- from dataclasses import dataclass, field
3
- from typing import List, Optional, Iterator, Tuple, Dict
4
-
5
- # Type aliases
6
- Polygon = np.ndarray
7
- Point = np.ndarray
8
- GridKey = Tuple[int, int]
9
- Circle = Tuple[float, float, float]
10
-
11
-
12
- @dataclass
13
- class PackingConfig:
14
- """Configuration parameters for the circle packing algorithm."""
15
- padding: float = 1.5
16
- min_radius: float = 1.0
17
- grid_resolution_divisor: float = 25
18
- max_failed_attempts: int = 200
19
- mega_circle_threshold: float = 0.5
20
- ray_cast_epsilon: float = 1e-10
21
- sample_batch_size: int = 50
22
- fixed_radius: Optional[float] = None
23
- verbose: bool = False
24
-
25
-
26
- @dataclass
27
- class PackingProgress:
28
- """Tracks the current state of the packing algorithm."""
29
- circles_placed: int = 0
30
- failed_attempts: int = 0
31
- max_failed_attempts: int = 200
32
-
33
- @property
34
- def progress_ratio(self) -> float:
35
- """How close we are to stopping (0.0 = just started, 1.0 = about to stop)."""
36
- return self.failed_attempts / self.max_failed_attempts
37
-
38
- def __str__(self) -> str:
39
- return f"Placed: {self.circles_placed} | Failed attempts: {self.failed_attempts}/{self.max_failed_attempts} ({self.progress_ratio:.0%})"
40
-
41
-
42
- class PolygonGeometry:
43
- """Handles geometric calculations for polygon boundaries."""
44
-
45
- def __init__(self, polygons: List[Polygon], epsilon: float = 1e-10):
46
- self.polygons = [np.array(p, dtype=float) for p in polygons]
47
- self.epsilon = epsilon
48
- self._compute_bounds()
49
-
50
- def _compute_bounds(self) -> None:
51
- all_vertices = np.vstack(self.polygons)
52
- self.min_coords = np.min(all_vertices, axis=0)
53
- self.max_coords = np.max(all_vertices, axis=0)
54
-
55
- def contains_points(self, points: np.ndarray) -> np.ndarray:
56
- """Even-Odd Rule for interior detection, supports holes."""
57
- x, y = points[:, 0], points[:, 1]
58
- inside = np.zeros(len(points), dtype=bool)
59
-
60
- for poly in self.polygons:
61
- n = len(poly)
62
- for i in range(n):
63
- p1, p2 = poly[i], poly[(i + 1) % n]
64
- crosses_edge = (p1[1] > y) != (p2[1] > y)
65
- dy = p2[1] - p1[1] + self.epsilon
66
- x_intercept = (p2[0] - p1[0]) * (y - p1[1]) / dy + p1[0]
67
- inside ^= crosses_edge & (x < x_intercept)
68
-
69
- return inside
70
-
71
- def distance_to_boundary(self, point: Point) -> float:
72
- min_distance = float('inf')
73
- for poly in self.polygons:
74
- for i in range(len(poly)):
75
- p1, p2 = poly[i], poly[(i + 1) % len(poly)]
76
- min_distance = min(min_distance, self._point_to_segment_distance(point, p1, p2))
77
- return min_distance
78
-
79
- @staticmethod
80
- def _point_to_segment_distance(point: Point, seg_start: Point, seg_end: Point) -> float:
81
- segment_vec = seg_end - seg_start
82
- segment_length_sq = np.sum(segment_vec ** 2)
83
- if segment_length_sq == 0:
84
- return np.linalg.norm(point - seg_start)
85
- t = np.clip(np.dot(point - seg_start, segment_vec) / segment_length_sq, 0, 1)
86
- projection = seg_start + t * segment_vec
87
- return np.linalg.norm(point - projection)
88
-
89
-
90
- @dataclass
91
- class SpatialIndex:
92
- """Grid-based spatial index for efficient collision detection."""
93
- cell_size: float
94
- origin: np.ndarray
95
- mega_threshold: float
96
- grid: Dict[GridKey, List[int]] = field(default_factory=dict)
97
- mega_circles: List[int] = field(default_factory=list)
98
-
99
- def add_circle(self, index: int, center: Point, radius: float) -> None:
100
- if radius > self.cell_size * self.mega_threshold:
101
- self.mega_circles.append(index)
102
- else:
103
- key = self._get_cell_key(center)
104
- self.grid.setdefault(key, []).append(index)
105
-
106
- def get_nearby_indices(self, point: Point) -> Iterator[int]:
107
- yield from self.mega_circles
108
- center_key = self._get_cell_key(point)
109
- for dx in range(-1, 2):
110
- for dy in range(-1, 2):
111
- neighbor_key = (center_key[0] + dx, center_key[1] + dy)
112
- if neighbor_key in self.grid:
113
- yield from self.grid[neighbor_key]
114
-
115
- def _get_cell_key(self, point: Point) -> GridKey:
116
- cell_coords = ((point - self.origin) // self.cell_size).astype(int)
117
- return (int(cell_coords[0]), int(cell_coords[1]))
118
-
119
-
120
- class CirclePacker:
121
- """Packs circles within polygon boundaries using random sampling."""
122
-
123
- def __init__(self, polygons: List[Polygon], config: Optional[PackingConfig] = None):
124
- self.config = config or PackingConfig()
125
- self.geometry = PolygonGeometry(polygons, self.config.ray_cast_epsilon)
126
- self.centers: List[Point] = []
127
- self.radii: List[float] = []
128
- self.progress = PackingProgress(max_failed_attempts=self.config.max_failed_attempts)
129
-
130
- extent = max(self.geometry.max_coords - self.geometry.min_coords)
131
- cell_size = extent / self.config.grid_resolution_divisor
132
- self.spatial_index = SpatialIndex(
133
- cell_size=cell_size,
134
- origin=self.geometry.min_coords,
135
- mega_threshold=self.config.mega_circle_threshold
136
- )
137
-
138
- def _sample_candidate_points(self, count: int) -> np.ndarray:
139
- points = np.random.uniform(
140
- self.geometry.min_coords,
141
- self.geometry.max_coords,
142
- size=(count, 2)
143
- )
144
- return points[self.geometry.contains_points(points)]
145
-
146
- def _compute_max_radius(self, point: Point) -> float:
147
- max_radius = self.geometry.distance_to_boundary(point)
148
- for idx in self.spatial_index.get_nearby_indices(point):
149
- distance_to_circle = np.linalg.norm(self.centers[idx] - point) - self.radii[idx]
150
- max_radius = min(max_radius, distance_to_circle)
151
- return max_radius - self.config.padding
152
-
153
- def _find_best_placement(self, candidates: np.ndarray) -> Optional[Tuple[Point, float]]:
154
- best_point, best_radius = None, 0
155
- fixed = self.config.fixed_radius
156
-
157
- for point in candidates:
158
- radius = self._compute_max_radius(point)
159
- if fixed is not None:
160
- radius = fixed if radius >= fixed else -1
161
- if radius > best_radius:
162
- best_point, best_radius = point, radius
163
-
164
- if best_point is not None and best_radius >= self.config.min_radius:
165
- return best_point, best_radius
166
- return None
167
-
168
- def _place_circle(self, center: Point, radius: float) -> None:
169
- idx = len(self.centers)
170
- self.centers.append(center)
171
- self.radii.append(radius)
172
- self.spatial_index.add_circle(idx, center, radius)
173
-
174
- def generate(self) -> Iterator[Circle]:
175
- """
176
- Generate circles until no more can be placed.
177
-
178
- Yields:
179
- Tuples of (x, y, radius) for each placed circle.
180
- """
181
- self.progress = PackingProgress(max_failed_attempts=self.config.max_failed_attempts)
182
-
183
- while self.progress.failed_attempts < self.config.max_failed_attempts:
184
- candidates = self._sample_candidate_points(self.config.sample_batch_size)
185
- result = self._find_best_placement(candidates)
186
-
187
- if result is not None:
188
- center, radius = result
189
- self._place_circle(center, radius)
190
- self.progress.circles_placed += 1
191
- self.progress.failed_attempts = 0
192
-
193
- if self.config.verbose and self.progress.circles_placed % 25 == 0:
194
- print(self.progress)
195
-
196
- yield (float(center[0]), float(center[1]), float(radius))
197
- else:
198
- self.progress.failed_attempts += 1
199
-
200
- if self.config.verbose and self.progress.failed_attempts % 50 == 0:
201
- print(self.progress)
202
-
203
- if self.config.verbose:
204
- print(f"Done! {self.progress}")
205
-
206
- def pack(self) -> List[Circle]:
207
- """Pack circles and return them as a list."""
208
- return list(self.generate())
File without changes
File without changes
File without changes
File without changes