inkscape.Ypatern/y_pattern.py
2026-10-10 10:40:49 +02:00

243 lines
9.0 KiB
Python

#!/usr/bin/env python3
# coding=utf-8
"""
Extension Inkscape : remplit une forme fermee avec le motif « Y fleche ».
Reseau triangulaire de Y identiques dont chaque bras se termine par un
chevron ; la pointe de chaque fleche se loge dans le V arriere du Y voisin.
Les motifs sont soit decoupes au contour de la forme, soit conserves entiers
uniquement s'ils tiennent a l'interieur.
La geometrie vit dans `pattern_core`, sans dependance a inkex.
"""
import math
import os
import sys
import inkex
from inkex import bezier
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 pattern_core.
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from pattern_core import (fill_pattern, parse_color, polylines_to_d, # noqa: E402
rings_bbox)
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 « yarrowpattern » 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)
def flatten_path(path, flatness):
"""Convertit un inkex.Path en liste d'anneaux (listes de points)."""
csp = path.to_superpath()
bezier.cspsubdiv(csp, flatness)
rings = []
for subpath in csp:
ring = [(float(knot[1][0]), float(knot[1][1])) for knot in subpath]
# Un contour ferme repete son premier point : on le retire, la
# fermeture etant implicite dans pattern_core.
if (len(ring) > 1
and math.isclose(ring[0][0], ring[-1][0], abs_tol=1e-9)
and math.isclose(ring[0][1], ring[-1][1], abs_tol=1e-9)):
ring.pop()
if len(ring) >= 3:
rings.append(ring)
return rings
class YArrowPattern(inkex.EffectExtension):
"""Remplit chaque forme selectionnee avec le motif Y fleche."""
def add_arguments(self, pars):
pars.add_argument("--tab", default="motif")
pars.add_argument("--period", type=float, default=10.0)
pars.add_argument("--arm", type=float, default=83.0)
pars.add_argument("--barb", type=float, default=33.0)
pars.add_argument("--angle", type=float, default=0.0)
pars.add_argument("--stroke_width", type=float, default=0.5)
pars.add_argument("--stroke_color", default="3014898687")
pars.add_argument("--unit", default="mm")
pars.add_argument("--mode", default="clip")
pars.add_argument("--margin", type=float, default=0.0)
pars.add_argument("--anchor", default="document")
pars.add_argument("--flatness", type=float, default=0.2)
pars.add_argument("--keep_original", type=inkex.Boolean, default=True)
def effect(self):
opt = self.options
shapes = []
for elem in self.svg.selection.values():
if isinstance(elem, (inkex.Group, inkex.Layer)):
# Un groupe selectionne est explore pour en extraire les formes.
shapes.extend(child for child in elem.descendants()
if self.is_fillable(child))
elif self.is_fillable(elem):
shapes.append(elem)
if not shapes:
inkex.errormsg(_("Select at least one closed shape "
"(path, rectangle, circle, ellipse, polygon)."))
return
def to_uu(value):
return length_to_uu(self.svg, value, opt.unit)
period = to_uu(opt.period)
stroke_width = to_uu(opt.stroke_width)
margin = to_uu(opt.margin)
if period <= 0:
inkex.errormsg(_("The pattern period must be strictly positive."))
return
if not 0 < opt.arm < 100:
inkex.errormsg(_("The arm length must be between 0 and 100 % "
"of the period."))
return
color, opacity = parse_color(opt.stroke_color)
style = inkex.Style({
"fill": "none",
"stroke": color,
"stroke-opacity": str(opacity),
"stroke-width": str(stroke_width),
"stroke-linecap": "butt",
"stroke-linejoin": "miter",
"stroke-miterlimit": "4",
})
total = 0
for elem in shapes:
total += self.fill_shape(elem, period, stroke_width, margin, style)
if total == 0:
if opt.mode == "whole":
inkex.errormsg(_("No whole pattern fits inside the shape. "
"Reduce the period or the margin, or switch "
"to \"Clip patterns at the outline\"."))
else:
inkex.errormsg(_("No pattern could be placed: the shape is "
"probably smaller than the period."))
@staticmethod
def is_fillable(elem):
"""Une forme dont on peut extraire un contour ferme exploitable."""
return (isinstance(elem, inkex.ShapeElement)
and not isinstance(elem, (inkex.Group, inkex.Layer,
inkex.Line, inkex.Polyline)))
def fill_shape(self, elem, period, stroke_width, margin, style):
"""Genere le groupe de motifs d'une forme. Renvoie leur nombre."""
opt = self.options
try:
path = elem.path.transform(elem.composed_transform())
except (AttributeError, TypeError):
return 0
rings = flatten_path(path, opt.flatness)
if not rings:
return 0
if opt.anchor == "shape":
xmin, ymin, xmax, ymax = rings_bbox(rings)
origin = ((xmin + xmax) / 2.0, (ymin + ymax) / 2.0)
else:
origin = (0.0, 0.0)
motifs = fill_pattern(
rings, period,
arm_ratio=opt.arm / 100.0,
barb_ratio=opt.barb / 100.0,
angle=opt.angle,
mode=opt.mode,
# En mode « motifs entiers », le trait ne doit pas deborder :
# on ajoute sa demi-epaisseur a la marge demandee.
clearance=margin + stroke_width / 2.0,
origin=origin)
if not motifs:
return 0
parent = elem.getparent()
group = inkex.Group()
group.label = _("Y arrow pattern")
if parent is not None:
# Les motifs sont calcules 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)
# Un chemin par motif, toutes ses polylignes en sous-chemins : peu
# d'objets dans le document, ordre serpentin conserve pour la decoupe.
for polylines in motifs:
node = inkex.PathElement()
node.set("d", polylines_to_d(polylines))
node.style = style
group.add(node)
if not opt.keep_original:
elem.delete()
return len(motifs)
if __name__ == "__main__":
YArrowPattern().run()