147 lines
4.7 KiB
Python
147 lines
4.7 KiB
Python
#!/usr/bin/env python3
|
|
# coding=utf-8
|
|
"""
|
|
Noyau geometrique du circle packing.
|
|
|
|
Ce module ne depend pas d'inkex : il peut etre importe et teste sans Inkscape.
|
|
Un contour est represente par une liste d'anneaux (`rings`), chaque anneau
|
|
etant une liste de points (x, y) decrivant un polygone ferme implicitement
|
|
(le dernier point est relie au premier).
|
|
"""
|
|
|
|
import math
|
|
import random
|
|
|
|
|
|
def rings_bbox(rings):
|
|
"""Boite englobante (xmin, ymin, xmax, ymax), ou None si vide."""
|
|
xs = [p[0] for ring in rings for p in ring]
|
|
ys = [p[1] for ring in rings for p in ring]
|
|
if not xs:
|
|
return None
|
|
return min(xs), min(ys), max(xs), max(ys)
|
|
|
|
|
|
def point_in_rings(x, y, rings):
|
|
"""Test d'appartenance par crossing number, regle pair-impair.
|
|
|
|
La parite du nombre de traversees sur l'ensemble des anneaux donne
|
|
directement la gestion des trous : un point situe dans un trou traverse un
|
|
nombre pair de bords depuis l'infini.
|
|
"""
|
|
inside = False
|
|
for ring in rings:
|
|
n = len(ring)
|
|
for i in range(n):
|
|
x1, y1 = ring[i]
|
|
x2, y2 = ring[(i + 1) % n]
|
|
if (y1 > y) != (y2 > y):
|
|
# abscisse de l'intersection du segment avec la droite Y = y
|
|
xint = x1 + (y - y1) * (x2 - x1) / (y2 - y1)
|
|
if x < xint:
|
|
inside = not inside
|
|
return inside
|
|
|
|
|
|
def distance_point_segment(px, py, x1, y1, x2, y2):
|
|
"""Distance du point (px, py) au segment [(x1, y1), (x2, y2)]."""
|
|
dx = x2 - x1
|
|
dy = y2 - y1
|
|
seg_len2 = dx * dx + dy * dy
|
|
if seg_len2 == 0.0:
|
|
return math.hypot(px - x1, py - y1)
|
|
t = ((px - x1) * dx + (py - y1) * dy) / seg_len2
|
|
t = max(0.0, min(1.0, t))
|
|
return math.hypot(px - (x1 + t * dx), py - (y1 + t * dy))
|
|
|
|
|
|
def distance_to_rings(x, y, rings):
|
|
"""Distance minimale du point au contour (bord exterieur ou trous)."""
|
|
best = float('inf')
|
|
for ring in rings:
|
|
n = len(ring)
|
|
for i in range(n):
|
|
x1, y1 = ring[i]
|
|
x2, y2 = ring[(i + 1) % n]
|
|
d = distance_point_segment(x, y, x1, y1, x2, y2)
|
|
if d < best:
|
|
best = d
|
|
return best
|
|
|
|
|
|
class SpatialGrid:
|
|
"""Grille de hachage spatial pour retrouver rapidement les cercles voisins.
|
|
|
|
Sans elle, chaque tentative comparerait le point candidat a tous les cercles
|
|
deja poses, ce qui rend les dizaines de milliers de tentatives inutilisables.
|
|
"""
|
|
|
|
def __init__(self, cell_size):
|
|
self.cell_size = max(cell_size, 1e-9)
|
|
self.cells = {}
|
|
self.circles = []
|
|
|
|
def _key(self, x, y):
|
|
return (int(math.floor(x / self.cell_size)),
|
|
int(math.floor(y / self.cell_size)))
|
|
|
|
def add(self, x, y, r):
|
|
index = len(self.circles)
|
|
self.circles.append((x, y, r))
|
|
self.cells.setdefault(self._key(x, y), []).append(index)
|
|
|
|
def neighbors(self, x, y, reach):
|
|
"""Cercles dont le centre tombe dans les cellules couvrant `reach`."""
|
|
span = int(math.ceil(reach / self.cell_size))
|
|
ci, cj = self._key(x, y)
|
|
for i in range(ci - span, ci + span + 1):
|
|
for j in range(cj - span, cj + span + 1):
|
|
for index in self.cells.get((i, j), ()):
|
|
yield self.circles[index]
|
|
|
|
def __len__(self):
|
|
return len(self.circles)
|
|
|
|
|
|
def pack_circles(rings, min_radius, max_radius, gap=0.0, margin=0.0,
|
|
attempts=20000, max_circles=0, rng=None):
|
|
"""Greedy random circle packing. Renvoie la liste des (x, y, r) places.
|
|
|
|
A chaque tentative, un point est tire au hasard dans la boite englobante ;
|
|
s'il est interieur, on y place le plus grand cercle qui respecte `margin`
|
|
vis-a-vis du contour et `gap` vis-a-vis des cercles deja poses.
|
|
"""
|
|
rng = rng or random
|
|
bbox = rings_bbox(rings)
|
|
if bbox is None:
|
|
return []
|
|
xmin, ymin, xmax, ymax = bbox
|
|
if xmax - xmin <= 0 or ymax - ymin <= 0:
|
|
return []
|
|
|
|
grid = SpatialGrid(max_radius + gap)
|
|
# Un voisin ne peut contraindre le rayon que si son centre est a moins de
|
|
# son rayon (<= max_radius) + max_radius + gap.
|
|
reach = 2.0 * max_radius + gap
|
|
|
|
for _ in range(attempts):
|
|
if max_circles and len(grid) >= max_circles:
|
|
break
|
|
x = rng.uniform(xmin, xmax)
|
|
y = rng.uniform(ymin, ymax)
|
|
if not point_in_rings(x, y, rings):
|
|
continue
|
|
|
|
radius = min(max_radius, distance_to_rings(x, y, rings) - margin)
|
|
if radius < min_radius:
|
|
continue
|
|
|
|
for cx, cy, cr in grid.neighbors(x, y, reach):
|
|
radius = min(radius, math.hypot(x - cx, y - cy) - cr - gap)
|
|
if radius < min_radius:
|
|
break
|
|
if radius >= min_radius:
|
|
grid.add(x, y, radius)
|
|
|
|
return list(grid.circles)
|