####################################################################################################
# This program is inspired by the szg c++ demo cosmos.cpp, though some modifications have occured.
# It demonstrates how to construct a simple user interface from scratch, with the ability to
# manipulate (translate and rotate) the spinning torii, in addition to navigating around
# the scene.
# This is DISTINCT from using the Syzygy interaction framework, which is demo'ed in lorenz.py, for
# instance.
####################################################################################################

#############################################################################################
# Exercises:
# 1. Substitute a new navigator for the current one. It could operate via events 
#    (much like the manipulator) instead of polling. How about adding rotations to the 
#    navigator?
# 2. Write a new manipulator. Can you write one that uses polling instead of events?
#    How about a manipulator that does not translate the object? Or that uses
#    scaling to make the wand translation distance different from the resulting
#    object translation distance? How about adding the ability to scale the object?
# 3. Change the objects in the scene, eliminate light, or otherwise mess with the graphics.
#    Make the billboard appear/disappear.
#############################################################################################

# Import the Syzygy stub Python module.
from szg import *
# Need sys so we can parse command line arguments.
import sys
# Must measure wall clock time to get framerate independent animation.
import time
# If we use the "rays" they should be random colors.
import random

########################################
#    Initialize the global variables.  #
########################################

# trans1-trans4 are the scene graph nodes that hold the torii's transformations.
trans1 = None
trans2 = None
trans3 = None
trans4 = None
# Are we drawing rays? The default is no (but the command line arguments can change this).
useRays = 0
# The scene graph node that holds the points defining the radiating line segments (if such are used).
# NOTE: On slower computers (like a 600 MHz iBook), the framerate is actually limited by processing
# the ray points (but not, of course, in drawing them).
points = None
# If we are drawing rays, they start out at full length.
percentage = 1.0
# The visibility scene graph node associated with the billboard (so it can blink on and off).
viz = None
# The transfom node associated with the billboard.
billboardTrans = None
# A collection of objects that will receive input events and act on the scene graph. Only one
# is active in this sample code (a manipulator).
widgetList = []

# This class implements simple navigation. It is based on polling (though you could also come up
# with a navigator class based on input events, like the manipulator class below).
# The user travels through the scene following the wand direction by pushing the joystick forward
# and orthogonally to the wand direction by pushing the joystick from side to side.
# The class makes assumptions about the input device. Comments in the code explain what they are.
class navigator:
	# Constructor.
	def __init__(self):
		# The scene graph "navigation node". All geometry that will be effected by navigation
		# must be attached below this node. Note that it is possible to have some geometry
		# that does not move during navigation (think of a head's-up display or a tool the
		# user has).
		self.node = None
		# Must keep track of the time between updates so we can move at a fixed speed.
		self.lastTime = -1
		# The speed in units per second.
		self.speed = 1.0
		# The code assumes joystick axis values range between -1 and 1. Do not navigate
		# unless the absolute value of an axis is greater than threshold.
		self.threshold = 0.4

	# This function is called once per event loop. Thus, the navigator class works by "polling".
	# Must pass in the framework object (which gives us access to current input values) and the
	# current time.
	def update(self, framework, time):
		# If this is the first time calling the method, just set the time called and return.
		if self.lastTime < 0:
			self.lastTime = time
			return
		# Assumes that the "neutral" position of the wand (which is matrix 1) is pointed straight at 
		# the front CAVE wall (and that we are using the CAVE coordinate system, whereby a front pointing vector would
		# be (0,0,-1) ).
		# ar_ERM is shorthand for ar_extractRotationMatrix. This is related to some other calls ar_ETM (ar_extractTranslationMatrix)
		# and ar_ESM (ar_extractScaleMatrix). If a matrix is assumed to be the product of T*R*S (translation, rotation, and scale 
		# where the scale matrix is assumed to be uniform), then we can easily compute the decomposition, which is what these
		# functions do.
		wandDir = ar_ERM(framework.getMatrix(1))*arVector3(0,0,-1)
		# In the CAVE coordinate system, (1,0,0) would be to the right. Again, we "extract" the rotation from the wand's transform.
		wandTransDir = ar_ERM(framework.getMatrix(1))*arVector3(1,0,0)
		# Figure out the navigation deltas, based on the joystick values. This is another assumption 
		# about our input device. It should have two axes, each reporting values between -1 and 1. The
		# first axis should be horizontal and report 1 when pushed to the right. The second axis
		# should be vertical and report 1 when pushed forward.
		# To get around problems with the joystick not reporting exactly (0,0) in its neutral position,
		# there must be an action threshold.
		deltaX = framework.getAxis(0)
		deltaY = framework.getAxis(1)
		# Note how the joystick response above the threshold value gets scaled to give a full range.
		if abs(deltaX) < self.threshold:
			deltaX = 0
		else:
			deltaX = (deltaX - self.threshold) / (1.0 - self.threshold)
		if abs(deltaY) < self.threshold:
			deltaY = 0
		else:
			deltaY = (deltaY - self.threshold) / (1.0 - self.threshold)
		# t is the egocentric translation vector.
		t = (wandDir * deltaY + wandTransDir * deltaX) * (time - self.lastTime) * self.speed 
		# Since we are (in the scene graph context) moving the world instead, we must apply the inverse
		# transformation (egocentric motion to exocentric motion). ar_TM is shorthand for ar_TranslationMatrix and
		# returns an appropriate arMatrix4 from its arVector3 input.
		self.node.set(ar_TM(-t)*self.node.get())
		self.lastTime = time

