commit f8152a4ece028c225db40427535d1813b14b352a Author: Alex Date: Thu Oct 1 16:08:38 2026 +0200 Version initiale diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8aa9a2d --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# 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 + +# Page de presentation : hors depot (publiee sur le NAS par site/deploy.ps1 ; +# sa configuration contient hote, compte et chemin de cle SSH) +site/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..ab8a5b3 --- /dev/null +++ b/README.md @@ -0,0 +1,220 @@ +# Remplissage motif rubans ondulés — extension Inkscape + + + + + + +
+Cinq formes (rectangle, cercle, chemin courbe, rectangle troué, ellipse) remplies du motif de rubans ondulés + + +Extension Inkscape 1.x qui remplit les formes sélectionnées d'un motif de +tapisserie : un fond de traits verticaux traversé par des rubans en S, rangés en +quinconce et penchant tour à tour à droite et à gauche, ce qui donne un effet de +tressage. Le motif reproduit le modèle [`doc/patern.jpg`](doc/patern.jpg) ; il est +découpé le long du contour de chaque forme, trous compris, et livré sous la forme +d'un seul chemin sans remplissage, prêt pour la gravure, le traçage ou la découpe. + +
+ +## Sommaire + +- [Fonctionnalités](#fonctionnalités) +- [Installation](#installation) +- [Utilisation](#utilisation) +- [Paramètres](#paramètres) +- [Structure du projet](#structure-du-projet) +- [Développement](#développement) + +## Fonctionnalités + +- Remplit rectangles, cercles, ellipses, polygones, chemins et groupes (toutes les + formes d'un groupe sont remplies ensemble) ; les textes, images et clones sont + ignorés (convertir un texte avec **Chemin > Objet en chemin**). +- Respecte les trous d'un chemin (règle pair-impair) et les transformations des + formes et de leurs groupes parents. +- Motif entièrement réglable : pas des traits, nombre de traits par ruban, + décalage, hauteur et rayon de l'onde, quinconce, rotation. +- Origine du motif au centre de chaque forme, ou à l'origine du document pour un + motif continu d'une forme à l'autre. +- SVG propre : un groupe par forme, contenant un seul chemin ; les traits qui se + suivent sont soudés en tracés continus. +- Rapide : une page A3 au pas de 1 mm (environ 110 000 points) se calcule en + 3 secondes environ. +- Interface traduite en français et en anglais, selon la langue d'Inkscape + (anglais pour toute autre langue). +- Script de déploiement et de désinstallation pour Windows. + +## Installation + +Aucune dépendance externe : l'extension utilise `inkex`, fourni avec Inkscape. + +### Windows + +Fermer Inkscape, puis dans PowerShell, depuis le dossier du projet : + +```powershell +.\deploy.ps1 # installe dans %APPDATA%\inkscape\extensions\wavyRibbonFill +.\deploy.ps1 -Uninstall # désinstalle +.\deploy.ps1 -Force # installe même si Inkscape est ouvert +``` + +### Installation manuelle + +Copier dans un sous-dossier du dossier des extensions utilisateur d'Inkscape : + +- `wavy_ribbon_fill.inx`, `wavy_ribbon_fill.py`, `wavy_ribbon_fill_core.py` ; +- le dossier `images/` (schéma de l'onglet « Aide ») ; +- le dossier `locale/` (traductions compilées). + +| Système | Dossier des extensions | +| ------- | --------------------------------------------------- | +| Windows | `%APPDATA%\inkscape\extensions` | +| Linux | `~/.config/inkscape/extensions` | +| macOS | `~/Library/Application Support/org.inkscape.Inkscape/config/inkscape/extensions` | + +Redémarrer Inkscape : les extensions ne sont lues qu'au démarrage. + +## Utilisation + +1. Sélectionner une ou plusieurs formes fermées. +2. Ouvrir **Extensions > AlexDesign > Remplissage motif rubans ondulés…** + (*Wavy Ribbon Pattern Fill*). +3. Régler le motif (onglet « Motif ») et son rendu (onglet « Remplissage ») ; + cocher « Aperçu en direct » pour voir le résultat. +4. Valider avec **Appliquer**. + +Chaque forme reçoit un groupe « Motif rubans ondulés », placé juste au-dessus +d'elle, qui contient un seul chemin sans remplissage. + +### Conseils + +- **Motif trop dense ou trop lâche** : changer le pas des traits ; tout le motif + suit, les autres réglages étant comptés en pas. +- **Rubans plus larges** : augmenter le nombre de traits par ruban, et le pas des + colonnes d'autant. +- **Ondes plus douces** : augmenter la hauteur d'onde et le rayon de virage (le + rayon est limité à la moitié de la hauteur). +- **Traits coupés entre deux rubans** : les rubans se chevauchent ; augmenter le + pas des colonnes ou le pas des rangées. +- **Motif qui doit se raccorder entre plusieurs formes** : choisir l'origine + « Origine du document ». +- **Message « demanderait plus de 3000 traits »** : augmenter le pas des traits. + +## Paramètres + +![Schéma des paramètres](docs/parametres.png) + +| Paramètre | Défaut | Rôle | +| ---------------------------- | ------- | -------------------------------------------------------------------------------------- | +| Pas des traits | 2 mm | Distance entre deux traits voisins ; unité de toutes les autres dimensions du motif. | +| Unité | mm | Unité du pas et de l'épaisseur du trait (mm, cm, px, pt, in). | +| Traits par ruban | 5 | Nombre de traits d'un ruban ; sa largeur vaut (traits − 1) pas. | +| Décalage du ruban (pas) | 6 | Déplacement latéral d'un ruban le long de son onde (nombre entier de pas). | +| Hauteur d'onde (pas) | 7 | Hauteur sur laquelle un trait passe de sa position basse à sa position haute. | +| Rayon de virage (pas) | 2,8 | Rayon des deux virages d'une onde, limité à la moitié de la hauteur ; 0 = traits droits. | +| Décalage des traits (pas) | 1 | Décalage vertical d'un trait du ruban au suivant ; 0 resserre le ruban dans l'oblique. | +| Pas des colonnes (pas) | 8 | Décalage latéral d'une rangée de rubans à la suivante ; période = 2 pas de colonne. | +| Pas des rangées (pas) | 7,75 | Distance verticale d'une rangée à la suivante, qui penche de l'autre côté. | +| Rotation du motif (°) | 0 | Tourne le motif autour de son origine, sens horaire. | +| Origine du motif | Centre de chaque forme | Ou origine du document : motif continu d'une forme à l'autre. | +| Épaisseur du trait | 0,25 | Dans l'unité choisie. | +| Couleur du trait | noir | Couleur et opacité du chemin produit. | +| Conserver la forme d'origine | oui | Décocher pour ne garder que le motif. | + +Les valeurs par défaut reproduisent les proportions du modèle `doc/patern.jpg`. + +## Structure du projet + +```text +wavy_ribbon_fill.inx Boîte de dialogue (textes source en anglais) +wavy_ribbon_fill.py Couche inkex : sélection, unités, écriture SVG +wavy_ribbon_fill_core.py Calcul du motif, sans inkex +i18n.py Chaîne de traduction (extract / update / compile) +po/ Modèle .pot et catalogues en.po, fr.po +locale/ Catalogues compilés .mo (déployés) +images/parameters_en.png Schéma de l'onglet « Aide » (déployé) +doc/patern.jpg Modèle du motif +docs/schema_parametres.py Générateur du schéma (fr pour le README, en pour le dialogue) +docs/parametres.png Schéma du README +docs/exemple.png Exemple de résultat (README) +tests/data/shapes.svg Formes d'exemple pour les tests de bout en bout +test_wavy_ribbon_fill.py Tests pytest +deploy.ps1 Déploiement / désinstallation Windows +``` + +### API du noyau (`wavy_ribbon_fill_core.py`) + +Les contours sont des listes d'anneaux (listes de points, trous en pair-impair), +dans le repère SVG (y vers le bas). `shift`, `height`, `radius`, `stagger`, +`columns` et `rows` sont exprimés en nombre de pas. + +| Fonction | Rôle | +| --------------------------------------------------------------- | -------------------------------------------------------------------- | +| `fill_polylines(rings, spacing, lines, shift, height, radius, stagger, columns, rows, angle, origin)` | Motif découpé par le contour, rotation comprise : liste de polylignes. | +| `RibbonPattern(spacing, …, origin)` | Géométrie du motif pour un jeu de réglages. | +| `RibbonPattern.fill(rings)` | Polylignes du motif (traits du fond et rubans) dans le contour. | +| `RibbonPattern.pieces(bounds)` | Rubans dont l'onde touche une boîte englobante. | +| `RibbonPattern.piece_curves(piece)` | Les traits d'un ruban, du bas vers le haut. | +| `RibbonPattern.piece_region(piece, inset)` | Polygone couvert par l'onde d'un ruban. | +| `RibbonPattern.hidden_interval(piece, index)` | Hauteur sur laquelle un trait du fond passe sous un ruban. | +| `wave_profile(shift, height, radius, tolerance)` | Profil en S d'un trait : arc, droite, arc. | +| `EdgeIndex(rings)` : `inside(x, y)`, `crossings(p, q)`, `clip(polyline, keep_inside)` | Découpe de polylignes par un contour. | +| `join_polylines(polylines)` | Soude les polylignes bout à bout et retire les points alignés. | +| `subtract_intervals(intervals, holes)` | Différence d'intervalles (traits du fond masqués). | +| `rotate_points(points, angle, center)`, `rings_bounds(rings)` | Rotation (sens horaire à l'écran) et boîte englobante. | +| `parse_color(value)`, `polylines_to_d(polylines)` | Couleur Inkscape → CSS et opacité ; polylignes → données `d`. | + +## Développement + +```powershell +python -m venv .venv +.venv\Scripts\Activate.ps1 +pip install pytest +python -m pytest -q +``` + +Sans `inkex`, les tests de bout en bout sont ignorés : **31 réussis, 9 ignorés**. +Avec `inkex` (celui d'Inkscape), **40 réussis** : + +```powershell +pip install lxml tinycss2 cssselect2 cssselect +$env:PYTHONPATH = 'C:\Program Files\Inkscape\share\inkscape\extensions' +python -m pytest -q +``` + +Les tests couvrent le noyau (profil de l'onde, découpe, trous, périodicité, +miroir, rotation, chevauchement des rubans, entrées vides ou invalides), +l'extension de bout en bout (formes courbes, trous, groupe transformé, unités, +couleur) et la cohérence entre le `.inx`, les arguments Python, le schéma et les +traductions. + +### Traductions + +Les textes source (`.inx` et appels `_()` des `.py`) sont en anglais. + +```powershell +python i18n.py # extract + update + compile ; liste les chaînes à traduire +python i18n.py extract # po/wavyribbonfill.pot +python i18n.py update # reporte le modèle dans po/en.po et po/fr.po +python i18n.py compile # locale//LC_MESSAGES/wavyribbonfill.mo +``` + +Après toute modification d'un texte : lancer `python i18n.py`, compléter les +`msgstr` vides de `po/fr.po`, relancer `python i18n.py` (`en.po` se remplit tout +seul). Ne jamais modifier les `.mo`. Pour ajouter une langue, l'ajouter à +`LANGUAGES` dans `i18n.py` et traduire le nouveau `po/.po`. + +### Schéma des paramètres + +```powershell +python docs/schema_parametres.py # les deux versions +python docs/schema_parametres.py fr # docs/parametres.png (1400 px, README) +python docs/schema_parametres.py en # images/parameters_en.png (900 px, onglet « Aide ») +``` + +Le schéma est dessiné avec les fonctions du noyau, il reste donc fidèle au rendu +réel. L'export PNG passe par `inkscape.com`. La version anglaise sert à la boîte +de dialogue, Inkscape ne traduisant pas le chemin d'une image ; elle y est +affichée aux 3/4 de sa taille (675 × 617). diff --git a/deploy.ps1 b/deploy.ps1 new file mode 100644 index 0000000..d003b27 --- /dev/null +++ b/deploy.ps1 @@ -0,0 +1,107 @@ +<# +.SYNOPSIS + Déploie l'extension « Remplissage motif rubans ondulés » dans Inkscape. + +.DESCRIPTION + Copie wavy_ribbon_fill.inx, wavy_ribbon_fill.py, wavy_ribbon_fill_core.py, le schéma de l'onglet + « Help » (images\parameters_en.png) et les traductions + compilées (locale\\LC_MESSAGES\wavyribbonfill.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 : wavyRibbonFill + +.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 'autre-nom' +#> + +[CmdletBinding()] +param( + [string] $ExtensionsDir = (Join-Path $env:APPDATA 'inkscape\extensions'), + [string] $FolderName = 'wavyRibbonFill', + [switch] $Uninstall, + [switch] $Force +) + +$ErrorActionPreference = 'Stop' + +$files = @('wavy_ribbon_fill.inx', 'wavy_ribbon_fill.py', 'wavy_ribbon_fill_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 rubans ondulés..." diff --git a/doc/patern.jpg b/doc/patern.jpg new file mode 100644 index 0000000..27ca757 Binary files /dev/null and b/doc/patern.jpg differ diff --git a/docs/exemple.png b/docs/exemple.png new file mode 100644 index 0000000..7c733e7 Binary files /dev/null and b/docs/exemple.png differ diff --git a/docs/parametres.png b/docs/parametres.png new file mode 100644 index 0000000..19b8112 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..0415ef2 --- /dev/null +++ b/docs/schema_parametres.py @@ -0,0 +1,393 @@ +#!/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 + # « Help » de la boite de dialogue) + +Les formes sont calculees par wavy_ribbon_fill_core (memes fonctions que l'extension), +le schema reste donc fidele au resultat reel. L'export PNG passe par +inkscape.com, qui attend la fin de l'export (contrairement au raccourci +Chocolatey « inkscape »). +""" + +import math +import os +import subprocess +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(HERE) +sys.path.insert(0, ROOT) + +import wavy_ribbon_fill_core as core # noqa: E402 + +W, H = 1400, 1280 # 4 panneaux de 700 x 640 + +INK = "#343a40" # element principal +GHOST = "#c9ced4" # elements secondaires / 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 : +# 900 px de large, textes x 1.25 pour rester lisibles). +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": "Traits et ruban", + "spacing": "Pas", + "lines": "5 traits par ruban", + "shift": "Décalage du ruban = 6 pas", + "note1": ["Le ruban glisse sur les traits du fond et retombe dessus :", + "son décalage est un nombre entier de pas."], + "title2": "Forme de l'onde", + "height": "Hauteur d'onde", + "height_value": "7 pas", + "radius": "Rayon de virage", + "radius_value": "2,8 pas", + "stagger": "Décalage", + "stagger_2": "des traits", + "stagger_value": "1 pas", + "note2": ["Chaque trait suit la même onde (virage, droite, virage),", + "décalée d'un pas sur le côté et du décalage vers le bas."], + "title3": "Quinconce des rubans", + "columns": "Pas des colonnes = 8 pas", + "rows": "Pas des rangées", + "rows_value": "7,75 pas", + "note3": ["Une rangée sur deux penche de l'autre côté.", + "Le motif se répète toutes les deux colonnes et deux rangées."], + "title4": "Rotation et origine", + "angle": "Rotation 30°", + "origin": "Origine", + "note4": ["Origine au centre de chaque forme, ou à l'origine du document", + "pour un motif continu d'une forme à l'autre."], + }, + }, + "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": "Lines and ribbon", + "spacing": "Spacing", + "lines": "5 lines per ribbon", + "shift": "Ribbon shift = 6 spacings", + "note1": ["The ribbon slides over the background lines and", + "lands on them: a whole number of spacings."], + "title2": "Wave shape", + "height": "Wave height", + "height_value": "7 spacings", + "radius": "Bend radius", + "radius_value": "2.8 spacings", + "stagger": "Line", + "stagger_2": "stagger", + "stagger_value": "1 spacing", + "note2": ["Every line follows the same wave (bend, straight,", + "bend), moved one spacing sideways and down."], + "title3": "Staggered ribbons", + "columns": "Column step = 8 spacings", + "rows": "Row step", + "rows_value": "7.75 spacings", + "note3": ["Every other row leans the other way. The pattern", + "repeats every two columns and two rows."], + "title4": "Rotation and origin", + "angle": "Rotation 30°", + "origin": "Origin", + "note4": ["Origin at the centre of each shape, or at the document", + "origin for a pattern continuous across shapes."], + }, + }, +} + +def _unit(angle): + """Vecteur unitaire, angle en degres, sens anti-horaire a l'ecran (y vers le bas).""" + a = math.radians(angle) + return math.cos(a), -math.sin(a) + + +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) + + + +# -------------------------------------------------------------------------- +# Panneaux : un par groupe de parametres, numerotes 1 a 4. Le motif est +# calcule par wavy_ribbon_fill_core, puis cote avec dimension() / arc(). +# -------------------------------------------------------------------------- + +def rect_ring(x1, y1, x2, y2): + return [(x1, y1), (x2, y1), (x2, y2), (x1, y2)] + + +def dot(point, color, radius=4.5): + out.append(''.format( + fmt(point[0]), fmt(point[1]), fmt(radius), color)) + + +def guide(p, q, color): + polyline([p, q], color, 1, 'stroke-dasharray="3,3"') + + +def vertical_dimension(x, y1, y2, labels, color, side): + """Cote verticale ; textes a gauche (side = -1) ou a droite (side = 1).""" + polyline([(x, y1), (x, y2)], color, 1.6) + arrow_head((x, y1), (0, -1), color) + arrow_head((x, y2), (0, 1), color) + step = 22 * FS + y = (y1 + y2) / 2.0 - (len(labels) - 1) * step / 2.0 + 6 * FS + for k, label in enumerate(labels): + text(x + side * 12, y + k * step, label, color=color, + anchor="end" if side < 0 else "start", weight="bold") + + +def panel_1(): + panel_title(30, 50, 1, T["title1"]) + s = 30.0 + pattern = core.RibbonPattern(s, origin=(200.0, 315.0)) + window = rect_ring(155, 110, 545, 480) + motif(pattern.fill([window]), GHOST, 2.2) + piece = (0, 0, 0, 315.0 - pattern.region_height / 2, 315.0 + pattern.region_height / 2, False) + curves = pattern.piece_curves(piece) + motif(curves[1:], INK, 2.6) + motif(curves[:1], DIM, 3.2) + dimension((170, 110), (200, 110), -22, T["spacing"], color=DIM2, label_shift=16) + dimension((380, 110), (500, 110), -22, T["lines"], label_shift=16) + dimension((200, 480), (380, 480), 22, T["shift"], label_shift=20) + note(30, 610, T["note1"]) + + +def panel_2(): + ox = 700 + panel_title(ox + 30, 50, 2, T["title2"]) + s = 30.0 + top = 125.0 + pattern = core.RibbonPattern(s, origin=(ox + 225.0, top + 165.0)) + piece = (0, 0, 0, top, top + pattern.region_height, False) + curves = pattern.piece_curves(piece) + left, low = pattern.grid_x(0), top + pattern.height + radius = 2.8 * s + # Cercle du virage bas du premier trait. + out.append(''.format(fmt(left + radius), fmt(low), fmt(radius), DIM2)) + motif(curves[1:], INK, 2.6) + motif(curves[:1], DIM, 3.2) + center = (left + radius, low) + ux, uy = _unit(150) + tip = (center[0] + radius * ux, center[1] + radius * uy) + polyline([center, tip], DIM2, 1.6) + arrow_head(tip, (ux, uy), DIM2) + dot(center, DIM2, 3.5) + text(center[0] + 12, center[1] + 30 * FS, T["radius"], color=DIM2, weight="bold") + text(center[0] + 12, center[1] + 52 * FS, T["radius_value"], color=DIM2, weight="bold") + # Hauteur d'onde du premier trait. + guide((left - 62, top), (pattern.grid_x(6), top), DIM) + guide((left - 62, low), (left, low), DIM) + vertical_dimension(left - 50, top, low, [T["height"], T["height_value"]], DIM, -1) + # Decalage entre les deux derniers traits. + right = pattern.grid_x(10) + y3, y4 = top + 3 * pattern.stagger, top + 4 * pattern.stagger + dot((pattern.grid_x(9), y3), DIM) + dot((right, y4), DIM) + guide((pattern.grid_x(9), y3), (right + 42, y3), DIM) + guide((right, y4), (right + 42, y4), DIM) + vertical_dimension(right + 30, y3, y4, [T["stagger"], T["stagger_2"], T["stagger_value"]], + DIM, 1) + note(ox + 30, 610, T["note2"]) + + +def panel_3(): + oy = 640 + panel_title(30, oy + 50, 3, T["title3"]) + s = 15.0 + pattern = core.RibbonPattern(s, origin=(150.0, oy + 215.0)) + window = rect_ring(60, oy + 95, 640, oy + 500) + motif(pattern.fill([window]), GHOST, 1.6) + half = pattern.region_height / 2 + centers = [] + for row in (0, 1): + y = oy + 215.0 + row * pattern.row_step + piece = (row, 0, row * pattern.columns, y - half, y + half, bool(row)) + motif(pattern.piece_curves(piece), INK, 2.2) + centers.append((pattern.grid_x(piece[2]) + pattern.span * s / 2, y)) + (x0, y0), (x1, y1) = centers + for center in centers: + dot(center, DIM) + guide((x0, y0), (x0, y1 + 100), DIM) + guide((x1, y1), (x1, y1 + 100), DIM) + guide((x0, y0), (x1 + 150, y0), DIM) + guide((x1, y1), (x1 + 150, y1), DIM) + dimension((x0, y1 + 100), (x1, y1 + 100), 0, T["columns"], label_shift=22) + vertical_dimension(x1 + 135, y0, y1, [T["rows"], T["rows_value"]], DIM, 1) + note(30, oy + 610, T["note3"]) + + +def panel_4(): + ox, oy = 700, 640 + panel_title(ox + 30, oy + 50, 4, T["title4"]) + center = (ox + 350.0, oy + 315.0) + shape = [circle_ring(center[0], center[1], 205)] + motif(core.fill_polylines(shape, 13.0, angle=30.0, origin=center), INK, 1.5) + polyline(shape[0] + shape[0][:1], EDGE, 2) + ux, uy = _unit(60) + guide((center[0], center[1]), (center[0], center[1] - 240), DIM2) + polyline([center, (center[0] + 240 * ux, center[1] + 240 * uy)], DIM2, 1.6) + arc(center, 225, 90, 60, DIM2) + text(center[0] + 150, center[1] - 212, T["angle"], color=DIM2, weight="bold") + dot(center, DIM, 6) + text(center[0] + 14, center[1] + 6 * FS, T["origin"], color=DIM, weight="bold") + note(ox + 30, oy + 610, 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_1() + panel_2() + panel_3() + panel_4() + svg = ('\n' + '\n{2}\n\n').format(W, H, "\n".join(out)) + with open(version["svg"], "w", encoding="utf-8", newline="\n") as handle: + handle.write(svg) + print(version["svg"]) + export_png(version["svg"], version["png"], version["png_width"]) + + +def main(argv): + 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..a800d7f --- /dev/null +++ b/i18n.py @@ -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/.pot depuis le .inx et les .py + python i18n.py update # reporte le modele dans po/.po + python i18n.py compile # po/.po -> locale//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) : +, attributs gui-text et gui-description, contenu de