163 lines
6.5 KiB
Python
163 lines
6.5 KiB
Python
#!/usr/bin/env python3
|
|
# coding=utf-8
|
|
"""
|
|
Extension Inkscape : remplit les formes selectionnees d'un motif de rubans ondules.
|
|
|
|
Le motif est un fond de traits verticaux sur lequel passent des rubans en S,
|
|
ranges en quinconce et alternativement inclines a droite et a gauche (effet de
|
|
tressage). Il est decoupe par le contour de chaque forme, trous compris.
|
|
|
|
La logique vit dans `wavy_ribbon_fill_core`, sans dependance a inkex.
|
|
"""
|
|
|
|
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 wavy_ribbon_fill_core.
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
from wavy_ribbon_fill_core import ( # noqa: E402
|
|
fill_polylines, parse_color, polylines_to_d, rings_bounds)
|
|
|
|
# Installe _() dans les builtins : catalogue « wavyribbonfill » de la langue
|
|
# de l'interface (Inkscape transmet domaine et dossier par l'environnement),
|
|
# textes anglais d'origine sinon.
|
|
localize()
|
|
|
|
# Au-dela, le calcul et le SVG produit deviennent trop lourds pour Inkscape.
|
|
MAX_LINES = 3000
|
|
|
|
|
|
class WavyRibbonFill(inkex.EffectExtension):
|
|
"""Pose un groupe « motif » au-dessus de chaque element selectionne."""
|
|
|
|
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("--spacing", type=float, default=2.0)
|
|
pars.add_argument("--unit", default="mm")
|
|
pars.add_argument("--lines", type=int, default=5)
|
|
pars.add_argument("--shift", type=int, default=6)
|
|
pars.add_argument("--height", type=float, default=7.0)
|
|
pars.add_argument("--radius", type=float, default=2.8)
|
|
pars.add_argument("--stagger", type=float, default=1.0)
|
|
pars.add_argument("--columns", type=int, default=8)
|
|
pars.add_argument("--rows", type=float, default=7.75)
|
|
pars.add_argument("--angle", type=float, default=0.0)
|
|
pars.add_argument("--origin", default="shape")
|
|
pars.add_argument("--stroke_width", type=float, default=0.25)
|
|
pars.add_argument("--stroke_color", default="255")
|
|
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
|
|
|
|
spacing = self.svg.unittouu("{}{}".format(opt.spacing, opt.unit))
|
|
if spacing <= 0:
|
|
inkex.errormsg(_("The line spacing must be strictly positive."))
|
|
return
|
|
|
|
color, opacity = parse_color(opt.stroke_color, ("#000000", 1.0))
|
|
style = inkex.Style({
|
|
"fill": "none",
|
|
"stroke": color,
|
|
"stroke-opacity": str(opacity),
|
|
"stroke-width": str(self.svg.unittouu("{}{}".format(opt.stroke_width, opt.unit))),
|
|
"stroke-linecap": "round",
|
|
"stroke-linejoin": "round",
|
|
})
|
|
|
|
for elem in elements:
|
|
self.process(elem, spacing, style)
|
|
|
|
def element_rings(self, elem, flatness):
|
|
"""Contours de l'element (ou des formes d'un groupe) en coordonnees
|
|
absolues du document, courbes aplaties."""
|
|
if isinstance(elem, inkex.Group):
|
|
shapes = [node for node in elem.descendants()
|
|
if isinstance(node, inkex.ShapeElement) and not isinstance(node, inkex.Group)]
|
|
else:
|
|
shapes = [elem]
|
|
rings = []
|
|
for shape in shapes:
|
|
if not isinstance(shape, inkex.ShapeElement) or isinstance(
|
|
shape, (inkex.TextElement, inkex.Image, inkex.Use)):
|
|
continue
|
|
path = shape.path.transform(shape.composed_transform())
|
|
superpath = path.to_superpath()
|
|
bezier.cspsubdiv(superpath, flatness)
|
|
for subpath in superpath:
|
|
ring = [(node[1][0], node[1][1]) for node in subpath]
|
|
if len(ring) >= 3:
|
|
rings.append(ring)
|
|
return rings
|
|
|
|
def process(self, elem, spacing, style):
|
|
"""Traite un element : calcul dans wavy_ribbon_fill_core, ecriture dans le SVG."""
|
|
opt = self.options
|
|
name = elem.label or elem.get_id()
|
|
|
|
rings = self.element_rings(elem, spacing / 20.0)
|
|
bounds = rings_bounds(rings)
|
|
if bounds is None:
|
|
inkex.errormsg(_("“{}” has no usable outline: select closed shapes or paths "
|
|
"(convert texts with Path > Object to Path).").format(name))
|
|
return
|
|
|
|
xmin, ymin, xmax, ymax = bounds
|
|
if max(xmax - xmin, ymax - ymin) / spacing > MAX_LINES:
|
|
inkex.errormsg(_("“{}” would need more than {} lines: increase the line "
|
|
"spacing.").format(name, MAX_LINES))
|
|
return
|
|
|
|
# Origine du motif : centre de la forme (motif centre), ou origine du
|
|
# document (motif continu d'une forme a l'autre).
|
|
origin = (0.0, 0.0)
|
|
if opt.origin == "shape":
|
|
origin = ((xmin + xmax) / 2.0, (ymin + ymax) / 2.0)
|
|
|
|
polylines = fill_polylines(
|
|
rings, spacing, lines=max(2, opt.lines), shift=max(1, opt.shift),
|
|
height=opt.height, radius=max(0.0, opt.radius), stagger=max(0.0, opt.stagger),
|
|
columns=max(1, opt.columns), rows=opt.rows, angle=opt.angle, origin=origin)
|
|
if not polylines:
|
|
inkex.errormsg(_("“{}” is too small for the pattern: reduce the line "
|
|
"spacing.").format(name))
|
|
return
|
|
|
|
parent = elem.getparent()
|
|
group = inkex.Group()
|
|
group.label = _("Wavy ribbon pattern")
|
|
if parent is not None:
|
|
# Motif 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)
|
|
|
|
# Un seul chemin : une polyligne par sous-chemin.
|
|
node = inkex.PathElement()
|
|
node.set("d", polylines_to_d(polylines))
|
|
node.style = style
|
|
group.add(node)
|
|
|
|
if not opt.keep_original:
|
|
elem.delete()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
WavyRibbonFill().run()
|