# This class implements a simple means of grabbing, manipulating an object's transform (rotation and translation), 
# and then releasing the object. It is event based (processes a stream of Syzygy input events), instead of polling based.	
class manipulator:
	# The constructor.
	def __init__(self):
		# The transform node to be manipulated.
		self.node = None
		# The matrix held by the manipulation tool at the grab.
		self.wandMatrixAtGrab = None
		# The matrix held by the manipulated transform node at the grab.
		# This member is used as a flag indicating whether the object is currently grabbed.
		self.objMatrixAtGrab = None

	# Start grabbing the object.
	def attemptGrab(self, matrix):
		if not self.node:
			return
		self.objMatrixAtGrab = self.node.get()
		self.wandMatrixAtGrab = matrix

	# Release a grabbed object (if any is currently grabbed).
	def attemptRelease(self):
		# This data member is used as a flag indicating if the object is currently grabbed.
		self.objMatrixAtGrab = None

	# If an object is grabbed, change its transform in response to user interface events.
	def attemptDrag(self, matrix):
		# Check to see if the object is currently grabbed.
		if self.objMatrixAtGrab:
			# Recall that ar_ETM is shorthand for ar_extractTranslationMatrix and ar_ERM is shorthand
			# for ar_extractRotationMatrix. They assume the matrix can be written like T*R*S, where
			# S is a uniform scaling.
			tmp = ar_ETM(self.wandMatrixAtGrab)
			# Note how translation and rotation and treated seperately for our interaction technique.
			trans = ar_ETM(matrix)*tmp.inverse()*ar_ETM(self.objMatrixAtGrab)
			rot = ar_ERM(matrix)*ar_ERM(self.wandMatrixAtGrab.inverse())*ar_ERM(self.objMatrixAtGrab)
			# Don't forget to update the scene graph's transform node!
			self.node.set(trans*rot)

	# Processes the event stream. We also need access to the framework object (to get current event values).
	def update(self, framework, event):
		# Other valid event types are AR_EVENT_AXIS and AR_EVENT_MATRIX, with corresponding getAxis and getMatrix methods.
		if event.getType() == AR_EVENT_BUTTON and event.getIndex() == 0:
			# Button 0 (see getIndex above) has been pushed. Grab object.
			if event.getButton() == 1:
				self.attemptGrab(framework.getMatrix(1))
			# Button 0 has been released. Release object.
			else:
				self.attemptRelease()
		# Drag a grabbed object.
		self.attemptDrag(framework.getMatrix(1))
		

# By default, the scene graph uses lighting. Consequently, we must add lights, otherwise everything will
# be dark!
def addLights(g):
	# Get the scene graph's root node. Our light nodes will be its children.
	r = g.getRoot()
	# Create a new light node as a child of the root.
	l = r.new("light")
	# Create a light object (which will be stored in the light node).
	light = arLight()
	# All lights must have an ID (0-7). This should be unique (if not, the last defined light w/ that ID is used).
	light.lightID = 0
	# Since the last entry is 0, this is a directional light (as in OpenGL). If the last position were 1,
	# it would be a positional light.
	light.position = arVector4(0,0,-1,0)
	# No ambient component to the light.
        light.ambient = arVector3(0,0,0)
	# Dim white diffuse lighting.
        light.diffuse = arVector3(0.5, 0.5, 0.5)
	# Store the light in the scene graph node.
	l.set(light)
	# Create a new light node.
	l = r.new("light")
	# Create a new light.
	light = arLight()
	# Note that this lightID is different than 0 (the lightID of the previous light).
	light.lightID = 1
	light.position = arVector4(0,0,1,0)
        light.ambient = arVector3(0,0,0)
        light.diffuse = arVector3(0.5, 0.5, 0.5)
	# Store the light in the scene graph node.
	l.set(light)

