import Blender
from Blender import Object, NMesh, Window, Draw, Mathutils
from Blender.Mathutils import *
from math import sin, cos, sqrt, pi, hypot

# Import Psyco if available, psyco can often give substantial speed ups
try:
	import psyco
	psyco.log()
	psyco.profile()
	psyco.full()
except ImportError:
	pass


#
# ideas for the future are
# 1) back face culling
# 2) image based alpha maps
# basically just need to implement the image loading and selection code
# see this link for ideas on how to do it
# http://www.elysiun.com/forum/viewtopic.php?t=16326&highlight=getpixel
#
# 1) select edge loops that are perpendicular to the direction of motion
# 2) select face loops
# those would be nice for doing muscles for instance
# will implement rectanuglar formulaic alpha maps shortly
# tricolor displacement might be intersting
# ie each color represents displacement in the x, y, or z
# I'd like paintable selection
# and paintable fall off
# for symetric selection have a look at makehuman and the file
# MHsymmetry.py
# I'd also like to have an option where the alpha maps direction
# follows the direction of the mouse for implementing 'nudge'
# and pinch - perhaps use the mouse direction as a vector to affect the mesh
#
# other ideas are to make all of the changes to displacement maps
# which would then make it easy to 'drag' features around
# as well a maintaining ultra low poly models
# we could also create alpha maps from an object which would be pretty sweet
# a paint roller and tiling mode for alpha maps
# paint roller mode only affects mesh when you are moving between a specifed
# direction/angle range
# tiling mode treats the alphamap as a a tile

displaceMode = 1 # 0 is off, 1 is displaceOut, -1 is displaceIn, 2 is Smooth
# smooth isn't implemented yet

#displacementHeight should be turned into displacement ratio
#that is usally we want the height to stay proportional to the radius
#of the selection
#the level of zoom right now significantly affects the selection, because
#selection is in pixels, whereas displacement is in vert coordinates
#we can if we want, scale the displacement to pixels... thus a zoomed in
#view would displace much less than a zoomed out view.
displacementHeight = .5
allCoords = {} # this dictionary contains a mapping from the vertex ID to 
the verts screen coordinates
selectedVerts = []
dictOfAlphaMasks = {}
win_min_x = win_min_y = win_max_x = win_max_y = win_mid_x = win_mid_y = 
win_size_x = win_size_y = 0
boundx_max = boundy_max = boundx_min = boundy_min = 0 #these are the 
bounding coordinates for the object
mouseX = mouseY = 0
obj_mat_times_persp_mat = Matrix()
#the above are the corners of the 3d view window, and the scaling factors
myMesh = NMesh.New("temp")
radiusMinimum = 5 # the smallest size of the selection radius in pixels

# no user friendly gui yet
# to change the formula used, change fallofftype to
# one of None, Linear, Sin, Cos, SquareRoot, Squared, Cubic, and Quadratic
# then change the selectionRadius
# the xsize and ysize of the image are the first two numbers of the MaskID
# except in the case of the radial functions, in which case it is the radius
# also may change it to inner and outer radius or other such stuff
# the string is the id of the mask ID for formula based it is falloffType + 
maxRadius
falloffType = "Linear"
selection_size_x = selection_size_y = selectionRadius = 30
maskName = falloffType+repr(selectionRadius)
MaskID = (selectionRadius,selectionRadius,maskName)
boundingBox = []

myObject = Blender.Object.New('Mesh')
lastViewVector = [-100,100,-100] #right now these doesn't do much
currentViewVector = [-100,-100,-100] #but, if we allow screen rotation then 
they will trigger an update of the matrices

obj_mat = Matrix() #contains the matrix of the object (local? or global? I 
think global.)
perspMatrix = Matrix() #contains the perspective Matrix, and allows the 
screen coordinates to be projected to the screen

