commit 2105bb96a08102ec996ffba74a7262acca8d714b Author: Alex Date: Thu Sep 24 10:07:14 2026 +0200 Version initiale diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d5c4b8c --- /dev/null +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..570a7fb --- /dev/null +++ b/README.md @@ -0,0 +1,208 @@ +# Remplissage motif Y fléché — extension Inkscape + + + + + + +
+Une forme courbe percée d'un trou et un cercle, remplis par l'extension d'un réseau de Y fléchés découpé au contour + + +Extension Inkscape 1.x qui remplit la surface d'une forme fermée avec un motif +géométrique de « Y fléchés » : un réseau triangulaire de Y identiques (bras à 0°, +120° et 240°), chaque bras se terminant par un chevron dont les barbes sont +parallèles aux deux autres bras. La pointe de chaque flèche vient se loger dans le +« V » arrière du Y voisin, ce qui produit l'effet de chevrons imbriqués. + +
+ +## Table des matières + +- [Fonctionnalités](#fonctionnalités) +- [Installation](#installation) +- [Utilisation](#utilisation) +- [Paramètres](#paramètres) +- [Structure du projet](#structure-du-projet) +- [Développement](#développement) + +## Fonctionnalités + +- Remplissage de `path`, `rect`, `circle`, `ellipse`, `polygon` (les groupes + sélectionnés sont explorés ; `line` et `polyline` sont ignorées) +- Deux modes au bord de la forme : **découpe géométrique** des traits sur le + contour, ou **motifs entiers uniquement** avec marge réglable +- Gestion des trous : les sous-chemins internes sont exclus (règle pair-impair) +- Proportions réglables : période, longueur des bras et des barbes (en % de la + période), rotation, épaisseur et couleur du trait +- Ancrage du réseau sur l'origine du document (motif continu d'une forme à + l'autre) ou au centre de chaque forme +- Un chemin par motif, sans attribut `transform` sur les chemins : SVG directement + exploitable par les logiciels de découpe laser +- Motifs ordonnés en serpentin, ligne par ligne, pour limiter les déplacements à vide +- Index spatial des arêtes du contour : plusieurs milliers de motifs en une fraction + de seconde +- Interface traduite en français et en anglais, selon la langue d'Inkscape + (anglais pour toute autre langue) +- Script de déploiement PowerShell (installation, mise à jour, désinstallation) + +## Installation + +Aucune dépendance externe : l'extension utilise `inkex`, fourni avec Inkscape. + +### Windows — script de déploiement + +Inkscape fermé, depuis le dossier du projet : + +```powershell +.\deploy.ps1 # copie vers %APPDATA%\inkscape\extensions\yArrowPattern +.\deploy.ps1 -Uninstall # retire le sous-dossier de l'extension +.\deploy.ps1 -Force # passe outre le contrôle « Inkscape ouvert » +``` + +### Installation manuelle + +Copier `y_pattern.inx`, `y_pattern.py`, `pattern_core.py` et les dossiers `locale/` +et `images/` dans un sous-dossier (par exemple `yArrowPattern`) du dossier des extensions +utilisateur, puis redémarrer Inkscape : + +- Windows : `%APPDATA%\inkscape\extensions` +- Linux : `~/.config/inkscape/extensions` +- macOS : `~/Library/Application Support/org.inkscape.Inkscape/config/inkscape/extensions` + +## Utilisation + +1. Sélectionner une ou plusieurs formes fermées. +2. **Extensions → AlexDesign → Remplissage motif Y fléché…** + (*Y Arrow Pattern Fill…* avec une interface en anglais) +3. Régler les paramètres, puis **Appliquer**. L'onglet **Schéma** illustre chaque + paramètre. + +Le résultat est placé dans un groupe « Motif Y fléché » juste au-dessus de la forme. + +Conseils : + +- motif trop serré ou trop lâche → ajuster la **période** ; les proportions + restent identiques +- traits qui se touchent → réduire l'épaisseur ou la longueur des bras (l'écart + entre une pointe et le Y suivant vaut `100 % − bras`) +- en mode « motifs entiers », la demi-épaisseur du trait est ajoutée à la marge + pour que rien ne déborde du contour +- en mode découpe, les extrémités coupées en biais peuvent dépasser du contour + d'au plus une demi-épaisseur de trait + +## Paramètres + +![Schéma des paramètres : période, bras, barbes, épaisseur, rotation, bord de la forme et ancrage du réseau](docs/parametres.png) + +| Paramètre | Défaut | Rôle | +| ---------------------------- | ------- | ------------------------------------------------------------- | +| Période | 10.0 | Distance entre les centres de deux Y voisins | +| Longueur des bras | 83 % | En pourcentage de la période | +| Longueur des barbes | 33 % | En pourcentage de la période ; 0 donne des Y simples | +| Rotation du motif | 0° | Sens anti-horaire | +| Épaisseur du trait | 0.5 | Largeur du trait | +| Unité | mm | Unité de la période, de l'épaisseur et de la marge | +| Couleur du trait | #b3b3b3 | Couleur et opacité | +| Bord de la forme | Découpe | Découpe au contour, ou motifs entiers uniquement | +| Marge au contour | 0.0 | Mode « motifs entiers » seulement | +| Ancrage du réseau | Document| Origine du document, ou centre de chaque forme | +| Finesse d'échantillonnage | 0.2 | Précision de l'aplatissement des courbes de Bézier | +| Conserver la forme d'origine | oui | Sinon la forme source est supprimée | + +Les valeurs par défaut (83 % / 33 %) reproduisent les proportions du motif +d'origine. + +## Structure du projet + +```text +y_pattern.inx Description de la boîte de dialogue Inkscape +y_pattern.py Extension : lecture de la sélection, aplatissement des + chemins, écriture des motifs dans le SVG +pattern_core.py Noyau géométrique sans dépendance à inkex +deploy.ps1 Déploiement / désinstallation sous Windows +test_pattern.py Tests pytest +i18n.py Chaîne de traduction : extraction, mise à jour, compilation +po/ Catalogues source : modèle .pot, en.po, fr.po +locale/ Catalogues compilés (.mo) chargés par Inkscape +images/ Schéma anglais de l'onglet « Diagram » (déployé) +tests/data/shapes.svg Formes d'exemple (rectangle, cercle, courbe, forme trouée) +docs/ Images du README, SVG des schémas et leur générateur + (non déployés) +``` + +API de `pattern_core.py` (utilisable seule, hors Inkscape) : + +| Fonction | Rôle | +| ----------------------------------------------- | -------------------------------------------------------- | +| `motif_polylines(cx, cy, arm, barb, angle)` | Polylignes d'un Y fléché | +| `lattice_centers(bbox, period, angle, reach)` | Centres du réseau triangulaire, en ordre serpentin | +| `EdgeIndex(rings)` | Index spatial du contour : appartenance, intersections | +| `clip_polyline(polyline, index)` | Morceaux d'une polyligne intérieurs au contour | +| `motif_fits(polylines, index, clearance)` | Motif entier intérieur, à distance minimale du bord | +| `fill_pattern(rings, period, …)` | Remplissage complet, renvoie une liste de motifs | +| `polylines_to_d(polylines)` | Données `d` d'un chemin SVG | +| `parse_color(value)` | Décode la couleur RGBA transmise par Inkscape | + +## Développement + +```powershell +python -m venv .venv +.venv\Scripts\activate # Linux/macOS : source .venv/bin/activate +pip install pytest +python -m pytest test_pattern.py -q +``` + +Résultat attendu : **23 passés, 3 ignorés** (tests de bout en bout sautés quand +`inkex` n'est pas importable). Pour les exécuter : + +```powershell +pip install lxml tinycss2 cssselect2 cssselect +$env:PYTHONPATH = 'C:\Program Files\Inkscape\share\inkscape\extensions' +python -m pytest test_pattern.py -q # 26 passés +``` + +Les tests couvrent la géométrie du motif (barbes parallèles aux bras, voisins à une +période), la couverture du réseau, l'absence de croisement entre motifs voisins aux +proportions par défaut, la découpe (y compris à travers un trou), la marge du mode +« motifs entiers » et la continuité du motif entre deux formes adjacentes. + +### Traductions + +Les textes source du `.inx` et des appels `_()` de `y_pattern.py` sont en anglais ; +Inkscape les traduit avec le catalogue `yarrowpattern` (attribut +`translationdomain` du `.inx`) trouvé dans `locale//LC_MESSAGES/`. +`i18n.py` n'a besoin que de Python (ni `xgettext` ni `msgfmt`) : + +```powershell +python i18n.py # tout : extract + update + compile +python i18n.py extract # po/yarrowpattern.pot depuis le .inx et le .py +python i18n.py update # reporte les nouveaux textes dans po/en.po et po/fr.po +python i18n.py compile # po/*.po -> locale/*/LC_MESSAGES/yarrowpattern.mo +``` + +Après modification d'un texte d'interface : lancer `python i18n.py`, compléter les +`msgstr` vides de `po/fr.po` (liste affichée par la commande), relancer +`python i18n.py`, puis redéployer. `po/en.po` est rempli automatiquement avec les +textes source. Ajouter une langue : l'ajouter à `LANGUAGES` dans `i18n.py`. +Le test `test_translations_up_to_date_and_complete` échoue si un catalogue est +incomplet ou si un `.mo` n'a pas été recompilé. + +### Schéma des paramètres + +Le schéma existe en deux versions, produites à partir de `pattern_core` (même +géométrie que l'extension) : + +- `docs/parametres.png` : version française du README (1400 px) ; +- `images/parameters_en.png` : version anglaise, textes agrandis, affichée à + 900 × 694 px dans l'onglet **Schéma** / **Diagram** de la boîte de dialogue. + Inkscape ne traduit pas le chemin d'une `` : cet onglet montre la version + anglaise quelle que soit la langue. + +```powershell +python docs/schema_parametres.py # les deux versions (ou : fr, en) +``` + +Le script écrit les SVG dans `docs/` puis les exporte en PNG avec +`C:\Program Files\Inkscape\bin\inkscape.com`. Il faut passer par `inkscape.com` : la +commande `inkscape` installée par Chocolatey rend la main avant la fin de l'export. diff --git a/deploy.ps1 b/deploy.ps1 new file mode 100644 index 0000000..89d46f1 --- /dev/null +++ b/deploy.ps1 @@ -0,0 +1,107 @@ +<# +.SYNOPSIS + Déploie l'extension « Remplissage motif Y fléché » dans Inkscape. + +.DESCRIPTION + Copie y_pattern.inx, y_pattern.py, pattern_core.py, le schéma de l'onglet + « Diagram » (images\parameters_en.png) et les traductions + compilées (locale\\LC_MESSAGES\yarrowpattern.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 est ouvert : les extensions ne sont chargées qu'au démarrage. + +.PARAMETER ExtensionsDir + Répertoire des extensions Inkscape. Par défaut : %APPDATA%\inkscape\extensions + +.PARAMETER FolderName + Nom du sous-dossier de l'extension. Par défaut : yArrowPattern + +.PARAMETER Uninstall + Supprime le sous-dossier de l'extension au lieu de le créer. + +.PARAMETER Force + Déploie même si Inkscape est en cours d'exécution. + +.EXAMPLE + .\deploy.ps1 + .\deploy.ps1 -Uninstall + .\deploy.ps1 -FolderName 'motif-y' +#> + +[CmdletBinding()] +param( + [string] $ExtensionsDir = (Join-Path $env:APPDATA 'inkscape\extensions'), + [string] $FolderName = 'yArrowPattern', + [switch] $Uninstall, + [switch] $Force +) + +$ErrorActionPreference = 'Stop' + +$files = @('y_pattern.inx', 'y_pattern.py', 'pattern_core.py', 'images\parameters_en.png') +$localeDir = 'locale' +$source = $PSScriptRoot +$destination = Join-Path $ExtensionsDir $FolderName + +# --- Inkscape doit être fermé : les extensions sont lues au démarrage --------- +$running = Get-Process -Name 'inkscape' -ErrorAction SilentlyContinue +if ($running -and -not $Force) { + Write-Error "Inkscape est ouvert. Fermez-le puis relancez le script (ou utilisez -Force)." + exit 1 +} + +# --- Désinstallation --------------------------------------------------------- +if ($Uninstall) { + if (Test-Path -LiteralPath $destination) { + Remove-Item -LiteralPath $destination -Recurse -Force + Write-Host "Supprimé : $destination" -ForegroundColor Green + } + else { + Write-Host "Rien à supprimer : $destination n'existe pas." -ForegroundColor Yellow + } + exit 0 +} + +# --- Vérification des sources ------------------------------------------------ +$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) { + Write-Error "Fichier(s) introuvable(s) dans $source : $($missing -join ', ')" + exit 1 +} + +# --- Copie ------------------------------------------------------------------- +if (-not (Test-Path -LiteralPath $destination)) { + New-Item -ItemType Directory -Path $destination -Force | Out-Null + Write-Host "Dossier créé : $destination" +} + +foreach ($file in $files) { + $from = Join-Path $source $file + $to = Join-Path $destination $file + $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 + Write-Host " $action $file" +} + +# Traductions : le dossier est remplacé en entier (langue retirée => supprimée) +$localeTo = Join-Path $destination $localeDir +if (Test-Path -LiteralPath $localeTo) { + Remove-Item -LiteralPath $localeTo -Recurse -Force +} +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 +$pycache = Join-Path $destination '__pycache__' +if (Test-Path -LiteralPath $pycache) { + Remove-Item -LiteralPath $pycache -Recurse -Force +} + +Write-Host "" +Write-Host "Extension déployée dans $destination" -ForegroundColor Green +Write-Host "Redémarrez Inkscape, puis : Extensions > AlexDesign > Remplissage motif Y fléché..." diff --git a/docs/capture-desktop.png b/docs/capture-desktop.png new file mode 100644 index 0000000..db6a526 Binary files /dev/null and b/docs/capture-desktop.png differ diff --git a/docs/parametres.png b/docs/parametres.png new file mode 100644 index 0000000..a828bb1 Binary files /dev/null and b/docs/parametres.png differ diff --git a/docs/schema_parametres.py b/docs/schema_parametres.py new file mode 100644 index 0000000..f386cdb --- /dev/null +++ b/docs/schema_parametres.py @@ -0,0 +1,424 @@ +#!/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 fr # docs/parametres.svg + .png (README) + python docs/schema_parametres.py en # docs/parameters_en.svg + # + images/parameters_en.png (onglet + # « Diagram » de la boite de dialogue) + +Les motifs sont calcules par pattern_core (memes fonctions que l'extension), +le schema reste donc fidele a la geometrie reelle. 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) + +from pattern_core import (EdgeIndex, clip_polyline, fill_pattern, # noqa: E402 + motif_polylines, unit) + +ARM, BARB = 0.83, 0.33 # valeurs par defaut de l'extension +W, H = 1400, 1080 + +INK = "#343a40" # motif principal +GHOST = "#dde1e5" # motifs voisins +DIM = "#d9480f" # cotes +DIM2 = "#1971c2" # cotes secondaires +EDGE = "#212529" # contour des formes +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"] + +# Version par langue : textes, fichiers produits, largeur du PNG et agrandissement +# des textes (la version anglaise est affichee reduite dans la boite de dialogue). +VERSIONS = { + "fr": { + "svg": os.path.join(HERE, "parametres.svg"), + "png": os.path.join(HERE, "parametres.png"), + "png_width": 1400, + "font_scale": 1.0, + "text": { + "title1": "Le motif Y fléché", + "center": "centre", + "neighbor": "centre voisin", + "period": "Période P", + "arm": "bras a = 83 % × P", + "barb": "barbe b = 33 % × P", + "stroke": "épaisseur du trait e", + "note1": ["Les barbes sont parallèles aux deux autres bras ; la pointe vient", + "se loger dans le « V » arrière du Y voisin. Barbe à 0 % : Y simples."], + "title2": "Rotation du motif θ", + "note2": ["Angle en degrés, sens anti-horaire. Le réseau entier tourne", + "autour du point d'ancrage (voir 4)."], + "title3": "Bord de la forme", + "clip": "Découper au contour", + "whole": "Motifs entiers uniquement", + "margin": "marge m", + "note3": ["Seuls les Y qui tiennent à m + e/2 du contour sont gardés."], + "title4": "Ancrage du réseau", + "document": "Origine du document", + "shape": "Centre de chaque forme", + "note4": ["Document : motif continu d'une forme à l'autre. Centre : chaque", + "forme est centrée sur un Y (point bleu), raccord visible."], + }, + }, + "en": { + "svg": os.path.join(HERE, "parameters_en.svg"), + "png": os.path.join(ROOT, "images", "parameters_en.png"), + "png_width": 900, + "font_scale": 1.25, + "text": { + "title1": "The Y arrow pattern", + "center": "center", + "neighbor": "neighbor", + "period": "Period P", + "arm": "arm a = 83 % × P", + "barb": "barb b = 33 % × P", + "stroke": "stroke width e", + "note1": ["Barbs are parallel to the two other arms; each tip", + "nests in the back “V” of the neighboring Y.", + "Barb at 0 %: plain Ys."], + "title2": "Pattern rotation θ", + "note2": ["Angle in degrees, counterclockwise. The whole", + "grid rotates around the anchor point (see 4)."], + "title3": "Shape edge", + "clip": "Clip at the outline", + "whole": "Whole patterns only", + "margin": "margin m", + "note3": ["Only Ys at least m + e/2 from the outline are kept."], + "title4": "Grid anchor", + "document": "Document origin", + "shape": "Center of each shape", + "note4": ["Document: pattern continuous across shapes.", + "Center: each shape is centered on a Y (blue dot)."], + }, + }, +} + +out = [] +T = {} +FS = 1.0 + + +def fmt(v): + return "{:.2f}".format(v).rstrip("0").rstrip(".") + + +def pts(polyline): + return " ".join("{},{}".format(fmt(x), fmt(y)) for x, y in polyline) + + +def polyline(points, color, width, extra=""): + out.append(''.format(pts(points), color, fmt(width), extra)) + + +def motif(polylines, color, width): + for pl in polylines: + polyline(pl, color, width) + + +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 arc(center, radius, a0, a1, color, label=None, label_radius=None, size=16): + """Arc de a0 a a1 degres (sens anti-horaire a l'ecran), fleche au bout.""" + steps = 40 + points = [] + for k in range(steps + 1): + ux, uy = unit(a0 + (a1 - a0) * k / steps) + points.append((center[0] + radius * ux, center[1] + radius * uy)) + polyline(points[:-2], color, 1.6) + last, before = points[-1], points[-3] + d = math.hypot(last[0] - before[0], last[1] - before[1]) + arrow_head(last, ((last[0] - before[0]) / d, (last[1] - before[1]) / d), color, 8) + if label: + ux, uy = unit((a0 + a1) / 2) + r = label_radius or radius + 24 + text(center[0] + r * ux, center[1] + r * uy + 6 * FS, label, size=size, + color=color, anchor="middle", weight="bold") + + +def circle_ring(cx, cy, r, n=180): + return [(cx + r * math.cos(2 * math.pi * k / n), cy + r * math.sin(2 * math.pi * k / n)) + for k in range(n)] + + +def panel_title(x, y, number, title): + text(x, y, "{} {}".format(number, title), size=22, weight="bold", halo=False) + + +def clip_group(motifs, rings, color, width): + index = EdgeIndex(rings) + for polylines in motifs: + for pl in polylines: + for piece in clip_polyline(pl, index): + polyline(piece, color, width) + + +# -------------------------------------------------------------------------- +# 1. Un motif : periode, bras, barbes, epaisseur +# -------------------------------------------------------------------------- + +def panel_motif(): + panel_title(30, 44, "1", T["title1"]) + P = 250.0 + c = (270.0, 290.0) + n = (c[0] + P, c[1]) + arm, barb = ARM * P, BARB * P + stroke = 0.05 * P # 0,5 pour une periode de 10 + + # Voisins : reseau triangulaire (vecteurs a 0 et 60 degres, longueur P), + # limites au cadre du panneau + out.append('' + ''.format(fmt(515 - 22 * FS * 2))) + for i, j in ((1, 0), (0, -1), (1, -1), (0, 1), (-1, 1), (-1, 0)): + a1, a2 = unit(0), unit(60) + x = c[0] + (i * a1[0] + j * a2[0]) * P + y = c[1] + (i * a1[1] + j * a2[1]) * P + motif(motif_polylines(x, y, arm, barb), GHOST, stroke) + out.append('') + motif(motif_polylines(c[0], c[1], arm, barb), INK, stroke) + + tip = (c[0] + arm, c[1]) + # Centres + for point in (c, n): + out.append(''.format( + fmt(point[0]), fmt(point[1]), DIM2)) + text(c[0] + 14, c[1] + 26 * FS, T["center"], size=15, color=DIM2, weight="bold") + text(n[0] + 12, n[1] + 30 * FS, T["neighbor"], size=15, color=DIM2, weight="bold") + + # Periode, bras, ecart + dimension(c, n, 150, T["period"], color=DIM2) + dimension(c, tip, -62, T["arm"]) + dimension(tip, n, -62, "P − a", color=DIM2, label_shift=40, size=15) + + # Barbe : du bout du bras vers l'arriere, a 120 degres + b_end = (tip[0] + barb * unit(-120)[0], tip[1] + barb * unit(-120)[1]) + dimension(tip, b_end, -26, T["barb"], label_shift=30, label_pos=0.3) + + # Angle entre deux bras + arc(c, 58, 120, 240, DIM, "120°", label_radius=92) + + # Epaisseur du trait, en travers du bras 240 degres + u = unit(240) + mid = (c[0] + 0.55 * arm * u[0], c[1] + 0.55 * arm * u[1]) + nrm = (-u[1], u[0]) + p = (mid[0] - stroke / 2 * nrm[0], mid[1] - stroke / 2 * nrm[1]) + q = (mid[0] + stroke / 2 * nrm[0], mid[1] + stroke / 2 * nrm[1]) + for base, sign in ((p, -1), (q, 1)): + start = (base[0] + sign * 30 * nrm[0], base[1] + sign * 30 * nrm[1]) + polyline([start, base], DIM, 1.6) + arrow_head(base, (-sign * nrm[0], -sign * nrm[1]), DIM, 8) + text(q[0] + 36 * nrm[0] - 4, q[1] + 36 * nrm[1] + 16 * FS, T["stroke"], + size=17, color=DIM, anchor="end", weight="bold") + + note(30, 612, T["note1"]) + + +# -------------------------------------------------------------------------- +# 2. Rotation +# -------------------------------------------------------------------------- + +def panel_rotation(): + x0 = 740 + panel_title(x0, 44, "2", T["title2"]) + r = 200.0 + cx, cy = x0 + 310, 290.0 + theta = 20.0 + P = 70.0 + ring = [circle_ring(cx, cy, r)] + motifs = fill_pattern(ring, P, ARM, BARB, angle=theta, origin=(cx, cy)) + clip_group(motifs, ring, GHOST, 3.5) + motif(motif_polylines(cx, cy, ARM * P, BARB * P, theta), INK, 3.5) + out.append('' + .format(fmt(cx), fmt(cy), fmt(r), EDGE)) + # Reference horizontale et direction du bras 0 + polyline([(cx, cy), (cx + r + 40, cy)], DIM2, 1.6, 'stroke-dasharray="6,4"') + text(cx + r + 44, cy + 6 * FS, "0°", size=16, color=DIM2, weight="bold") + u = unit(theta) + polyline([(cx, cy), (cx + (r + 40) * u[0], cy + (r + 40) * u[1])], DIM, 1.6, + 'stroke-dasharray="6,4"') + arc((cx, cy), 150, 0, theta, DIM, "θ", label_radius=178, size=20) + note(x0, 612, T["note2"]) + + +# -------------------------------------------------------------------------- +# 3. Bord de la forme : decoupe ou motifs entiers + marge +# -------------------------------------------------------------------------- + +def panel_edge(): + y0 = 670 + panel_title(30, y0, "3", T["title3"]) + P = 40.0 + r = 118.0 + cy = y0 + 162 + stroke = 3.0 + + # Decoupe au contour + cx = 180.0 + ring = [circle_ring(cx, cy, r)] + clip_group(fill_pattern(ring, P, ARM, BARB, origin=(cx, cy)), ring, INK, stroke) + out.append('' + .format(fmt(cx), fmt(cy), fmt(r), EDGE)) + text(cx, cy + r + 30 * FS, T["clip"], size=17, anchor="middle", + weight="bold", halo=False) + + # Motifs entiers + marge + cx = 510.0 + margin = 14.0 + ring = [circle_ring(cx, cy, r)] + motifs = fill_pattern(ring, P, ARM, BARB, mode="whole", + clearance=margin + stroke / 2, origin=(cx, cy)) + for polylines in motifs: + motif(polylines, INK, stroke) + out.append('' + .format(fmt(cx), fmt(cy), fmt(r), EDGE)) + out.append(''.format(fmt(cx), fmt(cy), fmt(r - margin), DIM)) + u = unit(35) + dimension((cx + (r - margin) * u[0], cy + (r - margin) * u[1]), + (cx + r * u[0], cy + r * u[1]), 0, "", color=DIM) + text(cx + (r + 12) * u[0], cy + (r + 12) * u[1] - 4, T["margin"], size=17, + color=DIM, weight="bold") + text(cx, cy + r + 30 * FS, T["whole"], size=17, anchor="middle", + weight="bold", halo=False) + note(30, H - 28, T["note3"]) + + +# -------------------------------------------------------------------------- +# 4. Ancrage du reseau +# -------------------------------------------------------------------------- + +def panel_anchor(): + x0, y0 = 740, 670 + panel_title(x0, y0, "4", T["title4"]) + P = 40.0 + stroke = 3.0 + w, h = 150.0, 220.0 + top = y0 + 45 + + def pair(left, anchor_shape): + shapes = [[[(left, top), (left + w, top), (left + w, top + h), (left, top + h)]], + [[(left + w, top), (left + 2 * w, top), (left + 2 * w, top + h), + (left + w, top + h)]]] + for ring in shapes: + (xa, ya), _, (xb, yb), _ = ring[0] + origin = ((xa + xb) / 2, (ya + yb) / 2) if anchor_shape else (x0, y0) + clip_group(fill_pattern(ring, P, ARM, BARB, origin=origin), ring, INK, stroke) + out.append(''.format( + fmt(xa), fmt(ya), fmt(w), fmt(h), EDGE)) + if anchor_shape: + out.append(''.format( + fmt(origin[0]), fmt(origin[1]), DIM2)) + + pair(x0 + 15, False) + pair(x0 + 335, True) + text(x0 + 15 + w, top + h + 30 * FS, T["document"], size=17, anchor="middle", + weight="bold", halo=False) + text(x0 + 335 + w, top + h + 30 * FS, T["shape"], size=17, + anchor="middle", weight="bold", halo=False) + note(x0, H - 28, T["note4"]) + + +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(language): + global FS + version = VERSIONS[language] + out.clear() + T.clear() + T.update(version["text"]) + FS = version["font_scale"] + + out.append(''.format(W, H)) + for x1, y1, x2, y2 in ((700, 20, 700, H - 20), (20, 640, W - 20, 640)): + out.append(''.format(x1, y1, x2, y2)) + panel_motif() + panel_rotation() + panel_edge() + panel_anchor() + svg = ('\n' + '\n{2}\n\n').format(W, H, "\n".join(out)) + with open(version["svg"], "w", encoding="utf-8", newline="\n") as handle: + handle.write(svg) + print(version["svg"]) + export_png(version["svg"], version["png"], version["png_width"]) + + +def main(argv): + languages = argv or sorted(VERSIONS) + for language in languages: + build(language) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/i18n.py b/i18n.py new file mode 100644 index 0000000..bb37df2 --- /dev/null +++ b/i18n.py @@ -0,0 +1,283 @@ +#!/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/yarrowpattern.pot depuis le .inx et le .py + python i18n.py update # reporte le modele dans po/.po + python i18n.py compile # po/.po -> locale//LC_MESSAGES/*.mo + +Les textes source (.inx et appels _() du .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) : +, attributs gui-text et gui-description, contenu de