#!/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 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 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) 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 « wavyribbonfill » 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) # 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 = length_to_uu(self.svg, 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(length_to_uu(self.svg, 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()