#!/usr/bin/python
#copyright 2007 ben lipkowitz
#you may use and distribute this program under the terms of the
#GNU General Public License v2 or later
#cxf2cnc.py - convert qcad .cxf fonts to rs274ngc g-code
#see http://wiki.linuxcnc.org/uploads/CXF%20Format.jpg
#TODO: get parameters from block at beginning of file
#TODO: properly support unicode characters

version = 0.5

import math
import re

#input
string = '''Pack my box with
five dozen liquor jugs'''

fontfile = "cxf_fonts/romans2.cxf"

#machining parameters
safe_height = 2
depth = -1.5
feed = 1000  # mm/min
xoffset = 0
yoffset = 0
scale = 1

#misc parameters
char_spacing = 3
word_spacing = 3
line_spacing = 0 #not used yet
monospace = False
cursive = False
#cursives = 'abcdefghjklmnopqrsuvwyzADHJLVNMRYUOZ'
cursives = 'abcdefghjklmnopqrsuvwyzADHJLVNMRYUOZitx' #add 'itx' for a lazy look
#cursives = 'abdefghjklmnopqrsuvwyzADHJLVNMRYUOZx' #for neat-splash
small = 0.001
oldx, oldy = 0, 0 #(0,0) is at the lower left corner of the first letter
old_key, new_key = None, None
new_stroke = True
arc_flop_test = True

class Character:
	def __init__(self, key):
		self.key = key
		self.stroke_list = []
		
	def __repr__(self):
		return "%s" % (self.stroke_list)
	
	def get_xmax(self):	
		try: return max([s.xmax for s in self.stroke_list[:]])
		except ValueError: return 0
	
	def get_ymax(self): 
		try: return max([s.ymax for s in self.stroke_list[:]])
		except ValueError: return 0
	
	def print_gcode(self):
		global xoffset, yoffset
		print "(character %s)" % sanitize(self.key)		
		for stroke in self.stroke_list:
			stroke.print_gcode()
		print ""

def continuous(newx, newy, new_char=False):
	dx = abs(newx - oldx)
	dy = abs(newy - oldy)
	if new_stroke: #line breaks, word boundaries
		return False
	elif cursive and old_key and new_key and (old_key in cursives) and (new_key in cursives):
		return True
	elif new_char:
		return False
	elif (dx < small) and (dy < small):
		return True
	else: return False

class Line:
	def __init__(self, coords, new_char=False, parent_char=None):
		self.xstart, self.ystart, self.xend, self.yend = coords
		self.new_char = new_char #start of a new character
		self.parent_char = parent_char
		#I'm assuming that the character's lower left corner is at (0,0)
		#this may not always be a valid assumption. oh well.
		self.xmax = max(self.xstart, self.xend)
		self.ymax = max(self.ystart, self.yend)
		
	def __repr__(self):
		return "Line([%s, %s, %s, %s], %s)" % (self.xstart, self.ystart, self.xend, self.yend, self.new_char)

	def print_gcode(self):
		global oldx, oldy, new_stroke, old_key, new_key
		new_key = self.parent_char
		if not continuous(self.xstart, self.ystart, self.new_char):
			jump(self.xstart, self.ystart); plunge()
		x = (self.xend + xoffset) * scale
		y = (self.yend + yoffset) * scale
		print "G1 X%.4f Y%.4f" % (x, y)
		oldx, oldy = self.xend, self.yend
		new_stroke = False
		old_key = self.parent_char
	
class Arc:
	def __init__(self, coords, new_char=False, parent_char=None):
		self.xcenter, self.ycenter, self.radius, self.start_angle, self.end_angle = coords
		self.new_char = new_char #start of a new character
		self.parent_char = parent_char
		self.xstart = math.cos(self.start_angle * math.pi/180) * self.radius + self.xcenter
		self.ystart = math.sin(self.start_angle * math.pi/180) * self.radius + self.ycenter
		self.xend = math.cos(self.end_angle * math.pi/180) * self.radius + self.xcenter
		self.yend = math.sin(self.end_angle * math.pi/180) * self.radius + self.ycenter
		self.direction = 3 #default counterclockwise
		#print "xcenter %s, ycenter %s, radius %s, start_a %s, end_a %s, xstart %s, ystart %s, xend %s, yend %s" % \
		#	(xcenter, ycenter, radius, start_angle, end_angle, xstart, ystart, xend, yend)
		self.xmax = max(self.xstart, self.xend)
		self.ymax = max(self.ystart, self.yend)

	
	def __repr__(self):
		return "Arc([%s, %s, %s, %s, %s], %s])" %(self.xcenter, self.ycenter, self.radius, self.start_angle, self.end_angle, self.new_char)
	
	def print_gcode(self):
		global oldx, oldy, new_stroke, old_key
		new_key = self.parent_char
		#flop the arc over to reduce unnecessary jumps
		#if(not arc_flop_test):
		#	if continuous(self.xend, self.yend):
		#		self.direction = 2 #clockwise
		#		self.xstart, self.xend = self.xend, self.xstart
		#		self.ystart, self.yend = self.yend, self.ystart
			
		if not continuous(self.xstart, self.ystart, self.new_char):
			jump(self.xstart, self.ystart); plunge()
		x = (self.xend + xoffset) * scale
		y = (self.yend + yoffset) * scale
		i = (self.xcenter - self.xstart) * scale
		j = (self.ycenter - self.ystart) * scale
		print "G%d X%.4f Y%.4f I%.4f J%.4f" % (self.direction, x, y, i, j)
		oldx, oldy = self.xend, self.yend
		new_stroke = False
		old_key = self.parent_char

def jump(x,y):
	x = (x + xoffset) * scale
	y = (y + yoffset) * scale
	print "G0 Z%s" % safe_height
	print "G0 X%.5f Y%.5f" % (x, y)

def plunge():
	print "G1 Z%s F%s" % (depth,feed)


def sanitize(string):
	retval = ''
	for char in string:
		if char != ')' and char !='(':
			retval += char	
		else: retval += '?'
	return retval.__repr__()[0:200] #show pesky \n's, truncate for line length

def parse(file):
	font = {}
	key = None
	num_cmds = 0
	line_num = 0
	for text in file:
		#format for a typical letter (lowercase r):
		##comment, with a blank line after it
		#
		#[r] 3
		#L 0,0,0,6
		#L 0,6,2,6
		#A 2,5,1,0,90
		#
		line_num += 1
		end_char = re.match('^$', text) #blank line
		if end_char and key: #save the character to our dictionary
			font[key] = Character(key)
			font[key].stroke_list = stroke_list
			font[key].xmax = xmax
			if (num_cmds != cmds_readed):
				print "(warning: discrepancy in number of commands %s, line %s, %s != %s )" % (fontfile, line_num, num_cmds, cmds_readed)
			
		new_cmd = re.match('^\[(.*)\]\s(\d+)', text)
		if new_cmd: #new character
			key = new_cmd.group(1)
			num_cmds = int(new_cmd.group(2)) #for debug
			cmds_readed = 0
			stroke_list = []
			xmax, ymax = 0, 0
			new_char = True #jump to beginning of new character or else the first stroke is wrong
		
		line_cmd = re.match('^L (.*)', text)
		if line_cmd:
			cmds_readed += 1
			coords = line_cmd.group(1)
			coords = [float(n) for n in coords.split(',')]
			stroke_list += [Line(coords, new_char, key)]
			xmax = max(xmax, coords[0], coords[2])
			new_char = False
		
		arc_cmd = re.match('^A (.*)', text)
		if arc_cmd:
			cmds_readed += 1
			coords = arc_cmd.group(1)
			coords = [float(n) for n in coords.split(',')]
			stroke_list += [Arc(coords, new_char, key)]
			new_char = False
	return font

def print_string(font, string):
	global xoffset, yoffset, new_stroke
	line_height = max(font[key].get_ymax() for key in font)
	yoffset -= line_height #origin is at upper left corner of first character
	
	for char in string:
		if char == '\n': #carriage return
			xoffset = 0
			yoffset -= line_height
			new_stroke = True
			pass
		
		if char == ' ':
			xoffset += word_spacing
			new_stroke = True
			pass
			
		try:
			font[char].print_gcode()
			#move over for next character
			char_width = font[char].get_xmax()
			if monospace:
				xoffset += char_spacing
			elif cursive:
				xoffset += char_width
			else:
				xoffset += char_spacing + char_width
				
		except KeyError: 
			print "(warning: character %s not found)" % sanitize(char)
		
		#if char not in cursives:
		#	new_stroke = True

def main():
	file = open(fontfile)
	font = parse(file)
	print "(This file written by cxf2cnc.py version %s using %s)" % (version, sanitize(fontfile))
	print "G21"
	if cursive:
		print "G64 P0.3"
	#dump the contents of the font
	#string = ''
	#for char in font:
	#	string += char
	print "(%s)" % sanitize(string)
	print_string(font, string)
	print "m2"

if __name__ == "__main__":
	main()

