inkscape.motif.relief/relief_fill.py
2026-10-19 20:51:52 +02:00

221 lines
8.4 KiB
Python

#!/usr/bin/env python3
# coding=utf-8
"""
Extension Inkscape : remplit l'interieur des formes selectionnees avec un
motif isometrique de « Y » en relief.
Chaque forme (chemin, rectangle, cercle... ; les groupes sont parcourus) est
aplatie en polygone, puis le motif, calcule par `relief_core`, est decoupe a
l'interieur de ce polygone (trous compris). Le resultat est un groupe place
juste au-dessus de la forme, avec un seul chemin par forme.
La logique vit dans `relief_core`, sans dependance a inkex.
"""
import os
import sys
import inkex
from inkex.bezier import cspsubdiv
from inkex.localization import localize
from inkex.units import convert_unit
# L'extension vit dans son propre sous-dossier des extensions Inkscape, qui
# n'est pas forcement dans sys.path : on l'y ajoute pour trouver relief_core.
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from relief_core import (fill_rings, parse_color, polylines_to_d, # noqa: E402
rings_bbox, segments_to_polylines)
def use_system_language():
"""Renseigne LANGUAGE avec la langue du systeme si rien ne la fixe.
Sous Windows, Inkscape regle sur « langue du systeme » ne transmet aucune
variable de langue au script : gettext retomberait sur l'anglais alors que
la boite de dialogue, traduite par Inkscape lui-meme, est dans la langue
de l'utilisateur.
"""
if any(os.environ.get(name) for name in ("LANGUAGE", "LC_ALL", "LC_MESSAGES", "LANG")):
return
try:
import ctypes
import locale
language = locale.windows_locale.get(
ctypes.windll.kernel32.GetUserDefaultUILanguage())
except Exception: # pylint: disable=broad-except
return # hors Windows : LANG est toujours defini
if language:
os.environ["LANGUAGE"] = language
# Installe _() dans les builtins : catalogue « relief_fill » de la langue
# de l'interface (Inkscape transmet domaine et dossier par l'environnement),
# textes anglais d'origine sinon.
use_system_language()
localize()
def uu_per_mm(svg):
"""Unites utilisateur par millimetre, comme Inkscape les affiche.
svg.unittouu() devine l'unite du document par arrondi a l'unite connue la
plus proche : il se trompe des que l'echelle n'est pas standard (page
redimensionnee). On prend le rapport reel page / viewBox ; Inkscape
applique une echelle uniforme (preserveAspectRatio « meet »), soit la
plus petite des deux.
"""
try:
vb_w, vb_h = svg.viewbox_width, svg.viewbox_height
page_w = svg.to_dimensional(svg.viewport_width, "mm")
page_h = svg.to_dimensional(svg.viewport_height, "mm")
except AttributeError: # inkex anterieur a 1.2
return svg.unittouu("1mm")
if not (vb_w and vb_h and page_w and page_h):
return svg.unittouu("1mm")
return 1 / min(page_w / vb_w, page_h / vb_h)
def length_to_uu(svg, value, unit="mm"):
"""Longueur reelle (valeur + unite) -> unites utilisateur du document."""
return convert_unit("{}{}".format(value, unit), "mm") * uu_per_mm(svg)
# Formes dont on sait lire le contour ; les groupes sont parcourus.
SHAPE_TYPES = tuple(
getattr(inkex, name) for name in
("PathElement", "Rectangle", "Circle", "Ellipse", "Polygon", "Polyline", "Line")
if hasattr(inkex, name))
def shape_rings(elem, flatness):
"""Contour de `elem` en coordonnees absolues : liste d'anneaux de points.
Les courbes sont aplaties a la tolerance `flatness` (unites utilisateur).
Un sous-chemin ouvert est traite comme ferme, comme le fait un remplissage.
"""
path = elem.path.transform(elem.composed_transform())
csp = path.to_superpath()
cspsubdiv(csp, flatness)
rings = []
for sub in csp:
ring = [(node[1][0], node[1][1]) for node in sub]
if len(ring) > 1 and ring[0] == ring[-1]:
ring.pop()
if len(ring) >= 3:
rings.append(ring)
return rings
class ReliefFill(inkex.EffectExtension):
"""Remplit chaque forme selectionnee avec le motif de Y en relief."""
def add_arguments(self, pars):
# Un argument par <param> du .inx, memes noms et memes valeurs par defaut.
pars.add_argument("--tab", default="pattern")
pars.add_argument("--edge", type=float, default=5.0)
pars.add_argument("--unit", default="mm")
pars.add_argument("--angle", type=float, default=0.0)
pars.add_argument("--origin", default="shape")
pars.add_argument("--stroke_color", default="255")
pars.add_argument("--stroke_width", type=float, default=0.3)
pars.add_argument("--draw_outline", type=inkex.Boolean, default=False)
pars.add_argument("--keep_original", type=inkex.Boolean, default=True)
def effect(self):
opt = self.options
elements = list(self.svg.selection.values())
if not elements:
inkex.errormsg(_("Select at least one shape."))
return
edge = length_to_uu(self.svg, opt.edge, opt.unit)
if edge <= 0:
inkex.errormsg(_("The edge length must be strictly positive."))
return
width = length_to_uu(self.svg, opt.stroke_width, opt.unit)
if width < 0:
inkex.errormsg(_("The stroke width cannot be negative."))
return
color, opacity = parse_color(opt.stroke_color, default=("#000000", 1.0))
style = inkex.Style({
"fill": "none",
"stroke": color,
"stroke-opacity": str(opacity),
"stroke-width": str(width),
"stroke-linecap": "round",
"stroke-linejoin": "round",
})
flatness = length_to_uu(self.svg, 0.02) # 0.02 mm : invisible a l'impression
for elem in elements:
shapes = [e for e in elem.descendants() if isinstance(e, SHAPE_TYPES)]
if not shapes:
inkex.errormsg(_("The selection contains no shape with an outline "
"(path, rectangle, circle...). Convert text to "
"paths first."))
continue
if not self.process(elem, shapes, edge, style, flatness):
return
def process(self, elem, shapes, edge, style, flatness):
"""Remplit les `shapes` de `elem` ; renvoie False en cas d'erreur fatale."""
opt = self.options
results = [] # (anneaux, segments) par forme
for shape in shapes:
rings = shape_rings(shape, flatness)
if not rings:
continue
if opt.origin == "document":
anchor = (0.0, 0.0)
else:
xmin, ymin, xmax, ymax = rings_bbox(rings)
anchor = ((xmin + xmax) / 2.0, (ymin + ymax) / 2.0)
try:
segments = fill_rings(rings, edge, opt.angle, anchor)
except OverflowError:
inkex.errormsg(_("The pattern would have too many lines. "
"Increase the edge length or select a smaller shape."))
return False
results.append((rings, segments))
if not any(segments for _rings, segments in results):
inkex.errormsg(_("The edge length is too large for this shape: no line "
"fits inside. Reduce the edge length."))
return True
parent = elem.getparent()
group = inkex.Group()
group.label = _("Relief fill")
if parent is not None:
# Resultat calcule en coordonnees absolues du document : on
# neutralise la transformation heritee du parent d'accueil.
parent_transform = parent.composed_transform()
if parent_transform != inkex.Transform():
group.transform = -parent_transform
parent.insert(parent.index(elem) + 1, group)
else:
self.svg.add(group)
for rings, segments in results:
if opt.draw_outline:
outline = inkex.PathElement()
outline.path = polylines_to_d([ring + [ring[0]] for ring in rings])
outline.style = style
group.add(outline)
if segments:
node = inkex.PathElement()
node.path = polylines_to_d(segments_to_polylines(segments), precision=3)
node.style = style
group.add(node)
if not opt.keep_original:
elem.delete()
return True
if __name__ == "__main__":
ReliefFill().run()