#!/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 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()