From b90478c10d9cad4cbe3e05e6341d6d8aacd41eaa Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 12 Sep 2026 18:48:05 +0200 Subject: [PATCH] =?UTF-8?q?Possibilit=C3=A9=20d'utiliser=20un=20motif?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 35 +++++++++++++--- circle_packing.inx | 13 ++++++ circle_packing.py | 90 ++++++++++++++++++++++++++++++++++++---- packing_core.py | 18 ++++++++ test_packing.py | 101 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 243 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 1b39c2b..c0008c3 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,9 @@ possible qui ne déborde pas du contour et ne touche aucun cercle déjà posé. - Remplissage de `path`, `rect`, `circle`, `ellipse`, `polygon` (les groupes sélectionnés sont explorés pour en extraire les formes ; `line` et `polyline` sont ignorées) +- Forme déposée au choix : cercle (par défaut) ou **forme personnalisée** — le motif + est le dernier objet sélectionné, chaque copie étant mise à l'échelle pour + tenir dans le disque calculé, orientée au hasard et stylée comme le motif - Gestion des trous : les sous-chemins internes sont exclus (règle pair-impair) - Rayons minimal et maximal réglables, écart entre cercles (*gap*) et marge intérieure au contour, exprimés dans l'unité de votre choix @@ -67,9 +70,27 @@ ensemble, `circle_packing.py` important `packing_core.py`. ## Utilisation 1. Sélectionner une ou plusieurs formes fermées. -2. **Extensions → Rendu → Remplissage par cercles (circle packing)…** +2. **Extensions → AlexDesign → Remplissage par cercles (circle packing)…** 3. Régler les paramètres, puis **Appliquer**. +### Remplir avec une forme personnalisée + +Une extension Inkscape n'a pas accès au presse-papiers : la forme copiée doit +d'abord être collée dans le document. + +1. Coller la forme voulue (**Ctrl+V**). +2. Sélectionner les formes à remplir, puis ajouter le motif à la sélection + **en dernier** (**Maj+clic**). +3. Lancer l'extension, choisir « Forme déposée : Forme personnalisée ». + +Le dernier objet sélectionné sert de motif et n'est pas rempli ; il reste +en place même si « Conserver la forme d'origine » est décoché. Chaque copie est +mise à l'échelle pour que son cercle circonscrit vaille le rayon calculé — la +garantie de non-chevauchement et de non-débordement est donc conservée — puis +orientée au hasard (reproductible à graine fixée) et stylée comme le motif. +Les motifs très allongés donnent un remplissage plus lâche, leur cercle +circonscrit étant large devant leur surface. + Le fichier `tests/data/shapes.svg` contient quatre cas d'essai : rectangle, cercle, chemin courbe et chemin avec trou. @@ -86,6 +107,7 @@ Conseils de réglage : | Paramètre | Défaut | Rôle | |---|---|---| +| Forme déposée | Cercle | Cercle, ou forme personnalisée (dernier objet sélectionné) | | Rayon minimal | 1.0 | En dessous, le cercle candidat est rejeté | | Rayon maximal | 20.0 | Taille maximale d'un cercle | | Écart entre cercles | 0.5 | Distance imposée entre deux cercles voisins | @@ -116,6 +138,7 @@ API de `packing_core.py` (utilisable seule, hors Inkscape) : | Fonction | Rôle | |---|---| | `rings_bbox(rings)` | Boîte englobante d'un contour | +| `bounding_circle(rings)` | Centre de la bbox et rayon circonscrit — sert à inscrire un motif dans le disque calculé | | `point_in_rings(x, y, rings)` | Point dans la forme, règle pair-impair | | `distance_point_segment(px, py, x1, y1, x2, y2)` | Distance point → segment | | `distance_to_rings(x, y, rings)` | Distance au contour le plus proche | @@ -138,13 +161,15 @@ pip install pytest python -m pytest test_packing.py -q ``` -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 -des extensions d'Inkscape au `PYTHONPATH` : +Résultat attendu : **24 passés, 3 ignorés**. Les tests ignorés sont ceux de bout en +bout, sautés quand `inkex` n'est pas importable. Pour les exécuter, ajouter le dossier +des extensions d'Inkscape au `PYTHONPATH` et installer les dépendances d'`inkex` +absentes d'un Python standard : ```powershell +pip install lxml tinycss2 cssselect2 cssselect $env:PYTHONPATH = 'C:\Program Files\Inkscape\share\inkscape\extensions' -python -m pytest test_packing.py -q +python -m pytest test_packing.py -q # 27 passés ``` Les tests couvrent l'appartenance à une forme trouée, les distances au contour, la diff --git a/circle_packing.inx b/circle_packing.inx index b8f7402..8669ceb 100644 --- a/circle_packing.inx +++ b/circle_packing.inx @@ -5,6 +5,11 @@ + + + + 1.0 10 assert "#3366cc" in out + + +def test_extension_fills_with_custom_shape(tmp_path): + pytest.importorskip("inkex") + from circle_packing import CirclePacking + + svg = tmp_path / "in.svg" + svg.write_text( + '' + '' + '', + encoding="utf-8") + + ext = CirclePacking() + # le motif est le dernier --id, soit le dernier objet selectionne + ext.run(["--id=r", "--id=m", "--shape_mode=custom", "--unit=px", + "--min_radius=2", "--max_radius=10", "--attempts=2000", + "--seed=1", str(svg)], + output=str(tmp_path / "out.svg")) + + out = (tmp_path / "out.svg").read_text(encoding="utf-8") + assert " 10 + # les copies conservent le style du motif, pas celui de la forme remplie + assert "#cc3366" in out + + import inkex + svg = inkex.load_svg(str(tmp_path / "out.svg")).getroot() + group = [g for g in svg.iter() if isinstance(g, inkex.Group)][0] + copies = [c for c in group if isinstance(c, inkex.PathElement)] + assert len(copies) > 10 + # la transformation est cuite dans les donnees du chemin + assert all(not c.get("transform") for c in copies) + # echelles et rotations tirees au hasard : aucune copie identique + assert len({str(c.path) for c in copies}) == len(copies) + # le motif source reste en place + assert svg.getElementById("m") is not None + + +def test_custom_motif_is_the_last_selected(tmp_path): + """L'ordre de selection decide du motif, pas l'ordre du document.""" + pytest.importorskip("inkex") + import inkex + from circle_packing import CirclePacking + + svg = tmp_path / "in.svg" + svg.write_text( + '' + '' + '', + encoding="utf-8") + + # ordre inverse : le rectangle est selectionne en dernier, c'est lui le + # motif, et c'est le triangle qui est rempli + ext = CirclePacking() + ext.run(["--id=m", "--id=r", "--shape_mode=custom", "--unit=px", + "--min_radius=2", "--max_radius=10", "--attempts=2000", + "--seed=1", str(svg)], + output=str(tmp_path / "out.svg")) + + out = inkex.load_svg(str(tmp_path / "out.svg")).getroot() + group = [g for g in out.iter() if isinstance(g, inkex.Group)][0] + copies = [c for c in group if isinstance(c, inkex.PathElement)] + assert len(copies) > 5 + # les copies portent le style du rectangle-motif + assert all(copy.style.get("fill") == "#3366cc" for copy in copies) + # le groupe est insere apres le triangle rempli, pas apres le rectangle + assert group.getprevious().get("id") == "m"