# Add the rays to the scene graph below the node t.
def attachLineSet(t):
	# Must declare that we are using the global variable points in this function.
	# Otherwise, we'll only create a local copy (problematic since other functions
	# want to use this variable).
	global points
	plist = []
	# Make a list of line segment end points (for 150 line segments).
	for i in range(150):
		# A random direction.
		p = arVector3(-5 + 10 * random.random(),
                              -5 + 10 * random.random(),
                              -5 + 10 * random.random())
		if p.magnitude() == 0:
			p = arVector3(0,0,1)
		# Make sure this is a unit vector!
		p.normalize()
		plist.append(p)
		# The other endpoint of each line segment is the origin.
		plist.append(arVector3(0,0,0))
	# Create the points node as a child of t.
	points = t.new("points")
	# Put the points data in the node.
	points.set(plist)
	# We want to individually color the lines. Here, there is a color listed for each vertex in the points node.
	# The line segments are non-uniform in color, with the first vertex having a random color and the second 
	# vertex being black.
	clist = []
	for i in range(150):
		c = arVector4(random.random(),
                              random.random(),
                              random.random(),
                              1)
		clist.append(c)
		clist.append(arVector4(0,0,0,0))
	# Create a new color4 node as a child of the points node.
	colors = points.new("color4")
	# Import the color data into the node.
	colors.set(clist)
	# Make a new "drawable" node as a child of the color4 node. When drawing, this node will use the data from
	# its ancestor points and color4 nodes.
	draw = colors.new("drawable")
	# The drawable node will draw 150 independent line segments.
	draw.set(("lines",150))

# This function is computationally expense in our Python implementation!
def lineChange(percentage):
	# Retrieve a list of arVector3's held by the points node.
	plist = points.get()
	# Change the 2nd vertex of each line segment.
	for i in range(150):
		plist[2*i+1] = plist[2*i]*percentage
	# Return the data to our node.
	points.set(plist)

# Attach geometry to the scene graph below node t.
def worldInit(t):
	# These variables must be declared global so they can be used by other functions.
	global trans1
	global trans2
	global trans3
	global trans4
	global viz
	global billboardTrans

	# Attach the first torus. Start with a transform node.
 	trans1 = t.new("transform")
	# We want to draw our torus using a texture (the geometry of arTorusMesh already has built-in texture coordinates).
 	tex = trans1.new("texture")
	# The file name where the texture resides.
 	tex.set("WallTexture1.ppm")
	# It is also possible to give the object an overall coloring (in addition to the texture). You'll notice that
	# one of the torii is a little red!  Create a new material node to hold the material.
 	m = tex.new("material")
	# Create a new material object.
 	mat = arMaterial()
	# Set the diffuse color (there are also the other components of the OpenGL color model...)
 	mat.diffuse = arVector3(1,0.6,0.6)
	# Store the material in a scene graph node.
 	m.set(mat)
	# Make a torus object with 60 polygons around its "big radius" (around the hole) and 30 polygons around its "small radius"
	# (around the tube). This object will have 60*30*2=3600 triangles. The distance from the center of the object to the center
	# of the tube is 4. The distance from the center of the tube to the tube's edge is 0.5.
  	torus = arTorusMesh(60,30,4,0.5)
	# Attach the torus geometry below the material node. That way, when the geometry is drawn, it will use its ancestor
	# material and texture nodes.
	torus.attachMesh(m)

	# Add another torus to the scene graph.
	trans2 = t.new("transform")
 	tex = trans2.new("texture")
 	tex.set("WallTexture2.ppm")
  	torus.reset(60,30,2,0.5)
	torus.attachMesh(tex)

	# Add another torus to the scene graph.
	trans3 = t.new("transform")
 	tex = trans3.new("texture")
 	tex.set("WallTexture3.ppm")
  	torus.reset(60,30,1,0.5)
	torus.attachMesh(tex)

	# Add another torus to the scene graph.
	trans4 = t.new("transform")
 	tex = trans4.new("texture")
 	tex.set("WallTexture4.ppm")
  	torus.reset(60,30,3,0.5)
	torus.attachMesh(tex)

	viz = trans1.new("visibility")
	viz.set(1)
	billboardTrans = viz.new("transform")
	billboardTrans.set(ar_TM(4.55,0,0)*ar_RM('x', -1.571)*ar_RM('y', -1.571)*ar_SM(0.1))
	billboard = billboardTrans.new("billboard")
	billboard.set(" myriad scene graph ")

# Called each frame to update the scene graph.
def worldAlter(elapsedTime):
	# We want to access the following global variables.
	global trans1
	global trans2
	global trans3
	global trans4
	global percentage

	# Rotate the torii (based on elapsed times) and varying speeds.
	# ar_RM is shorthand for ar_rotationMatrix. Given an axis and an angle (in radians), it returns the appropriate rotation matrix.
        trans1.set(ar_RM('x', elapsedTime *  1.6))
	trans2.set(ar_RM('y', elapsedTime *  3.1))
	trans3.set(ar_RM('z', elapsedTime *  2.1))
	trans4.set(ar_RM('z', elapsedTime * -5.5))

	# Rotate the billboard around the perimeter of one of the torii.
	billboardTrans.set(ar_RM('z', elapsedTime*0.02)*billboardTrans.get())
	# If we are using the rays, change their representation by a little bit.
	if useRays:
		percentage -= 0.05
		if percentage < 0:
			percentage = 1
		lineChange(percentage)

