166 lines
6.9 KiB
Python
166 lines
6.9 KiB
Python
#!/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 <param> 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()
|