facesDirtyDict = {}
faceNormalWorldVecDict = {}
facedotVecDict = {}
vectorDirtyDict = {}
cacheVecCoMultDict = {}
vectorDirty = True
# gets the mesh data from the selected object
# finds the selected verts
# and then selects verts based on the alpha mask
def initialize():
	global myObject, myMesh
	myObject = Object.GetSelected()[0]
	if myObject.getType() == 'Mesh':
		myMesh = myObject.getData()
		name = myObject.name
	else:
		print "we either did not have a mesh or did not have an object selected"
	mouseX, mouseY = Window.GetMouseCoords()
	updateViewCoords(mouseX, mouseY)
	mouseInView3DWindow(mouseX, mouseY)
	updateRadius(MaskID[0], 0, falloffType) #this initializes the starting 
alphamask

# takes a vert and based on its screen location relative to the mouse
# is displaced according to an alphaMaskDict
# radius and falloff are done via computing the mask once
# the alphaMaskDict is specified by a MaskID
# mask ID is a tuple
# with four values,
# Xoffset, Yoffset, "maskName"
# for radius functions, maskName is "FallOffTypeRadius"
# ie for a linear of radius 8, it would be Linear8
# and the tuple would be (8,8,"Linear8")
# for imagebased, it is usually the image name
def DisplaceSelection(MaskID, mouseX, mouseY, displaceMode=0, 
displacementHeight= 0.0):
	global myMesh, dictOfAlphaMasks, allCoords
	selection = {}
	if MaskID not in dictOfAlphaMasks:
		# should handle image masks as well, but for now, we will handle only the 
radius masks
		updateRadius(MaskID[0], 0, falloffType)
	alphaMaskDict = dictOfAlphaMasks[MaskID]
	maskOffset = []
	maskOffset.append(MaskID[0])
	maskOffset.append(MaskID[1])
	displaceMH = displacementHeight*displaceMode
	mouse_and_maskX = maskOffset[0] - mouseX
	mouse_and_maskY = maskOffset[1] - mouseY
	viewVector = Vector(Window.GetViewVector())
	objectWorldMatrix = myObject.getMatrix('worldspace')
	perspMatrix = Blender.Window.GetPerspMatrix()
	obj_mat_times_persp_mat = objectWorldMatrix*perspMatrix
	for face in myMesh.faces:
		face = face
		#face = (tuple(face.normal))
		if face not in facesDirtyDict:
			gfn = VecMultMat(Vector(list(face.normal[:3])+[1.0]), objectWorldMatrix)
			gfn = Vector(list(gfn[:3]))
			faceNormalWorldVecDict[face] = gfn
			facesDirtyDict[face] = face.normal
			facedotVecDict[face] = DotVecs(gfn, viewVector)
		elif vectorDirty:
			gfn = faceNormalWorldVecDict[face]
			facedotVecDict[face] = DotVecs(gfn, viewVector)

		facedotVec = facedotVecDict[face]
		if facedotVec > 0: # simple backface-culling
			faceDirtyCount = 0
			for vertex in face.v:
				if (vertex.index not in selection):
					# we need to calculate the distance of the vert from the mouse
					# thus we need either the screen x and y of the vert
					# or the UV x,y of the mouse and of the vert
					# the offset is the distance from the mouse location, to the lower left 
