Optimisation de l'ordre des layer générés
This commit is contained in:
parent
c94656d87e
commit
6af57f759d
11
README.md
11
README.md
@ -27,6 +27,8 @@ possible qui ne déborde pas du contour et ne touche aucun cercle déjà posé.
|
|||||||
conservée par défaut
|
conservée par défaut
|
||||||
- Transformations respectées : le groupe généré annule celle de son parent
|
- Transformations respectées : le groupe généré annule celle de son parent
|
||||||
- Graine aléatoire pour un résultat reproductible
|
- Graine aléatoire pour un résultat reproductible
|
||||||
|
- Cercles ordonnés de proche en proche (chaîne du plus proche voisin) : les logiciels
|
||||||
|
de découpe laser qui suivent l'ordre du document minimisent leurs déplacements à vide
|
||||||
- Grille de hachage spatial : des dizaines de milliers de tentatives restent rapides
|
- Grille de hachage spatial : des dizaines de milliers de tentatives restent rapides
|
||||||
- Script de déploiement PowerShell (installation, mise à jour, désinstallation)
|
- Script de déploiement PowerShell (installation, mise à jour, désinstallation)
|
||||||
|
|
||||||
@ -118,7 +120,8 @@ API de `packing_core.py` (utilisable seule, hors Inkscape) :
|
|||||||
| `distance_point_segment(px, py, x1, y1, x2, y2)` | Distance point → segment |
|
| `distance_point_segment(px, py, x1, y1, x2, y2)` | Distance point → segment |
|
||||||
| `distance_to_rings(x, y, rings)` | Distance au contour le plus proche |
|
| `distance_to_rings(x, y, rings)` | Distance au contour le plus proche |
|
||||||
| `SpatialGrid(cell_size)` | Grille de hachage des cercles posés |
|
| `SpatialGrid(cell_size)` | Grille de hachage des cercles posés |
|
||||||
| `pack_circles(rings, min_radius, max_radius, …)` | Boucle de packing, renvoie `[(x, y, r), …]` |
|
| `order_by_proximity(circles)` | Réordonne en chaîne du plus proche voisin (départ au coin haut-gauche) |
|
||||||
|
| `pack_circles(rings, min_radius, max_radius, …)` | Boucle de packing, renvoie `[(x, y, r), …]` déjà ordonnés |
|
||||||
|
|
||||||
Un contour (`rings`) est une liste d'anneaux, chaque anneau étant une liste de
|
Un contour (`rings`) est une liste d'anneaux, chaque anneau étant une liste de
|
||||||
points `(x, y)` fermée implicitement.
|
points `(x, y)` fermée implicitement.
|
||||||
@ -135,7 +138,7 @@ pip install pytest
|
|||||||
python -m pytest test_packing.py -q
|
python -m pytest test_packing.py -q
|
||||||
```
|
```
|
||||||
|
|
||||||
Résultat attendu : **14 passés, 1 ignoré**. Le test ignoré est celui de bout en
|
Résultat attendu : **20 passés, 1 ignoré**. Le test ignoré est celui de bout en
|
||||||
bout, sauté quand `inkex` n'est pas importable. Pour l'exécuter, ajouter le dossier
|
bout, sauté quand `inkex` n'est pas importable. Pour l'exécuter, ajouter le dossier
|
||||||
des extensions d'Inkscape au `PYTHONPATH` :
|
des extensions d'Inkscape au `PYTHONPATH` :
|
||||||
|
|
||||||
@ -146,6 +149,8 @@ python -m pytest test_packing.py -q
|
|||||||
|
|
||||||
Les tests couvrent l'appartenance à une forme trouée, les distances au contour, la
|
Les tests couvrent l'appartenance à une forme trouée, les distances au contour, la
|
||||||
grille spatiale, et les invariants du packing : aucun chevauchement, rayons dans
|
grille spatiale, et les invariants du packing : aucun chevauchement, rayons dans
|
||||||
les bornes, cercles tous intérieurs, reproductibilité à graine fixée.
|
les bornes, cercles tous intérieurs, reproductibilité à graine fixée. L'ordre de sortie est
|
||||||
|
comparé à une recherche du plus proche voisin en force brute et à la longueur du parcours
|
||||||
|
d'un ordre mélangé.
|
||||||
|
|
||||||
Aucun outil de lint n'est configuré dans le projet ; le code suit PEP 8.
|
Aucun outil de lint n'est configuré dans le projet ; le code suit PEP 8.
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -103,6 +103,92 @@ class SpatialGrid:
|
|||||||
return len(self.circles)
|
return len(self.circles)
|
||||||
|
|
||||||
|
|
||||||
|
def order_by_proximity(circles, cell_size=None):
|
||||||
|
"""Reordonne les cercles en chaine du plus proche voisin.
|
||||||
|
|
||||||
|
Deux cercles consecutifs dans la liste sont physiquement proches, ce qui
|
||||||
|
minimise les deplacements a vide d'une tete de decoupe laser suivant
|
||||||
|
l'ordre du document. Le depart est le cercle de (y, x) minimal, soit le
|
||||||
|
coin haut-gauche : le resultat est deterministe.
|
||||||
|
|
||||||
|
La recherche du voisin le plus proche balaie des anneaux de cellules de
|
||||||
|
plus en plus larges autour du point courant, et s'arrete des que l'anneau
|
||||||
|
suivant ne peut plus contenir mieux. Les cercles consommes sont retires de
|
||||||
|
leur cellule, donc la grille se vide au fil du parcours.
|
||||||
|
"""
|
||||||
|
if len(circles) < 2:
|
||||||
|
return list(circles)
|
||||||
|
|
||||||
|
if cell_size is None:
|
||||||
|
xs = [c[0] for c in circles]
|
||||||
|
ys = [c[1] for c in circles]
|
||||||
|
width = max(xs) - min(xs)
|
||||||
|
height = max(ys) - min(ys)
|
||||||
|
area = max(width * height, 1e-9)
|
||||||
|
# vise environ un cercle par cellule
|
||||||
|
cell_size = math.sqrt(area / len(circles))
|
||||||
|
cell_size = max(cell_size, 1e-9)
|
||||||
|
|
||||||
|
def key(x, y):
|
||||||
|
return (int(math.floor(x / cell_size)), int(math.floor(y / cell_size)))
|
||||||
|
|
||||||
|
cells = {}
|
||||||
|
for index, (x, y, _r) in enumerate(circles):
|
||||||
|
cells.setdefault(key(x, y), []).append(index)
|
||||||
|
|
||||||
|
def take(index):
|
||||||
|
"""Retire un cercle de sa cellule."""
|
||||||
|
x, y, _r = circles[index]
|
||||||
|
bucket = cells[key(x, y)]
|
||||||
|
bucket.remove(index)
|
||||||
|
if not bucket:
|
||||||
|
del cells[key(x, y)]
|
||||||
|
|
||||||
|
# depart : coin haut-gauche
|
||||||
|
current = min(range(len(circles)),
|
||||||
|
key=lambda i: (circles[i][1], circles[i][0]))
|
||||||
|
take(current)
|
||||||
|
order = [current]
|
||||||
|
|
||||||
|
imin = min(c[0] for c in cells)
|
||||||
|
imax = max(c[0] for c in cells)
|
||||||
|
jmin = min(c[1] for c in cells)
|
||||||
|
jmax = max(c[1] for c in cells)
|
||||||
|
|
||||||
|
remaining = len(circles) - 1
|
||||||
|
while remaining:
|
||||||
|
cx, cy, _r = circles[current]
|
||||||
|
ci, cj = key(cx, cy)
|
||||||
|
# au-dela, les anneaux sortent completement de la grille
|
||||||
|
k_max = max(ci - imin, imax - ci, cj - jmin, jmax - cj)
|
||||||
|
best_index = None
|
||||||
|
best_dist = float('inf')
|
||||||
|
k = 0
|
||||||
|
while k <= k_max:
|
||||||
|
# borne inferieure de la distance aux cellules de l'anneau k
|
||||||
|
if best_index is not None and (k - 1) * cell_size >= best_dist:
|
||||||
|
break
|
||||||
|
for i in range(ci - k, ci + k + 1):
|
||||||
|
for j in range(cj - k, cj + k + 1):
|
||||||
|
# anneau seul : l'interieur a deja ete balaye
|
||||||
|
if k and abs(i - ci) != k and abs(j - cj) != k:
|
||||||
|
continue
|
||||||
|
for index in cells.get((i, j), ()):
|
||||||
|
x, y, _r2 = circles[index]
|
||||||
|
d = math.hypot(x - cx, y - cy)
|
||||||
|
if d < best_dist:
|
||||||
|
best_dist = d
|
||||||
|
best_index = index
|
||||||
|
k += 1
|
||||||
|
|
||||||
|
current = best_index
|
||||||
|
take(current)
|
||||||
|
order.append(current)
|
||||||
|
remaining -= 1
|
||||||
|
|
||||||
|
return [circles[i] for i in order]
|
||||||
|
|
||||||
|
|
||||||
def pack_circles(rings, min_radius, max_radius, gap=0.0, margin=0.0,
|
def pack_circles(rings, min_radius, max_radius, gap=0.0, margin=0.0,
|
||||||
attempts=20000, max_circles=0, rng=None):
|
attempts=20000, max_circles=0, rng=None):
|
||||||
"""Greedy random circle packing. Renvoie la liste des (x, y, r) places.
|
"""Greedy random circle packing. Renvoie la liste des (x, y, r) places.
|
||||||
@ -110,6 +196,8 @@ def pack_circles(rings, min_radius, max_radius, gap=0.0, margin=0.0,
|
|||||||
A chaque tentative, un point est tire au hasard dans la boite englobante ;
|
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`
|
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.
|
vis-a-vis du contour et `gap` vis-a-vis des cercles deja poses.
|
||||||
|
|
||||||
|
La liste renvoyee est ordonnee par `order_by_proximity`.
|
||||||
"""
|
"""
|
||||||
rng = rng or random
|
rng = rng or random
|
||||||
bbox = rings_bbox(rings)
|
bbox = rings_bbox(rings)
|
||||||
@ -143,4 +231,5 @@ def pack_circles(rings, min_radius, max_radius, gap=0.0, margin=0.0,
|
|||||||
if radius >= min_radius:
|
if radius >= min_radius:
|
||||||
grid.add(x, y, radius)
|
grid.add(x, y, radius)
|
||||||
|
|
||||||
return list(grid.circles)
|
# ordre de sortie pense pour la decoupe laser, pas pour le rendu
|
||||||
|
return order_by_proximity(grid.circles)
|
||||||
|
|||||||
@ -11,6 +11,7 @@ from packing_core import (
|
|||||||
SpatialGrid,
|
SpatialGrid,
|
||||||
distance_point_segment,
|
distance_point_segment,
|
||||||
distance_to_rings,
|
distance_to_rings,
|
||||||
|
order_by_proximity,
|
||||||
pack_circles,
|
pack_circles,
|
||||||
point_in_rings,
|
point_in_rings,
|
||||||
rings_bbox,
|
rings_bbox,
|
||||||
@ -125,6 +126,74 @@ def test_empty_rings():
|
|||||||
assert pack_circles([], min_radius=1, max_radius=5) == []
|
assert pack_circles([], min_radius=1, max_radius=5) == []
|
||||||
|
|
||||||
|
|
||||||
|
# --- ordre de sortie (decoupe laser) ----------------------------------------
|
||||||
|
|
||||||
|
def _travel(circles):
|
||||||
|
"""Longueur du deplacement de centre a centre dans l'ordre donne."""
|
||||||
|
return sum(math.hypot(circles[i][0] - circles[i + 1][0],
|
||||||
|
circles[i][1] - circles[i + 1][1])
|
||||||
|
for i in range(len(circles) - 1))
|
||||||
|
|
||||||
|
|
||||||
|
def _grid_circles(cols=20, rows=20, step=10):
|
||||||
|
return [(c * step, r * step, 1.0) for r in range(rows) for c in range(cols)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_order_is_a_permutation():
|
||||||
|
circles = _grid_circles()
|
||||||
|
ordered = order_by_proximity(circles)
|
||||||
|
assert sorted(ordered) == sorted(circles)
|
||||||
|
|
||||||
|
|
||||||
|
def test_order_starts_top_left():
|
||||||
|
circles = [(50, 50, 1), (0, 0, 1), (0, 50, 1), (50, 0, 1)]
|
||||||
|
assert order_by_proximity(circles)[0] == (0, 0, 1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_order_shortens_travel_on_shuffled_grid():
|
||||||
|
circles = _grid_circles()
|
||||||
|
shuffled = circles[:]
|
||||||
|
random.Random(11).shuffle(shuffled)
|
||||||
|
ordered = order_by_proximity(shuffled)
|
||||||
|
# serpentin optimal : 400 points espaces de 10
|
||||||
|
optimal = _travel(circles)
|
||||||
|
assert _travel(ordered) < _travel(shuffled) / 3
|
||||||
|
assert _travel(ordered) < 2 * optimal
|
||||||
|
|
||||||
|
|
||||||
|
def test_order_matches_brute_force_nearest_neighbour():
|
||||||
|
rng = random.Random(4)
|
||||||
|
circles = [(rng.uniform(0, 100), rng.uniform(0, 100), 1.0)
|
||||||
|
for _ in range(200)]
|
||||||
|
ordered = order_by_proximity(circles)
|
||||||
|
|
||||||
|
remaining = list(circles)
|
||||||
|
remaining.remove(min(circles, key=lambda c: (c[1], c[0])))
|
||||||
|
current = ordered[0]
|
||||||
|
for step in ordered[1:]:
|
||||||
|
expected = min(remaining,
|
||||||
|
key=lambda c: math.hypot(c[0] - current[0],
|
||||||
|
c[1] - current[1]))
|
||||||
|
assert math.hypot(step[0] - current[0], step[1] - current[1]) == \
|
||||||
|
pytest.approx(math.hypot(expected[0] - current[0],
|
||||||
|
expected[1] - current[1]))
|
||||||
|
remaining.remove(step)
|
||||||
|
current = step
|
||||||
|
|
||||||
|
|
||||||
|
def test_order_edge_cases():
|
||||||
|
assert order_by_proximity([]) == []
|
||||||
|
assert order_by_proximity([(1, 2, 3)]) == [(1, 2, 3)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_pack_circles_output_is_ordered():
|
||||||
|
circles = pack_circles(SQUARE, min_radius=2, max_radius=10, gap=0.5,
|
||||||
|
margin=0.5, attempts=3000, rng=random.Random(9))
|
||||||
|
shuffled = circles[:]
|
||||||
|
random.Random(2).shuffle(shuffled)
|
||||||
|
assert _travel(circles) < _travel(shuffled) / 3
|
||||||
|
|
||||||
|
|
||||||
# --- bout en bout dans Inkscape (saute si inkex absent) ---------------------
|
# --- bout en bout dans Inkscape (saute si inkex absent) ---------------------
|
||||||
|
|
||||||
def test_extension_runs_on_sample_svg(tmp_path):
|
def test_extension_runs_on_sample_svg(tmp_path):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user