diff --git a/boxes/svg_import.py b/boxes/svg_import.py new file mode 100644 index 000000000..5e1c36892 --- /dev/null +++ b/boxes/svg_import.py @@ -0,0 +1,74 @@ +"""Allow paths to be imported from an SVG file into boxes.py. + +This is intended to make it easier to add custom cut-outs and engraving. +Currently, only a single path can be imported. This is generally sufficient +to import shapes drawn in OpenSCAD. +""" + +import svg.path +from lxml import etree +from boxes.drawing import Context +import numpy as np + +def draw_path_on_ctx(ctx: Context, path: svg.path.Path): + """Draw an SVG path into the current context. + + This transforms the points according to the current context, but otherwise + should just add the commands from the path into the output. + """ + for seg in path: + if isinstance(seg, svg.path.Move): + ctx.move_to(seg.end.real, seg.end.imag) + elif isinstance(seg, svg.path.Close) or isinstance(seg, svg.path.Line): + ctx.line_to(seg.end.real, seg.end.imag) + elif isinstance(seg, svg.path.Arc): + raise NotImplementedError + # xc, yc, radius, angle1, angle2, direction + #ctx._arc(seg.arc, seg.sweep) + elif isinstance(seg, svg.path.CubicBezier): + ctx.curve_to( + seg.control1.real, + seg.control1.imag, + seg.control2.real, + seg.control2.imag, + seg.end.real, + seg.end.imag, + ) + elif isinstance(seg, svg.path.QuadraticBezier): + ctx.curve_to( + seg.control.real, + seg.control.imag, + seg.control.real, + seg.control.imag, + seg.end.real, + seg.end.imag, + ) + +def path_extent(path: svg.path.Path) -> tuple[tuple[float, float], tuple[float, float]]: + """Determine max/min x,y positions in a path.""" + extent_x = [np.inf, -np.inf] + extent_y = [np.inf, -np.inf] + for seg in path: + x, y = seg.end.real, seg.end.imag + for extent, point in [(extent_x, x), (extent_y, y)]: + extent[0] = min(extent[0], point) + extent[1] = max(extent[1], point) + return tuple(extent_x), tuple(extent_y) + +def path_centre(path: svg.path.Path) -> tuple[float, float]: + """Determine the middle point of a path.""" + (x0, x1), (y0, y1) = path_extent(path) + return (x0 + x1)/2, (y0 + y1)/2 + +def load_path_from_svg(filename: str) -> svg.path.Path: + """Load a path from an SVG file. + + This assumes there is a single path in the file, as is true for OpenSCAD + exports. + """ + tree = etree.parse(filename) + path_nodes = tree.xpath("//svg:path", namespaces={'svg': "http://www.w3.org/2000/svg"}) + if len(path_nodes) > 1: + raise RuntimeError("I can only cope with one path in an SVG!") + data = path_nodes[0].get("d") + return svg.path.parse_path(data) diff --git a/requirements.txt b/requirements.txt index 58ee28c6f..6ca9c2c64 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,8 @@ affine>=2.0 markdown +numpy qrcode>=7.3.1 setuptools shapely>=1.8.2 +svg.path typing_extensions>=4.5.0