corner
					# of the mask
					if(vertex.index in vectorDirtyDict) and (vectorDirtyDict[vertex.index] 
== vertex.co):
						vs = cacheVecCoMultDict[vertex.index]
					else:
						vectorDirtyDict[vertex.index] = vertex.co
						tempVec = vertex.co
						hvs = VecMultMat(Vector(list(tempVec[:3])+[1.0]), 
obj_mat_times_persp_mat)
						hvs[0] /= hvs[3]
						hvs[1] /= hvs[3]

						vs = [int(win_mid_x + (hvs[0] * win_size_x)),
							int(win_mid_y + (hvs[1] * win_size_y))]
						cacheVecCoMultDict[vertex.index] = vs

					deltaX = vs[0] + mouse_and_maskX
					deltaY = vs[1] + mouse_and_maskY
					try:
						thisDisplacement = alphaMaskDict[(deltaX, deltaY)]
						vertex.sel = 1
					except KeyError:
						thisDisplacement = 0.0
						vertex.sel = 0
					if vertex.sel == 1:
						#if we are just updating the selection
						if displaceMode == 0:
							pass
						#if we are displacing the vertex out
						#or if we are displacing the vertex in
						elif (displaceMode == 1) or (displaceMode == -1):
							tempDisplace = thisDisplacement*displaceMH
							vertex.co[0] += vertex.no[0]*tempDisplace
							vertex.co[1] += vertex.no[1]*tempDisplace
							vertex.co[2] += vertex.no[2]*tempDisplace
							#we need to update the screenx and screeny of the vertex in allCoords
							#based on the new vertex location
							faceDirtyCount += 1
						#this is to let the loop know that we've already handled this vertex
						#so it doesn't do calculations on it twice
						selection[vertex.index]=True
						#right now we don'l have smooth implemented
			if(faceDirtyCount > 0):
				try:
					del faceNormalWorldVecDict[face]
					del facesDirtyDict[face]
					del facedotVecDict[face]
				except KeyError:
					print "error deleting key"


#displaces verts towards the vert average normal
def SmoothSelection():
#we need to first calculate the average normal for the selected verts
#probably we should exlude outliers to give better results
#then find the difference between average normal and the current verts 
normal
	print "we smoothed the selection"
	pass

# takes an image and its size (or just an image and gets the size from the 
image?
# use code ideas from this link to implement
# http://www.elysiun.com/forum/viewtopic.php?t=16326&highlight=getpixel
# instead of doing positive and negative numbers
# i define all locations from the lower left corner
def alphaMaskDictFromAlphaMap(xsize, ysize, alphamap):
	global dictOfAlphaMasks
	alphaMaskDict = {}
	for x in range(0, xsize):
		for y in range (0, ysize):
			alphacoords = (x,y)
			thisDisplacement = alphamap.alphacoords
			alphaMaskDict[alphacoords] = thisDisplacement
	MaskID = (int(xsize/2),int(ysize/2), alphamap.name)
	dictOfAlphaMasks[MaskID] = alphaMaskDict
#
#
def updateRadius(selectionRadius, deltaRadius, falloffType):
	global MaskID, dictOfAlphaMasks
	print "update radius"
	selectionRadius += deltaRadius
	if selectionRadius < radiusMinimum:
		selectionRadius = radiusMinimum
	maskName = falloffType+repr(selectionRadius)
	MaskID = (selectionRadius, selectionRadius, maskName)
	if MaskID not in dictOfAlphaMasks:
		dictOfAlphaMasks[MaskID] = alphaMaskDictFromRadius(selectionRadius, 
falloffType)
	selection_size_x = selection_size_y = selectionRadius
	return selectionRadius

def updateDisplacement(displacementHeight, deltaHeight):
	displacementHeight = displacementHeight + deltaHeight
	return displacementHeight
#
# not needed but would be nice
def drawSelectionCircle(selectionRadius):
	#print "Drawing the selection circle"
	pass

# creates an alphamask from the radius supplied
# this gives two benefits - the first is that we can use any alphamask
# instead of using just radius based functions with falloff
# the second is that this gives us a lookup table and we don't have to
# calculate the radius function a bazillion times
def alphaMaskDictFromRadius(radius = 8, falloffType = "None"):
	#note that instead of negative numbers, I'm using an offset of
	#of the radius, and use all positive numbers
	thisRadius2 = 0
	radius2 = radius*radius
	alphaMaskDict = {}
	for x in range(0, radius):
		for y in range(0, radius):
			thisRadius2 = x^2+y^2
			if thisRadius2 <= radius2:
				thisRadius = sqrt(thisRadius2)
				thisDisplacement = radiusFunction(thisRadius, radius, falloffType)
				alphaMaskDict[( x+radius,-y+radius)]=thisDisplacement
				alphaMaskDict[(-x+radius, y+radius)]=thisDisplacement
				alphaMaskDict[(-x+radius,-y+radius)]=thisDisplacement
				alphaMaskDict[( x+radius, y+radius)]=thisDisplacement
	return alphaMaskDict

