diff --git a/README.md b/README.md index f51277b..126c34a 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,32 @@ # Boîte à créneaux pour Inkscape + + + + + +
+Patron généré pour une boîte fermée de 100 × 80 × 50 mm : six pièces à créneaux rouges (avant, arrière, gauche, droite, fond, couvercle) disposées en grille + + Extension Inkscape 1.x qui génère le patron d'une boîte à découper au laser. Les faces s'assemblent par queues d'aronde droites (créneaux, _finger joints_) dont la largeur est paramétrable. Chaque pièce est un objet indépendant, déplaçable seul pour optimiser le placement sur la plaque. +
+ ## Table des matières - [Fonctionnalités](#fonctionnalités) - [Installation](#installation) - [Utilisation](#utilisation) - [En ligne de commande](#en-ligne-de-commande) +- [Paramètres](#paramètres) - [Le kerf : compensation du trait laser](#le-kerf--compensation-du-trait-laser) - [Le problème](#le-problème) - [Ce que fait l'extension](#ce-que-fait-lextension) - [Mesurer son kerf](#mesurer-son-kerf) - [Régler l'ajustement](#régler-lajustement) - [Structure du projet](#structure-du-projet) +- [Développement](#développement) ## Fonctionnalités @@ -29,12 +42,21 @@ Extension Inkscape 1.x qui génère le patron d'une boîte à découper au laser ## Installation -Copier `box_maker.inx`, `box_maker.py` et `box_core.py` dans `%APPDATA%\inkscape\extensions\boxMaker\`, puis redémarrer Inkscape. +Inkscape fermé, depuis le dossier du projet (PowerShell) : + +```powershell +.\deploy.ps1 # installe dans %APPDATA%\inkscape\extensions\boxMaker +.\deploy.ps1 -Uninstall # désinstalle +``` + +Le script copie `box_maker.inx`, `box_maker.py`, `box_core.py` et le dossier `images\` (schéma de l'onglet « Schéma »). Options : `-ExtensionsDir`, `-FolderName`, `-Force` (déploie même si Inkscape est ouvert). Redémarrer ensuite Inkscape. + +Installation manuelle : copier ces mêmes fichiers et le dossier `images\` dans `%APPDATA%\inkscape\extensions\boxMaker\`. ## Utilisation 1. Menu **Extensions > AlexDesign > Boîte à créneaux…** -2. Régler les paramètres, cocher **Aperçu en direct** si besoin +2. Régler les paramètres (illustrés dans l'onglet **Schéma**), cocher **Aperçu en direct** si besoin 3. **Appliquer** ### En ligne de commande @@ -51,6 +73,23 @@ $env:PYTHONPATH = "C:\Program Files\Inkscape\share\inkscape\extensions" Les options booléennes exigent une valeur (`--lid=true` / `--lid=false`). La couleur est un entier RGBA (`4278190335` = `0xff0000ff`, rouge). +## Paramètres + +![Schéma des paramètres : dimensions de la boîte, épaisseur et dimensions intérieures ou extérieures, largeur des créneaux, compensation du kerf](docs/parametres.png) + +| Onglet | Paramètre | Défaut | Rôle | +| ------------------- | -------------------------------------------- | ------------- | ------------------------------------------------------------------------ | +| Dimensions | Longueur / Largeur / Hauteur (mm) | 100 / 80 / 50 | Taille de la boîte (schéma 1) | +| Dimensions | Les dimensions sont | Extérieures | Intérieures : l'épaisseur est ajoutée (schéma 2) | +| Dimensions | Boîte fermée (avec couvercle) | coché | Décoché : 5 pièces, bord haut des côtés droit | +| Matière et créneaux | Épaisseur de la matière (mm) | 3 | Profondeur des créneaux et des encoches | +| Matière et créneaux | Largeur des créneaux (mm, 0 = 3 × épaisseur) | 0 | Ajustée pour un nombre impair de créneaux par bord (schéma 3) | +| Matière et créneaux | Compensation du trait laser, kerf (mm) | 0,1 | Contour élargi de kerf / 2 (schéma 4, [détails](#le-kerf--compensation-du-trait-laser)) | +| Tracé | Espacement entre les pièces (mm) | 5 | Écart entre les pièces de la grille | +| Tracé | Épaisseur du trait (mm) | 0,1 | Largeur du trait de découpe | +| Tracé | Couleur du trait | rouge | Couleur du trait de découpe | +| Tracé | Nommer les pièces (texte gravé !) | décoché | Ajoute le nom de chaque pièce en texte bleu | + ## Le kerf : compensation du trait laser ### Le problème @@ -98,6 +137,39 @@ Le kerf dépend de la matière : refaire la mesure en changeant de matériau ou ├── box_maker.py # Extension inkex : conversion en SVG, placement dans le calque ├── box_core.py # Géométrie pure Python (mm) : créneaux, kerf, mise en page ├── deploy.ps1 # Installation / désinstallation dans Inkscape +├── images/ +│ └── parametres.png # Schéma de l'onglet « Schéma » (déployé) +├── docs/ # Illustrations du README (non déployées) +│ ├── apercu-boite.png # Patron d'exemple +│ ├── parametres.png # Schéma des paramètres (+ sources .svg) +│ └── schema_parametres.py # Générateur des deux versions du schéma └── tests/ └── test_box_core.py # Tests unitaires de la géométrie ``` + +## Développement + +Environnement et tests (13 tests, géométrie pure, sans `inkex`) : + +```powershell +python -m venv .venv +.\.venv\Scripts\Activate.ps1 +pip install pytest +python -m pytest -q +``` + +### Schéma des paramètres + +`docs/schema_parametres.py` dessine le schéma avec les fonctions de `box_core.py` (il reste fidèle au rendu réel) et l'exporte en PNG via `C:\Program Files\Inkscape\bin\inkscape.com` : + +```powershell +python docs/schema_parametres.py # les deux versions +python docs/schema_parametres.py readme # docs/parametres.png, 1400 px +python docs/schema_parametres.py dialogue # images/parametres.png, 900 px, textes agrandis +``` + +Relancer après toute modification de la géométrie ou des paramètres, puis `.\deploy.ps1`. + +### Patron d'exemple + +`docs/apercu-boite.png` est le résultat de l'extension sur un document vierge de 300 × 300 mm (boîte fermée 100 × 80 × 50 mm, épaisseur 3 mm, étiquettes activées, trait 0,4 mm), exporté par `inkscape.com --export-area-drawing --export-width=900`. diff --git a/box_maker.inx b/box_maker.inx index a37aad3..66f9269 100644 --- a/box_maker.inx +++ b/box_maker.inx @@ -30,6 +30,11 @@ 0xff0000ff false + + + + images/parametres.png + diff --git a/deploy.ps1 b/deploy.ps1 index b6bbc00..b9ae2a8 100644 --- a/deploy.ps1 +++ b/deploy.ps1 @@ -3,7 +3,7 @@ Déploie l'extension « Boîte à créneaux » dans Inkscape. .DESCRIPTION - Copie box_maker.inx, box_maker.py et box_core.py dans un + Copie box_maker.inx, box_maker.py, box_core.py et images dans un sous-dossier dédié du répertoire des extensions utilisateur d'Inkscape (Inkscape explore récursivement ce répertoire). Refuse de s'exécuter si Inkscape est ouvert : les extensions ne sont chargées qu'au démarrage. @@ -97,6 +97,13 @@ foreach ($file in $files) { Write-Host " $action $file" } +# Schéma affiché dans l'onglet « Schéma » de la boîte de dialogue +$images = Join-Path $source 'images' +if (Test-Path -LiteralPath $images) { + Copy-Item -LiteralPath $images -Destination $destination -Recurse -Force + Write-Host " copié images\" +} + # Le cache Python d'une version précédente peut masquer les fichiers copiés $pycache = Join-Path $destination '__pycache__' if (Test-Path -LiteralPath $pycache) { diff --git a/docs/apercu-boite.png b/docs/apercu-boite.png new file mode 100644 index 0000000..9df4b5b Binary files /dev/null and b/docs/apercu-boite.png differ diff --git a/docs/parametres.png b/docs/parametres.png new file mode 100644 index 0000000..f8e7ec2 Binary files /dev/null and b/docs/parametres.png differ diff --git a/docs/parametres.svg b/docs/parametres.svg new file mode 100644 index 0000000..4ea3b43 --- /dev/null +++ b/docs/parametres.svg @@ -0,0 +1,108 @@ + + + + + + +1 Dimensions de la boîte + + + +Avant +Couvercle + + + + + +Longueur + + + + + +Largeur + + + + + +Hauteur +Couvercle (pointillés) : « Boîte fermée ». Décochée : 5 pièces, +bord haut des côtés droit. Minimum : 5 × l'épaisseur. +2 Épaisseur et type de dimensions + + + +Fond + + + + + +Extérieure + + + + + +Intérieure + + + + + + +Épaisseur +Extérieures : cotes hors tout. Intérieures : l'extension ajoute +2 épaisseurs (1 seule en hauteur si la boîte est ouverte). +3 Largeur des créneaux + + +Couvercle +Avant + + + + + +Créneau + + + + + + +Profondeur = épaisseur +0 = 3 × l'épaisseur. Largeur ajustée pour un nombre impair +de créneaux par bord (ici 7 sur l'avant). +4 Compensation du kerf + + + + + + + + +Créneau voulu + + + + + +Dessiné : créneau + kerf + + + + + + +kerf / 2 + +trait dessiné + +cote après découpe +Le laser enlève kerf / 2 de chaque côté du trait : le contour +est élargi d'autant. Défaut 0,1 mm ; 0 = pas de compensation. + diff --git a/docs/parametres_dialogue.svg b/docs/parametres_dialogue.svg new file mode 100644 index 0000000..6270a69 --- /dev/null +++ b/docs/parametres_dialogue.svg @@ -0,0 +1,108 @@ + + + + + + +1 Dimensions de la boîte + + + +Avant +Couvercle + + + + + +Longueur + + + + + +Largeur + + + + + +Hauteur +Couvercle (pointillés) : « Boîte fermée ». Décochée : 5 pièces, +bord haut des côtés droit. Minimum : 5 × l'épaisseur. +2 Épaisseur et type de dimensions + + + +Fond + + + + + +Extérieure + + + + + +Intérieure + + + + + + +Épaisseur +Extérieures : cotes hors tout. Intérieures : l'extension ajoute +2 épaisseurs (1 seule en hauteur si la boîte est ouverte). +3 Largeur des créneaux + + +Couvercle +Avant + + + + + +Créneau + + + + + + +Profondeur = épaisseur +0 = 3 × l'épaisseur. Largeur ajustée pour un nombre impair +de créneaux par bord (ici 7 sur l'avant). +4 Compensation du kerf + + + + + + + + +Créneau voulu + + + + + +Dessiné : créneau + kerf + + + + + + +kerf / 2 + +trait dessiné + +cote après découpe +Le laser enlève kerf / 2 de chaque côté du trait : le contour +est élargi d'autant. Défaut 0,1 mm ; 0 = pas de compensation. + diff --git a/docs/schema_parametres.py b/docs/schema_parametres.py new file mode 100644 index 0000000..8847849 --- /dev/null +++ b/docs/schema_parametres.py @@ -0,0 +1,318 @@ +#!/usr/bin/env python3 +# coding=utf-8 +""" +Genere le schema explicatif des parametres de l'extension. + + python docs/schema_parametres.py # les deux versions + python docs/schema_parametres.py readme # docs/parametres.svg + .png (README) + python docs/schema_parametres.py dialogue # images/parametres.png (onglet + # « Schéma » de la boite de dialogue) + +Les formes sont calculees par box_core (memes fonctions que l'extension), +le schema reste donc fidele au resultat reel. L'export PNG passe par +inkscape.com, qui attend la fin de l'export (contrairement au raccourci +Chocolatey « inkscape »). +""" + +import math +import os +import subprocess +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(HERE) +sys.path.insert(0, ROOT) + +import box_core # noqa: E402 + +W, H = 1400, 1080 # 4 panneaux de 700 x 540 + +INK = "#343a40" # element principal +GHOST = "#adb5bd" # elements secondaires / voisins +FILL = "#f1f3f5" # remplissage des pieces +DIM = "#d9480f" # cotes +DIM2 = "#1971c2" # cotes secondaires +EDGE = "#212529" # contour des formes +LASER = "#e03131" # trait de decoupe +FONT = "font-family:Arial,Helvetica,sans-serif" + +INKSCAPE = [r"C:\Program Files\Inkscape\bin\inkscape.com", "/usr/bin/inkscape", + "/Applications/Inkscape.app/Contents/MacOS/inkscape"] + +# L'extension n'existe qu'en francais : deux versions du meme schema. +# La version « dialogue » est affichee reduite (900 px de large) dans la boite de +# dialogue : textes x 1.25 pour rester lisibles. +VERSIONS = { + "readme": { + "svg": os.path.join(HERE, "parametres.svg"), + "png": os.path.join(HERE, "parametres.png"), + "png_width": 1400, + "font_scale": 1.0, + }, + "dialogue": { + "svg": os.path.join(HERE, "parametres_dialogue.svg"), + "png": os.path.join(ROOT, "images", "parametres.png"), + "png_width": 900, + "font_scale": 1.25, + }, +} + +out = [] +FS = 1.0 + + +def fmt(v): + return "{:.2f}".format(v).rstrip("0").rstrip(".") + + +def pts(points): + return " ".join("{},{}".format(fmt(x), fmt(y)) for x, y in points) + + +def polyline(points, color, width, extra=""): + out.append(''.format(pts(points), color, fmt(width), extra)) + + +def polygon(points, stroke, width, fill="none", extra=""): + out.append(''.format(pts(points), fill, stroke, fmt(width), extra)) + + +def text(x, y, s, size=17, color=INK, anchor="start", weight="normal", halo=True): + style = "{};font-size:{}px;font-weight:{};fill:{}".format( + FONT, fmt(size * FS), weight, color) + if halo: + style += ";paint-order:stroke;stroke:#ffffff;stroke-width:5px;stroke-linejoin:round" + out.append('{}'.format( + fmt(x), fmt(y), anchor, style, s)) + + +def note(x, y, lines): + """Legende de bas de panneau, derniere ligne sur la ligne de base `y`.""" + step = 22 * FS + for k, line in enumerate(lines): + text(x, y - (len(lines) - 1 - k) * step, line, size=16, halo=False) + + +def arrow_head(tip, direction, color, size=9): + ux, uy = direction + nx, ny = -uy, ux + base = (tip[0] - size * ux, tip[1] - size * uy) + out.append(''.format(pts([ + tip, (base[0] + 0.45 * size * nx, base[1] + 0.45 * size * ny), + (base[0] - 0.45 * size * nx, base[1] - 0.45 * size * ny)]), color)) + + +def dimension(p, q, offset, label, color=DIM, label_pos=0.5, label_shift=18, size=17): + """Cote entre p et q, decalee de `offset` perpendiculairement.""" + dx, dy = q[0] - p[0], q[1] - p[1] + length = math.hypot(dx, dy) + ux, uy = dx / length, dy / length + nx, ny = -uy, ux + a = (p[0] + offset * nx, p[1] + offset * ny) + b = (q[0] + offset * nx, q[1] + offset * ny) + sign = 1 if offset >= 0 else -1 + for base, end in ((p, a), (q, b)): + polyline([(base[0] + sign * 4 * nx, base[1] + sign * 4 * ny), + (end[0] + sign * 6 * nx, end[1] + sign * 6 * ny)], color, 1, + 'stroke-dasharray="3,3"') + polyline([a, b], color, 1.6) + arrow_head(a, (-ux, -uy), color) + arrow_head(b, (ux, uy), color) + lx = a[0] + label_pos * (b[0] - a[0]) + sign * label_shift * FS * nx + ly = a[1] + label_pos * (b[1] - a[1]) + sign * label_shift * FS * ny + 6 * FS + text(lx, ly, label, size=size, color=color, anchor="middle", weight="bold") + + +def panel_title(x, y, number, title): + text(x, y, "{} {}".format(number, title), size=22, weight="bold", halo=False) + + +def place(points, x0, y0, scale): + return [(x0 + x * scale, y0 + y * scale) for x, y in points] + + +# -------------------------------------------------------------------------- +# Panneaux +# -------------------------------------------------------------------------- + +def panel_1(): + """Longueur, largeur, hauteur : boite en perspective cavaliere.""" + panel_title(30, 50, 1, "Dimensions de la boîte") + s = 3.2 # px par mm + length, width, height = 100, 80, 50 + dx, dy = 0.5 * math.cos(math.radians(35)), -0.5 * math.sin(math.radians(35)) + x0, y0 = 150, 365 # coin avant bas gauche + + def p(x, y, z): # x longueur, y profondeur, z hauteur + return (x0 + (x + y * dx) * s, y0 + (y * dy - z) * s) + + L, P, Z = length, width, height + # Faces visibles : avant, dessus (couvercle), droite + polygon([p(0, 0, 0), p(L, 0, 0), p(L, 0, Z), p(0, 0, Z)], EDGE, 2, FILL) + polygon([p(0, 0, Z), p(L, 0, Z), p(L, P, Z), p(0, P, Z)], EDGE, 2, "#e9ecef", + 'stroke-dasharray="7,5"') + polygon([p(L, 0, 0), p(L, P, 0), p(L, P, Z), p(L, 0, Z)], EDGE, 2, "#dee2e6") + text(*p(L / 2, 0, Z / 2 - 3), "Avant", size=16, color=GHOST, anchor="middle", halo=False) + text(*p(L / 2, P / 2, Z + 2), "Couvercle", size=16, color=GHOST, anchor="middle", + halo=False) + dimension(p(0, 0, 0), p(L, 0, 0), 30, "Longueur") + dimension(p(L, 0, 0), p(L, P, 0), 34, "Largeur", label_shift=44) + dimension(p(0, 0, 0), p(0, 0, Z), -30, "Hauteur", label_shift=48) + note(30, 500, ["Couvercle (pointillés) : « Boîte fermée ». Décochée : 5 pièces,", + "bord haut des côtés droit. Minimum : 5 × l'épaisseur."]) + + +def panel_2(): + """Epaisseur et dimensions interieures / exterieures : coupe verticale.""" + ox = 700 + panel_title(ox + 30, 50, 2, "Épaisseur et type de dimensions") + s = 5.0 + t, width, height = 6, 80, 44 # epaisseur exageree pour la lisibilite + x0, y0 = ox + 150, 150 + hatch = 'fill="url(#hachures)"' + + def rect(x, y, w, h): + polygon(place([(x, y), (x + w, y), (x + w, y + h), (x, y + h)], x0, y0, s), + EDGE, 1.6, "url(#hachures)") + + rect(0, 0, t, height) # cote gauche + rect(width - t, 0, t, height) # cote droit + rect(0, height - t, width, t) # fond + del hatch + text(x0 + width / 2 * s, y0 + (height - t / 2) * s + 6, "Fond", size=15, + color=INK, anchor="middle") + + top = y0 + dimension((x0, top), (x0 + width * s, top), -58, "Extérieure", label_shift=18) + dimension((x0 + t * s, top + 60), (x0 + (width - t) * s, top + 60), 0, + "Intérieure", color=DIM2) + dimension((x0 + (width - t) * s, y0 + 12 * s), (x0 + width * s, y0 + 12 * s), 0, + "", color=DIM) + text(x0 + width * s + 16, y0 + 12 * s + 6, "Épaisseur", size=17, color=DIM, + weight="bold") + note(ox + 30, 500, ["Extérieures : cotes hors tout. Intérieures : l'extension ajoute", + "2 épaisseurs (1 seule en hauteur si la boîte est ouverte)."]) + + +def panel_3(): + """Creneaux : bord male de l'avant et bord femelle du couvercle.""" + oy = 540 + panel_title(30, oy + 50, 3, "Largeur des créneaux") + length, t = 100, 4 + finger = 3 * t # valeur par defaut (parametre a 0) + s = 5.6 + x0 = 70 + + def edge(kind, y_top, depth): + """Bord haut d'une piece de longueur `length`, corps de hauteur `depth` px.""" + corner = 0 if kind == box_core.MALE else t + pts_ = [(x0, y_top + depth), (x0, y_top + corner * s), (x0 + t * s, y_top + corner * s)] + for a, b, level in box_core.edge_segments(length, t, finger, kind): + pts_.append((x0 + a * s, y_top + level * s)) + pts_.append((x0 + b * s, y_top + level * s)) + pts_ += [(x0 + (length - t) * s, y_top + corner * s), + (x0 + length * s, y_top + corner * s), (x0 + length * s, y_top + depth)] + return pts_ + + # Couvercle (femelle) retourne au-dessus, l'avant (male) dessous + lid_y, front_y = oy + 150, oy + 200 + lid = [(x, 2 * lid_y - y) for x, y in edge(box_core.FEMALE, lid_y, 50)] + polygon(lid, GHOST, 1.4, "#f8f9fa") + polygon(edge(box_core.MALE, front_y, 150), EDGE, 2, FILL) + text(x0 + length * s + 12, lid_y - 20, "Couvercle", size=15, color=GHOST) + text(x0 + length * s + 12, front_y + 60, "Avant", size=15, color=GHOST) + + segs = box_core.edge_segments(length, t, finger, box_core.MALE) + a, b, _ = segs[4] + dimension((x0 + a * s, front_y), (x0 + b * s, front_y), 50, "Créneau", label_shift=20) + c, d, _ = segs[1] + mid = x0 + (c + d) / 2 * s + dimension((mid, front_y), (mid, front_y + t * s), 0, "", color=DIM2) + text(mid, front_y + t * s + 28, "Profondeur = épaisseur", size=15, color=DIM2, + weight="bold", anchor="middle") + note(30, oy + 500, ["0 = 3 × l'épaisseur. Largeur ajustée pour un nombre impair", + "de créneaux par bord (ici {} sur l'avant).".format(len(segs))]) + + +def panel_4(): + """Kerf : contour dessine decale de kerf / 2 vers l'exterieur.""" + ox, oy = 700, 540 + panel_title(ox + 30, oy + 50, 4, "Compensation du kerf") + t, finger, kerf = 4, 8, 1.2 # kerf exagere pour la lisibilite + s = 18 + x0, y0 = ox + 150, oy + 160 + nominal = [(0, t), (6, t), (6, 0), (6 + finger, 0), (6 + finger, t), + (20 + 2, t), (22, 13), (0, 13)] + drawn = box_core.offset_orthogonal(nominal, kerf / 2) + polygon(place(nominal, x0, y0, s), INK, 1.6, FILL, 'stroke-dasharray="6,4"') + polygon(place(drawn, x0, y0, s), LASER, 2.2) + + k = kerf / 2 + dimension((x0 + 6 * s, y0 + 0), (x0 + (6 + finger) * s, y0), 0, "") + text(x0 + (6 + finger / 2) * s, y0 + 36, "Créneau voulu", size=15, color=DIM, + anchor="middle", weight="bold") + dimension((x0 + (6 - k) * s, y0 - k * s), (x0 + (6 + finger + k) * s, y0 - k * s), -40, + "Dessiné : créneau + kerf", color=LASER, size=16) + dimension((x0 + 22 * s, y0 + 6 * s), (x0 + (22 + k) * s, y0 + 6 * s), 0, "", color=DIM2) + text(x0 + (22 + k) * s + 14, y0 + 6 * s + 6, "kerf / 2", size=16, color=DIM2, + weight="bold") + + lx, ly = x0 + 10, y0 + 13 * s + 36 + polyline([(lx, ly), (lx + 40, ly)], LASER, 2.2) + text(lx + 50, ly + 6, "trait dessiné", size=15, halo=False) + polyline([(lx + 190, ly), (lx + 230, ly)], INK, 1.6, 'stroke-dasharray="6,4"') + text(lx + 240, ly + 6, "cote après découpe", size=15, halo=False) + note(ox + 30, oy + 500, ["Le laser enlève kerf / 2 de chaque côté du trait : le contour", + "est élargi d'autant. Défaut 0,1 mm ; 0 = pas de compensation."]) + + +def export_png(svg_path, png_path, width): + exe = next((path for path in INKSCAPE if os.path.exists(path)), None) + if exe is None: + print("Inkscape introuvable : exporter {} a la main".format(svg_path)) + return + os.makedirs(os.path.dirname(png_path), exist_ok=True) + subprocess.run([exe, svg_path, "--export-type=png", + "--export-width={}".format(width), + "--export-filename={}".format(png_path)], + check=True, stderr=subprocess.DEVNULL) + print(png_path) + + +def build(name): + global FS + version = VERSIONS[name] + out.clear() + FS = version["font_scale"] + + out.append('' + '' + '' + ''.format(FILL, GHOST)) + out.append(''.format(W, H)) + for x1, y1, x2, y2 in ((700, 20, 700, H - 20), (20, 540, W - 20, 540)): + out.append(''.format(x1, y1, x2, y2)) + panel_1() + panel_2() + panel_3() + panel_4() + svg = ('\n' + '\n{2}\n\n').format(W, H, "\n".join(out)) + with open(version["svg"], "w", encoding="utf-8", newline="\n") as handle: + handle.write(svg) + print(version["svg"]) + export_png(version["svg"], version["png"], version["png_width"]) + + +def main(argv): + for name in argv or sorted(VERSIONS): + build(name) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/images/parametres.png b/images/parametres.png new file mode 100644 index 0000000..1d260e7 Binary files /dev/null and b/images/parametres.png differ