194 lines
7.1 KiB
Python
194 lines
7.1 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
|
|
|
|
# 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)
|
|
|
|
# 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.
|
|
localize()
|
|
|
|
|
|
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 self.svg.unittouu("{}{}".format(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()
|