# falloffTypes are None, Linear, Sin, Cos, SquareRoot, Squared, Cubic, and 
Quadratic
# these are adapted from 'Taper and Twist'
# by flippyneck
def radiusFunction(thisRadius, maxRadius, falloffType="None"):
	if falloffType == "None":
		return 1.0
	if falloffType == "Cubic":
		return interpolateCubic(0,maxRadius, thisRadius)
	elif falloffType == "Quadratic":
		return interpolateQuadratic(0,maxRadius, thisRadius)
	elif falloffType == "Linear":
		return interpolateLinear(0, maxRadius, thisRadius)
	elif falloffType == "Sin":
		return interpolateSin(0, maxRadius, thisRadius)
	elif falloffType == "Cos":
		return interpolateCos(0,maxRadius, thisRadius)
	elif falloffType == "SquareRoot":
		return interpolateRoot(0,maxRadius, thisRadius)
	elif falloffType == "Squared":
		return interpolateLinear(0,maxRadius, thisRadius)

def interpolateLinear(min, max, myVal):
	''' returns 0.0 <= myVal <=1.0 '''
	return myVal/float((max-min))

def interpolateSin(min, max, myVal):
	n=interpolateLinear(min, max, myVal)
	return sin(n)

def interpolateCos(min, max, myVal):
	n=interpolateLinear(min, max, myVal)
	return cos(n)

def interpolateSquared(min, max, myVal):
	n=interpolateLinear(min, max, myVal)
	return n**2

def interpolateRoot(min, max, myVal):
	n=interpolateLinear(min, max, myVal)
	return sqrt(n)

def interpolateCubic(min, max, myVal):
	n=interpolateLinear(min, max, myVal)
	return n**3

def interpolateQuadratic(min, max, myVal):
	n=interpolateLinear(min, max, myVal)
	return n**4

# 
********************************************************************************

# init screen coordinates
# these should be updated if the screen is resized, etc.
def updateViewCoords(screen_x, screen_y):
	global win3d, win_mid_x, win_mid_y, win_max_x, win_max_y, win_min_x, 
win_min_y
	global win_size_x, win_size_y, acceptable_min_x, acceptable_min_y, 
acceptable_max_x, acceptable_max_y
	global selection_size_x, selection_size_y
	for win3d in Blender.Window.GetScreenInfo(Blender.Window.Types.VIEW3D):
		win_min_x, win_min_y, win_max_x, win_max_y = win3d.get('vertices')
		win_mid_x  = (win_max_x + win_min_x) * 0.5
		win_mid_y  = (win_max_y + win_min_y) * 0.5
		win_size_x = (win_max_x - win_min_x) * 0.5
		win_size_y = (win_max_y - win_min_y) * 0.5

		acceptable_min_x = screen_x - selection_size_x
		acceptable_min_y = screen_y - selection_size_y
		acceptable_max_x = screen_x + selection_size_x
		acceptable_max_y = screen_y + selection_size_y
		if (win_max_x > screen_x > win_min_x) and (win_max_y > screen_y > 
win_min_y):
			return True
		else:
			return False

def mouseInView3DWindow(mouseX, mouseY):
# we calculate the screens midpoints and the scaling factor for the screen
# and then check if the mouse is in the view3d area
# we should really only do the calculations the first time
# thus it should prbably
	global win3d, win_mid_x, win_mid_y, win_max_x, win_max_y, win_min_x, 
win_min_y
	global win_size_x, win_size_y, acceptable_min_x, acceptable_min_y, 
