This commit is contained in:
Alex 2026-09-24 18:42:22 +02:00
parent 5dc977f9dd
commit 41987f8ea8
17 changed files with 1258 additions and 210 deletions

11
.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
# Cache Python et outils
__pycache__/*
*.pyc
.pytest_cache/
.venv/
# Archives de distribution
*.zip
# SVG intermediaires du schema (regeneres par docs/schema_parametres.py)
docs/*.svg

View File

@ -39,6 +39,7 @@ Extension Inkscape 1.x qui génère le patron d'une boîte à découper au laser
- Couleur et épaisseur du trait réglables (défaut : rouge, 0,1 mm) - Couleur et épaisseur du trait réglables (défaut : rouge, 0,1 mm)
- Étiquettes optionnelles (texte bleu). Désactivées par défaut : le laser graverait le texte - Étiquettes optionnelles (texte bleu). Désactivées par défaut : le laser graverait le texte
- Message d'erreur si une dimension est trop petite (minimum 5 × l'épaisseur en dimension extérieure) - Message d'erreur si une dimension est trop petite (minimum 5 × l'épaisseur en dimension extérieure)
- Interface **en français et en anglais** selon la langue d'Inkscape : libellés, onglet **Aide**, noms des pièces et messages d'erreur. Le schéma de l'onglet **Schéma** / **Diagram** est en anglais (Inkscape ne traduit pas les images)
## Installation ## Installation
@ -49,14 +50,14 @@ Inkscape fermé, depuis le dossier du projet (PowerShell) :
.\deploy.ps1 -Uninstall # désinstalle .\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. Le script copie `box_maker.inx`, `box_maker.py`, `box_core.py`, `images\parameters_en.png` (schéma de la boîte de dialogue) et le dossier `locale\` (traductions compilées). 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\`. Installation manuelle : copier ces mêmes fichiers et dossiers, en gardant l'arborescence, dans `%APPDATA%\inkscape\extensions\boxMaker\`.
## Utilisation ## Utilisation
1. Menu **Extensions > AlexDesign > Boîte à créneaux…** 1. Menu **Extensions > AlexDesign > Boîte à créneaux…** (en anglais : _Finger-Joint Box…_)
2. Régler les paramètres (illustrés dans l'onglet **Schéma**), cocher **Aperçu en direct** si besoin 2. Régler les paramètres (illustrés dans l'onglet **Schéma**, résumés dans l'onglet **Aide**), cocher **Aperçu en direct** si besoin
3. **Appliquer** 3. **Appliquer**
### En ligne de commande ### En ligne de commande
@ -133,39 +134,58 @@ Le kerf dépend de la matière : refaire la mesure en changeant de matériau ou
``` ```
. .
├── box_maker.inx # Interface de l'extension (paramètres, menu) ├── box_maker.inx # Interface de l'extension (textes source en anglais)
├── box_maker.py # Extension inkex : conversion en SVG, placement dans le calque ├── box_maker.py # Extension inkex : conversion en SVG, placement, textes traduits
├── box_core.py # Géométrie pure Python (mm) : créneaux, kerf, mise en page ├── box_core.py # Géométrie pure Python (mm) : créneaux, kerf, mise en page
├── i18n.py # Chaîne de traduction (extract / update / compile), Python pur
├── po/ # box_maker.pot, en.po, fr.po (seul fr.po se traduit à la main)
├── locale/ # Catalogues .mo compilés (générés, déployés)
├── deploy.ps1 # Installation / désinstallation dans Inkscape ├── deploy.ps1 # Installation / désinstallation dans Inkscape
├── images/ ├── images/
│ └── parametres.png # Schéma de l'onglet « Schéma » (déployé) │ └── parameters_en.png # Schéma de l'onglet « Diagram » (déployé)
├── docs/ # Illustrations du README (non déployées) ├── docs/ # Illustrations du README (non déployées)
│ ├── apercu-boite.png # Patron d'exemple │ ├── apercu-boite.png # Patron d'exemple
│ ├── parametres.png # Schéma des paramètres (+ sources .svg) │ ├── parametres.png # Schéma des paramètres en français
│ └── schema_parametres.py # Générateur des deux versions du schéma │ └── schema_parametres.py # Générateur du schéma fr et en
└── tests/ └── tests/
└── test_box_core.py # Tests unitaires de la géométrie ├── test_box_core.py # Tests unitaires de la géométrie
└── test_extension.py # Traductions, cohérence .inx, bout en bout avec inkex
``` ```
## Développement ## Développement
Environnement et tests (13 tests, géométrie pure, sans `inkex`) : Environnement et tests :
```powershell ```powershell
python -m venv .venv python -m venv .venv
.\.venv\Scripts\Activate.ps1 .\.venv\Scripts\Activate.ps1
pip install pytest pip install pytest
python -m pytest -q python -m pytest -q # 18 réussis, 3 ignorés (sans inkex)
$env:PYTHONPATH = "C:\Program Files\Inkscape\share\inkscape\extensions"
python -m pytest -q # 21 réussis (avec inkex)
``` ```
Les tests couvrent la géométrie (`box_core`), les catalogues de traduction (complets, `.mo` à jour, champs de format conservés), la cohérence `.inx` ↔ `add_argument`, la présence des images de la boîte de dialogue et l'extension de bout en bout (6 pièces nommées, message d'erreur).
### Traductions
Les textes source (`.inx` et appels `_("...")` de `box_maker.py`) sont en anglais ; Inkscape les affiche traduits grâce à l'attribut `translationdomain="box_maker"` du `.inx`.
```powershell
python i18n.py # extract + update + compile ; liste les chaînes à traduire
```
Après modification d'un texte : lancer `python i18n.py`, compléter les `msgstr` vides de `po/fr.po`, relancer `python i18n.py`, puis `.\deploy.ps1`. `po/en.po` se remplit tout seul ; ne jamais modifier les `.mo`. Pour une nouvelle langue : l'ajouter à `LANGUAGES` dans `i18n.py`, puis même démarche avec `po/<langue>.po`.
### Schéma des paramètres ### 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` : `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 ```powershell
python docs/schema_parametres.py # les deux versions python docs/schema_parametres.py # les deux versions
python docs/schema_parametres.py readme # docs/parametres.png, 1400 px python docs/schema_parametres.py fr # docs/parametres.png, 1400 px (README)
python docs/schema_parametres.py dialogue # images/parametres.png, 900 px, textes agrandis python docs/schema_parametres.py en # images/parameters_en.png, 900 px, textes agrandis (boîte de dialogue)
``` ```
Relancer après toute modification de la géométrie ou des paramètres, puis `.\deploy.ps1`. Relancer après toute modification de la géométrie ou des paramètres, puis `.\deploy.ps1`.

View File

@ -33,6 +33,27 @@ class Panel:
return Panel(self.name, [(x + dx, y + dy) for x, y in self.points]) return Panel(self.name, [(x + dx, y + dy) for x, y in self.points])
_DIMENSION_NAMES = {"length": "longueur", "width": "largeur", "height": "hauteur"}
class DimensionTooSmall(ValueError):
"""Dimension exterieure trop petite pour l'epaisseur.
Les champs (dimension = "length" | "width" | "height", size, thickness,
minimum) permettent a l'extension de traduire le message.
"""
def __init__(self, dimension, size, thickness):
self.dimension = dimension
self.size = size
self.thickness = thickness
self.minimum = 5 * thickness
super().__init__(
f"La {_DIMENSION_NAMES[dimension]} ({size:.1f} mm extérieure) est trop petite "
f"pour l'épaisseur ({thickness:g} mm) : il faut au moins {self.minimum:g} mm."
)
def finger_count(middle, finger): def finger_count(middle, finger):
"""Nombre impair (≥ 3) de créneaux sur une longueur utile `middle`.""" """Nombre impair (≥ 3) de créneaux sur une longueur utile `middle`."""
n = max(1, round(middle / finger)) n = max(1, round(middle / finger))
@ -119,13 +140,10 @@ def box_panels(length, width, height, thickness, finger=0.0, interior=False, lid
width += 2 * t width += 2 * t
height += 2 * t if lid else t height += 2 * t if lid else t
for label, size in (("longueur", length), ("largeur", width), ("hauteur", height)): for dimension, size in (("length", length), ("width", width), ("height", height)):
middle = size - 2 * t middle = size - 2 * t
if middle <= 0 or middle / finger_count(middle, finger) < t: if middle <= 0 or middle / finger_count(middle, finger) < t:
raise ValueError( raise DimensionTooSmall(dimension, size, t)
f"La {label} ({size:.1f} mm extérieure) est trop petite pour "
f"l'épaisseur ({t:g} mm) : il faut au moins {5 * t:g} mm."
)
top = MALE if lid else PLAIN top = MALE if lid else PLAIN
front = (top, MALE, MALE, MALE) front = (top, MALE, MALE, MALE)

View File

@ -1,39 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension"> <!--
<name>Boîte à créneaux</name> Textes source en anglais, traduits par le catalogue « box_maker »
(locale/<langue>/LC_MESSAGES/box_maker.mo). Après toute modification
d'un texte : python i18n.py, puis compléter po/fr.po et relancer.
-->
<inkscape-extension translationdomain="box_maker"
xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Finger-Joint Box</name>
<id>fr.alex.box_maker</id> <id>fr.alex.box_maker</id>
<param name="tab" type="notebook"> <param name="tab" type="notebook">
<page name="dimensions" gui-text="Dimensions"> <page name="dimensions" gui-text="Dimensions">
<param name="length" type="float" min="5" max="5000" precision="1" gui-text="Longueur (mm) :">100</param> <param name="length" type="float" min="5" max="5000" precision="1" gui-text="Length (mm):">100</param>
<param name="width" type="float" min="5" max="5000" precision="1" gui-text="Largeur (mm) :">80</param> <param name="width" type="float" min="5" max="5000" precision="1" gui-text="Width (mm):">80</param>
<param name="height" type="float" min="5" max="5000" precision="1" gui-text="Hauteur (mm) :">50</param> <param name="height" type="float" min="5" max="5000" precision="1" gui-text="Height (mm):">50</param>
<param name="dim_type" type="optiongroup" appearance="combo" gui-text="Les dimensions sont :"> <param name="dim_type" type="optiongroup" appearance="combo" gui-text="Dimensions are:">
<option value="exterior">Extérieures</option> <option value="exterior">Exterior</option>
<option value="interior">Intérieures</option> <option value="interior">Interior</option>
</param> </param>
<param name="lid" type="bool" gui-text="Boîte fermée (avec couvercle)">true</param> <param name="lid" type="bool" gui-text="Closed box (with lid)">true</param>
</page> </page>
<page name="material" gui-text="Matière et créneaux"> <page name="material" gui-text="Material and fingers">
<param name="thickness" type="float" min="0.5" max="50" precision="2" gui-text="Épaisseur de la matière (mm) :">3</param> <param name="thickness" type="float" min="0.5" max="50" precision="2" gui-text="Material thickness (mm):">3</param>
<param name="finger" type="float" min="0" max="500" precision="2" gui-text="Largeur des créneaux (mm, 0 = 3 × épaisseur) :">0</param> <param name="finger" type="float" min="0" max="500" precision="2" gui-text="Finger width (mm, 0 = 3 × thickness):">0</param>
<param name="kerf" type="float" min="0" max="2" precision="3" gui-text="Compensation du trait laser, kerf (mm) :">0.1</param> <param name="kerf" type="float" min="0" max="2" precision="3" gui-text="Laser beam compensation, kerf (mm):">0.1</param>
<label appearance="header">Remarques</label> <label appearance="header">Notes</label>
<label>La largeur des créneaux est ajustée pour obtenir un nombre impair de créneaux sur chaque bord.</label> <label>The finger width is adjusted to get an odd number of fingers on each edge.</label>
<label>Le kerf élargit les créneaux et rétrécit les encoches pour un assemblage serré.</label> <label>The kerf widens the fingers and narrows the slots for a tight fit.</label>
</page> </page>
<page name="drawing" gui-text="Tracé"> <page name="drawing" gui-text="Drawing">
<param name="gap" type="float" min="0" max="100" precision="1" gui-text="Espacement entre les pièces (mm) :">5</param> <param name="gap" type="float" min="0" max="100" precision="1" gui-text="Spacing between parts (mm):">5</param>
<param name="stroke_width" type="float" min="0.001" max="5" precision="3" gui-text="Épaisseur du trait (mm) :">0.1</param> <param name="stroke_width" type="float" min="0.001" max="5" precision="3" gui-text="Stroke width (mm):">0.1</param>
<param name="stroke_color" type="color" gui-text="Couleur du trait :">0xff0000ff</param> <param name="stroke_color" type="color" gui-text="Stroke color:">0xff0000ff</param>
<param name="labels" type="bool" gui-text="Nommer les pièces (texte gravé !)">false</param> <param name="labels" type="bool" gui-text="Name the parts (engraved text!)">false</param>
</page> </page>
<!-- Image générée par docs/schema_parametres.py --> <!-- Schéma généré par docs/schema_parametres.py, en anglais :
<page name="diagram" gui-text="Schéma"> Inkscape ne traduit pas le chemin d'une image. -->
<image width="900" height="694">images/parametres.png</image> <page name="diagram" gui-text="Diagram">
<image width="900" height="694">images/parameters_en.png</image>
</page>
<page name="help" gui-text="Help">
<label xml:space="preserve">Draws the pattern of a laser-cut box whose
panels interlock with straight finger joints.
No selection is needed: the parts are added
to the current layer, centered on the view.
Dimensions: exterior sizes, or interior sizes
(the thickness is then added). Unchecking
"Closed box" gives 5 parts, with a straight
top edge on the sides.
Fingers: 0 = 3 × the thickness. The width is
adjusted to get an odd number per edge.
Kerf: the outline is moved outwards by
kerf / 2 so that parts keep their nominal size
after cutting. Measure it: cut a 20 mm square,
kerf = 20 − measured size. Larger = tighter.
Each part is a separate closed path, named
(Front, Bottom…), that can be moved alone.
Labels are text: the laser would engrave them.</label>
</page> </page>
</param> </param>

View File

@ -7,9 +7,27 @@ pour pouvoir être sélectionnée et déplacée seule.
import inkex import inkex
from inkex import Group, PathElement, TextElement from inkex import Group, PathElement, TextElement
from inkex.localization import localize
import box_core import box_core
# Installe _() dans les builtins : catalogue « box_maker » de la langue de
# l'interface (Inkscape transmet domaine et dossier par l'environnement),
# textes anglais d'origine sinon.
localize()
def panel_names():
"""Noms affiches des pieces, indexes par leur nom dans box_core."""
return {
"Avant": _("Front"),
"Arrière": _("Back"),
"Gauche": _("Left"),
"Droite": _("Right"),
"Fond": _("Bottom"),
"Couvercle": _("Lid"),
}
class BoxMaker(inkex.EffectExtension): class BoxMaker(inkex.EffectExtension):
def add_arguments(self, pars): def add_arguments(self, pars):
@ -35,8 +53,18 @@ class BoxMaker(inkex.EffectExtension):
finger=opt.finger, kerf=opt.kerf, finger=opt.finger, kerf=opt.kerf,
interior=opt.dim_type == "interior", lid=opt.lid, gap=opt.gap, interior=opt.dim_type == "interior", lid=opt.lid, gap=opt.gap,
) )
except ValueError as err: except box_core.DimensionTooSmall as err:
raise inkex.AbortExtension(str(err)) dimension = {
"length": _("length"),
"width": _("width"),
"height": _("height"),
}[err.dimension]
inkex.errormsg(_(
"The {dimension} ({size:.1f} mm exterior) is too small for the "
"thickness ({thickness:g} mm): it must be at least {minimum:g} mm."
).format(dimension=dimension, size=err.size, thickness=err.thickness,
minimum=err.minimum))
return
mm = self._uu_per_mm() mm = self._uu_per_mm()
origin_x, origin_y = self._origin(panels, mm) origin_x, origin_y = self._origin(panels, mm)
@ -50,10 +78,12 @@ class BoxMaker(inkex.EffectExtension):
"stroke-width": opt.stroke_width * mm, "stroke-width": opt.stroke_width * mm,
} }
names = panel_names()
for panel in panels: for panel in panels:
name = names[panel.name]
path = PathElement() path = PathElement()
path.set_id(self.svg.get_unique_id("boite-")) path.set_id(self.svg.get_unique_id("boite-"))
path.label = panel.name path.label = name
path.style = style path.style = style
path.path = "M " + " L ".join( path.path = "M " + " L ".join(
f"{origin_x + x * mm:.4f},{origin_y + y * mm:.4f}" for x, y in panel.points f"{origin_x + x * mm:.4f},{origin_y + y * mm:.4f}" for x, y in panel.points
@ -62,10 +92,10 @@ class BoxMaker(inkex.EffectExtension):
if opt.labels: if opt.labels:
group = Group() group = Group()
group.set_id(self.svg.get_unique_id("boite-")) group.set_id(self.svg.get_unique_id("boite-"))
group.label = panel.name group.label = name
group.transform = to_layer group.transform = to_layer
group.add(path) group.add(path)
group.add(self._label(panel, origin_x, origin_y, mm)) group.add(self._label(panel, name, origin_x, origin_y, mm))
layer.add(group) layer.add(group)
else: else:
path.transform = to_layer path.transform = to_layer
@ -97,7 +127,7 @@ class BoxMaker(inkex.EffectExtension):
cx, cy = self.svg.get_page_bbox().center cx, cy = self.svg.get_page_bbox().center
return cx - max_x / 2, cy - max_y / 2 return cx - max_x / 2, cy - max_y / 2
def _label(self, panel, origin_x, origin_y, mm): def _label(self, panel, name, origin_x, origin_y, mm):
min_x, min_y, max_x, max_y = panel.bbox() min_x, min_y, max_x, max_y = panel.bbox()
size = min(5.0, (max_y - min_y) / 4) * mm size = min(5.0, (max_y - min_y) / 4) * mm
text = TextElement() text = TextElement()
@ -110,7 +140,7 @@ class BoxMaker(inkex.EffectExtension):
"fill": "#0000ff", "fill": "#0000ff",
"stroke": "none", "stroke": "none",
} }
text.text = panel.name text.text = name
return text return text

View File

@ -1,10 +1,11 @@
<# <#
.SYNOPSIS .SYNOPSIS
Déploie l'extension « Boîte à créneaux » dans Inkscape. Déploie l'extension « Boîte à créneaux » dans Inkscape.
.DESCRIPTION .DESCRIPTION
Copie box_maker.inx, box_maker.py, box_core.py et images dans un Copie box_maker.inx, box_maker.py, box_core.py, le schéma de l'onglet
sous-dossier dédié du répertoire des extensions utilisateur d'Inkscape « Diagram » (images\parameters_en.png) et les traductions compilées
(locale\<langue>\LC_MESSAGES\box_maker.mo) 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 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. Inkscape est ouvert : les extensions ne sont chargées qu'au démarrage.
@ -36,7 +37,8 @@ param(
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$files = @('box_maker.inx', 'box_maker.py', 'box_core.py') $files = @('box_maker.inx', 'box_maker.py', 'box_core.py', 'images\parameters_en.png')
$localeDir = 'locale'
$source = $PSScriptRoot $source = $PSScriptRoot
$destination = Join-Path $ExtensionsDir $FolderName $destination = Join-Path $ExtensionsDir $FolderName
@ -69,6 +71,9 @@ if ($Uninstall) {
# --- Vérification des sources ------------------------------------------------ # --- Vérification des sources ------------------------------------------------
$missing = $files | Where-Object { -not (Test-Path -LiteralPath (Join-Path $source $_)) } $missing = $files | Where-Object { -not (Test-Path -LiteralPath (Join-Path $source $_)) }
if (-not (Test-Path -LiteralPath (Join-Path $source $localeDir))) {
$missing = @($missing) + "$localeDir\ (lancer : python i18n.py)"
}
if ($missing) { if ($missing) {
Write-Error "Fichier(s) introuvable(s) dans $source : $($missing -join ', ')" Write-Error "Fichier(s) introuvable(s) dans $source : $($missing -join ', ')"
exit 1 exit 1
@ -89,20 +94,29 @@ if (-not (Test-Path -LiteralPath $destination)) {
Write-Host "Dossier créé : $destination" Write-Host "Dossier créé : $destination"
} }
# Images : le dossier est recréé (une image renommée ne reste pas en double)
$imagesTo = Join-Path $destination 'images'
if (Test-Path -LiteralPath $imagesTo) {
Remove-Item -LiteralPath $imagesTo -Recurse -Force
}
foreach ($file in $files) { foreach ($file in $files) {
$from = Join-Path $source $file $from = Join-Path $source $file
$to = Join-Path $destination $file $to = Join-Path $destination $file
$action = if (Test-Path -LiteralPath $to) { 'remplacé' } else { 'copié ' } $action = if (Test-Path -LiteralPath $to) { 'remplacé' } else { 'copié ' }
New-Item -ItemType Directory -Path (Split-Path $to) -Force | Out-Null
Copy-Item -LiteralPath $from -Destination $to -Force Copy-Item -LiteralPath $from -Destination $to -Force
Write-Host " $action $file" Write-Host " $action $file"
} }
# Schéma affiché dans l'onglet « Schéma » de la boîte de dialogue # Traductions : le dossier est remplacé en entier (langue retirée => supprimée)
$images = Join-Path $source 'images' $localeTo = Join-Path $destination $localeDir
if (Test-Path -LiteralPath $images) { if (Test-Path -LiteralPath $localeTo) {
Copy-Item -LiteralPath $images -Destination $destination -Recurse -Force Remove-Item -LiteralPath $localeTo -Recurse -Force
Write-Host " copié images\"
} }
Copy-Item -LiteralPath (Join-Path $source $localeDir) -Destination $localeTo -Recurse
$languages = Get-ChildItem -LiteralPath $localeTo -Directory | ForEach-Object Name
Write-Host " copié $localeDir\ ($($languages -join ', '))"
# Le cache Python d'une version précédente peut masquer les fichiers copiés # Le cache Python d'une version précédente peut masquer les fichiers copiés
$pycache = Join-Path $destination '__pycache__' $pycache = Join-Path $destination '__pycache__'

View File

@ -1,108 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="1080" viewBox="0 0 1400 1080">
<defs><pattern id="hachures" width="8" height="8" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><rect width="8" height="8" fill="#f1f3f5"/><line x1="0" y1="0" x2="0" y2="8" stroke="#adb5bd" stroke-width="1.5"/></pattern></defs>
<rect width="1400" height="1080" fill="#ffffff"/>
<line x1="700" y1="20" x2="700" y2="1060" stroke="#dee2e6" stroke-width="1.5"/>
<line x1="20" y1="540" x2="1380" y2="540" stroke="#dee2e6" stroke-width="1.5"/>
<text x="30" y="50" text-anchor="start" style="font-family:Arial,Helvetica,sans-serif;font-size:27.5px;font-weight:bold;fill:#343a40">1 Dimensions de la boîte</text>
<polygon points="150,365 470,365 470,205 150,205" fill="#f1f3f5" stroke="#212529" stroke-width="2" stroke-linejoin="miter" />
<polygon points="150,205 470,205 574.85,131.58 254.85,131.58" fill="#e9ecef" stroke="#212529" stroke-width="2" stroke-linejoin="miter" stroke-dasharray="7,5"/>
<polygon points="470,365 574.85,291.58 574.85,131.58 470,205" fill="#dee2e6" stroke="#212529" stroke-width="2" stroke-linejoin="miter" />
<text x="310" y="294.6" text-anchor="middle" style="font-family:Arial,Helvetica,sans-serif;font-size:20px;font-weight:normal;fill:#adb5bd">Avant</text>
<text x="362.43" y="161.89" text-anchor="middle" style="font-family:Arial,Helvetica,sans-serif;font-size:20px;font-weight:normal;fill:#adb5bd">Couvercle</text>
<polyline points="150,369 150,401" fill="none" stroke="#d9480f" stroke-width="1" stroke-linejoin="miter" stroke-dasharray="3,3"/>
<polyline points="470,369 470,401" fill="none" stroke="#d9480f" stroke-width="1" stroke-linejoin="miter" stroke-dasharray="3,3"/>
<polyline points="150,395 470,395" fill="none" stroke="#d9480f" stroke-width="1.6" stroke-linejoin="miter" />
<polygon points="150,395 159,390.95 159,399.05" fill="#d9480f"/>
<polygon points="470,395 461,399.05 461,390.95" fill="#d9480f"/>
<text x="310" y="425" text-anchor="middle" style="font-family:Arial,Helvetica,sans-serif;font-size:21.25px;font-weight:bold;fill:#d9480f;paint-order:stroke;stroke:#ffffff;stroke-width:5px;stroke-linejoin:round">Longueur</text>
<polyline points="472.29,368.28 492.94,397.77" fill="none" stroke="#d9480f" stroke-width="1" stroke-linejoin="miter" stroke-dasharray="3,3"/>
<polyline points="577.15,294.86 597.79,324.35" fill="none" stroke="#d9480f" stroke-width="1" stroke-linejoin="miter" stroke-dasharray="3,3"/>
<polyline points="489.5,392.85 594.35,319.43" fill="none" stroke="#d9480f" stroke-width="1.6" stroke-linejoin="miter" />
<polygon points="489.5,392.85 494.55,384.37 499.2,391.01" fill="#d9480f"/>
<polygon points="594.35,319.43 589.3,327.91 584.66,321.28" fill="#d9480f"/>
<text x="573.47" y="408.7" text-anchor="middle" style="font-family:Arial,Helvetica,sans-serif;font-size:21.25px;font-weight:bold;fill:#d9480f;paint-order:stroke;stroke:#ffffff;stroke-width:5px;stroke-linejoin:round">Largeur</text>
<polyline points="146,365 114,365" fill="none" stroke="#d9480f" stroke-width="1" stroke-linejoin="miter" stroke-dasharray="3,3"/>
<polyline points="146,205 114,205" fill="none" stroke="#d9480f" stroke-width="1" stroke-linejoin="miter" stroke-dasharray="3,3"/>
<polyline points="120,365 120,205" fill="none" stroke="#d9480f" stroke-width="1.6" stroke-linejoin="miter" />
<polygon points="120,365 115.95,356 124.05,356" fill="#d9480f"/>
<polygon points="120,205 124.05,214 115.95,214" fill="#d9480f"/>
<text x="60" y="292.5" text-anchor="middle" style="font-family:Arial,Helvetica,sans-serif;font-size:21.25px;font-weight:bold;fill:#d9480f;paint-order:stroke;stroke:#ffffff;stroke-width:5px;stroke-linejoin:round">Hauteur</text>
<text x="30" y="472.5" text-anchor="start" style="font-family:Arial,Helvetica,sans-serif;font-size:20px;font-weight:normal;fill:#343a40">Couvercle (pointillés) : « Boîte fermée ». Décochée : 5 pièces,</text>
<text x="30" y="500" text-anchor="start" style="font-family:Arial,Helvetica,sans-serif;font-size:20px;font-weight:normal;fill:#343a40">bord haut des côtés droit. Minimum : 5 × l'épaisseur.</text>
<text x="730" y="50" text-anchor="start" style="font-family:Arial,Helvetica,sans-serif;font-size:27.5px;font-weight:bold;fill:#343a40">2 Épaisseur et type de dimensions</text>
<polygon points="850,150 880,150 880,370 850,370" fill="url(#hachures)" stroke="#212529" stroke-width="1.6" stroke-linejoin="miter" />
<polygon points="1220,150 1250,150 1250,370 1220,370" fill="url(#hachures)" stroke="#212529" stroke-width="1.6" stroke-linejoin="miter" />
<polygon points="850,340 1250,340 1250,370 850,370" fill="url(#hachures)" stroke="#212529" stroke-width="1.6" stroke-linejoin="miter" />
<text x="1050" y="361" text-anchor="middle" style="font-family:Arial,Helvetica,sans-serif;font-size:18.75px;font-weight:normal;fill:#343a40;paint-order:stroke;stroke:#ffffff;stroke-width:5px;stroke-linejoin:round">Fond</text>
<polyline points="850,146 850,86" fill="none" stroke="#d9480f" stroke-width="1" stroke-linejoin="miter" stroke-dasharray="3,3"/>
<polyline points="1250,146 1250,86" fill="none" stroke="#d9480f" stroke-width="1" stroke-linejoin="miter" stroke-dasharray="3,3"/>
<polyline points="850,92 1250,92" fill="none" stroke="#d9480f" stroke-width="1.6" stroke-linejoin="miter" />
<polygon points="850,92 859,87.95 859,96.05" fill="#d9480f"/>
<polygon points="1250,92 1241,96.05 1241,87.95" fill="#d9480f"/>
<text x="1050" y="77" text-anchor="middle" style="font-family:Arial,Helvetica,sans-serif;font-size:21.25px;font-weight:bold;fill:#d9480f;paint-order:stroke;stroke:#ffffff;stroke-width:5px;stroke-linejoin:round">Extérieure</text>
<polyline points="880,214 880,216" fill="none" stroke="#1971c2" stroke-width="1" stroke-linejoin="miter" stroke-dasharray="3,3"/>
<polyline points="1220,214 1220,216" fill="none" stroke="#1971c2" stroke-width="1" stroke-linejoin="miter" stroke-dasharray="3,3"/>
<polyline points="880,210 1220,210" fill="none" stroke="#1971c2" stroke-width="1.6" stroke-linejoin="miter" />
<polygon points="880,210 889,205.95 889,214.05" fill="#1971c2"/>
<polygon points="1220,210 1211,214.05 1211,205.95" fill="#1971c2"/>
<text x="1050" y="240" text-anchor="middle" style="font-family:Arial,Helvetica,sans-serif;font-size:21.25px;font-weight:bold;fill:#1971c2;paint-order:stroke;stroke:#ffffff;stroke-width:5px;stroke-linejoin:round">Intérieure</text>
<polyline points="1220,214 1220,216" fill="none" stroke="#d9480f" stroke-width="1" stroke-linejoin="miter" stroke-dasharray="3,3"/>
<polyline points="1250,214 1250,216" fill="none" stroke="#d9480f" stroke-width="1" stroke-linejoin="miter" stroke-dasharray="3,3"/>
<polyline points="1220,210 1250,210" fill="none" stroke="#d9480f" stroke-width="1.6" stroke-linejoin="miter" />
<polygon points="1220,210 1229,205.95 1229,214.05" fill="#d9480f"/>
<polygon points="1250,210 1241,214.05 1241,205.95" fill="#d9480f"/>
<text x="1235" y="240" text-anchor="middle" style="font-family:Arial,Helvetica,sans-serif;font-size:21.25px;font-weight:bold;fill:#d9480f;paint-order:stroke;stroke:#ffffff;stroke-width:5px;stroke-linejoin:round"></text>
<text x="1266" y="216" text-anchor="start" style="font-family:Arial,Helvetica,sans-serif;font-size:21.25px;font-weight:bold;fill:#d9480f;paint-order:stroke;stroke:#ffffff;stroke-width:5px;stroke-linejoin:round">Épaisseur</text>
<text x="730" y="472.5" text-anchor="start" style="font-family:Arial,Helvetica,sans-serif;font-size:20px;font-weight:normal;fill:#343a40">Extérieures : cotes hors tout. Intérieures : l'extension ajoute</text>
<text x="730" y="500" text-anchor="start" style="font-family:Arial,Helvetica,sans-serif;font-size:20px;font-weight:normal;fill:#343a40">2 épaisseurs (1 seule en hauteur si la boîte est ouverte).</text>
<text x="30" y="590" text-anchor="start" style="font-family:Arial,Helvetica,sans-serif;font-size:27.5px;font-weight:bold;fill:#343a40">3 Largeur des créneaux</text>
<polygon points="70,640 70,667.6 92.4,667.6 92.4,667.6 166,667.6 166,690 239.6,690 239.6,667.6 313.2,667.6 313.2,690 386.8,690 386.8,667.6 460.4,667.6 460.4,690 534,690 534,667.6 607.6,667.6 607.6,667.6 630,667.6 630,640" fill="#f8f9fa" stroke="#adb5bd" stroke-width="1.4" stroke-linejoin="miter" />
<polygon points="70,890 70,740 92.4,740 92.4,740 166,740 166,762.4 239.6,762.4 239.6,740 313.2,740 313.2,762.4 386.8,762.4 386.8,740 460.4,740 460.4,762.4 534,762.4 534,740 607.6,740 607.6,740 630,740 630,890" fill="#f1f3f5" stroke="#212529" stroke-width="2" stroke-linejoin="miter" />
<text x="642" y="670" text-anchor="start" style="font-family:Arial,Helvetica,sans-serif;font-size:18.75px;font-weight:normal;fill:#adb5bd;paint-order:stroke;stroke:#ffffff;stroke-width:5px;stroke-linejoin:round">Couvercle</text>
<text x="642" y="800" text-anchor="start" style="font-family:Arial,Helvetica,sans-serif;font-size:18.75px;font-weight:normal;fill:#adb5bd;paint-order:stroke;stroke:#ffffff;stroke-width:5px;stroke-linejoin:round">Avant</text>
<polyline points="386.8,744 386.8,796" fill="none" stroke="#d9480f" stroke-width="1" stroke-linejoin="miter" stroke-dasharray="3,3"/>
<polyline points="460.4,744 460.4,796" fill="none" stroke="#d9480f" stroke-width="1" stroke-linejoin="miter" stroke-dasharray="3,3"/>
<polyline points="386.8,790 460.4,790" fill="none" stroke="#d9480f" stroke-width="1.6" stroke-linejoin="miter" />
<polygon points="386.8,790 395.8,785.95 395.8,794.05" fill="#d9480f"/>
<polygon points="460.4,790 451.4,794.05 451.4,785.95" fill="#d9480f"/>
<text x="423.6" y="822.5" text-anchor="middle" style="font-family:Arial,Helvetica,sans-serif;font-size:21.25px;font-weight:bold;fill:#d9480f;paint-order:stroke;stroke:#ffffff;stroke-width:5px;stroke-linejoin:round">Créneau</text>
<polyline points="198.8,740 196.8,740" fill="none" stroke="#1971c2" stroke-width="1" stroke-linejoin="miter" stroke-dasharray="3,3"/>
<polyline points="198.8,762.4 196.8,762.4" fill="none" stroke="#1971c2" stroke-width="1" stroke-linejoin="miter" stroke-dasharray="3,3"/>
<polyline points="202.8,740 202.8,762.4" fill="none" stroke="#1971c2" stroke-width="1.6" stroke-linejoin="miter" />
<polygon points="202.8,740 206.85,749 198.75,749" fill="#1971c2"/>
<polygon points="202.8,762.4 198.75,753.4 206.85,753.4" fill="#1971c2"/>
<text x="180.3" y="758.7" text-anchor="middle" style="font-family:Arial,Helvetica,sans-serif;font-size:21.25px;font-weight:bold;fill:#1971c2;paint-order:stroke;stroke:#ffffff;stroke-width:5px;stroke-linejoin:round"></text>
<text x="202.8" y="790.4" text-anchor="middle" style="font-family:Arial,Helvetica,sans-serif;font-size:18.75px;font-weight:bold;fill:#1971c2;paint-order:stroke;stroke:#ffffff;stroke-width:5px;stroke-linejoin:round">Profondeur = épaisseur</text>
<text x="30" y="1012.5" text-anchor="start" style="font-family:Arial,Helvetica,sans-serif;font-size:20px;font-weight:normal;fill:#343a40">0 = 3 × l'épaisseur. Largeur ajustée pour un nombre impair</text>
<text x="30" y="1040" text-anchor="start" style="font-family:Arial,Helvetica,sans-serif;font-size:20px;font-weight:normal;fill:#343a40">de créneaux par bord (ici 7 sur l'avant).</text>
<text x="730" y="590" text-anchor="start" style="font-family:Arial,Helvetica,sans-serif;font-size:27.5px;font-weight:bold;fill:#343a40">4 Compensation du kerf</text>
<polygon points="850,772 958,772 958,700 1102,700 1102,772 1246,772 1246,934 850,934" fill="#f1f3f5" stroke="#343a40" stroke-width="1.6" stroke-linejoin="miter" stroke-dasharray="6,4"/>
<polygon points="839.2,761.2 947.2,761.2 947.2,689.2 1112.8,689.2 1112.8,761.2 1256.8,761.2 1256.8,944.8 839.2,944.8" fill="none" stroke="#e03131" stroke-width="2.2" stroke-linejoin="miter" />
<polyline points="958,704 958,706" fill="none" stroke="#d9480f" stroke-width="1" stroke-linejoin="miter" stroke-dasharray="3,3"/>
<polyline points="1102,704 1102,706" fill="none" stroke="#d9480f" stroke-width="1" stroke-linejoin="miter" stroke-dasharray="3,3"/>
<polyline points="958,700 1102,700" fill="none" stroke="#d9480f" stroke-width="1.6" stroke-linejoin="miter" />
<polygon points="958,700 967,695.95 967,704.05" fill="#d9480f"/>
<polygon points="1102,700 1093,704.05 1093,695.95" fill="#d9480f"/>
<text x="1030" y="730" text-anchor="middle" style="font-family:Arial,Helvetica,sans-serif;font-size:21.25px;font-weight:bold;fill:#d9480f;paint-order:stroke;stroke:#ffffff;stroke-width:5px;stroke-linejoin:round"></text>
<text x="1030" y="736" text-anchor="middle" style="font-family:Arial,Helvetica,sans-serif;font-size:18.75px;font-weight:bold;fill:#d9480f;paint-order:stroke;stroke:#ffffff;stroke-width:5px;stroke-linejoin:round">Créneau voulu</text>
<polyline points="947.2,685.2 947.2,643.2" fill="none" stroke="#e03131" stroke-width="1" stroke-linejoin="miter" stroke-dasharray="3,3"/>
<polyline points="1112.8,685.2 1112.8,643.2" fill="none" stroke="#e03131" stroke-width="1" stroke-linejoin="miter" stroke-dasharray="3,3"/>
<polyline points="947.2,649.2 1112.8,649.2" fill="none" stroke="#e03131" stroke-width="1.6" stroke-linejoin="miter" />
<polygon points="947.2,649.2 956.2,645.15 956.2,653.25" fill="#e03131"/>
<polygon points="1112.8,649.2 1103.8,653.25 1103.8,645.15" fill="#e03131"/>
<text x="1030" y="634.2" text-anchor="middle" style="font-family:Arial,Helvetica,sans-serif;font-size:20px;font-weight:bold;fill:#e03131;paint-order:stroke;stroke:#ffffff;stroke-width:5px;stroke-linejoin:round">Dessiné : créneau + kerf</text>
<polyline points="1246,812 1246,814" fill="none" stroke="#1971c2" stroke-width="1" stroke-linejoin="miter" stroke-dasharray="3,3"/>
<polyline points="1256.8,812 1256.8,814" fill="none" stroke="#1971c2" stroke-width="1" stroke-linejoin="miter" stroke-dasharray="3,3"/>
<polyline points="1246,808 1256.8,808" fill="none" stroke="#1971c2" stroke-width="1.6" stroke-linejoin="miter" />
<polygon points="1246,808 1255,803.95 1255,812.05" fill="#1971c2"/>
<polygon points="1256.8,808 1247.8,812.05 1247.8,803.95" fill="#1971c2"/>
<text x="1251.4" y="838" text-anchor="middle" style="font-family:Arial,Helvetica,sans-serif;font-size:21.25px;font-weight:bold;fill:#1971c2;paint-order:stroke;stroke:#ffffff;stroke-width:5px;stroke-linejoin:round"></text>
<text x="1270.8" y="814" text-anchor="start" style="font-family:Arial,Helvetica,sans-serif;font-size:20px;font-weight:bold;fill:#1971c2;paint-order:stroke;stroke:#ffffff;stroke-width:5px;stroke-linejoin:round">kerf / 2</text>
<polyline points="860,970 900,970" fill="none" stroke="#e03131" stroke-width="2.2" stroke-linejoin="miter" />
<text x="910" y="976" text-anchor="start" style="font-family:Arial,Helvetica,sans-serif;font-size:18.75px;font-weight:normal;fill:#343a40">trait dessiné</text>
<polyline points="1050,970 1090,970" fill="none" stroke="#343a40" stroke-width="1.6" stroke-linejoin="miter" stroke-dasharray="6,4"/>
<text x="1100" y="976" text-anchor="start" style="font-family:Arial,Helvetica,sans-serif;font-size:18.75px;font-weight:normal;fill:#343a40">cote après découpe</text>
<text x="730" y="1012.5" text-anchor="start" style="font-family:Arial,Helvetica,sans-serif;font-size:20px;font-weight:normal;fill:#343a40">Le laser enlève kerf / 2 de chaque côté du trait : le contour</text>
<text x="730" y="1040" text-anchor="start" style="font-family:Arial,Helvetica,sans-serif;font-size:20px;font-weight:normal;fill:#343a40">est élargi d'autant. Défaut 0,1 mm ; 0 = pas de compensation.</text>
</svg>

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -3,10 +3,11 @@
""" """
Genere le schema explicatif des parametres de l'extension. Genere le schema explicatif des parametres de l'extension.
python docs/schema_parametres.py # les deux versions python docs/schema_parametres.py # les deux versions
python docs/schema_parametres.py readme # docs/parametres.svg + .png (README) python docs/schema_parametres.py fr # docs/parametres.svg + .png (README)
python docs/schema_parametres.py dialogue # images/parametres.png (onglet python docs/schema_parametres.py en # docs/parameters_en.svg
# « Schéma » de la boite de dialogue) # + images/parameters_en.png (onglet
# « Diagram » de la boite de dialogue)
Les formes sont calculees par box_core (memes fonctions que l'extension), 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 le schema reste donc fidele au resultat reel. L'export PNG passe par
@ -39,25 +40,85 @@ FONT = "font-family:Arial,Helvetica,sans-serif"
INKSCAPE = [r"C:\Program Files\Inkscape\bin\inkscape.com", "/usr/bin/inkscape", INKSCAPE = [r"C:\Program Files\Inkscape\bin\inkscape.com", "/usr/bin/inkscape",
"/Applications/Inkscape.app/Contents/MacOS/inkscape"] "/Applications/Inkscape.app/Contents/MacOS/inkscape"]
# L'extension n'existe qu'en francais : deux versions du meme schema. # Version par langue : textes, fichiers produits, largeur du PNG et agrandissement
# La version « dialogue » est affichee reduite (900 px de large) dans la boite de # des textes (la version anglaise est affichee reduite dans la boite de dialogue :
# dialogue : textes x 1.25 pour rester lisibles. # 900 px de large, textes x 1.25 pour rester lisibles). Inkscape ne traduit pas
# le chemin d'une image : la boite de dialogue montre la version anglaise.
VERSIONS = { VERSIONS = {
"readme": { "fr": {
"svg": os.path.join(HERE, "parametres.svg"), "svg": os.path.join(HERE, "parametres.svg"),
"png": os.path.join(HERE, "parametres.png"), "png": os.path.join(HERE, "parametres.png"),
"png_width": 1400, "png_width": 1400,
"font_scale": 1.0, "font_scale": 1.0,
"text": {
"title1": "Dimensions de la boîte",
"front": "Avant",
"lid": "Couvercle",
"length": "Longueur",
"width": "Largeur",
"height": "Hauteur",
"note1": ["Couvercle (pointillés) : « Boîte fermée ». Décochée : 5 pièces,",
"bord haut des côtés droit. Minimum : 5 × l'épaisseur."],
"title2": "Épaisseur et type de dimensions",
"bottom": "Fond",
"exterior": "Extérieure",
"interior": "Intérieure",
"thickness": "Épaisseur",
"note2": ["Extérieures : cotes hors tout. Intérieures : l'extension ajoute",
"2 épaisseurs (1 seule en hauteur si la boîte est ouverte)."],
"title3": "Largeur des créneaux",
"finger": "Créneau",
"depth": "Profondeur = épaisseur",
"note3": ["0 = 3 × l'épaisseur. Largeur ajustée pour un nombre impair",
"de créneaux par bord (ici {} sur l'avant)."],
"title4": "Compensation du kerf",
"wanted": "Créneau voulu",
"drawn": "Dessiné : créneau + kerf",
"legend_drawn": "trait dessiné",
"legend_cut": "cote après découpe",
"note4": ["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."],
},
}, },
"dialogue": { "en": {
"svg": os.path.join(HERE, "parametres_dialogue.svg"), "svg": os.path.join(HERE, "parameters_en.svg"),
"png": os.path.join(ROOT, "images", "parametres.png"), "png": os.path.join(ROOT, "images", "parameters_en.png"),
"png_width": 900, "png_width": 900,
"font_scale": 1.25, "font_scale": 1.25,
"text": {
"title1": "Box dimensions",
"front": "Front",
"lid": "Lid",
"length": "Length",
"width": "Width",
"height": "Height",
"note1": ["Lid (dashed): \"Closed box\". Unchecked: 5 parts, straight",
"top edge on the sides. Minimum: 5 × the thickness."],
"title2": "Thickness and dimension type",
"bottom": "Bottom",
"exterior": "Exterior",
"interior": "Interior",
"thickness": "Thickness",
"note2": ["Exterior: overall sizes. Interior: the extension adds",
"2 thicknesses (only 1 in height for an open box)."],
"title3": "Finger width",
"finger": "Finger",
"depth": "Depth = thickness",
"note3": ["0 = 3 × the thickness. The width is adjusted to get an",
"odd number of fingers per edge ({} on the front here)."],
"title4": "Kerf compensation",
"wanted": "Wanted finger",
"drawn": "Drawn: finger + kerf",
"legend_drawn": "drawn stroke",
"legend_cut": "size after cutting",
"note4": ["The laser removes kerf / 2 on each side of the stroke: the",
"outline is widened accordingly. Default 0.1 mm; 0 = none."],
},
}, },
} }
out = [] out = []
T = {}
FS = 1.0 FS = 1.0
@ -139,7 +200,7 @@ def place(points, x0, y0, scale):
def panel_1(): def panel_1():
"""Longueur, largeur, hauteur : boite en perspective cavaliere.""" """Longueur, largeur, hauteur : boite en perspective cavaliere."""
panel_title(30, 50, 1, "Dimensions de la boîte") panel_title(30, 50, 1, T["title1"])
s = 3.2 # px par mm s = 3.2 # px par mm
length, width, height = 100, 80, 50 length, width, height = 100, 80, 50
dx, dy = 0.5 * math.cos(math.radians(35)), -0.5 * math.sin(math.radians(35)) dx, dy = 0.5 * math.cos(math.radians(35)), -0.5 * math.sin(math.radians(35))
@ -154,24 +215,22 @@ def panel_1():
polygon([p(0, 0, Z), p(L, 0, Z), p(L, P, Z), p(0, P, Z)], EDGE, 2, "#e9ecef", polygon([p(0, 0, Z), p(L, 0, Z), p(L, P, Z), p(0, P, Z)], EDGE, 2, "#e9ecef",
'stroke-dasharray="7,5"') 'stroke-dasharray="7,5"')
polygon([p(L, 0, 0), p(L, P, 0), p(L, P, Z), p(L, 0, Z)], EDGE, 2, "#dee2e6") 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, 0, Z / 2 - 3), T["front"], size=16, color=GHOST, anchor="middle", halo=False)
text(*p(L / 2, P / 2, Z + 2), "Couvercle", size=16, color=GHOST, anchor="middle", text(*p(L / 2, P / 2, Z + 2), T["lid"], size=16, color=GHOST, anchor="middle",
halo=False) halo=False)
dimension(p(0, 0, 0), p(L, 0, 0), 30, "Longueur") dimension(p(0, 0, 0), p(L, 0, 0), 30, T["length"])
dimension(p(L, 0, 0), p(L, P, 0), 34, "Largeur", label_shift=44) dimension(p(L, 0, 0), p(L, P, 0), 34, T["width"], label_shift=44)
dimension(p(0, 0, 0), p(0, 0, Z), -30, "Hauteur", label_shift=48) dimension(p(0, 0, 0), p(0, 0, Z), -30, T["height"], label_shift=48)
note(30, 500, ["Couvercle (pointillés) : « Boîte fermée ». Décochée : 5 pièces,", note(30, 500, T["note1"])
"bord haut des côtés droit. Minimum : 5 × l'épaisseur."])
def panel_2(): def panel_2():
"""Epaisseur et dimensions interieures / exterieures : coupe verticale.""" """Epaisseur et dimensions interieures / exterieures : coupe verticale."""
ox = 700 ox = 700
panel_title(ox + 30, 50, 2, "Épaisseur et type de dimensions") panel_title(ox + 30, 50, 2, T["title2"])
s = 5.0 s = 5.0
t, width, height = 6, 80, 44 # epaisseur exageree pour la lisibilite t, width, height = 6, 80, 44 # epaisseur exageree pour la lisibilite
x0, y0 = ox + 150, 150 x0, y0 = ox + 150, 150
hatch = 'fill="url(#hachures)"'
def rect(x, y, w, h): def rect(x, y, w, h):
polygon(place([(x, y), (x + w, y), (x + w, y + h), (x, y + h)], x0, y0, s), polygon(place([(x, y), (x + w, y), (x + w, y + h), (x, y + h)], x0, y0, s),
@ -180,26 +239,24 @@ def panel_2():
rect(0, 0, t, height) # cote gauche rect(0, 0, t, height) # cote gauche
rect(width - t, 0, t, height) # cote droit rect(width - t, 0, t, height) # cote droit
rect(0, height - t, width, t) # fond rect(0, height - t, width, t) # fond
del hatch text(x0 + width / 2 * s, y0 + (height - t / 2) * s + 6, T["bottom"], size=15,
text(x0 + width / 2 * s, y0 + (height - t / 2) * s + 6, "Fond", size=15,
color=INK, anchor="middle") color=INK, anchor="middle")
top = y0 top = y0
dimension((x0, top), (x0 + width * s, top), -58, "Extérieure", label_shift=18) dimension((x0, top), (x0 + width * s, top), -58, T["exterior"], label_shift=18)
dimension((x0 + t * s, top + 60), (x0 + (width - t) * s, top + 60), 0, dimension((x0 + t * s, top + 60), (x0 + (width - t) * s, top + 60), 0,
"Intérieure", color=DIM2) T["interior"], color=DIM2)
dimension((x0 + (width - t) * s, y0 + 12 * s), (x0 + width * s, y0 + 12 * s), 0, dimension((x0 + (width - t) * s, y0 + 12 * s), (x0 + width * s, y0 + 12 * s), 0,
"", color=DIM) "", color=DIM)
text(x0 + width * s + 16, y0 + 12 * s + 6, "Épaisseur", size=17, color=DIM, text(x0 + width * s + 16, y0 + 12 * s + 6, T["thickness"], size=17, color=DIM,
weight="bold") weight="bold")
note(ox + 30, 500, ["Extérieures : cotes hors tout. Intérieures : l'extension ajoute", note(ox + 30, 500, T["note2"])
"2 épaisseurs (1 seule en hauteur si la boîte est ouverte)."])
def panel_3(): def panel_3():
"""Creneaux : bord male de l'avant et bord femelle du couvercle.""" """Creneaux : bord male de l'avant et bord femelle du couvercle."""
oy = 540 oy = 540
panel_title(30, oy + 50, 3, "Largeur des créneaux") panel_title(30, oy + 50, 3, T["title3"])
length, t = 100, 4 length, t = 100, 4
finger = 3 * t # valeur par defaut (parametre a 0) finger = 3 * t # valeur par defaut (parametre a 0)
s = 5.6 s = 5.6
@ -221,25 +278,24 @@ def panel_3():
lid = [(x, 2 * lid_y - y) for x, y in edge(box_core.FEMALE, lid_y, 50)] lid = [(x, 2 * lid_y - y) for x, y in edge(box_core.FEMALE, lid_y, 50)]
polygon(lid, GHOST, 1.4, "#f8f9fa") polygon(lid, GHOST, 1.4, "#f8f9fa")
polygon(edge(box_core.MALE, front_y, 150), EDGE, 2, FILL) 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, lid_y - 20, T["lid"], size=15, color=GHOST)
text(x0 + length * s + 12, front_y + 60, "Avant", size=15, color=GHOST) text(x0 + length * s + 12, front_y + 60, T["front"], size=15, color=GHOST)
segs = box_core.edge_segments(length, t, finger, box_core.MALE) segs = box_core.edge_segments(length, t, finger, box_core.MALE)
a, b, _ = segs[4] a, b, _ = segs[4]
dimension((x0 + a * s, front_y), (x0 + b * s, front_y), 50, "Créneau", label_shift=20) dimension((x0 + a * s, front_y), (x0 + b * s, front_y), 50, T["finger"], label_shift=20)
c, d, _ = segs[1] c, d, _ = segs[1]
mid = x0 + (c + d) / 2 * s mid = x0 + (c + d) / 2 * s
dimension((mid, front_y), (mid, front_y + t * s), 0, "", color=DIM2) 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, text(mid, front_y + t * s + 28, T["depth"], size=15, color=DIM2,
weight="bold", anchor="middle") weight="bold", anchor="middle")
note(30, oy + 500, ["0 = 3 × l'épaisseur. Largeur ajustée pour un nombre impair", note(30, oy + 500, [line.format(len(segs)) for line in T["note3"]])
"de créneaux par bord (ici {} sur l'avant).".format(len(segs))])
def panel_4(): def panel_4():
"""Kerf : contour dessine decale de kerf / 2 vers l'exterieur.""" """Kerf : contour dessine decale de kerf / 2 vers l'exterieur."""
ox, oy = 700, 540 ox, oy = 700, 540
panel_title(ox + 30, oy + 50, 4, "Compensation du kerf") panel_title(ox + 30, oy + 50, 4, T["title4"])
t, finger, kerf = 4, 8, 1.2 # kerf exagere pour la lisibilite t, finger, kerf = 4, 8, 1.2 # kerf exagere pour la lisibilite
s = 18 s = 18
x0, y0 = ox + 150, oy + 160 x0, y0 = ox + 150, oy + 160
@ -251,21 +307,20 @@ def panel_4():
k = kerf / 2 k = kerf / 2
dimension((x0 + 6 * s, y0 + 0), (x0 + (6 + finger) * s, y0), 0, "") 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, text(x0 + (6 + finger / 2) * s, y0 + 36, T["wanted"], size=15, color=DIM,
anchor="middle", weight="bold") anchor="middle", weight="bold")
dimension((x0 + (6 - k) * s, y0 - k * s), (x0 + (6 + finger + k) * s, y0 - k * s), -40, 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) T["drawn"], color=LASER, size=16)
dimension((x0 + 22 * s, y0 + 6 * s), (x0 + (22 + k) * s, y0 + 6 * s), 0, "", color=DIM2) 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, text(x0 + (22 + k) * s + 14, y0 + 6 * s + 6, "kerf / 2", size=16, color=DIM2,
weight="bold") weight="bold")
lx, ly = x0 + 10, y0 + 13 * s + 36 lx, ly = x0 + 10, y0 + 13 * s + 36
polyline([(lx, ly), (lx + 40, ly)], LASER, 2.2) polyline([(lx, ly), (lx + 40, ly)], LASER, 2.2)
text(lx + 50, ly + 6, "trait dessiné", size=15, halo=False) text(lx + 50, ly + 6, T["legend_drawn"], size=15, halo=False)
polyline([(lx + 190, ly), (lx + 230, ly)], INK, 1.6, 'stroke-dasharray="6,4"') 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) text(lx + 240, ly + 6, T["legend_cut"], size=15, halo=False)
note(ox + 30, oy + 500, ["Le laser enlève kerf / 2 de chaque côté du trait : le contour", note(ox + 30, oy + 500, T["note4"])
"est élargi d'autant. Défaut 0,1 mm ; 0 = pas de compensation."])
def export_png(svg_path, png_path, width): def export_png(svg_path, png_path, width):
@ -285,6 +340,8 @@ def build(name):
global FS global FS
version = VERSIONS[name] version = VERSIONS[name]
out.clear() out.clear()
T.clear()
T.update(version["text"])
FS = version["font_scale"] FS = version["font_scale"]
out.append('<defs><pattern id="hachures" width="8" height="8" ' out.append('<defs><pattern id="hachures" width="8" height="8" '

304
i18n.py Normal file
View File

@ -0,0 +1,304 @@
#!/usr/bin/env python3
# coding=utf-8
"""
Chaine de traduction de l'extension, sans dependance a gettext.
python i18n.py # extract + update + compile
python i18n.py extract # po/<domaine>.pot depuis le .inx et les .py
python i18n.py update # reporte le modele dans po/<langue>.po
python i18n.py compile # po/<langue>.po -> locale/<langue>/LC_MESSAGES/*.mo
Le domaine est lu dans l'attribut translationdomain du .inx ; les sources sont
le .inx et tous les .py du dossier (hors i18n.py et tests).
Les textes source (.inx et appels _() des .py) sont en anglais : c'est ce
qu'Inkscape affiche pour une langue sans catalogue. Le catalogue anglais est
rempli automatiquement avec les textes source ; seul fr.po se traduit a la main.
Les chaines retenues dans le .inx suivent les regles d'Inkscape (inx.its) :
<name>, attributs gui-text et gui-description, contenu de <option>, <item> et
<label>, sauf translatable="no". Les espaces sont normalises, sauf sous
xml:space="preserve".
"""
import ast
import os
import re
import struct
import sys
import xml.etree.ElementTree as ET
HERE = os.path.dirname(os.path.abspath(__file__))
LANGUAGES = ["en", "fr"]
SOURCE_LANGUAGE = "en"
PO_DIR = os.path.join(HERE, "po")
LOCALE_DIR = os.path.join(HERE, "locale")
XML_SPACE = "{http://www.w3.org/XML/1998/namespace}space"
INX_NS = "{http://www.inkscape.org/namespace/inkscape/extension}"
def _find_inx():
found = sorted(name for name in os.listdir(HERE) if name.endswith(".inx"))
if len(found) != 1:
sys.exit("i18n.py attend un seul fichier .inx dans {} (trouve : {})".format(
HERE, found))
return found[0]
INX = _find_inx()
_INX_ROOT = ET.parse(os.path.join(HERE, INX)).getroot()
DOMAIN = _INX_ROOT.get("translationdomain")
if not DOMAIN:
sys.exit("Attribut translationdomain absent de " + INX)
EXTENSION_NAME = (_INX_ROOT.findtext(INX_NS + "name") or DOMAIN).strip()
SOURCES = [INX] + sorted(
name for name in os.listdir(HERE)
if name.endswith(".py") and name != "i18n.py" and not name.startswith("test"))
# --------------------------------------------------------------------------
# Extraction
# --------------------------------------------------------------------------
def _local(tag):
return tag.rsplit("}", 1)[-1]
def _normalize(text, preserve):
return text if preserve else " ".join(text.split())
def extract_inx(path):
"""Chaines traduisibles d'un fichier .inx, dans l'ordre du document."""
messages = []
def walk(elem, preserve):
preserve = elem.get(XML_SPACE, "preserve" if preserve else "default") == "preserve"
if elem.get("translatable") != "no":
for attr in ("gui-text", "gui-description"):
if elem.get(attr):
messages.append(_normalize(elem.get(attr), preserve))
if _local(elem.tag) in ("name", "option", "item", "label") and elem.text:
messages.append(_normalize(elem.text, preserve))
for child in elem:
walk(child, preserve)
walk(ET.parse(path).getroot(), False)
return [m for m in messages if m]
def extract_py(path):
"""Arguments litteraux des appels _("...") d'un script Python."""
with open(path, encoding="utf-8") as handle:
tree = ast.parse(handle.read(), path)
calls = [node for node in ast.walk(tree)
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name)
and node.func.id == "_" and len(node.args) == 1
and isinstance(node.args[0], ast.Constant)
and isinstance(node.args[0].value, str)]
calls.sort(key=lambda node: (node.lineno, node.col_offset))
return [node.args[0].value for node in calls]
def extract():
"""Liste ordonnee et sans doublon de (msgid, [fichiers sources])."""
found = {}
for name in SOURCES:
path = os.path.join(HERE, name)
strings = extract_inx(path) if name.endswith(".inx") else extract_py(path)
for msgid in strings:
found.setdefault(msgid, [])
if name not in found[msgid]:
found[msgid].append(name)
return list(found.items())
# --------------------------------------------------------------------------
# Lecture / ecriture des fichiers .po
# --------------------------------------------------------------------------
_ESCAPES = {"n": "\n", "t": "\t", '"': '"', "\\": "\\"}
def _unquote(line):
body = line.strip()[1:-1]
return re.sub(r'\\(.)', lambda m: _ESCAPES.get(m.group(1), m.group(1)), body)
def _quote(text):
text = text.replace("\\", "\\\\").replace('"', '\\"').replace("\t", "\\t")
lines = text.split("\n")
parts = [line + "\\n" for line in lines[:-1]]
if lines[-1]:
parts.append(lines[-1])
if len(parts) <= 1:
return '"{}"'.format(parts[0] if parts else "")
return '""\n' + "\n".join('"{}"'.format(part) for part in parts)
def read_po(path):
"""Dictionnaire msgid -> (msgstr, fuzzy). L'en-tete a pour cle ""."""
entries = {}
if not os.path.exists(path):
return entries
msgid = msgstr = None
field = None
fuzzy = False
def flush():
if msgid is not None:
entries[msgid] = (msgstr or "", fuzzy)
with open(path, encoding="utf-8") as handle:
for raw in handle:
line = raw.strip()
if line.startswith("#,") and "fuzzy" in line:
flush()
msgid = msgstr = field = None
fuzzy = True
elif line.startswith("msgid "):
if field == "msgstr":
flush()
fuzzy = False
msgid, msgstr, field = _unquote(line[6:]), None, "msgid"
elif line.startswith("msgstr "):
msgstr, field = _unquote(line[7:]), "msgstr"
elif line.startswith('"') and field == "msgid":
msgid += _unquote(line)
elif line.startswith('"') and field == "msgstr":
msgstr += _unquote(line)
elif not line and field == "msgstr":
flush()
msgid = msgstr = field = None
fuzzy = False
flush()
return entries
def _header(language):
fields = [
("Project-Id-Version", DOMAIN),
("Language", language or ""),
("MIME-Version", "1.0"),
("Content-Type", "text/plain; charset=UTF-8"),
("Content-Transfer-Encoding", "8bit"),
("Plural-Forms", {"fr": "nplurals=2; plural=(n > 1);",
"en": "nplurals=2; plural=(n != 1);"}.get(language, "")),
]
return "".join("{}: {}\n".format(key, value) for key, value in fields if value)
def write_po(path, language, messages, translations):
title = ("Modele de traduction" if language is None
else "Traduction ({})".format(language))
out = ["# {} de l'extension Inkscape « {} ».".format(title, EXTENSION_NAME),
"# Genere par i18n.py ; ne modifier que les msgstr.",
"msgid \"\"",
"msgstr " + _quote(_header(language)),
""]
for msgid, refs in messages:
msgstr, fuzzy = translations.get(msgid, ("", False))
out.append("#: " + " ".join(refs))
if fuzzy:
out.append("#, fuzzy")
out.append("msgid " + _quote(msgid))
out.append("msgstr " + _quote(msgstr))
out.append("")
with open(path, "w", encoding="utf-8", newline="\n") as handle:
handle.write("\n".join(out))
# --------------------------------------------------------------------------
# Compilation .mo (format GNU gettext)
# --------------------------------------------------------------------------
def write_mo(path, catalog):
"""Ecrit un catalogue {msgid: msgstr} au format .mo, cles triees."""
keys = sorted(catalog)
ids = [key.encode("utf-8") for key in keys]
strs = [catalog[key].encode("utf-8") for key in keys]
count = len(keys)
ids_start = 7 * 4 + 16 * count
strs_start = ids_start + sum(len(b) + 1 for b in ids)
table_ids, table_strs = [], []
offset = ids_start
for data in ids:
table_ids += [len(data), offset]
offset += len(data) + 1
offset = strs_start
for data in strs:
table_strs += [len(data), offset]
offset += len(data) + 1
output = struct.pack("<7I", 0x950412DE, 0, count, 7 * 4, 7 * 4 + 8 * count, 0, 0)
output += struct.pack("<{}I".format(2 * count), *table_ids)
output += struct.pack("<{}I".format(2 * count), *table_strs)
output += b"".join(data + b"\0" for data in ids)
output += b"".join(data + b"\0" for data in strs)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "wb") as handle:
handle.write(output)
# --------------------------------------------------------------------------
# Commandes
# --------------------------------------------------------------------------
def po_path(language):
return os.path.join(PO_DIR, "{}.po".format(language))
def mo_path(language):
return os.path.join(LOCALE_DIR, language, "LC_MESSAGES", DOMAIN + ".mo")
def cmd_extract():
os.makedirs(PO_DIR, exist_ok=True)
messages = extract()
write_po(os.path.join(PO_DIR, DOMAIN + ".pot"), None, messages, {})
print("{} chaines -> po/{}.pot".format(len(messages), DOMAIN))
def cmd_update():
messages = extract()
for language in LANGUAGES:
translations = read_po(po_path(language))
if language == SOURCE_LANGUAGE:
translations = {msgid: (msgid, False) for msgid, _refs in messages}
write_po(po_path(language), language, messages, translations)
missing = [m for m, _r in messages if not translations.get(m, ("", False))[0]]
print("po/{}.po : {} a traduire".format(language, len(missing)))
for msgid in missing:
print(" " + msgid.splitlines()[0])
def cmd_compile():
for language in LANGUAGES:
entries = read_po(po_path(language))
catalog = {msgid: msgstr for msgid, (msgstr, fuzzy) in entries.items()
if msgid and msgstr and not fuzzy}
catalog[""] = _header(language)
write_mo(mo_path(language), catalog)
print("{} -> {} ({} chaines)".format(
os.path.relpath(po_path(language), HERE),
os.path.relpath(mo_path(language), HERE), len(catalog) - 1))
def main(argv):
commands = {"extract": [cmd_extract], "update": [cmd_update],
"compile": [cmd_compile],
"all": [cmd_extract, cmd_update, cmd_compile]}
name = argv[0] if argv else "all"
if name not in commands:
print(__doc__)
return 1
for command in commands[name]:
command()
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))

BIN
images/parameters_en.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Binary file not shown.

165
po/box_maker.pot Normal file
View File

@ -0,0 +1,165 @@
# Modele de traduction de l'extension Inkscape « Finger-Joint Box ».
# Genere par i18n.py ; ne modifier que les msgstr.
msgid ""
msgstr ""
"Project-Id-Version: box_maker\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: box_maker.inx
msgid "Finger-Joint Box"
msgstr ""
#: box_maker.inx
msgid "Dimensions"
msgstr ""
#: box_maker.inx
msgid "Length (mm):"
msgstr ""
#: box_maker.inx
msgid "Width (mm):"
msgstr ""
#: box_maker.inx
msgid "Height (mm):"
msgstr ""
#: box_maker.inx
msgid "Dimensions are:"
msgstr ""
#: box_maker.inx
msgid "Exterior"
msgstr ""
#: box_maker.inx
msgid "Interior"
msgstr ""
#: box_maker.inx
msgid "Closed box (with lid)"
msgstr ""
#: box_maker.inx
msgid "Material and fingers"
msgstr ""
#: box_maker.inx
msgid "Material thickness (mm):"
msgstr ""
#: box_maker.inx
msgid "Finger width (mm, 0 = 3 × thickness):"
msgstr ""
#: box_maker.inx
msgid "Laser beam compensation, kerf (mm):"
msgstr ""
#: box_maker.inx
msgid "Notes"
msgstr ""
#: box_maker.inx
msgid "The finger width is adjusted to get an odd number of fingers on each edge."
msgstr ""
#: box_maker.inx
msgid "The kerf widens the fingers and narrows the slots for a tight fit."
msgstr ""
#: box_maker.inx
msgid "Drawing"
msgstr ""
#: box_maker.inx
msgid "Spacing between parts (mm):"
msgstr ""
#: box_maker.inx
msgid "Stroke width (mm):"
msgstr ""
#: box_maker.inx
msgid "Stroke color:"
msgstr ""
#: box_maker.inx
msgid "Name the parts (engraved text!)"
msgstr ""
#: box_maker.inx
msgid "Diagram"
msgstr ""
#: box_maker.inx
msgid "Help"
msgstr ""
#: box_maker.inx
msgid ""
"Draws the pattern of a laser-cut box whose\n"
"panels interlock with straight finger joints.\n"
"No selection is needed: the parts are added\n"
"to the current layer, centered on the view.\n"
"\n"
"Dimensions: exterior sizes, or interior sizes\n"
"(the thickness is then added). Unchecking\n"
"\"Closed box\" gives 5 parts, with a straight\n"
"top edge on the sides.\n"
"\n"
"Fingers: 0 = 3 × the thickness. The width is\n"
"adjusted to get an odd number per edge.\n"
"\n"
"Kerf: the outline is moved outwards by\n"
"kerf / 2 so that parts keep their nominal size\n"
"after cutting. Measure it: cut a 20 mm square,\n"
"kerf = 20 − measured size. Larger = tighter.\n"
"\n"
"Each part is a separate closed path, named\n"
"(Front, Bottom…), that can be moved alone.\n"
"Labels are text: the laser would engrave them."
msgstr ""
#: box_maker.py
msgid "Front"
msgstr ""
#: box_maker.py
msgid "Back"
msgstr ""
#: box_maker.py
msgid "Left"
msgstr ""
#: box_maker.py
msgid "Right"
msgstr ""
#: box_maker.py
msgid "Bottom"
msgstr ""
#: box_maker.py
msgid "Lid"
msgstr ""
#: box_maker.py
msgid "length"
msgstr ""
#: box_maker.py
msgid "width"
msgstr ""
#: box_maker.py
msgid "height"
msgstr ""
#: box_maker.py
msgid "The {dimension} ({size:.1f} mm exterior) is too small for the thickness ({thickness:g} mm): it must be at least {minimum:g} mm."
msgstr ""

188
po/en.po Normal file
View File

@ -0,0 +1,188 @@
# Traduction (en) de l'extension Inkscape « Finger-Joint Box ».
# Genere par i18n.py ; ne modifier que les msgstr.
msgid ""
msgstr ""
"Project-Id-Version: box_maker\n"
"Language: en\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: box_maker.inx
msgid "Finger-Joint Box"
msgstr "Finger-Joint Box"
#: box_maker.inx
msgid "Dimensions"
msgstr "Dimensions"
#: box_maker.inx
msgid "Length (mm):"
msgstr "Length (mm):"
#: box_maker.inx
msgid "Width (mm):"
msgstr "Width (mm):"
#: box_maker.inx
msgid "Height (mm):"
msgstr "Height (mm):"
#: box_maker.inx
msgid "Dimensions are:"
msgstr "Dimensions are:"
#: box_maker.inx
msgid "Exterior"
msgstr "Exterior"
#: box_maker.inx
msgid "Interior"
msgstr "Interior"
#: box_maker.inx
msgid "Closed box (with lid)"
msgstr "Closed box (with lid)"
#: box_maker.inx
msgid "Material and fingers"
msgstr "Material and fingers"
#: box_maker.inx
msgid "Material thickness (mm):"
msgstr "Material thickness (mm):"
#: box_maker.inx
msgid "Finger width (mm, 0 = 3 × thickness):"
msgstr "Finger width (mm, 0 = 3 × thickness):"
#: box_maker.inx
msgid "Laser beam compensation, kerf (mm):"
msgstr "Laser beam compensation, kerf (mm):"
#: box_maker.inx
msgid "Notes"
msgstr "Notes"
#: box_maker.inx
msgid "The finger width is adjusted to get an odd number of fingers on each edge."
msgstr "The finger width is adjusted to get an odd number of fingers on each edge."
#: box_maker.inx
msgid "The kerf widens the fingers and narrows the slots for a tight fit."
msgstr "The kerf widens the fingers and narrows the slots for a tight fit."
#: box_maker.inx
msgid "Drawing"
msgstr "Drawing"
#: box_maker.inx
msgid "Spacing between parts (mm):"
msgstr "Spacing between parts (mm):"
#: box_maker.inx
msgid "Stroke width (mm):"
msgstr "Stroke width (mm):"
#: box_maker.inx
msgid "Stroke color:"
msgstr "Stroke color:"
#: box_maker.inx
msgid "Name the parts (engraved text!)"
msgstr "Name the parts (engraved text!)"
#: box_maker.inx
msgid "Diagram"
msgstr "Diagram"
#: box_maker.inx
msgid "Help"
msgstr "Help"
#: box_maker.inx
msgid ""
"Draws the pattern of a laser-cut box whose\n"
"panels interlock with straight finger joints.\n"
"No selection is needed: the parts are added\n"
"to the current layer, centered on the view.\n"
"\n"
"Dimensions: exterior sizes, or interior sizes\n"
"(the thickness is then added). Unchecking\n"
"\"Closed box\" gives 5 parts, with a straight\n"
"top edge on the sides.\n"
"\n"
"Fingers: 0 = 3 × the thickness. The width is\n"
"adjusted to get an odd number per edge.\n"
"\n"
"Kerf: the outline is moved outwards by\n"
"kerf / 2 so that parts keep their nominal size\n"
"after cutting. Measure it: cut a 20 mm square,\n"
"kerf = 20 − measured size. Larger = tighter.\n"
"\n"
"Each part is a separate closed path, named\n"
"(Front, Bottom…), that can be moved alone.\n"
"Labels are text: the laser would engrave them."
msgstr ""
"Draws the pattern of a laser-cut box whose\n"
"panels interlock with straight finger joints.\n"
"No selection is needed: the parts are added\n"
"to the current layer, centered on the view.\n"
"\n"
"Dimensions: exterior sizes, or interior sizes\n"
"(the thickness is then added). Unchecking\n"
"\"Closed box\" gives 5 parts, with a straight\n"
"top edge on the sides.\n"
"\n"
"Fingers: 0 = 3 × the thickness. The width is\n"
"adjusted to get an odd number per edge.\n"
"\n"
"Kerf: the outline is moved outwards by\n"
"kerf / 2 so that parts keep their nominal size\n"
"after cutting. Measure it: cut a 20 mm square,\n"
"kerf = 20 − measured size. Larger = tighter.\n"
"\n"
"Each part is a separate closed path, named\n"
"(Front, Bottom…), that can be moved alone.\n"
"Labels are text: the laser would engrave them."
#: box_maker.py
msgid "Front"
msgstr "Front"
#: box_maker.py
msgid "Back"
msgstr "Back"
#: box_maker.py
msgid "Left"
msgstr "Left"
#: box_maker.py
msgid "Right"
msgstr "Right"
#: box_maker.py
msgid "Bottom"
msgstr "Bottom"
#: box_maker.py
msgid "Lid"
msgstr "Lid"
#: box_maker.py
msgid "length"
msgstr "length"
#: box_maker.py
msgid "width"
msgstr "width"
#: box_maker.py
msgid "height"
msgstr "height"
#: box_maker.py
msgid "The {dimension} ({size:.1f} mm exterior) is too small for the thickness ({thickness:g} mm): it must be at least {minimum:g} mm."
msgstr "The {dimension} ({size:.1f} mm exterior) is too small for the thickness ({thickness:g} mm): it must be at least {minimum:g} mm."

190
po/fr.po Normal file
View File

@ -0,0 +1,190 @@
# Traduction (fr) de l'extension Inkscape « Finger-Joint Box ».
# Genere par i18n.py ; ne modifier que les msgstr.
msgid ""
msgstr ""
"Project-Id-Version: box_maker\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: box_maker.inx
msgid "Finger-Joint Box"
msgstr "Boîte à créneaux"
#: box_maker.inx
msgid "Dimensions"
msgstr "Dimensions"
#: box_maker.inx
msgid "Length (mm):"
msgstr "Longueur (mm) :"
#: box_maker.inx
msgid "Width (mm):"
msgstr "Largeur (mm) :"
#: box_maker.inx
msgid "Height (mm):"
msgstr "Hauteur (mm) :"
#: box_maker.inx
msgid "Dimensions are:"
msgstr "Les dimensions sont :"
#: box_maker.inx
msgid "Exterior"
msgstr "Extérieures"
#: box_maker.inx
msgid "Interior"
msgstr "Intérieures"
#: box_maker.inx
msgid "Closed box (with lid)"
msgstr "Boîte fermée (avec couvercle)"
#: box_maker.inx
msgid "Material and fingers"
msgstr "Matière et créneaux"
#: box_maker.inx
msgid "Material thickness (mm):"
msgstr "Épaisseur de la matière (mm) :"
#: box_maker.inx
msgid "Finger width (mm, 0 = 3 × thickness):"
msgstr "Largeur des créneaux (mm, 0 = 3 × épaisseur) :"
#: box_maker.inx
msgid "Laser beam compensation, kerf (mm):"
msgstr "Compensation du trait laser, kerf (mm) :"
#: box_maker.inx
msgid "Notes"
msgstr "Remarques"
#: box_maker.inx
msgid "The finger width is adjusted to get an odd number of fingers on each edge."
msgstr "La largeur des créneaux est ajustée pour obtenir un nombre impair de créneaux sur chaque bord."
#: box_maker.inx
msgid "The kerf widens the fingers and narrows the slots for a tight fit."
msgstr "Le kerf élargit les créneaux et rétrécit les encoches pour un assemblage serré."
#: box_maker.inx
msgid "Drawing"
msgstr "Tracé"
#: box_maker.inx
msgid "Spacing between parts (mm):"
msgstr "Espacement entre les pièces (mm) :"
#: box_maker.inx
msgid "Stroke width (mm):"
msgstr "Épaisseur du trait (mm) :"
#: box_maker.inx
msgid "Stroke color:"
msgstr "Couleur du trait :"
#: box_maker.inx
msgid "Name the parts (engraved text!)"
msgstr "Nommer les pièces (texte gravé !)"
#: box_maker.inx
msgid "Diagram"
msgstr "Schéma"
#: box_maker.inx
msgid "Help"
msgstr "Aide"
#: box_maker.inx
msgid ""
"Draws the pattern of a laser-cut box whose\n"
"panels interlock with straight finger joints.\n"
"No selection is needed: the parts are added\n"
"to the current layer, centered on the view.\n"
"\n"
"Dimensions: exterior sizes, or interior sizes\n"
"(the thickness is then added). Unchecking\n"
"\"Closed box\" gives 5 parts, with a straight\n"
"top edge on the sides.\n"
"\n"
"Fingers: 0 = 3 × the thickness. The width is\n"
"adjusted to get an odd number per edge.\n"
"\n"
"Kerf: the outline is moved outwards by\n"
"kerf / 2 so that parts keep their nominal size\n"
"after cutting. Measure it: cut a 20 mm square,\n"
"kerf = 20 − measured size. Larger = tighter.\n"
"\n"
"Each part is a separate closed path, named\n"
"(Front, Bottom…), that can be moved alone.\n"
"Labels are text: the laser would engrave them."
msgstr ""
"Dessine le patron d'une boîte à découper au laser,\n"
"dont les faces s'assemblent par créneaux droits.\n"
"Aucune sélection nécessaire : les pièces sont\n"
"ajoutées au calque courant, centrées sur la vue.\n"
"\n"
"Dimensions : extérieures, ou intérieures\n"
"(l'épaisseur est alors ajoutée). Décocher\n"
"« Boîte fermée » donne 5 pièces, bord haut\n"
"des côtés droit.\n"
"\n"
"Créneaux : 0 = 3 × l'épaisseur. La largeur est\n"
"ajustée pour un nombre impair par bord.\n"
"\n"
"Kerf : le contour est décalé vers l'extérieur\n"
"de kerf / 2 : les pièces gardent leurs cotes\n"
"après découpe. Mesure : découper un carré de\n"
"20 mm, kerf = 20 − cote mesurée. Plus grand =\n"
"plus serré.\n"
"\n"
"Chaque pièce est un chemin fermé indépendant,\n"
"nommé (Avant, Fond…), déplaçable seul.\n"
"Les étiquettes sont du texte : le laser les\n"
"graverait."
#: box_maker.py
msgid "Front"
msgstr "Avant"
#: box_maker.py
msgid "Back"
msgstr "Arrière"
#: box_maker.py
msgid "Left"
msgstr "Gauche"
#: box_maker.py
msgid "Right"
msgstr "Droite"
#: box_maker.py
msgid "Bottom"
msgstr "Fond"
#: box_maker.py
msgid "Lid"
msgstr "Couvercle"
#: box_maker.py
msgid "length"
msgstr "longueur"
#: box_maker.py
msgid "width"
msgstr "largeur"
#: box_maker.py
msgid "height"
msgstr "hauteur"
#: box_maker.py
msgid "The {dimension} ({size:.1f} mm exterior) is too small for the thickness ({thickness:g} mm): it must be at least {minimum:g} mm."
msgstr "La {dimension} ({size:.1f} mm extérieure) est trop petite pour l'épaisseur ({thickness:g} mm) : il faut au moins {minimum:g} mm."

128
tests/test_extension.py Normal file
View File

@ -0,0 +1,128 @@
# coding=utf-8
"""Tests de la boite de dialogue, des traductions et de l'extension (pytest)."""
import os
import re
import sys
import xml.etree.ElementTree as ET
import pytest
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
sys.path.insert(0, ROOT)
INX = os.path.join(ROOT, "box_maker.inx")
import box_core # noqa: E402
# --------------------------------------------------------------------------
# Traductions
# --------------------------------------------------------------------------
def test_translations_up_to_date_and_complete():
"""Chaque texte du .inx et des .py a sa traduction dans chaque catalogue."""
import gettext
import i18n
msgids = [msgid for msgid, _refs in i18n.extract()]
assert "Finger-Joint Box" in msgids
assert "Front" in msgids # noms des pieces, marques dans box_maker.py
for language in i18n.LANGUAGES:
entries = i18n.read_po(i18n.po_path(language))
missing = [m for m in msgids if not entries.get(m, ("", False))[0]]
assert not missing, "{}.po incomplet : {}".format(language, missing)
catalog = gettext.translation(i18n.DOMAIN, i18n.LOCALE_DIR, [language])
for msgid in msgids:
assert catalog.gettext(msgid) == entries[msgid][0], \
"{} : .mo a recompiler (python i18n.py)".format(language)
def test_french_error_message_keeps_placeholders():
"""La traduction du message d'erreur garde les memes champs de format."""
import i18n
fields = re.compile(r"\{\w+(?::[^}]*)?\}")
for msgid, (msgstr, _fuzzy) in i18n.read_po(i18n.po_path("fr")).items():
assert sorted(fields.findall(msgid)) == sorted(fields.findall(msgstr)), msgid
def test_po_roundtrip(tmp_path):
import i18n
messages = [("Simple", ["a"]), ('Quote "x" and \\ back', ["a"]),
("Two\nlines", ["a"]), ("Tab\tend\n", ["a"])]
path = str(tmp_path / "xx.po")
i18n.write_po(path, "fr", messages,
{m: ("<" + m + ">", False) for m, _r in messages})
entries = i18n.read_po(path)
for msgid, _refs in messages:
assert entries[msgid] == ("<" + msgid + ">", False)
# --------------------------------------------------------------------------
# Boite de dialogue
# --------------------------------------------------------------------------
def test_inx_matches_arguments():
"""Chaque <param> du .inx a son add_argument, et inversement."""
root = ET.parse(INX).getroot()
params = {elem.get("name") for elem in root.iter()
if elem.tag.rsplit("}", 1)[-1] == "param"}
with open(os.path.join(ROOT, "box_maker.py"), encoding="utf-8") as handle:
arguments = set(re.findall(r'add_argument\("--(\w+)"', handle.read()))
assert params == arguments
def test_inx_images_exist():
"""Les images de la boite de dialogue sont presentes (chemin relatif au .inx)."""
root = ET.parse(INX).getroot()
images = [elem.text.strip() for elem in root.iter()
if elem.tag.rsplit("}", 1)[-1] == "image"]
assert images
for image in images:
assert os.path.isfile(os.path.join(ROOT, image)), image
def test_every_core_panel_has_a_translated_name():
inkex = pytest.importorskip("inkex") # noqa: F841
import box_maker
names = {p.name for p in box_core.box_panels(100, 80, 50, 3)}
assert names == set(box_maker.panel_names())
# --------------------------------------------------------------------------
# Bout en bout (avec inkex)
# --------------------------------------------------------------------------
BLANK = ('<svg xmlns="http://www.w3.org/2000/svg" '
'xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" '
'width="300mm" height="300mm" viewBox="0 0 300 300">'
'<g inkscape:groupmode="layer" id="layer1"/></svg>')
def _run(tmp_path, *args):
pytest.importorskip("inkex")
import box_maker
source = tmp_path / "in.svg"
source.write_text(BLANK, encoding="utf-8")
output = tmp_path / "out.svg"
box_maker.BoxMaker().run(list(args) + ["--output={}".format(output), str(source)])
return output.read_text(encoding="utf-8") if output.exists() else ""
def test_end_to_end_closed_box_with_labels(tmp_path):
svg = _run(tmp_path, "--labels=true")
assert svg.count("<path") == 6
for name in ("Front", "Back", "Left", "Right", "Bottom", "Lid"):
assert ">{}<".format(name) in svg
def test_end_to_end_too_small_reports_error(tmp_path, capsys):
_run(tmp_path, "--length=10")
err = capsys.readouterr().err
assert "length" in err and "at least 15 mm" in err