#!/usr/bin/env python3 # coding=utf-8 """ Extension Inkscape : remplit une forme avec un motif de Voronoi separe par un filet de largeur parametrable. Des germes (aleatoires, Poisson ou grille hexagonale bruitee) sont semes sur la forme ; chaque cellule de Voronoi est retrecie de la moitie du filet de chaque cote, puis decoupee par la forme. Le resultat est soit le filet (un seul chemin pair-impair : la forme percee des cellules), soit les cellules. La logique vit dans `voronoi_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 voronoi_core. sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from voronoi_core import ( # noqa: E402 FillError, fill_shape, net_rings, parse_color, rings_to_d) # Installe _() dans les builtins : catalogue « voronoi_fill » de la langue # de l'interface (Inkscape transmet domaine et dossier par l'environnement), # textes anglais d'origine sinon. localize() # Formes acceptees ; les groupes sont explores, le texte doit etre converti. SHAPES = (inkex.PathElement, inkex.Rectangle, inkex.Circle, inkex.Ellipse, inkex.Polygon, inkex.Polyline) class VoronoiFill(inkex.EffectExtension): """Remplit chaque forme selectionnee d'un pavage de Voronoi a filet.""" 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("--distribution", default="poisson") pars.add_argument("--cell_size", type=float, default=10.0) pars.add_argument("--irregularity", type=float, default=30.0) pars.add_argument("--seed", type=int, default=1) pars.add_argument("--unit", default="mm") pars.add_argument("--net_width", type=float, default=1.0) pars.add_argument("--border", type=inkex.Boolean, default=True) pars.add_argument("--corner_radius", type=float, default=0.0) pars.add_argument("--roundness", type=float, default=0.0) pars.add_argument("--result", default="net") pars.add_argument("--net_color", default="255") pars.add_argument("--cell_color", default="3014898687") 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, SHAPES): shapes.append(elem) elif isinstance(elem, inkex.Group): shapes.extend(d for d in elem.descendants() if isinstance(d, SHAPES)) if not shapes: inkex.errormsg(_("Select at least one closed shape (path, rectangle, " "ellipse, polygon). Convert text to a path first.")) return def length(value): return self.svg.unittouu("{}{}".format(value, opt.unit)) cell_size = length(opt.cell_size) net_width = length(opt.net_width) corner_radius = length(opt.corner_radius) if cell_size <= 0: inkex.errormsg(_("The cell size must be strictly positive.")) return if net_width >= cell_size: inkex.errormsg(_("The net width must be smaller than the cell size: " "reduce the net width or enlarge the cells.")) return net_color, net_opacity = parse_color(opt.net_color, ("#000000", 1.0)) cell_color, cell_opacity = parse_color(opt.cell_color) self.net_style = inkex.Style({ "fill": net_color, "fill-opacity": str(net_opacity), "fill-rule": "evenodd", "stroke": "none"}) self.cell_style = inkex.Style({ "fill": cell_color, "fill-opacity": str(cell_opacity), "fill-rule": "evenodd", "stroke": "none"}) # Tolerance d'aplatissement des courbes : fine devant le filet et les # cellules, pour que le cadre garde une largeur reguliere. tolerance = max(min(net_width / 4.0, cell_size / 50.0), cell_size / 2000.0) for elem in shapes: rings = self.flatten(elem, tolerance) try: cells = fill_shape( rings, cell_size, net_width, distribution=opt.distribution, irregularity=opt.irregularity, seed=opt.seed, border=opt.border, corner_radius=corner_radius, roundness=max(0.0, min(opt.roundness, 100.0)) / 100.0, tolerance=tolerance) except FillError as error: if error.code == "too_many_cells": inkex.errormsg(_("Too many cells ({}): increase the cell size " "or fill a smaller shape.").format(error.count)) else: inkex.errormsg(_("Invalid settings: sizes must be positive.")) return if not cells: inkex.errormsg(_("No cell fits in the shape: reduce the cell size, " "the net width or the corner radius.")) continue self.write(elem, rings, cells) @staticmethod def flatten(elem, tolerance): """Contour de l'element en coordonnees absolues, aplati en anneaux.""" path = elem.path.transform(elem.composed_transform()) csp = path.to_superpath() bezier.cspsubdiv(csp, tolerance) return [[(point[1][0], point[1][1]) for point in sub] for sub in csp] def write(self, elem, rings, cells): """Groupe resultat insere juste au-dessus de l'element source.""" opt = self.options parent = elem.getparent() group = inkex.Group() group.label = _("Voronoi fill") if parent is not None: # Resultat 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) if opt.result in ("cells", "both"): for cell in cells: node = inkex.PathElement() node.set("d", rings_to_d(cell)) node.style = self.cell_style node.label = _("Cell") group.add(node) if opt.result in ("net", "both"): node = inkex.PathElement() node.set("d", rings_to_d(net_rings(rings, cells))) node.style = self.net_style node.label = _("Net") group.add(node) if not opt.keep_original: elem.delete() if __name__ == "__main__": VoronoiFill().run()