#!/usr/bin/python
# $Header: /jch/play/cvsroot/twist/twist6.py,v 1.4 2003/09/07 03:57:36 jch Exp $
#
# DO SOME ART!!! jch - April 2003 - twist.py
# originally: /python1.5/site-packages/OpenGL/Demo - with python 1.5.6?
# currently: Python 2.3b1 (#1, Apr 28 2003, 20:29:14)
#  OpenGL.__version__ = '2.0.0.44'
#
# This is a total abortion of an application, but, it is still kind of cool.
# Working in progress started April 2003. The next version anitwist.py
# has a mass spring engine built in to drive the control points... but...
# Anyway, back to the matter at hand:
#
# twist1.py currently has 6 buttons: save, load, snap, points, regen, reset
#  save - saves the current NURB curve state. NO VIEWING INFO IS SAVED.
#  load - load the saved NURB curve
#  snap - saves a .jpg
#  points - toggles between showing the control points and the extrustions.
#  regen - regenerate the extrusions from the control points.
#  reset - does it do anything???
#
# TERRIBLE HACK UI for moving control points.
#  Mouse over the control point. When it goes to being a cross,
#  then hold down CTRL and click. It is now stuck to the cursor until
#  you control click again.
#
# I am currently running a mish mosh of versions - Python 2.3, PyOpenGL 2.0X,
# and I was not able to get wx to build, so, I stuck with tkIntr. Anything,
# as long as I don't have to think in order to know what to do.
#
# Humbly, YON - Jan C. Hardenbergh http://www.jch.com/jch
#
# This is statement is required by the build system to query build info
if __name__ == '__build__':
	raise Exception

import string
__version__ = ' One '
__date__    = '$Date: 2003/09/07 03:57:36 $'
__author__ = '$Author: jch $ aka YON - Jan C. Hardenbergh'
#'YON - jch@jch.com' # Tarn Weisner Burton <twburton@users.sourceforge.net>'

gApp = 0

# ========================================================================
# NURBS Playground
# ========================================================================
# ported to Python 26-APR-2003
#include <math.h>
#include <stdio.h>
#
#// Copyright YON - Jan C. Hardenbergh. Permission to copy is
#// granted as long as this copyright and this URL are maintained:
#// http://www.jch.com/NURBS/
#
#// Primary reference Les Peigl - On NURBS: A Survey published
#// in IEEE Computer Graphics and Applications (CG&A) January 1991.
#
#// http://www.cs.wpi.edu/~matt/courses/cs563/talks/nurbs.html
#
#// this is intended to explore NURBS - how they are evaluated.
#// It is explicitly unoptimized.

import Numeric
from Point import Point

def B(i,k,t,knots):
    ret = 0
    if k>0:
        n1 = (t-knots[i])*B(i,k-1,t,knots)
        d1 = knots[i+k] - knots[i]
        n2 = (knots[i+k+1] - t) * B(i+1,k-1,t,knots)
        d2 = knots[i+k+1] - knots[i+1]
        if d1 > 0.0001 or d1 < -0.0001:
            a = n1 / d1
        else:
            a = 0
	if d2 > 0.0001 or d2 < -0.0001:
            b = n2 / d2
        else:
            b = 0
        ret = a + b
        #print "B i = %d, k = %d, ret = %g, a = %g, b = %g\n"%(i,k,ret,a,b)
    else:
        if knots[i] <= t and t <= knots[i+1]:
            ret = 1
        else:
            ret = 0
    return ret

def C(t, order, points, weights, knots):
    c = Point([0,0,0])
    rational = 0
    i = 0
    while i < len(points):
	b = B(i, order, t, knots)
        p = points[i] * (b * weights[i])
        c = c + p
        rational = rational + b*weights[i]
        i = i + 1
    return c * (1.0/rational)

