Created
October 15, 2016 03:34
-
-
Save ryansturmer/9f80aee98738b4fe6f63ba8cf14aa8fc to your computer and use it in GitHub Desktop.
Convert @PrimitivePic output to g-code for painting (quadratic bezier curves only)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import xml.etree.ElementTree as ET | |
import sys | |
import math | |
import re | |
import argparse | |
import os | |
def gx(n, x=None,y=None,z=None, f=None): | |
retval = ['G%d' % n] | |
for a,b in zip(('X','Y','Z','F'), (x, y, z, f)): | |
if b is not None: | |
if a == 'F': | |
b *= 60.0 # Feedrates in ipm not ips | |
retval.append(' %s%0.4f' % (a,b)) | |
return ''.join(retval) | |
def g0(x=None,y=None,z=None): | |
return gx(0,x,y,z) | |
def g1(x=None,y=None,z=None, f=None): | |
return gx(1,x,y,z,f) | |
def dist(a,b): | |
return math.sqrt((b[1]-a[1])**2 + (b[0]-b[0])**2) | |
def interpolate_quad(p0,p1,p2, segment_size=0.010): | |
length = dist(p0,p1) + dist(p1,p2) | |
segments = int(length/segment_size) | |
dt = 1.0/segments | |
points = [] | |
for i in range(segments): | |
t = i*dt | |
x = (1-t)*((1-t)*p0[0] + t*p1[0]) + t*((1-t)*p1[0] + t*p2[0]) | |
y = (1-t)*((1-t)*p0[1] + t*p1[1]) + t*((1-t)*p1[1] + t*p2[1]) | |
points.append((x,y)) | |
return points | |
# Given an SVG file (output from @primitivepic) extract all the quadratic splines | |
# Return them as tuples of 3 points (2 endpoints and a control point) | |
def extract_quads(filename): | |
tree = ET.parse(filename) | |
root = tree.getroot() | |
# Parse scale | |
g = root.find('{http://www.w3.org/2000/svg}g') | |
scale = float(re.match('scale\((\d+.?\d*)\)', g.attrib['transform']).group(1)) | |
# Get viewport | |
width = float(root.attrib['width'])/scale | |
height = float(root.attrib['height'])/scale | |
quads = [] | |
for element in root.iter('{http://www.w3.org/2000/svg}path'): | |
quad = element.attrib['d'] | |
m, x0, y0, q, x1, y1, x2, y2 = map(lambda x : x.strip(','), quad.split()) | |
p0 = tuple(map(float, (x0,y0))) | |
p1 = tuple(map(float, (x1,y1))) | |
p2 = tuple(map(float, (x2,y2))) | |
quads.append((p0,p1,p2)) | |
return quads, (width, height) | |
def scale_quads(quads, xscale, yscale): | |
scaled_quads = [] | |
for p0,p1,p2 in quads: | |
scaled_quads.append(( | |
(p0[0]*xscale,p0[1]*yscale), | |
(p1[0]*xscale,p1[1]*yscale), | |
(p2[0]*xscale,p2[1]*yscale), | |
)) | |
return scaled_quads | |
def translate_quads(quads, dx=0, dy=0): | |
translated_quads = [] | |
for p0,p1,p2 in quads: | |
translated_quads.append(( | |
(p0[0]+dx,p0[1]+dy), | |
(p1[0]+dx,p1[1]+dy), | |
(p2[0]+dx,p2[1]+dy), | |
)) | |
return translated_quads | |
def gcode_setup(args): | |
retval = [ | |
'G20', | |
g0(z=args.zclear) | |
] | |
return retval | |
def gcode_quad_stroke(args, quad): | |
points = interpolate_quad(*quad, segment_size=args.segment_size) | |
x,y = points[0] | |
retval = [g0(x,y,-3*args.zstroke)] | |
for x,y in points[1:-1]: | |
retval.append(g1(x,y,args.zstroke,args.stroke_speed)) | |
x,y = points[-1] | |
retval.append(g0(x,y,-5*args.zstroke)) | |
return retval | |
def gcode_get_paint(args): | |
retval = [ | |
g0(x=args.xpaint - args.clean_length, y=args.ypaint, z=args.zclear), | |
g0(args.xpaint, args.ypaint), | |
g0(z=args.zdip), | |
'G4 P%0.3f' % args.dip_time, | |
] | |
if args.clean_strokes: | |
retval.append(g0(z=args.zclean)) | |
for i in range(args.clean_strokes): | |
retval.extend([ | |
g0(x=args.xpaint + args.clean_length), | |
g0(x=args.xpaint) | |
]) | |
retval.extend([ | |
g0(z=args.zclear), | |
g0(x=args.xpaint - args.clean_length) | |
]) | |
return retval | |
if __name__ == "__main__": | |
parser = argparse.ArgumentParser(description='Convert a @PrimitivePic SVG to g-code for watercolor painting') | |
parser.add_argument('filename', type=str) | |
# Painting | |
parser.add_argument('--width', type=float, nargs='?', help='Painting width', required=True) | |
parser.add_argument('--height', type=float, nargs='?', help='Painting height', required=True) | |
parser.add_argument('-x', type=float, nargs='?', help='Painting origin (x)', default=0.0) | |
parser.add_argument('-y', type=float, nargs='?', help='Painting origin (y)', default=0.0) | |
# Stroke properties | |
parser.add_argument('--zstroke', type=float, nargs='?', help='Z height for brush strokes', default=-0.010) | |
parser.add_argument('--stroke_speed', type=float, nargs='?', help='Speed for brush strokes (in/sec)', default=3.0) | |
# Paint Jar | |
parser.add_argument('--zdip', type=float, nargs='?', help='Z height for dipping the brush in the paint', required=True) | |
parser.add_argument('--dip_time', type=float, nargs='?', help='Dwell time for dipping the brush (s)', default=0.25) | |
parser.add_argument('--zclear', type=float, nargs='?', help='Z height for clearing the paint jar', required=True) | |
parser.add_argument('--zclean', type=float, nargs='?', help='Z height for cleaning the brush on the side of the jar', required=True) | |
parser.add_argument('--xpaint', type=float, nargs='?', help='X location of the paint jar', required=True) | |
parser.add_argument('--ypaint', type=float, nargs='?', help='Y location of the paint jar', required=True) | |
parser.add_argument('--clean_length', type=float, nargs='?', help='Y distance of the cleaning stroke', default=1.0) | |
parser.add_argument('--clean_strokes', type=int, nargs='?', help='Number of strokes to clean the brush with', default=2) | |
parser.add_argument('--segment_size', type=float, nargs='?', help='Interpolation segment size (inches, approx)', default=0.010) | |
args = parser.parse_args(sys.argv[1:]) | |
# Get the quads and input image dimensions | |
quads, (width, height) = extract_quads(args.filename) | |
# Fit the quads to the page | |
painting_width = args.width | |
painting_height = args.height | |
if width >= height: | |
scale_factor = painting_width/width | |
else: | |
scale_factor = painting_height/height | |
# Scale, and flip in the Y so painted right-side-up | |
quads = scale_quads(quads, scale_factor, -scale_factor) | |
quads = translate_quads(quads, dy=painting_height) | |
quads = translate_quads(quads, args.x, args.y) | |
# G-Code program output | |
program = [] | |
program.extend(gcode_setup(args)) | |
for quad in quads: | |
program.extend(gcode_get_paint(args)) | |
program.extend(gcode_quad_stroke(args, quad)) | |
# Return to the jar so the brush doesn't dry out | |
program.append(g0(z=args.zclear)) | |
program.append(g0(args.xpaint, args.ypaint)) | |
program.append(g0(z=args.zdip)) | |
# Write the ouput to a file | |
output_name = os.path.splitext(os.path.basename(args.filename))[0] + '.g' | |
with open(output_name, 'w' ) as fp: | |
fp.write('\n'.join(program)) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment