#!/usr/bin/python
#copyright 2007 ben lipkowitz
#portions copyright jeff epler
#you may use and distribute this program under the terms of the
#GNU General Public License v2 or later
#pocket.py - draw a set of offset polyarcs from a given outline
#in order to simulate machining of a pocket

import gtk
import cairo
import math
import copy
endmill_dia = 0.05

def get_scale(cr):
	tmp = cr.user_to_device(1,1)
	scale = (tmp[0] + tmp[1]) / 2.
	return scale

class Point: # thanks jeff!
	def __init__(self, x=None, y=None):
		self.x, self.y = x, y
		
	def __add__(self, other):
		return Point(self.x + other.x, self.y + other.y)
	
	def __sub__(self, other):
		return Point(self.x - other.x, self.y - other.y)
	
	def __mul__(self, other):
		if isinstance(other, Point): #dot product
			return self.x * other.x + self.y * other.y
		return Point(self.x * other, self.y * other)
	
	__rmul__ = __mul__
	
	def __div__(self, other):
		return Point(self.x / other, self.y / other)
	
	def mag(self):
		return math.hypot(self.x, self.y)
	
	def unit(self):
		h = self.mag()
		if h: return self / h
		else: return Point(0,0)
	
	def rot(self, theta):
		c = math.cos(theta)
		s = math.sin(theta)	
		return Point(self.x * c - self.y * s,
			self.x * s + self.y * c)
	
	def angle(self):
		return math.atan2(self.y, self.x)
	
	def __repr__(self): return "<%.2f, %.2f>" % (self.x, self.y)
	
	def draw(self, cr):
		cr.arc(self.x, self.y, 3. / get_scale(cr), 0, 2 * math.pi)
		cr.fill()
	
class Segment:
	'''a doubly linked list'''
	def __init__(self):
		self.next = None
		self.previous = None
		self.segment_type = None
	
	#def __iter__(self):
	def next(self):
		if self.next == None:
			raise StopIteration
		return self.next
		
	def connect(self, next):
		self.next = next
		next.previous = self
		next.update()
		

class Line(Segment):
	'''well, its really just the endpoint'''
	def __init__(self, x, y):
		Segment.__init__(self)
		self.start = Point() #default, gets set when added to an outline
		self.midpoint = Point() #default, gets set when added to an outline
		self.end = Point(x,y)
		self.segment_type = "line"
		
	def trace(self,cr):
		cr.line_to(self.end.x, self.end.y)
		
	def dump(self):
		print "line: x", self.end.x, "y", self.end.y
		
	def update(self):
		self.start = self.previous.end
		self.midpoint = (self.start + (self.end - self.previous.end)/2)

class Arc(Segment):
	'''its really just the endpoint (and radius)'''
	def __init__(self, x, y, r):
		Segment.__init__(self)
		self.start = Point() #default, gets set when added to outline
		self.midpoint = Point() #the chord midpoint, because i'm lazy
		self.center = Point()
		self.end = Point(x,y)
 		self.r = r
		self.segment_type = "arc"

	def dump(self):
		print "arc: x", self.x, "y", self.y, "r", self.r
		
	def update(self):
		self.start = self.previous.end
		self.midpoint = (self.start + (self.end - self.previous.end)/2)
		#self.center = ..

	def arc_to(self, xe, ye, r, cr): #implicit direction
		'''for some stupid reason cairo doesn't have an arc_to command,
		so we have to make one. '''
		xs, ys = cr.get_current_point() #start of arc	
		l = math.hypot(ye-ys, xe-xs) #chord length
		
		#this may be buggy
		discrepancy = l-2*r
		if discrepancy > 0.0001:
			print "(warning: chord length greater than diameter by", discrepancy, ")"
		elif discrepancy > 0: r += discrepancy/2 		#just fix it in small cases

		ca = math.atan2(ye-ys, xe-xs) #chord angle
		
		#You know l and r. Then (from http://mathforum.org/dr.math/faq/faq.circle.segment.html#7)
		theta = 2*math.asin(l/(2*r))  #subtended arc angle
		#s     = r*theta		#arc length
		dc     = r*math.cos(theta/2) #distance to center
		#h     = r - dc 		#arc height
		#K     = r**2*(theta-math.sin(theta))/2 #arc area
		
		xc = xs + math.cos(ca)*l/2 + math.cos(ca-math.pi/2)*dc     #move over to chord midpoint and down to circle center
		yc = ys + math.sin(ca)*l/2 + math.sin(ca-math.pi/2)*dc		#+/- depending on cw/ccw
		
		start = math.atan2(ys-yc, xs-xc)
		end = math.atan2(ye-yc, xe-xc)
		angle = end - start

		cr.arc_negative(xc, yc, r, start, end)

		cr.move_to(xe, ye)

		
	def trace(self, cr):
		self.arc_to(self.end.x, self.end.y, self.r, cr)

class Outline:
	'''a series of lines and arcs defining a (hopefully closed) contour'''
	def __init__(self):
		self.head = None
		self.tail = self.head
	
	def push(self, segment):
		if self.head == None: #we're the first segment
			self.head = segment
			self.tail = self.head #initialize pointer
			return
		
		self.tail.connect(segment)
		self.tail = segment #new pointer
		
	def __add__(self, segment):
		#tmp = Outline()
		tmp = copy.copy(self) #i think i'm in over my head here
		tmp.push(segment)
		return tmp
		
	def trace(self, cr): #cr = cairo context
		'''return a cairo path for the outline'''
		cr.move_to(self.tail.start.x, self.tail.start.y)
		i = self.head
		while i:
			i.trace(cr)
			i = i.next
			
	def dump(self):
		i = self.head
		while i:
			i.dump()
			i = i.next
			
	def draw_endpoints(self, cr):
		i = self.head
		while i:
			i.end.draw(cr)
			i = i.next
		##this is what i want to do
		#for i in self:
		#	i.end.draw(cr)
			
	def draw_midpoints(self, cr):
		i = self.head.next
		while i:
			i.midpoint.draw(cr)
			i = i.next
	
	def offset(self, offset):
		retval = Outline()
		i = self.head
		while i:
			if i.segment_type == "line":
				#dx = i.x - i.previous.x
				#dy = i.y - i.previous.y
				retval += Line(i.end.x+0.1, i.end.y+0.1)
				#if i.previous.segment_type == "line":
				#find line angle, convex or concave
			elif i.segment_type == "arc":				
				retval += Arc(i.end.x+0.1, i.end.y+0.1, i.r)
			i = i.next
		return retval