def handleMotion(event):
	if gApp.getDragging():
		gApp.drag(event.x,event.y)
	else:
		found = gApp.testControlPoint(event.x,event.y)
		gApp.setHotControlPoint(found)
		if found > 0:
			gApp.gl.configure(cursor="crosshair")
		else:
			gApp.gl.configure(cursor="arrow")


def handleCB1(event):
	if gApp.getDragging():
		gApp.setDragging(0)
	else:
		gApp.setDragging(gApp.getHotControlPoint())
	
def ignore(event):
	pass

def close(x,y,px,py):
	tol = 4
	tx = x - px
	if tx < -tol or tx > tol:
		return 0
	ty = y - py
	if ty < -tol or ty > tol:
		return 0
	return 1

# ========================================================================
# Based on some code from the GL Extrusion 
# ========================================================================

from OpenGL.GL import *
from OpenGL.Tk import *
from OpenGL.GLE import *
from Tkinter import *
import sys, os, time, string
import LinearAlgebra
from tkFileDialog import *

# ========================================================================
# ========================================================================
# ========================================================================

class MyApp(Frame):

	def initLights(self):
		glMaterialfv(GL_FRONT, GL_AMBIENT, [0.2, 0.2, 0.2, 1.0])
		glMaterialfv(GL_FRONT, GL_DIFFUSE, [0.8, 0.8, 0.8, 1.0])
		glMaterialfv(GL_FRONT, GL_SPECULAR, [1.0, 0.0, 1.0, 1.0])
		glMaterialfv(GL_FRONT, GL_SHININESS, 10.0)
		glLightfv(GL_LIGHT0, GL_AMBIENT, [0.2, 0.1, 0.1, 1.0])
		glLightfv(GL_LIGHT0, GL_DIFFUSE, [0.5, 0.5, 0.5, 1.0])
		glLightfv(GL_LIGHT0, GL_SPECULAR, [1.0, 1.0, 1.0, 1.0])
		glLightfv(GL_LIGHT0, GL_POSITION, [1.0, 1.0, 1.0, 1.0]);   
		glLightfv(GL_LIGHT1, GL_AMBIENT, [0, 0, 0, 1.0])
		glLightfv(GL_LIGHT1, GL_DIFFUSE, [0.6, 0.6, 0.6, 1.0])
		glLightfv(GL_LIGHT1, GL_SPECULAR, [1.0, 1.0, 1.0, 1.0])
		glLightfv(GL_LIGHT1, GL_POSITION, [-1.0, 0, 0, 0.0]);   
		glLightfv(GL_LIGHT2, GL_AMBIENT, [0, 0, 0, 1.0])
		glLightfv(GL_LIGHT2, GL_DIFFUSE, [0.5, 0.6, 0.7, 1.0])
		glLightfv(GL_LIGHT2, GL_SPECULAR, [0.3, 1.0, 0.0, 1.0])
		glLightfv(GL_LIGHT2, GL_POSITION, [30.0, 8, 0, 1.0]);   
		# glLightModelfv(GL_LIGHT_MODEL_AMBIENT, [0.2, 0.2, 0.2, 1.0])
		glEnable(GL_LIGHTING)
		glEnable(GL_LIGHT0)
		glEnable(GL_LIGHT1)
		glEnable(GL_LIGHT2)
		glCullFace(GL_BACK)
		glEnable(GL_CULL_FACE)

	# Hack to make circle look better in extrrusion code - init
	# self.path = [[-0.3,-10,0]] self.twists = [self.startAngle] self.colors = [[1,1,1]]
	# and close - total hack that almost makes a clean loop.
	# self.path = self.path + [[0.3,-10,0]]
	# self.twists = self.twists + [self.startAngle]
	# self.colors = self.colors + [[1,1,1]]

	def evaluate(self,maxt,steps):
		order = 2
		self.path = []
		self.twists = []
		self.colors = []
		for i in range(steps+1):
			t = i*(maxt*.999/steps)
			c = C(t, order, self.controlPoints, self.weights,
			      self.knots)
			# c.Print()
			self.path = self.path + [[c[0],c[1],c[2]]]
			self.twists = self.twists + [self.startAngle-i*self.dangle]
			self.colors = self.colors + [[1,1,1]]


	def initGeometry(self):
		# do 6 tubes. So, we want 6 points at a radius of 1.0 that form a hexagon
		# and we want to start at the mid point between two of those and we want to
		# step around 240 degrees. So, the centers are
		# [1,0], [.5,RAD3/2], [-.5,RAD3/2], [-1, 0], [-.5,-RAD3/2], [.5,-RAD3/2]
		# we want to star at the midpoint of two of these [0,RAD3/2]
		# all of those * 2
		rad3 = 1.732
		self.contour = [[0,rad3]]
		self.normals = [[1,0]]
		hc = [[-1,rad3], [-2, 0], [-1,-rad3], [1,-rad3], [2,0], [1,rad3]]
		count = 24
		nSteps = count*2/3
		angle = 0.0
		step = 0
		for i in range(6):
			step = 0
			while step < nSteps:
				angle = angle + 6.28/(count)
				self.contour.append([hc[i][0]+math.cos(angle),hc[i][1]+math.sin(angle)])
				self.normals.append([math.cos(angle),math.sin(angle)])
				step += 1
			angle -= 3.14
			# print "angle %g, far %g"%(angle, angle)
			
		# main **************************************************
		self.path =  [[0,0,0]]
		self.drawControlPoints = 0
		
		# def NURBSEval():
		#  {0,-.5, 0, 1},{.5,-.5, 0, 0.5},{0.25, -0.0669873, 0, 1},
		#  {0, 0.3880254, 0, 0.5},{-.25,  -0.0669873, 0, 1},
		#  {-.5,-.5, 0, 0.5},{0,-.5, 0, 1}};
		# zScale = -6
		unscaledPoints = [Point([0,-.5, 0]),
				  Point([.5,-.5, 0]),
				  Point([0.25, -0.0669873, 0]),
				  Point([0, 0.3880254, 0]),
				  Point([-.25,  -0.0669873, 0]),
				  Point([-.5,-.5, 0]),
				  Point([0,-.5, 0])]
		bigScale = 20
		self.controlPoints = []
		for cp in unscaledPoints:
			self.controlPoints = self.controlPoints + [cp*bigScale]
		self.knots = [0,0,0,1,1,2,2,3,3,3]
		self.maxtparam = 3
                self.strokes = 10
		self.dangle = 21
		self.weights = [1,0.5,1,0.5,1,0.5,1]
		self.evaluate(self.maxtparam,self.maxtparam*self.strokes)
		self.quad = gluNewQuadric()
		gluQuadricOrientation (self.quad,GLU_OUTSIDE)
		
	def setupView(self):
		width = self.gl.winfo_width()
		height = self.gl.winfo_height()
		aspect = 2*float(width)/float(height)
		viewMatrix = glGetFloatv(GL_MODELVIEW_MATRIX)
		zNear = -100
		zFar = 0
		for i in range(len(self.controlPoints)):
			cp = self.controlPoints[i]
			vertex = Numeric.array([cp[0], cp[1], cp[2], 1.0])
			npcPoint = Numeric.matrixmultiply(vertex,viewMatrix)
			# print npcPoint
			if npcPoint[2] > zNear:
				zNear = npcPoint[2]
			if npcPoint[2] - 5 < zFar:
				zFar = npcPoint[2] + 5
		zNear = zNear
		zFar = zFar
		if zNear > -1:
			zNear = -1
		# print "near %g, far %g"%(zNear, zFar)
		glMatrixMode(GL_PROJECTION)
		gluPerspective(90, aspect, zNear, zFar )
		glMatrixMode(GL_MODELVIEW)

		
	def redraw(self, o):
		self.setupView()
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
		gleSetJoinStyle(TUBE_CONTOUR_CLOSED | TUBE_NORM_EDGE | TUBE_JN_CAP | TUBE_JN_ANGLE )
		glMaterialfv(GL_FRONT, GL_AMBIENT, [0.3, 0.4, 0.5, 1.0])
		glMaterialfv(GL_FRONT, GL_DIFFUSE, [0.8, 0.8, 0.8, 1.0])
		if self.drawControlPoints == 0:
			gleTwistExtrusion(self.contour, self.normals, [0,0,1],
					  self.path, self.colors, self.twists)
		else:
			glMaterialfv(GL_FRONT, GL_AMBIENT, [0.6, 0.5, 0.4, 1.0])
			glMaterialfv(GL_FRONT, GL_DIFFUSE, [0.8, 0.8, 0.8, 1.0])
			contour = [[-1,-1],[1,-1],[0,1]]
			gleTwistExtrusion(contour, contour, [0,0,1], self.path,
					  self.colors, self.twists)
			glMatrixMode(GL_MODELVIEW)
			for i in range(len(self.controlPoints)):
				glPushMatrix()
				cp = self.controlPoints[i]
				# cp.Print()
				if self.dragging - 1 == i:
					glMaterialfv(GL_FRONT, GL_AMBIENT, [0.5, 1, 0.8, 1.0])
					glMaterialfv(GL_FRONT, GL_DIFFUSE, [0.5, 1, 0.8, 1.0])
				else:
					glMaterialfv(GL_FRONT, GL_AMBIENT, [1, 0.5, 0.8, 1.0])
					glMaterialfv(GL_FRONT, GL_DIFFUSE, [1, 0.5, 0.8, 1.0])
				glTranslated(cp[0], cp[1], cp[2])
				gluSphere (self.quad, 0.7, 8, 8)
				glPopMatrix()

	def run(self):
		if self.animate == 0:
			self.animate = 1
			# self.run.after(50,self.doTick)
			self.run.after_idle(self.doTick)
			# 12 * 5 == 60
			self.runCount = 20
			self.recording = 1
		else:
			self.recording = 0
			self.animate = 0

	def doTick(self):
		if self.recording > 0:
			self.frame += 1
			self.SaveTo("frame%s.jpg"%(string.zfill("%d"%(self.frame),4)))
		if self.animate > 0:
			for i in range(len(self.twists)):
				self.twists[i] += 3
			self.gl.tkRedraw()
			self.runCount -= 1
			if self.runCount > 0:
				self.run.after_idle(self.doTick)
			else:
				self.animate = 0


	def save(self):
		newfile = asksaveasfilename(filetypes=[("twist paramters", "*.tws")])
		if (newfile==None or newfile==""):
			return
		if os.path.splitext(newfile)[1] != '.tws':
			newfile = newfile + '.tws'
		paramfile = open(newfile, 'w')
		paramfile.write("# twisty parameters 0.1 "+time.strftime("%a, %d %b %Y %H:%M:%S GMT\n", time.gmtime(time.time())))
		paramfile.write("<controlpoints>\n")
		for cp in self.controlPoints:
			paramfile.write("cp %8.3f %8.3f %8.3f\n"%(cp[0], cp[1], cp[2]))
		paramfile.write("</controlpoints>\n")
		#paramfile.write(self.knots.tostring())
		kstring = "<knots>"
		for knot in self.knots:
			kstring = kstring + " %d"%(knot)
		kstring = kstring + " </knots>\n"
		paramfile.write(kstring)
		paramfile.write("</DONE>\n")
		paramfile.close()
		print "Wrote parameter file "+newfile;

	def load(self):
		# d = FileDialog(master)
		# file = d.go()
		myPath = askopenfilename(filetypes=[("twist paramters", "*.tws")])
		if (myPath==None or myPath==""):
			return
		lines = open(myPath, 'r').readlines()
		state = 0
		knots = 0
		cPoints = 0
		weights = 0
		for l in lines:
			tokens = string.split(l)
			first_word = tokens[0]

			if first_word[0] == '#':
				pass

			elif first_word == '<controlpoints>':
				state = 1

			elif first_word == 'cp':
				if state == 1 and len(tokens) == 4:
					cp = Point([string.atof(tokens[1]), string.atof(tokens[2]), string.atof(tokens[3])])
					if cPoints == 0:
						cPoints = [cp]
						weights = [1]
					else:
						cPoints = cPoints + [cp]
						weights = weights + [1]
				else:
					print "bad control point "
					print tokens

			elif first_word == '</controlpoints>' and state == 1:
				state = 2

			elif first_word == '<knots>':
				if state == 2 and tokens[len(tokens)-1] == '</knots>':
					knots = [string.atoi(tokens[1])]
					ki = 2
					while ki < len(tokens) - 1:
						knots = knots + [string.atoi(tokens[ki])]
						ki += 1
					state = 3
				else:
					print 'Bad knotty'

			elif first_word == '</DONE>' and state == 3:
				state = 4

			else:
				print tokens
				print 'Junk found'

		if state == 4:
			self.controlPoints = cPoints
			self.weights = weights
			self.knots = knots
			self.maxtparam = knots[-1]
			self.regen()
		else:
			print "failed to parse %s last state %d"%(myPath, state)
		

	def snap(self):
		self.SaveTo("snap.jpg")

	def SaveTo( self, filename, format="JPEG" ):
		"""Snap current buffer to filename in format"""
		import Image # get PIL's functionality...
		width = self.gl.winfo_width()
		height = self.gl.winfo_height()
		glPixelStorei(GL_PACK_ALIGNMENT, 1)
		data = glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE)
		image = Image.fromstring( "RGB", (width, height), data )
		image = image.transpose( Image.FLIP_TOP_BOTTOM)
		# print dir(image)
		image.save( filename, format, quality=100 )
		print 'Snapshot image in %s'% (os.path.abspath( filename))
		return image
	
	def resetView(self):
                self.strokes = 50
		self.dangle = 4
		self.evaluate(self.maxtparam,self.maxtparam*self.strokes)
		self.gl.tkRedraw()

	def regen(self):
		self.evaluate(self.maxtparam,self.maxtparam*self.strokes)
		self.gl.tkRedraw()
	
	def testControlPoint(self, x, y):
		# vpMatrix = Numeric.array([[scale,0,0,width/2.0],
		#   [0,scale,0,height/2.0],[0,0,1,0],[0,0,0,1]])
		width = self.gl.winfo_width()
		height = self.gl.winfo_height()
		viewMatrix = glGetFloatv(GL_MODELVIEW_MATRIX)
		projMatrix = glGetFloatv(GL_PROJECTION_MATRIX)
		# concat = Numeric.matrixmultiply(projMatrix,viewMatrix)
		concat = Numeric.matrixmultiply(viewMatrix,projMatrix)
		# ivy = LinearAlgebra.inverse(concat)
		for i in range(len(self.controlPoints)):
			cp = self.controlPoints[i]
			vertex = Numeric.array([cp[0], cp[1], cp[2], 1.0])
			# print vertex
			npcPoint = Numeric.matrixmultiply(vertex,concat)
			if (npcPoint[3] < 0):
				continue
			px = npcPoint[0]/npcPoint[3]*width/2.0 + width/2.0
			py = height/2 - npcPoint[1]/npcPoint[3]*height/2.0
			# print px, py
			# print "got %d %d %d - t %d %d"%(i,x,y,px,py)
			if close(x,y,px,py):
				return i+1
		# print "none %d %d"%(x,y)
		return 0
	
	def setDragging(self, index):
		self.dragging = index
		if index == 0:
			self.lastY = -1
		self.gl.tkRedraw()
		
	def getDragging(self):
		return self.dragging
		
	def setHotControlPoint(self, index):
		self.hotControlPoint = index
		
	def getHotControlPoint(self):
		return self.hotControlPoint
		
	def drag(self, x, y):
		if self.lastY == -1:
			self.lastX = x
			self.lastY = y
		else:
			tx = x - self.lastX
			ty = y - self.lastY
			##############################################
			width = self.gl.winfo_width()
			height = self.gl.winfo_height()
			viewMatrix = glGetFloatv(GL_MODELVIEW_MATRIX)
			projMatrix = glGetFloatv(GL_PROJECTION_MATRIX)
			concat = Numeric.matrixmultiply(viewMatrix,projMatrix)
			index = self.getHotControlPoint() - 1
			cp = self.controlPoints[index]
			vertex = Numeric.array([cp[0], cp[1], cp[2], 1.0])
			npcPoint = Numeric.matrixmultiply(vertex,concat)
			npcZ = npcPoint[2]/npcPoint[3]
			npcX = npcPoint[0]/npcPoint[3] + 2.0*tx/float(width)
			npcY = npcPoint[1]/npcPoint[3] - 2.0*ty/float(height)
			vertex = Numeric.array([npcX, npcY, npcZ, 1.0])
			ivy = LinearAlgebra.inverse(concat)
			newcp = Numeric.matrixmultiply(vertex,ivy)
			cpz = newcp[2]/newcp[3]
			cpx = newcp[0]/newcp[3]
			cpy = newcp[1]/newcp[3]
			# print "new cp %g %g %g"%(cpx, cpy, cpz)
			self.controlPoints[index].Assign(Point([cpx,cpy,cpz]))
			# self.controlPoints[index].Print()
			##############################################
			self.lastX = x
			self.lastY = y
			self.gl.tkRedraw()

	def togp(self):
		if self.drawControlPoints == 0:
			self.drawControlPoints = 1
			self.gl.bind("<Motion>",handleMotion)
			self.gl.configure(cursor="hand1")
		else:
			self.drawControlPoints = 0
			self.gl.bind("<Motion>",ignore)
			self.gl.configure(cursor="arrow")
		self.gl.tkRedraw()
		
	def __init__(self):
		global gApp
		import OpenGL
		print OpenGL.__version__
		self.f = Frame()
		self.f.pack()

		self.gl = Opengl(width = 704, height = 536, double = 1, depth = 1)
		self.gl.redraw = self.redraw
		self.gl.autospin_allowed = 1
		self.gl.pack(side = TOP, expand = YES, fill = BOTH)
		self.gl.set_background(255,0,0)
		#self.gl.bind("<Motion>",handleMotion) 
		# self.gl.bind("<B1-Motion>", ignore)
		# self.gl.help()
		self.hotControlPoint = 0
		self.dragging = 0
		self.animate = 0
		self.startAngle = 0
		self.frame = 0
		
		self.initLights()
		self.initGeometry()

		self.sv = Button(self.f, text="Save", command=self.save)
		self.sv.pack(side='left')
		self.ld = Button(self.f, text="Load", command=self.load)
		self.ld.pack(side='left')
		self.b = Button(self.f, text="Snap", command=self.snap)
		self.b.pack(side='left')
		self.cp = Button(self.f, text = 'Points', command = self.togp)
		self.cp.pack(side = 'left')
		self.reg = Button(self.f, text = 'Regen', command = self.regen)
		self.reg.pack(side = 'left')
		self.run = Button(self.f, text = 'Run',
				  command = self.run)
		self.run.pack(side = 'left')
		self.reset = Button(self.f, text = 'HRes',command = self.resetView)
		self.reset.pack(side = 'left')

		# print self.f.config()
		# print "next"
		# print dir(self.f)

		self.gl.bind("<Control-Button-1>",handleCB1)

		self.lastX = 0
		self.lastY = -1

		print "gApp %d"%(gApp)
		gApp = self
		
		self.gl.mainloop()


app = MyApp()
