Skip to content

Instantly share code, notes, and snippets.

@ryansturmer
Created October 15, 2016 03:34
Show Gist options
  • Save ryansturmer/9f80aee98738b4fe6f63ba8cf14aa8fc to your computer and use it in GitHub Desktop.
Save ryansturmer/9f80aee98738b4fe6f63ba8cf14aa8fc to your computer and use it in GitHub Desktop.
Convert @PrimitivePic output to g-code for painting (quadratic bezier curves only)
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