def draw_diagram(height, width, cr):
	
	#workpiece
	cr.set_source_rgb(1, 0, 0)
	workpiece.trace(cr)
	cr.fill()
	#pocket
	cr.set_source_rgb(0, 1, 0)
	#cr.rectangle(0.25, 0.25, 0.5, 0.5)
	pocket.trace(cr)
	cr.fill()

	#draw the cleared area - sorta hackish because i couldnt figure out patterns
	path.trace(cr)
	cr.set_line_width(endmill_dia)
	cr.set_line_cap(cairo.LINE_CAP_ROUND)
	cr.set_line_join(cairo.LINE_JOIN_ROUND)
	cr.set_operator(cairo.OPERATOR_DEST_IN)
	cr.stroke()
	cr.set_operator(cairo.OPERATOR_OVER)

	#draw the ideal outlines
	cr.set_line_width(2./get_scale(cr)) #constant 2 pixel thickness, reduces jaggies
		
	cr.set_source_rgb(0,0,1)
	path.trace(cr)
	cr.stroke()
	path.draw_endpoints(cr)
	path.draw_midpoints(cr)
	
	cr.set_source_rgb(1,1,0)
	pocket.trace(cr)
	cr.stroke()
	pocket.draw_endpoints(cr)
	pocket.draw_midpoints(cr)
	
	cr.set_source_rgb(1,1,1)
	workpiece.trace(cr)
	cr.stroke()
	
def paint_selection(cr, selection, height, width):
	cr.identity_matrix()
	cr.translate(0.5,0.5)
	if selection.active:
		cr.save()
		cr.rectangle(selection.x, selection.y, selection.w, selection.h)
		cr.set_source_rgba(0,0,1,0.2)
		cr.fill_preserve()
		cr.set_source_rgba(1,1,1,1)
		cr.set_line_width(1./get_scale(cr)) #constant 1 pixel thickness
		cr.stroke()
		cr.restore()
	else: return

def expose(widget, event, selection):
	height, width = widget.get_allocation().height, widget.get_allocation().width
	#widget must be a gtk.gdk.Drawable to use cairo_create
	cr = widget.window.cairo_create()
	#optional, only redraw newly exposed area, for speed
	cr.rectangle(event.area.x, event.area.y, event.area.width, event.area.height)
	cr.clip()
	
	cr.translate(0.5,0.5) #move everything so the pixels line up
	scale = (height+width)/2
	cr.scale(scale,scale)
	
	draw_diagram(height, width, cr)
	paint_selection(cr, selection, height, width)

def key_press(widget, event):
	if event.string == 'q':
		gtk.main_quit()
	else: print "unbound key: '%s'" % event.string
	
def event_button_press(widget, event, selection):
	selection.active = True
	selection.x = event.x
	selection.y = event.y
	selection.w = 0
	selection.h = 0
	widget.queue_draw()

def event_motion(widget, event, selection):
	selection.w_last = selection.w
	selection.h_last = selection.h
	selection.w = event.x - selection.x
	selection.h = event.y - selection.y
	widget.queue_draw()
	#widget.queue_draw_area(selection.x, selection.y, 
	#	max(selection.w, selection.w_last), max(selection.h, selection.h_last) )

def event_button_release(widget, event, selection):
	selection.active = False
	widget.queue_draw()

class Selection:
	def __init__(self):
		self.active = False  # /* whether the selection is active or not */
		self.x, self.y = 0, 0
		self.w, self.h = 0, 0
		
def main():	
	window   = gtk.Window()
	window.connect("delete-event", gtk.main_quit)
	canvas = gtk.DrawingArea()
	#canvas.set_size_request(DEFAULT_WIDTH, DEFAULT_HEIGHT)	
	#pass rubberband-box info to the event handlers
	selection = Selection()
	canvas.connect("button_press_event", event_button_press, selection)
	canvas.connect("button_release_event", event_button_release, selection)
	canvas.connect("motion_notify_event", event_motion, selection)
	canvas.connect("expose-event", expose, selection)
	canvas.add_events(gtk.gdk.BUTTON1_MOTION_MASK
					| gtk.gdk.BUTTON_PRESS_MASK
					| gtk.gdk.BUTTON_RELEASE_MASK)
	window.add(canvas)
	window.show_all()
	gtk.main()

pocket = Outline()
pocket += Line(0, 0.3)
pocket += Arc(0.2, 0.1,0.2)
pocket += Arc(0.5, 0.1, 0.15 )
pocket += Line(0.5,0.1)
pocket += Line(0.6, 0.6)
pocket += Line(0.6, 0.05)
pocket += Line(0.1, 0.1)

workpiece = Outline()
workpiece += Line(0.2, 0.2)
workpiece += Line(0.2, 0.8)
workpiece += Line(0.8, 0.8)
workpiece += Line(0.8, 0.2)
workpiece += Line(0.2, 0.2)

path = pocket.offset(0.1)

#path.dumPoint()

if __name__ == "__main__":
	main()
