#!/usr/bin/env python3 """Extension Inkscape : génère une boîte à créneaux à découper au laser. Chaque pièce est un objet indépendant, placé directement dans le calque courant, pour pouvoir être sélectionnée et déplacée seule. """ import inkex from inkex import Group, PathElement, TextElement from inkex.localization import localize import box_core # Installe _() dans les builtins : catalogue « box_maker » de la langue de # l'interface (Inkscape transmet domaine et dossier par l'environnement), # textes anglais d'origine sinon. localize() def panel_names(): """Noms affiches des pieces, indexes par leur nom dans box_core.""" return { "Avant": _("Front"), "Arrière": _("Back"), "Gauche": _("Left"), "Droite": _("Right"), "Fond": _("Bottom"), "Couvercle": _("Lid"), } class BoxMaker(inkex.EffectExtension): def add_arguments(self, pars): pars.add_argument("--tab") pars.add_argument("--length", type=float, default=100.0) pars.add_argument("--width", type=float, default=80.0) pars.add_argument("--height", type=float, default=50.0) pars.add_argument("--dim_type", default="exterior") pars.add_argument("--thickness", type=float, default=3.0) pars.add_argument("--finger", type=float, default=0.0) pars.add_argument("--kerf", type=float, default=0.1) pars.add_argument("--lid", type=inkex.Boolean, default=True) pars.add_argument("--gap", type=float, default=5.0) pars.add_argument("--stroke_color", type=inkex.Color, default=inkex.Color("red")) pars.add_argument("--stroke_width", type=float, default=0.1) pars.add_argument("--labels", type=inkex.Boolean, default=False) def effect(self): opt = self.options try: panels = box_core.make_box( opt.length, opt.width, opt.height, opt.thickness, finger=opt.finger, kerf=opt.kerf, interior=opt.dim_type == "interior", lid=opt.lid, gap=opt.gap, ) except box_core.DimensionTooSmall as err: dimension = { "length": _("length"), "width": _("width"), "height": _("height"), }[err.dimension] inkex.errormsg(_( "The {dimension} ({size:.1f} mm exterior) is too small for the " "thickness ({thickness:g} mm): it must be at least {minimum:g} mm." ).format(dimension=dimension, size=err.size, thickness=err.thickness, minimum=err.minimum)) return mm = self._uu_per_mm() origin_x, origin_y = self._origin(panels, mm) layer = self.svg.get_current_layer() # Les coordonnées sont calculées dans le repère du document : on annule # la transformation éventuelle du calque (et de ses parents). to_layer = -layer.composed_transform() style = { "fill": "none", "stroke": str(inkex.Color(opt.stroke_color).to_rgb()), "stroke-width": opt.stroke_width * mm, } names = panel_names() for panel in panels: name = names[panel.name] path = PathElement() path.set_id(self.svg.get_unique_id("boite-")) path.label = name path.style = style path.path = "M " + " L ".join( f"{origin_x + x * mm:.4f},{origin_y + y * mm:.4f}" for x, y in panel.points ) + " Z" if opt.labels: group = Group() group.set_id(self.svg.get_unique_id("boite-")) group.label = name group.transform = to_layer group.add(path) group.add(self._label(panel, name, origin_x, origin_y, mm)) layer.add(group) else: path.transform = to_layer layer.add(path) def _uu_per_mm(self): """Unités utilisateur par mm, comme Inkscape les affiche. svg.unittouu() se trompe quand le viewBox n'a pas les proportions de la page : Inkscape applique alors une échelle uniforme (preserveAspectRatio « meet »), soit la plus petite des deux échelles page / viewBox. """ svg = self.svg 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") 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 _origin(self, panels, mm): """Centre l'ensemble des pièces sur la vue (ou la page à défaut).""" max_x = max(p.bbox()[2] for p in panels) * mm max_y = max(p.bbox()[3] for p in panels) * mm namedview = self.svg.namedview if namedview is not None and namedview.get("inkscape:cx") is not None: cx, cy = namedview.center else: cx, cy = self.svg.get_page_bbox().center return cx - max_x / 2, cy - max_y / 2 def _label(self, panel, name, origin_x, origin_y, mm): min_x, min_y, max_x, max_y = panel.bbox() size = min(5.0, (max_y - min_y) / 4) * mm text = TextElement() text.set_id(self.svg.get_unique_id("boite-")) text.set("x", str(origin_x + (min_x + max_x) / 2 * mm)) text.set("y", str(origin_y + (min_y + max_y) / 2 * mm + size / 3)) text.style = { "font-size": size, "text-anchor": "middle", "fill": "#0000ff", "stroke": "none", } text.text = name return text if __name__ == "__main__": BoxMaker().run()