diff --git a/activate.sh b/activate.sh index ebe8050..017ba44 100755 --- a/activate.sh +++ b/activate.sh @@ -1,3 +1,4 @@ +sudo apt-get install python3-pil python3-pil.imagetk sudo apt install libcairo2-dev pkg-config virtualenv -p python3 .env source .env/bin/activate diff --git a/main.py b/main.py index 1733ae6..a018be0 100644 --- a/main.py +++ b/main.py @@ -1,212 +1,296 @@ -import math - -import numpy as np - -touch_height = 20 -raise_height = 2 -head_x_offset = 50 -speed = 500 -lift_markers = True - -PREAMBLE = ''' -G1 Z20 -M107 -M190 S0 -M104 S0 -G28 ; home all axes -G0 F{1} -G1 Z{0} -G1 Z{0} -'''.format(touch_height + raise_height, speed) - -FINISH = """ -G1 Z{0} F7000 -M104 S0 -G28 X0 Y0 -M84 -""".format(75) - -import cairo, subprocess, bezier, os +from tkinter import Tk, Label, filedialog, Button +from PIL import Image, ImageTk from svgpathtools import svg2paths, Line, QuadraticBezier, CubicBezier +import cairo, subprocess, bezier, os, math, time +import numpy as np -# Setup the file structure -if not os.path.exists("output"): - os.makedirs("output") - -# Convert the bmp to a vector svg -file_name = "geom" - -subprocess.call(["mogrify", "-format", "bmp", "input-images/{}.svg".format(file_name)]) - -subprocess.call(["mkbitmap", "input-images/{}.bmp".format(file_name), "-x", - "-f", "15", - #"-b", "0", - "-o", "input-images/{}-n.bmp".format(file_name) - ]) - -subprocess.call(["potrace", - "-t", "20", - "-z", "white", - "-b", "svg", - "input-images/{}-n.bmp".format(file_name), - "--rotate", "90", - "-o", "tmp/conversion-output.svg", - ]) - -# read in the svg -paths, attributes = svg2paths("tmp/conversion-output.svg") - -gcode = "" -gcode += PREAMBLE - -started = False - -scale = 0.0045 -offset_x = 75 + head_x_offset -offset_y = 20 - - -# Walk through the paths and create the GCODE -for path in paths: - - previous_x = None - previous_y = None - - # rotated = path.rotated(90) - - for part in path: - - start = part.start - end = part.end - - start_x = start.real * scale + offset_x - start_y = start.imag * scale + offset_y - - end_x = end.real * scale + offset_x - end_y = end.imag * scale + offset_y - - # Check to see if the endpoint of the last cycle continues and wether we need to lift the pen or not - lift = True - if previous_x is not None and previous_y is not None: - if abs(start.real - previous_x) < 30 and abs(start.imag - previous_y) < 30: - lift = False - - # if the pen needs to lift, - # if lift: - previous_x = end.real - previous_y = end.imag - - if lift: - gcode += "G1 Z{}\n".format(raise_height + touch_height) - else: - gcode += "# NOT LIFTING\n" - - if isinstance(part, CubicBezier): - - nodes = np.asfortranarray([ - [start.real, part.control1.real, part.control2.real, end.real], - [start.imag, part.control1.imag, part.control2.imag, end.imag], - ]) - - curve = bezier.Curve.from_nodes(nodes) - - evals = [] - pos = np.linspace(0.1, 1, 10) - for i in pos: - evals.append(curve.evaluate(i)) +class GCoder(Tk): + def __init__(self): + super().__init__() + + # Setup the file structure + if not os.path.exists("output"): + os.makedirs("output") + + # Height at which the pen touches and draws on the surface + self.touch_height = 20 + # How far to raise the pen tip to raise it off the page + self.raise_height = 2 + # The inherent offset from true 0 we have from the pen bracket + self.head_x_offset = 50 + # XY movement speed + self.speed = 500 + # Weather we render lift markers + self.lift_markers = True + + # X and Y offsets to place the image on A11 paper + self.offset_x = 75 + self.head_x_offset + self.offset_y = 20 + + # Bed dimensions to fit A11 paper + self.bed_max_x = 280 + self.bed_min_x = self.offset_x + self.bed_max_y = 280 + self.bed_min_y = 20 + self.started = False + + self.gcode_preamble = ''' + G91 ; Set to relative mode for the initial pen lift + G1 Z20 ; Lift head by 20 + G90 ; Set back to absolute position mode + M107 ; Fan off + M190 S0 ; Set bed temp + M104 S0 ; Set nozzle temp + G28 ; home all axes + G0 F{1} ; Set the feed rate + G1 Z{0} ; Move the pen to just above the paper + '''.format(self.touch_height + self.raise_height, self.speed) + + self.gcode_end = ''' + G1 Z{0} F7000 ; Raise the pen high up so we can fit a cap onto it + M104 S0 ; Set the nozzle to 0 + G28 X0 Y0 ; Home back to (0,0) for (x,y) + M84 ; Turn off the motors + '''.format(75) + + w, h = 300, 300 + + self.geometry("{}x{}".format(w, h)) + + self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 300, 300) + + self.context = cairo.Context(self.surface) + self.context.scale(1, 1) + self.context.set_line_width(0.4) + + + self.button = Button(self, text="Select Image", command=self.file_select_callback) + self.button.pack() + + self.mainloop() + + def file_select_callback(self): + filepath = filedialog.askopenfilename(initialdir=".", title="Select file", + filetypes=(("jpeg files", "*.jpg"), ("all files", "*.*"))) + if len(filepath) is 0: + return + + self.context.rectangle(0, 0, 300, 300) + self.context.set_source_rgba(1, 1, 1, 1.0) + self.context.fill() + + self.context.set_source_rgba(0, 0, 0, 1.0) + + filename = os.path.basename(filepath) + self.convert_image(filename) + self.convert_gcode() + self.render_gcode() + + self._image_ref = ImageTk.PhotoImage( + Image.frombuffer("RGBA", (300, 300), self.surface.get_data().tobytes(), "raw", "BGRA", 0, 1)) + self.label = Label(self, image=self._image_ref) + self.label.pack(expand=True, fill="both") + + def convert_image(self, file_name): + + base_name = file_name.split(".")[0] + + print("Converting input file [{}]".format(file_name)) + + print("Running mogrify...") + start = time.time() + subprocess.call(["mogrify", "-format", "bmp", "input-images/{}".format(file_name)]) + print("Run took [{:.2f}] seconds".format(time.time() - start)) + + print("Running mkbitmap...") + start = time.time() + subprocess.call(["mkbitmap", "input-images/{}.bmp".format(base_name), "-x", + "-f", "15", + # "-b", "0", + "-o", "input-images/{}-n.bmp".format(base_name) + ]) + print("Run took [{:.2f}] seconds".format(time.time() - start)) + + print("Running potrace...") + start = time.time() + subprocess.call(["potrace", + "-t", "20", + "-z", "white", + "-b", "svg", + "input-images/{}-n.bmp".format(base_name), + "--rotate", "0", + "-o", "tmp/conversion-output.svg", + ]) + print("Run took [{:.2f}] seconds\n".format(time.time() - start)) + + def render_gcode(self): + + file = open("output/gcode-output.gcode", "r") + + largest_x = 0 + largest_y = 0 + smallest_x = 300 + smallest_y = 300 + x = None + y = None + + for line in file: + + split = line.split(" ") + command = split[0] + operands = split[1:] + prev_x = x + prev_y = y - gcode += "G1 X{} Y{}\n".format(start_x, start_y) - gcode += "G1 Z{} \n".format(touch_height) + if command == "G1": + for operand in operands: + if operand.startswith("X"): + x = float(operand[1:]) + if x > largest_x: largest_x = x + if x < smallest_x: smallest_x = x + elif operand.startswith("Y"): + y = float(operand[1:]) + if y > largest_y: largest_y = y + if y < smallest_y: smallest_y = y + elif operand.startswith("Z{}".format(self.touch_height + self.raise_height)): - for i in evals: - x = i[0][0] - y = i[1][0] - gcode += "G1 X{} Y{}\n".format(x * scale + offset_x, y * scale + offset_y) + # signify a lift + if prev_x is not None and prev_y is not None and self.lift_markers: + self.context.arc(prev_x - self.head_x_offset, prev_y, 0.5, 0, 2*math.pi) + self.context.stroke() + prev_x = None + prev_y = None + x = None + y = None + + if (prev_x != x and prev_x is not None) or (prev_y != y and prev_y is not None): + self.context.line_to(prev_x - self.head_x_offset, prev_y) + self.context.line_to(x - self.head_x_offset, y) + self.context.stroke() + + + print("Largest X : " + str(largest_x)) + print("Smallest X : " + str(smallest_x)) - #gcode += "G1 X{} Y{}\n".format(end.real * scale + offset_x, end.imag * scale + offset_y) + print("Largest Y : " + str(largest_y)) + print("Smallest Y : " + str(smallest_y)) + + if largest_x > self.bed_max_x: + print("X OVERFLOW") + if largest_y > self.bed_max_y: + print("Y OVERFLOW") + + if smallest_x < self.bed_min_x: + print("X_UNDERFLOW") + if smallest_y < self.bed_min_y: + print("Y_UNDERFLOW") + + def convert_gcode(self): + + # read in the svg + paths, attributes = svg2paths("tmp/conversion-output.svg") + + bounding_x_max = None + bounding_x_min = None + bounding_y_max = None + bounding_y_min = None + + for path in paths: + + bbox = path.bbox() + + if bounding_x_max is None or bbox[0] > bounding_x_max: + bounding_x_max = bbox[0] + if bounding_x_min is None or bbox[1] < bounding_x_min: + bounding_x_min = bbox[1] + + if bounding_y_max is None or bbox[2] > bounding_y_max: + bounding_y_max = bbox[2] + if bounding_y_min is None or bbox[3] > bounding_y_min: + bounding_y_min = bbox[3] + print("Maximum X : {}".format(bounding_x_max)) + print("Minimum Y : {}".format(bounding_x_min)) + print("Maximum X : {}".format(bounding_y_max)) + print("Minimum Y : {}".format(bounding_y_min)) + max_dim = max(bounding_x_max, bounding_x_min, bounding_y_max, bounding_y_min) + scale = (300 - self.offset_x) / max_dim + print("Scaling to : {}\n".format(scale)) - if isinstance(part, Line): - gcode += "G1 X{} Y{}\n".format(start_x, start_y) - gcode += "G1 Z{} \n".format(touch_height) - gcode += "G1 X{} Y{}\n".format(end_x, end_y) + # Start the gcode + gcode = "" + gcode += self.gcode_preamble + # Walk through the paths and create the GCODE + for path in paths: -gcode += FINISH + previous_x = None + previous_y = None -output_gcode = open("output/gcode-output.gcode", "w") -output_gcode.write(gcode) -output_gcode.close() + for part in path: -file = open("output/gcode-output.gcode", "r") + start = part.start + end = part.end -x = None -y = None + start_x = start.real * scale + self.offset_x + start_y = start.imag * scale + self.offset_y -with cairo.SVGSurface("rendered-output.svg", 300, 300) as surface: + end_x = end.real * scale + self.offset_x + end_y = end.imag * scale + self.offset_y - context = cairo.Context(surface) - context.scale(1, 1) - context.set_line_width(0.4) + # Check to see if the endpoint of the last cycle continues and wether we need to lift the pen or not + lift = True + if previous_x is not None and previous_y is not None: + if abs(start.real - previous_x) < 30 and abs(start.imag - previous_y) < 30: + lift = False - largest_x = 0 - largest_y = 0 - smallest_x = 300 - smallest_y = 300 + # if the pen needs to lift, + # if lift: + previous_x = end.real + previous_y = end.imag - for line in file: + if lift: + gcode += "G1 Z{}\n".format(self.raise_height + self.touch_height) + else: + gcode += "# NOT LIFTING\n" - split = line.split(" ") - command = split[0] - operands = split[1:] + if isinstance(part, CubicBezier): - prev_x = x - prev_y = y + nodes = np.asfortranarray([ + [start.real, part.control1.real, part.control2.real, end.real], + [start.imag, part.control1.imag, part.control2.imag, end.imag], + ]) - if command == "G1": - for operand in operands: - if operand.startswith("X"): - x = float(operand[1:]) - if x > largest_x: largest_x = x - if x < smallest_x: smallest_x = x - elif operand.startswith("Y"): - y = float(operand[1:]) - if y > largest_y: largest_y = y - if y < smallest_y: smallest_y = y - elif operand.startswith("Z{}".format(touch_height + raise_height)): + curve = bezier.Curve.from_nodes(nodes) - # signify a lift - if prev_x is not None and prev_y is not None and lift_markers: - context.arc(prev_x, prev_y, 0.5, 0, 2*math.pi) - context.stroke() + evals = [] + pos = np.linspace(0.1, 1, 10) + for i in pos: + evals.append(curve.evaluate(i)) - prev_x = None - prev_y = None - x = None - y = None + gcode += "G1 X{} Y{}\n".format(start_x, start_y) + gcode += "G1 Z{} \n".format(self.touch_height) - if (prev_x != x and prev_x is not None) or (prev_y != y and prev_y is not None): - context.line_to(prev_x, prev_y) - context.line_to(x, y) - context.stroke() + for i in evals: + x = i[0][0] + y = i[1][0] + gcode += "G1 X{} Y{}\n".format(x * scale + self.offset_x, y * scale + self.offset_y) + if isinstance(part, Line): + gcode += "G1 X{} Y{}\n".format(start_x, start_y) + gcode += "G1 Z{} \n".format(self.touch_height) + gcode += "G1 X{} Y{}\n".format(end_x, end_y) - print("Largest X : " + str(largest_x)) - print("Largest Y : " + str(largest_y)) - print("Smallest X : " + str(smallest_x)) - print("Smallest Y : " + str(smallest_y)) + gcode += self.gcode_end - if largest_x > 280: - print("X OVERFLOW") - if largest_y > 280: - print("Y OVERFLOW") + output_gcode = open("output/gcode-output.gcode", "w") + output_gcode.write(gcode) + output_gcode.close() - if smallest_x < 125: - print("X_UNDERFLOW") - if smallest_y < 20: - print("Y_UNDERFLOW") +if __name__ == "__main__": + GCoder()