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
+
+
+
+
+
+
+
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
+
+
+
+| 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 @@
+
+
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 @@
+
+
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').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