inkscape.BoxFingering/box_maker.py
2026-09-24 18:42:22 +02:00

149 lines
5.7 KiB
Python

#!/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()