221 lines
8.4 KiB
Python
221 lines
8.4 KiB
Python
#!/usr/bin/env python3
|
|
# coding=utf-8
|
|
"""
|
|
Extension Inkscape : remplit l'interieur des formes selectionnees avec un
|
|
motif isometrique de « Y » en relief.
|
|
|
|
Chaque forme (chemin, rectangle, cercle... ; les groupes sont parcourus) est
|
|
aplatie en polygone, puis le motif, calcule par `relief_core`, est decoupe a
|
|
l'interieur de ce polygone (trous compris). Le resultat est un groupe place
|
|
juste au-dessus de la forme, avec un seul chemin par forme.
|
|
|
|
La logique vit dans `relief_core`, sans dependance a inkex.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
|
|
import inkex
|
|
from inkex.bezier import cspsubdiv
|
|
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 relief_core.
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
from relief_core import (fill_rings, parse_color, polylines_to_d, # noqa: E402
|
|
rings_bbox, segments_to_polylines)
|
|
|
|
|
|
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 « relief_fill » 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)
|
|
|
|
|
|
# Formes dont on sait lire le contour ; les groupes sont parcourus.
|
|
SHAPE_TYPES = tuple(
|
|
getattr(inkex, name) for name in
|
|
("PathElement", "Rectangle", "Circle", "Ellipse", "Polygon", "Polyline", "Line")
|
|
if hasattr(inkex, name))
|
|
|
|
|
|
def shape_rings(elem, flatness):
|
|
"""Contour de `elem` en coordonnees absolues : liste d'anneaux de points.
|
|
|
|
Les courbes sont aplaties a la tolerance `flatness` (unites utilisateur).
|
|
Un sous-chemin ouvert est traite comme ferme, comme le fait un remplissage.
|
|
"""
|
|
path = elem.path.transform(elem.composed_transform())
|
|
csp = path.to_superpath()
|
|
cspsubdiv(csp, flatness)
|
|
rings = []
|
|
for sub in csp:
|
|
ring = [(node[1][0], node[1][1]) for node in sub]
|
|
if len(ring) > 1 and ring[0] == ring[-1]:
|
|
ring.pop()
|
|
if len(ring) >= 3:
|
|
rings.append(ring)
|
|
return rings
|
|
|
|
|
|
class ReliefFill(inkex.EffectExtension):
|
|
"""Remplit chaque forme selectionnee avec le motif de Y en relief."""
|
|
|
|
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("--edge", type=float, default=5.0)
|
|
pars.add_argument("--unit", default="mm")
|
|
pars.add_argument("--angle", type=float, default=0.0)
|
|
pars.add_argument("--origin", default="shape")
|
|
pars.add_argument("--stroke_color", default="255")
|
|
pars.add_argument("--stroke_width", type=float, default=0.3)
|
|
pars.add_argument("--draw_outline", type=inkex.Boolean, default=False)
|
|
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
|
|
|
|
edge = length_to_uu(self.svg, opt.edge, opt.unit)
|
|
if edge <= 0:
|
|
inkex.errormsg(_("The edge length must be strictly positive."))
|
|
return
|
|
width = length_to_uu(self.svg, opt.stroke_width, opt.unit)
|
|
if width < 0:
|
|
inkex.errormsg(_("The stroke width cannot be negative."))
|
|
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(width),
|
|
"stroke-linecap": "round",
|
|
"stroke-linejoin": "round",
|
|
})
|
|
flatness = length_to_uu(self.svg, 0.02) # 0.02 mm : invisible a l'impression
|
|
|
|
for elem in elements:
|
|
shapes = [e for e in elem.descendants() if isinstance(e, SHAPE_TYPES)]
|
|
if not shapes:
|
|
inkex.errormsg(_("The selection contains no shape with an outline "
|
|
"(path, rectangle, circle...). Convert text to "
|
|
"paths first."))
|
|
continue
|
|
if not self.process(elem, shapes, edge, style, flatness):
|
|
return
|
|
|
|
def process(self, elem, shapes, edge, style, flatness):
|
|
"""Remplit les `shapes` de `elem` ; renvoie False en cas d'erreur fatale."""
|
|
opt = self.options
|
|
results = [] # (anneaux, segments) par forme
|
|
for shape in shapes:
|
|
rings = shape_rings(shape, flatness)
|
|
if not rings:
|
|
continue
|
|
if opt.origin == "document":
|
|
anchor = (0.0, 0.0)
|
|
else:
|
|
xmin, ymin, xmax, ymax = rings_bbox(rings)
|
|
anchor = ((xmin + xmax) / 2.0, (ymin + ymax) / 2.0)
|
|
try:
|
|
segments = fill_rings(rings, edge, opt.angle, anchor)
|
|
except OverflowError:
|
|
inkex.errormsg(_("The pattern would have too many lines. "
|
|
"Increase the edge length or select a smaller shape."))
|
|
return False
|
|
results.append((rings, segments))
|
|
|
|
if not any(segments for _rings, segments in results):
|
|
inkex.errormsg(_("The edge length is too large for this shape: no line "
|
|
"fits inside. Reduce the edge length."))
|
|
return True
|
|
|
|
parent = elem.getparent()
|
|
group = inkex.Group()
|
|
group.label = _("Relief 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)
|
|
|
|
for rings, segments in results:
|
|
if opt.draw_outline:
|
|
outline = inkex.PathElement()
|
|
outline.path = polylines_to_d([ring + [ring[0]] for ring in rings])
|
|
outline.style = style
|
|
group.add(outline)
|
|
if segments:
|
|
node = inkex.PathElement()
|
|
node.path = polylines_to_d(segments_to_polylines(segments), precision=3)
|
|
node.style = style
|
|
group.add(node)
|
|
|
|
if not opt.keep_original:
|
|
elem.delete()
|
|
return True
|
|
|
|
|
|
if __name__ == "__main__":
|
|
ReliefFill().run()
|