inkscape.BoxFingering/box_maker.py
2026-09-10 13:54:19 +02:00

119 lines
4.6 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
import box_core
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 ValueError as err:
raise inkex.AbortExtension(str(err))
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,
}
for panel in panels:
path = PathElement()
path.set_id(self.svg.get_unique_id("boite-"))
path.label = panel.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 = panel.name
group.transform = to_layer
group.add(path)
group.add(self._label(panel, 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, 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 = panel.name
return text
if __name__ == "__main__":
BoxMaker().run()