#!/usr/bin/env python3 # coding=utf-8 """ Extension Inkscape : remplit des formes avec des hachures pour la gravure laser. Le taux de remplissage (pourcentage de noir) fixe le pas entre les traits, et les traits sont classes dans l'ordre ou la tete du laser les parcourt (fin d'un trait proche du debut du suivant) pour limiter les deplacements a vide. La logique vit dans `hachures_core`, sans dependance a inkex. """ import os import sys import inkex 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 hachures_core. sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from hachures_core import ( # noqa: E402 TooManyLines, flatten_cubic_subpaths, hatch, parse_color, polylines_to_d, spacing_from_density) 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 « hachures » 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) class HatchFill(inkex.EffectExtension): """Remplit chaque forme selectionnee avec des hachures ordonnees pour le laser.""" def add_arguments(self, pars): # Un argument par du .inx, memes noms et memes valeurs par defaut. pars.add_argument("--tab", default="main") pars.add_argument("--density", type=float, default=30.0) pars.add_argument("--line_width", type=float, default=0.2) pars.add_argument("--unit", default="mm") pars.add_argument("--angle", type=float, default=45.0) pars.add_argument("--cross", type=inkex.Boolean, default=False) pars.add_argument("--anchor", default="shape") pars.add_argument("--stroke_color", default="255") pars.add_argument("--keep_original", type=inkex.Boolean, default=True) def effect(self): opt = self.options shapes = [] for elem in self.svg.selection.values(): shapes.extend(self.iter_shapes(elem)) if not shapes: inkex.errormsg(_("Select at least one shape.")) return if not 0 < opt.density <= 100: inkex.errormsg(_("The coverage must be between 1 and 100 %.")) return width = length_to_uu(self.svg, opt.line_width, opt.unit) if width <= 0: inkex.errormsg(_("The line width must be strictly positive.")) return spacing = spacing_from_density(opt.density, width, opt.cross) color, opacity = parse_color(opt.stroke_color) style = inkex.Style({ "fill": "none", "stroke": color, "stroke-opacity": str(opacity), "stroke-width": str(width), }) for elem in shapes: if not self.process(elem, spacing, width, style): return @classmethod def iter_shapes(cls, elem): """Formes a remplir : les groupes sont parcourus, le reste doit avoir un contour.""" if isinstance(elem, inkex.Group): for child in elem: for shape in cls.iter_shapes(child): yield shape elif isinstance(elem, inkex.ShapeElement) and hasattr(elem, "path"): yield elem def process(self, elem, spacing, width, style): """Hachure une forme : calcul dans hachures_core, ecriture dans le SVG. Renvoie False si le calcul a ete refuse (message deja affiche). """ # Contour en coordonnees absolues du document, courbes aplaties assez # finement pour que l'ecart reste sous le quart de la largeur du trait. superpath = elem.path.transform(elem.composed_transform()).to_superpath() rings = flatten_cubic_subpaths(superpath, width / 4.0) if not rings: return True try: polylines = hatch(rings, spacing, self.options.angle, self.options.cross, self.options.anchor) except TooManyLines as error: inkex.errormsg(_("Too many hatch lines (%d). Increase the line width, " "lower the coverage, or select a smaller shape.") % error.args[0]) return False parent = elem.getparent() group = inkex.Group() group.label = _("Hatching") 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) # Un seul chemin : les sous-chemins restent dans l'ordre laser. node = inkex.PathElement() node.set("d", polylines_to_d(polylines)) node.style = style node.label = _("Laser path") group.add(node) if not self.options.keep_original: elem.delete() return True if __name__ == "__main__": HatchFill().run()