"""The cut line for print-then-cut (Bambu Suite and other cutters), in millimetres: the slab outline
plus the label and card windows, together as one path (one object to import, not several). The card
window is white_margin_mm larger than the card so the real card fits in.
Every file uses the full sheet (bleed included) as its coordinate system with the origin in the
top-left corner of print.png, so the cut line lands on the printed image without manual alignment.
"""
from __future__ import annotations
import base64
import io
import math
from pathlib import Path
from .layout import Layout, Rect
STROKE = "#ff0000" # what most cutter software reads as "cut"
def _f(v: float) -> str:
return f"{v:.3f}".rstrip("0").rstrip(".")
def _svg_path(r: Rect) -> str:
x0, y0, x1, y1 = r.x, r.y, r.x + r.w, r.y + r.h
k = min(r.radius, r.w / 2, r.h / 2)
if k <= 0:
return f"M{_f(x0)},{_f(y0)} H{_f(x1)} V{_f(y1)} H{_f(x0)} Z"
a = f"A{_f(k)},{_f(k)} 0 0 1"
return (f"M{_f(x0 + k)},{_f(y0)} H{_f(x1 - k)} {a} {_f(x1)},{_f(y0 + k)} "
f"V{_f(y1 - k)} {a} {_f(x1 - k)},{_f(y1)} H{_f(x0 + k)} {a} {_f(x0)},{_f(y1 - k)} "
f"V{_f(y0 + k)} {a} {_f(x0 + k)},{_f(y0)} Z")
def _svg(layout: Layout, body: str) -> str:
w, h = layout.total_w_mm, layout.total_h_mm
return (f'\n'
f'\n')
def cut_rects(layout: Layout) -> list[Rect]:
return [layout.trim, layout.label, layout.card.grow(layout.white_margin_mm)]
def _cut_element(layout: Layout) -> str:
d = " ".join(_svg_path(r) for r in cut_rects(layout))
return f' \n'
def write_print_svg(layout: Layout, png: Path, path: Path) -> None:
"""Printed image and cut line in one file, already aligned."""
data = base64.b64encode(png.read_bytes()).decode()
image = (f' \n')
path.write_text(_svg(layout, image + _cut_element(layout)))
def write_cutout_svg(layout: Layout, cutout, path: Path) -> None:
"""The slab alone (trim size, no bleed) as a transparent PNG inside an SVG that states its size in
mm. Bambu Suite's Print Then Cut traces the picture's edges, so this needs no cut line."""
buf = io.BytesIO()
cutout.save(buf, "PNG")
w, h = layout.width_mm, layout.height_mm
data = base64.b64encode(buf.getvalue()).decode()
path.write_text(
f'\n'
f'\n')
def _dxf_polyline(r: Rect, sheet_h: float) -> list[str]:
"""One closed polyline with arc bulges. DXF's y axis points up."""
x0, x1 = r.x, r.x + r.w
y0, y1 = sheet_h - (r.y + r.h), sheet_h - r.y
k = min(r.radius, r.w / 2, r.h / 2)
bulge = math.tan(math.radians(90) / 4) # quarter circle, counter-clockwise
if k > 0:
pts = [(x0 + k, y0, 0), (x1 - k, y0, bulge), (x1, y0 + k, 0), (x1, y1 - k, bulge),
(x1 - k, y1, 0), (x0 + k, y1, bulge), (x0, y1 - k, 0), (x0, y0 + k, bulge)]
else:
pts = [(x0, y0, 0), (x1, y0, 0), (x1, y1, 0), (x0, y1, 0)]
out = ["0", "POLYLINE", "8", "CUT", "62", "1", "66", "1", "70", "1", "10", "0", "20", "0", "30", "0"]
for x, y, b in pts:
out += ["0", "VERTEX", "8", "CUT", "10", _f(x), "20", _f(y), "30", "0"]
if b:
out += ["42", f"{b:.8f}"]
return out + ["0", "SEQEND", "8", "CUT"]
def write_dxf(layout: Layout, path: Path) -> None:
"""DXF R12, units mm, one closed polyline per cut, all on layer CUT."""
h = layout.total_h_mm
out = ["0", "SECTION", "2", "HEADER", "9", "$ACADVER", "1", "AC1009", "9", "$INSUNITS", "70", "4",
"9", "$EXTMIN", "10", "0", "20", "0", "9", "$EXTMAX", "10", _f(layout.total_w_mm), "20", _f(h),
"0", "ENDSEC", "0", "SECTION", "2", "ENTITIES"]
for r in cut_rects(layout):
out += _dxf_polyline(r, h)
out += ["0", "ENDSEC", "0", "EOF"]
path.write_text("\n".join(out) + "\n")
def write_all(layout: Layout, out_dir: Path) -> None:
write_dxf(layout, out_dir / "cut.dxf")
write_print_svg(layout, out_dir / "print.png", out_dir / "print-cut.svg")