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