acceptable_max_x, acceptable_max_y
	global selection_size_x, selection_size_y
	for win3d in Blender.Window.GetScreenInfo(Blender.Window.Types.VIEW3D):
		win_min_x, win_min_y, win_max_x, win_max_y = win3d.get('vertices')
		if (win_max_x > mouseX > win_min_x) and (win_max_y > mouseY > win_min_y):
			return True
		else:
			return False




# Here is the main loop
mouseStartX, mouseStartY = Window.GetMouseCoords()
mouseX = mouseY = 0
radiusIncrement = 5
updateFrequency = 20 # number of pixels to move before updating the vert 
selection
selectedVerts = []
updateBounds = True

initialize()
done = 0
while not done:
	evt, val = Window.QRead()
	if evt in [4]: #Draw.RIGHTMOUSE is 3 which is wrong...
		# first we need to check if the mouse is inside the view3d boundbox
		currentViewVector = Window.GetViewVector()
		if currentViewVector != lastViewVector:
			lastViewVector = currentViewVector
			vectorDirty = True
		while Window.GetMouseButtons() == 4:
			if Window.GetKeyQualifiers() == 0:
				mouseX, mouseY = Window.GetMouseCoords()
				if mouseInView3DWindow(mouseX, mouseY):
					# determine if the mouse has moved far enough for an update
				#	mouseDistX = mouseStartX - mouseX
				#	mouseDistY = mouseStartY - mouseY
				#	mouseDistanceMoved = hypot(mouseDistX, mouseDistY)
				#	if mouseDistanceMoved > updateFrequency:
				#		mouseStartX, mouseStartY = mouseX, mouseY
					old_mode = Window.EditMode()
					Window.EditMode(0)
					DisplaceSelection(MaskID, mouseX, mouseY, displaceMode, 
displacementHeight)
					myMesh.update()
					Window.EditMode(old_mode)
					Blender.Window.Redraw(Window.Types.VIEW3D)
			elif Window.GetKeyQualifiers() != 0:
				if displaceMode == +1: displacement_string = "out"
				elif displaceMode == -1: displacement_string = "in"
				menu_result = Draw.PupMenu("Interactive Paint%t|%l|Selection Size: 
"+str(selectionRadius)+"%x1|Displacement: 
"+displacement_string+"%x2|%l|Quit%x3")
				if (menu_result == 1):
					selection_size = Draw.PupIntInput("Selection Size:", selectionRadius, 
1, 100)
					deltaRadius = selection_size - selectionRadius
					selectionRadius = updateRadius(selectionRadius, deltaRadius, 
falloffType)
				if (menu_result == 2):
					displaceMode *= -1
				if (menu_result == 3):
					done = True
				Draw.Redraw (Window.QTest())



				#drawSelectionCircle(selectionRadius)

#	elif evt in [Draw.SKEY]:
#		if val:
#			displaceMode = 2 #smooth mode
#		else:
#			displaceMode = 0
	elif not val or evt in [Draw.MOUSEX, Draw.MOUSEY]: continue #helps speed
	elif evt in [Draw.ESCKEY, Draw.QKEY]: done = True
	elif evt in [Draw.LEFTARROWKEY] and val: 	#shrink radius
		selectionRadius = updateRadius(selectionRadius, -radiusIncrement, 
falloffType)
	elif evt in [Draw.RIGHTARROWKEY] and val: 		#grow radius
		selectionRadius = updateRadius(selectionRadius, radiusIncrement, 
falloffType)
	elif evt in [Draw.UPARROWKEY] and val:				#grow displacementHeight
		displacementHeight = updateDisplacement(displacementHeight, 0.1)
	elif evt in [Draw.DOWNARROWKEY] and val:			#shrink displacementHeight
		displacementHeight = updateDisplacement(displacementHeight, -0.1)
	else:	#otherwise pass the event back and let Blender handle it
		id = Window.GetScreenInfo(Window.Types.VIEW3D)[0].get('id')
		Window.QAdd(id, evt, val)
		Window.QHandle(id)
		Blender.Redraw(-1) #don't forget to redraw
		pass

