#!/usr/bin/env python import sys import xml.etree.ElementTree as ET import shapes as shapes_pkg from shapes import point_generator from config import * import re from math import isfinite from datetime import datetime as dt from optimise import optimise_path, get_total_distance from utils import * debug = False # todo fix total distance algorithm # todo can we guess the units? # todo add manual scale option # todo add z rise option # todo why do i need to flip? def get_shapes(path, auto_scale=True): t1 = dt.now() svg_shapes = set(['rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'path']) shapes = [] tree = ET.parse(path) root = tree.getroot() pointRatio = 0.352778 width = root.get('width') height = root.get('height') if width is None or height is None: viewbox = root.get('viewBox') if viewbox: _, _, width, height = viewbox.split() if width is None or height is None: print("Unable to get width and height for the svg") sys.exit(1) # width = float(re.sub("[^0-9]", "", width)) # height = float(re.sub("[^0-9]", "", height)) width = float(re.findall(r"[-+]?\d*\.\d+|\d+", width)[0]) height = float(re.findall(r"[-+]?\d*\.\d+|\d+", height)[0]) print("width / height ", width, height) if units == "points": width *= pointRatio height *= pointRatio for elem in root.iter(): try: _, tag_suffix = elem.tag.split('}') except ValueError: continue if tag_suffix in svg_shapes: shape_class = getattr(shapes_pkg, tag_suffix) shape_obj = shape_class(elem) d = shape_obj.d_path() m = shape_obj.transformation_matrix() # todo work out what d and m are coords = [] if d: # begin shape processing p = point_generator(d, m, smoothness) # tuples of x y coords first = True for x, y in p: # todo sort out this nightmare if units == "points": x *= pointRatio y *= pointRatio y = -y + height if first: #coords.append((x, -y + height)) coords.append((x, y)) else: if not (x, y) == coords[-1]: #coords.append((x, -y + height)) coords.append((x, y)) if first: first = False if coords: shapes.append(coords) if not (isfinite(bed_max_x) and isfinite(bed_max_y) and bed_max_x > 0 and bed_max_y > 0): raise ValueError("Bed dimensions must be finite and greater than zero") points = [point for shape in shapes for point in shape] if any(not (isfinite(x) and isfinite(y)) for x, y in points): raise ValueError(f"{path}: drawing coordinates must be finite") if auto_scale and points: min_x = min(x for x, y in points) max_x = max(x for x, y in points) min_y = min(y for x, y in points) max_y = max(y for x, y in points) if min_x < 0 or max_x > bed_max_x or min_y < 0 or max_y > bed_max_y: span_x = max_x - min_x span_y = max_y - min_y scale = min(1.0, bed_max_x / span_x if span_x else 1.0, bed_max_y / span_y if span_y else 1.0) print("auto fitting: scale", scale, "origin", (min_x, min_y)) # Bound floating-point roundoff at the fitted upper edges. shapes = [[(min(bed_max_x, (x - min_x) * scale), min(bed_max_y, (y - min_y) * scale)) for x, y in shape] for shape in shapes] for shape in shapes: for x, y in shape: if not (0 <= x <= bed_max_x and 0 <= y <= bed_max_y): raise ValueError( f"{path}: drawing coordinate X{x:.6f} Y{y:.6f} mm " f"is outside bed limits X[0, {bed_max_x}] " f"Y[0, {bed_max_y}] mm" ) timer(t1, "parsing gcode ") return shapes def g_string(x, y, z=False, prefix="G1", p=3): if z is not False: return f"{prefix} X{x:.{p}f} Y{y:.{p}f} Z{z:.{p}f}" else: return f"{prefix} X{x:.{p}f} Y{y:.{p}f}" def shapes_2_gcode(shapes): t1 = dt.now() with open("header.txt") as h: header = h.read() commands = [f"{header}", preamble, f'F{feed_rate}'] for i in shapes: commands += ['', shape_preamble, ""] commands.append(g_string(i[0][0], i[0][1], zTravel, "G0")) for j in i: commands.append(g_string(j[0], j[1], zDraw)) commands.append(g_string(i[-1][0], i[-1][1], zTravel, "G0")) commands += ["", shape_postamble, ""] commands += [" ",postamble, ""] commands += ["(home)", f"G0 Z{zTravel}", f"G0 X0 Y0"] timer(t1, "shapes_2_gcode ") return commands def write_file(output, commands): t1 = dt.now() with open(output, 'w+') as output_file: for i in commands: output_file.write(i + "\n") timer(t1, "writing file ") def main(file_path, output): sys.setrecursionlimit(max(sys.getrecursionlimit(), 100000)) shapes = get_shapes(file_path, auto_scale) if not shapes: raise ValueError(f"{file_path}: no drawable SVG shapes found") if optimise: pre_distance = get_total_distance(shapes) new_order = optimise_path(shapes) post_distance = get_total_distance(new_order) print("unoptimised distance: ", pre_distance) print("optimised distance ", post_distance) if pre_distance: print("factor: ", post_distance / pre_distance) else: print("factor: not applicable (no travel between shapes)") commands = shapes_2_gcode(new_order) else: commands = shapes_2_gcode(shapes) write_file(output, commands) print("done") if __name__ == "__main__": # file_path = "./svg/lines.svg" # file_path = "./svg/medium_example.svg" file_path = "./svg/text.svg" # file_path = "./svg/example.svg" # file_path = "./svg/lines.svg" output = "./gcode_optimised/1.gcode" main(file_path, output)