##########################################################
#                     Main Program                       #
##########################################################

class CosmosApp( arDistSceneGraphFramework ):
# The event processing callback. All it does is grab the events that have queued
# since last call and send them to the items on the widget list. A very generic function.
# NOTE: callbacks of this type must return 0/1 (i.e. bool on the C++ side).
  def onInputEventQueue(self, eventQueue):
    while not eventQueue.empty():
      e = eventQueue.popNextEvent()
      for w in widgetList:
        w.update(self, e)
    return 1
	
# This is a distributed scene graph program. We need a framework object to do the management.
f = CosmosApp()

# Distributed scene graph programs can run in one of two modes:
#  Automatic graphics buffer swap: In this case, the library
#   itself decides when the buffer swap should occur.
#   This is the default.
#  Manual graphics buffer swap: The programmer is responsible
#   for requesting a buffer swap. This is better for times
#   when precise control over the contents of successive frames
#   is desired.
# We choose manual swap (by setting auto swap to false). 
# This must occur BEFORE f.init(...).
f.setAutoBufferSwap(0)

# This initializes the framework, setting mode of operation and configuration.
# Configuration information comes from the Phleet (the Syzygy distributed OS).
# PLEASE NOTE: All Syzygy programs accept a collection of "special" Phleet args
# (of the form -szg foo=bar). These are stripped from the command line by init.
# Consequently, the framework init must come BEFORE argument parsing!
if f.init(sys.argv) != 1:
	sys.exit()

# Parse command line arguments. Valid ways to invoke the program:
#   python cosmos.py
#     Spinning torii with no rays. Better for slow machines. Computing the new ray sizes is actually inefficient in Python!
#   python cosmos.py 1
#     Spinning torii with rays. Similar to the original cosmos demo. Better for a modern (2005) machine.
if len(sys.argv) > 1:
	useRays = int(sys.argv[1])

# Get the scene graph database held by the framework object. All of our geometry, etc. goes in here.
g = f.getDatabase()
# Add lights to the scene. By default, lighting is turned on in the Myriad scene graph. So, without this
# statement, the scene would be dark!
addLights(g)
# Attach a transform node to the scene graph root node. This will be the "navigation node". All other scene
# graph geometry will attach to it and our "navigator" object will change its embedded transform in response
# to user action.
navNode = g.getRoot().new("transform")
# Attach a new transform node to the navigation node. This node places the scene in a convenient spot.
# NOTE: a path of transform nodes down the scene graph tree operates much like a sequence of matrices in the OpenGL stack.
worldTrans = navNode.new("transform")
# ar_TM is an abbreviation for ar_translationMatrix, which returns an arMatrix4 expresses the expected translation.
worldTrans.set(ar_TM(0,5,-5))
# Attach a transform node to worldTrans. This new node will be used to drag the objects around.
worldManip = worldTrans.new("transform")
# The torii are attached to the worldManip node. We want to be able to drag them around after all.
worldInit(worldManip)
# If rays are requested, attach them to the worldManip node as well (they should drag around with the torii).
if useRays:
	attachLineSet(worldManip)

# Create a navigator object and tell it the scene graph's navigation node.
nav = navigator()
nav.node = navNode
# Create a manipulator object and tell it the scene graph node to be manipulated.
manip = manipulator()
manip.node = worldManip
# The manipulator must be added to the widgetList (which contains all objects that will receive input events).
# See eventProcessing(...) above.
widgetList.append(manip)

# Starts all the services, windows, etc. NOTE: it makes the most sense to do
# this AFTER lengthy initializations of the scene graph (though you can issue
# this statement any time after initializing the framework).
if f.start() != 1:
	sys.exit()

# Just using the wall clock time for torus animation does not yield very smooth results. Hence this hack.
counter = 0
while 1:
	# The event processing callback is called from within this method
	f.processEventQueue()
	# Wall clock time seems OK for navigation. Here, we are using our custom navigator class.
	nav.update(f, time.clock())
	# Get the user head position/orientation from control device and post to
	# the framework. By making this step explicit, we allow greater user control.
	# Without this statement, the scene will not change in response to head
	# position changes!
	f.setViewer()
	# Change the scene graph geometry!
	worldAlter(counter)
	# counter stores the (HACK) time.
	counter += 0.02
	# We have chosen the manual swap mode of operation for the scene graph framework.
	# Consequently, we must issue the buffer swap command ourselves.
	f.swapBuffers()
