167 lines
6.9 KiB
Python
167 lines
6.9 KiB
Python
#!/usr/bin/env python3
|
|
# coding=utf-8
|
|
"""
|
|
Extension Inkscape : remplit les formes selectionnees avec un motif de
|
|
decoupe flexible (« living hinge »).
|
|
|
|
Le motif est fait de lumieres oblongues rangees en colonnes decalees en
|
|
quinconce ; une fois decoupe au laser, il rend le panneau souple dans le sens
|
|
perpendiculaire aux lumieres. Pres du bord, les lumieres sont raccourcies et
|
|
re-arrondies pour rester a distance de la marge.
|
|
|
|
La logique vit dans `living_hinge_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 living_hinge_core.
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
from living_hinge_core import hinge_slots, parse_color, slots_to_d # noqa: E402
|
|
|
|
# Installe _() dans les builtins : catalogue « livinghinge » de la langue
|
|
# de l'interface (Inkscape transmet domaine et dossier par l'environnement),
|
|
# textes anglais d'origine sinon.
|
|
localize()
|
|
|
|
# Elements dont le contour peut etre rempli ; textes, images et clones sont ignores.
|
|
SHAPES = (inkex.PathElement, inkex.Rectangle, inkex.Circle, inkex.Ellipse,
|
|
inkex.Polygon, inkex.Polyline)
|
|
|
|
|
|
class LivingHinge(inkex.EffectExtension):
|
|
"""Remplit chaque forme selectionnee avec des lumieres oblongues en quinconce."""
|
|
|
|
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("--slot_length", type=float, default=20.0)
|
|
pars.add_argument("--slot_width", type=float, default=3.0)
|
|
pars.add_argument("--bridge", type=float, default=2.0)
|
|
pars.add_argument("--pitch", type=float, default=5.0)
|
|
pars.add_argument("--unit", default="mm")
|
|
pars.add_argument("--stagger", type=float, default=50.0)
|
|
pars.add_argument("--angle", type=float, default=0.0)
|
|
pars.add_argument("--random_lengths", type=inkex.Boolean, default=False)
|
|
pars.add_argument("--random_min", type=float, default=3.0)
|
|
pars.add_argument("--seed", type=int, default=1)
|
|
pars.add_argument("--margin", type=float, default=2.0)
|
|
pars.add_argument("--min_length", type=float, default=6.0)
|
|
pars.add_argument("--stroke_color", default="255")
|
|
pars.add_argument("--stroke_width", type=float, default=0.2)
|
|
pars.add_argument("--keep_original", type=inkex.Boolean, default=True)
|
|
|
|
def effect(self):
|
|
opt = self.options
|
|
|
|
elements = self.shapes()
|
|
if not elements:
|
|
inkex.errormsg(_("Select at least one shape (rectangle, ellipse or path)."))
|
|
return
|
|
|
|
def to_uu(value):
|
|
return self.svg.unittouu("{}{}".format(value, opt.unit))
|
|
|
|
dims = {
|
|
"length": to_uu(opt.slot_length),
|
|
"width": to_uu(opt.slot_width),
|
|
"bridge": to_uu(opt.bridge),
|
|
"pitch": to_uu(opt.pitch),
|
|
"stagger": opt.stagger,
|
|
"angle": opt.angle,
|
|
"margin": to_uu(opt.margin),
|
|
"min_length": to_uu(opt.min_length),
|
|
}
|
|
if dims["width"] <= 0 or dims["pitch"] <= 0:
|
|
inkex.errormsg(_("The slot width and the column pitch must be strictly positive."))
|
|
return
|
|
if dims["length"] < dims["width"]:
|
|
inkex.errormsg(_("The slot length must be at least equal to the slot width. "
|
|
"Increase the length or reduce the width."))
|
|
return
|
|
if dims["pitch"] <= dims["width"]:
|
|
inkex.errormsg(_("The column pitch must be larger than the slot width, "
|
|
"otherwise neighbouring slots merge. Increase the pitch "
|
|
"or reduce the width."))
|
|
return
|
|
if opt.random_lengths:
|
|
# Le noyau ramene le minimum a la largeur d'une lumiere (trou rond).
|
|
dims["random_min"] = to_uu(opt.random_min)
|
|
dims["seed"] = opt.seed
|
|
if dims["random_min"] > dims["length"]:
|
|
inkex.errormsg(_("The shortest random slot must not exceed the slot length, "
|
|
"which is the longest one. Reduce the shortest length or "
|
|
"increase the slot length."))
|
|
return
|
|
|
|
color, opacity = parse_color(opt.stroke_color, default=("#000000", 1.0))
|
|
style = inkex.Style({
|
|
"fill": "none",
|
|
"stroke": color,
|
|
"stroke-opacity": str(opacity),
|
|
"stroke-width": str(to_uu(opt.stroke_width)),
|
|
})
|
|
# Finesse d'aplatissement des courbes, petite devant la largeur d'une lumiere.
|
|
flatness = min(self.svg.unittouu("0.05mm"), dims["width"] / 20.0)
|
|
|
|
empty = 0
|
|
for elem in elements:
|
|
if not self.process(elem, dims, style, flatness):
|
|
empty += 1
|
|
if empty:
|
|
inkex.errormsg(_("{} shape(s) too small: no slot fits inside. Reduce the "
|
|
"slot width, the minimum length or the margin.").format(empty))
|
|
|
|
def shapes(self):
|
|
"""Formes de la selection, en descendant dans les groupes."""
|
|
found = []
|
|
for elem in self.svg.selection.values():
|
|
candidates = [elem]
|
|
if isinstance(elem, inkex.Group):
|
|
candidates = list(elem.descendants())
|
|
for candidate in candidates:
|
|
if isinstance(candidate, SHAPES) and candidate not in found:
|
|
found.append(candidate)
|
|
return found
|
|
|
|
def process(self, elem, dims, style, flatness):
|
|
"""Remplit un element ; renvoie False si aucune lumiere ne tient dedans."""
|
|
# Contour en coordonnees absolues du document, courbes aplaties.
|
|
csp = elem.path.transform(elem.composed_transform()).to_superpath()
|
|
bezier.cspsubdiv(csp, flatness)
|
|
rings = [[(node[1][0], node[1][1]) for node in sub] for sub in csp]
|
|
|
|
slots = hinge_slots(rings, **dims)
|
|
if not slots:
|
|
return False
|
|
|
|
parent = elem.getparent()
|
|
group = inkex.Group()
|
|
group.label = _("Living hinge")
|
|
# 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)
|
|
|
|
# Un seul chemin pour toutes les lumieres : document leger.
|
|
node = inkex.PathElement()
|
|
node.set("d", slots_to_d(slots, dims["width"] / 2.0))
|
|
node.style = style
|
|
group.add(node)
|
|
|
|
if not self.options.keep_original:
|
|
elem.delete()
|
|
return True
|
|
|
|
|
|
if __name__ == "__main__":
|
|
LivingHinge().run()
|