#!BPY
"""
Name: 'Parametric Object'
Blender: 248
Group: 'Object'
Tooltip: 'Add an object based on mathematical equations.'
"""
__author__ = "Ed Mackey"
__url__ = ("http://www.blinken.com/blender_plugins.html", "blender", "elysiun", "http://www.python.org/doc/2.3.5/lib/module-math.html")
__version__ = "1.0 10/19/2005"
__bpydoc__ = """\
This script creates an object from a set of equations.

It's handy to know a little math to get the most out of using this.  It will
generate a 3D object from a 2D surface, using u and v as parameters to drive
three parametric equations for x, y, and z.  The plugin will ask Python to
compile these equations prior to iterating them, for speed.

You don't have to understand everything right off the bat.  This plugin 
comes with a "Presets..." button, that gives you a choice of common shapes 
(Sphere, Torus, Ripples, Sproingie, etc).  Click the preset shape you're
interested in, and the formulas are loaded into the interface for you.  Then
just click "New object" and you should see your requested shape!  Most of the
presets are low-res, but you can increase the resolution by upping the
number of steps for u and v.  Raise the values slowly, because the amount of
computation required can dramatically increase for large numbers of steps.

See link1 for screenshots and more information:
http://www.blinken.com/blender_plugins.html

See link4 for Python's math module documentation:
http://www.python.org/doc/2.3.5/lib/module-math.html
"""

# -------------------------------------------------------------------------- 
# Parametric Object plugin By Ed Mackey
# -------------------------------------------------------------------------- 
# ***** BEGIN GPL LICENSE BLOCK ***** 
# 
# This program is free software; you can redistribute it and/or 
# modify it under the terms of the GNU General Public License 
# as published by the Free Software Foundation; either version 2 
# of the License, or (at your option) any later version. 
# 
# This program is distributed in the hope that it will be useful, 
# but WITHOUT ANY WARRANTY; without even the implied warranty of 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
# GNU General Public License for more details. 
# 
# You should have received a copy of the GNU General Public License 
# along with this program; if not, write to the Free Software Foundation, 
# Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA. 
# 
# ***** END GPL LICENCE BLOCK ***** 
# -------------------------------------------------------------------------- 

import Blender
from Blender import Draw, BGL
from math import *

EVENT_NONE     = 0
EVENT_EXIT     = 1
EVENT_GENERATE = 2
EVENT_UPDATE   = 3
EVENT_SETTINGS = 5
EVENT_GETFROM  = 6
EVENT_PRESET   = 7
EVENT_LOADTEXT = 8
EVENT_SAVETEXT = 9

titleString = "Parametric Object v" + __version__ + " by Ed Mackey"
selObj = None
canUpdate = 0
equations = []
ranges = []
theName = Draw.Create("ParametricObj")
print titleString

class guiEquation:
	def __init__(self, name, value):
		self.name = name
		self.value = Draw.Create(value)

class guiRange:
	def __init__(self, name, min, step, max, wrap):
		self.name = name
		self.min  = Draw.Create(min)
		self.step = Draw.Create(step)
		self.max  = Draw.Create(max)
		self.wrap = Draw.Create(wrap)

def parametricObject(obj, mesh):
	global equations, ranges, theName, selObj, canUpdate

	selObj = obj
	canUpdate = 1
	writeSettings(obj)

	xFunc = compile(equations[0].value.val, '<x equation>', 'eval')
	yFunc = compile(equations[1].value.val, '<y equation>', 'eval')
	zFunc = compile(equations[2].value.val, '<z equation>', 'eval')

	uStep = (ranges[0].max.val - ranges[0].min.val) / ranges[0].step.val
	vStep = (ranges[1].max.val - ranges[1].min.val) / ranges[1].step.val

	uRange = ranges[0].step.val
	if (ranges[0].wrap.val == 0):
		uRange = uRange + 1

	vRange = ranges[1].step.val
	if (ranges[1].wrap.val == 0):
		vRange = vRange + 1

	#print "uRange %d, vRange %d" % (uRange, vRange)
	for vN in range(vRange):
		v = ranges[1].min.val + (vN * vStep)
		for uN in range(uRange):
			u = ranges[0].min.val + (uN * uStep)
			#vert = Blender.NMesh.Vert(u, 0.0, v)
			x = eval(xFunc)
			y = eval(yFunc)
			z = eval(zFunc)
			vert = Blender.NMesh.Vert(x, y, z)
			vert.uvco = (u, v, 0.0)
			mesh.verts.append(vert)

	for vN in range(1, ranges[1].step.val+1):
		vThis = vN
		if (vThis >= vRange):
			vThis = 0
		for uN in range(1, ranges[0].step.val+1):
			uThis = uN
			if (uThis >= uRange):
				uThis = 0
			face = Blender.NMesh.Face()
			#print "uThis %d, vThis %d, uN %d, vN %d" % (uThis, vThis, uN, vN)
			face.v.append(mesh.verts[(vThis * uRange) + uThis])
			face.v.append(mesh.verts[(vThis * uRange) + uN - 1])
			face.v.append(mesh.verts[((vN-1) * uRange) + uN - 1])
			face.v.append(mesh.verts[((vN-1) * uRange) + uThis])
			mesh.addFace(face)

	mesh.name = theName.val
	mesh.update()
	obj.setName(theName.val)
	obj.select(1)
	obj.makeDisplayList()
	Blender.Redraw()

def prepareGUI():
	global equations, ranges, theName
	equations = []
	ranges = []
	theName.val = "ParametricObj"
	equations.append(guiEquation("x", "u"))
	equations.append(guiEquation("y", "v"))
	equations.append(guiEquation("z", "0.0"))
	ranges.append(guiRange("u", 0.0, 16, 1.0, 0))
	ranges.append(guiRange("v", 0.0, 16, 1.0, 0))

def askForPreset():
	global equations, ranges, theName
	p = Blender.Draw.PupMenu("Load preset:%t|Sphere|Cylinder|Cone|Torus|Coil|Ripple 1|Ripple 2|Parabolic Dish|Sproingie")
	if (p > 0):
		prepareGUI()
	if (p == 1):   # Sphere
		theName.val = "Sphere"
		equations[0].value.val = "sin(2*pi*u)*sin(pi*v)"
		equations[1].value.val = "cos(2*pi*u)*sin(pi*v)"
		equations[2].value.val = "cos(pi*v)"
		ranges[0].step.val = 32
		ranges[0].wrap.val = 1
	elif (p == 2):   # Cylinder
		theName.val = "Cylinder"
		equations[0].value.val = "cos(2*pi*u)"
		equations[1].value.val = "sin(2*pi*u)"
		equations[2].value.val = "v"
		ranges[0].step.val = 32
		ranges[0].wrap.val = 1
		ranges[1].min.val = -1.0
		ranges[1].step.val = 8
	elif (p == 3):   # Cone
		theName.val = "Cone"
		equations[0].value.val = "cos(2*pi*u)*v"
		equations[1].value.val = "sin(2*pi*u)*v"
		equations[2].value.val = "v*2"
		ranges[0].step.val = 32
		ranges[0].wrap.val = 1
		ranges[1].step.val = 8
	elif (p == 4):   # Torus
		theName.val = "Torus"
		equations[0].value.val = "sin(2*pi*u)*(2+(0.5*sin(2*pi*v)))"
		equations[1].value.val = "cos(2*pi*u)*(2+(0.5*sin(2*pi*v)))"
		equations[2].value.val = "cos(2*pi*v)*0.5"
		ranges[0].step.val = 64
		ranges[0].wrap.val = 1
		ranges[1].step.val = 32
		ranges[1].wrap.val = 1
	elif (p == 5):   # Coil
		theName.val = "Coil"
		equations[0].value.val = "sin(2*pi*u)*(2+(u*0.1*sin(2*pi*v)))"
		equations[1].value.val = "cos(2*pi*u)*(2+(u*0.1*sin(2*pi*v)))"
		equations[2].value.val = "cos(2*pi*v)*u*0.1+u*u*0.11"
		ranges[0].step.val = 256
		ranges[0].max.val = 5.0
		ranges[1].step.val = 32
		ranges[1].wrap.val = 1
	elif (p == 6):   # Ripple 1
		theName.val = "Ripple1"
		equations[2].value.val = "cos(8*pi*sqrt((u*u)+(v*v)))*0.04"
		ranges[0].min.val = -1.0
		ranges[0].step.val = 128
		ranges[1].min.val = -1.0
		ranges[1].step.val = 128
	elif (p == 7):   # Ripple 2
		theName.val = "Ripple2"
		equations[0].value.val = "sin(2*pi*u)*v"
		equations[1].value.val = "cos(2*pi*u)*v"
		equations[2].value.val = "cos(8*pi*v)*0.04"
		ranges[0].step.val = 64
		ranges[0].wrap.val = 1
		ranges[1].step.val = 128
	elif (p == 8):   # Dish
		theName.val = "Parabolic_Dish"
		equations[0].value.val = "sin(2*pi*u)*v"
		equations[1].value.val = "cos(2*pi*u)*v"
		equations[2].value.val = "v*v*0.25"
		ranges[0].step.val = 64
		ranges[0].wrap.val = 1
	elif (p == 9):   # Sproingie
		theName.val = "Sproingie"
		equations[0].value.val = "(sin(pi*v)**0.5)*((v*v-v+1)*2.3-1.3)*cos(2*pi*u)*(cos(8*pi*(u+0.125))*0.02+1)*0.5"
		equations[1].value.val = "(sin(pi*v)**0.5)*((v*v-v+1)*2.3-1.3)*sin(2*pi*u)*(cos(8*pi*(u+0.125))*0.02+1)*0.5"
		equations[2].value.val = "atan(v*6-3)*0.395+0.485"
		ranges[0].step.val = 64
		ranges[0].wrap.val = 1
		ranges[1].step.val = 64
	elif (p > 0):
		Blender.Draw.PupMenu("OOPS!%t|This option not yet available.")

def getProp(obj, name, val):
	try:
		prop = obj.getProperty(name)
		val = prop.data
	except:
		pass
	return val

def setProp(obj, name, val, type):
	try:
		prop = obj.getProperty(name)
		obj.removeProperty(prop)
	except:
		pass
	obj.addProperty(name, val, type)

def readSettings(obj):
	global equations, ranges, canUpdate, theName
	prepareGUI()
	#theName.val = getProp(obj, "ParmObj_name", theName.val)
	try:
		prop = obj.getProperty("ParmObj_name")
		theName.val = prop.data
		canUpdate = 1
	except:
		canUpdate = 0
	for eq in equations:
		eq.value.val = getProp(obj, "ParmObj_f_" + eq.name, eq.value.val)
	for r in ranges:
		r.min.val  = getProp(obj, "ParmObj_" + r.name + "_min",  r.min.val)
		r.step.val = getProp(obj, "ParmObj_" + r.name + "_step", r.step.val)
		r.max.val  = getProp(obj, "ParmObj_" + r.name + "_max",  r.max.val)
		r.wrap.val = getProp(obj, "ParmObj_" + r.name + "_wrap", r.wrap.val)

def writeSettings(obj):
	global equations, ranges, theName
	setProp(obj, "ParmObj_name", theName.val, 'STRING')
	for eq in equations:
		setProp(obj, "ParmObj_f_" + eq.name, eq.value.val, 'STRING')
	for r in ranges:
		setProp(obj, "ParmObj_" + r.name + "_min",  r.min.val, 'FLOAT')
		setProp(obj, "ParmObj_" + r.name + "_step", r.step.val, 'INT')
		setProp(obj, "ParmObj_" + r.name + "_max",  r.max.val, 'FLOAT')
		setProp(obj, "ParmObj_" + r.name + "_wrap", r.wrap.val, 'INT')

def loadFromText():
	global equations, ranges, theName
	texts = Blender.Text.Get()
	numTex = len(texts)
	if (numTex < 1):
		Blender.Draw.PupMenu("Oops%t|No Blender text documents are available to load.")
	else:
		menu = "Load settings from text:%t"
		for txt in texts:
			menu += ("|" + txt.name)
		t = Blender.Draw.PupMenu(menu)
		if (t > 0):
			txt = texts[t - 1]
			lines = txt.asLines()
			if (len(lines) < 7) or (lines[0][0:12] != "# Parametric"):
				Blender.Draw.PupMenu("Error%t|This text does not describe a parametric object.")
			else:
				prepareGUI()
				for line in lines:
					if (len(line) < 5):
						continue
					if (len(line) > 6) and (line[0:6] == ("Name: ")):
						theName.val = line[6:]
					for eq in equations:
						if (line[0:4] == (eq.name + " = ")):
							eq.value.val = line[4:]
					for r in ranges:
						if (line[0:3] == (r.name + ": ")):
							words = line.split()
							r.min.val = float(words[2])
							r.step.val = int(words[4])
							r.max.val = float(words[6])
							if (words[8] == "Yes"):
								r.wrap.val = 1

def saveAsText():
	global equations, ranges, theName
	textName = "PO_" + theName.val
	txt = None
	try:
		txt = Blender.Text.Get(textName)
	except:
		pass
	if (txt is not None):
		m = Blender.Draw.PupMenu("Overwrite "+textName+"?%t|Yes|No, rename|Cancel")
		if (m == 2):
			txt = Blender.Text.New(textName)
		elif (m != 1):
			return
	else:
		txt = Blender.Text.New(textName)
	txt.clear()
	txt.write("# Parametric Object v" + __version__ + "\n\n")
	txt.write("Name: " + theName.val + "\n\n")
	for eq in equations:
		txt.write(eq.name + " = " + eq.value.val + "\n")
	txt.write("\n")
	for r in ranges:
		if (r.wrap.val != 0):
			wrap = "Yes"
		else:
			wrap = "No"
		txt.write("%s: min %f steps %d max %f wrap %s\n" % (r.name, r.min.val, r.step.val, r.max.val, wrap))
	Blender.Draw.PupMenu("Object saved%t|Saved to text document: " + txt.name)

def event(evt, val):    # the function to handle input events
	global EVENT_NONE, EVENT_EXIT, EVENT_GENERATE, EVENT_UPDATE, EVENT_SETTINGS, EVENT_GETFROM, EVENT_PRESET, EVENT_LOADTEXT, EVENT_SAVETEXT

	if (evt == Draw.QKEY):
		Draw.Exit()    # exit when user presses Q
		return

	# The GUI updates after any event, so it can respond to different objects being
	# selected.  Certain buttons will or won't be present based on the selected object.
	# Anyone know a better way?
	Draw.Redraw(1)

def button_event(evt):  # the function to handle Draw Button events
	global equations, ranges, theName, selObj, canUpdate
	global EVENT_NONE, EVENT_EXIT, EVENT_GENERATE, EVENT_UPDATE, EVENT_SETTINGS, EVENT_GETFROM, EVENT_PRESET, EVENT_LOADTEXT, EVENT_SAVETEXT
	if evt == EVENT_EXIT:
		Draw.Exit()
	elif evt == EVENT_GENERATE:
		Blender.Window.EditMode(0)
		objs = Blender.Object.GetSelected()
		for obj in objs:
			obj.select(0)
		obj = Blender.Object.New('Mesh', 'ParametricObj')
		mesh = Blender.NMesh.New('ParametricObj')
		obj.link(mesh)
		scene = Blender.Scene.GetCurrent()
		scene.link(obj)
		x,y,z = Blender.Window.GetCursorPos()
		obj.setLocation(x, y, z)
		parametricObject(obj, mesh)
	elif evt == EVENT_UPDATE:
		objs = Blender.Object.GetSelected()
		if len(objs) == 1:
			obj = objs[0]
			if ((selObj == obj) and (canUpdate != 0)):
				Blender.Window.EditMode(0)
				mesh = obj.data
				mesh.faces = []
				if (mesh.edges is not None):
					mesh.edges = []
				mesh.verts = []
				parametricObject(obj, mesh)
			else:
				Blender.Redraw()
				Blender.Draw.PupMenu("Error%t|Please re-select the parametric object.")
		else:
			Blender.Redraw()
			Blender.Draw.PupMenu("Error%t|Please re-select the parametric object.")
	elif evt == EVENT_GETFROM:
		selObj = None
		canUpdate = 0
		objs = Blender.Object.GetSelected()
		if len(objs) == 1:
			selObj = objs[0]
			if (selObj.getType() != 'Mesh'):
				selObj = None
			else:
				readSettings(selObj)
		if (canUpdate == 0):
			Blender.Draw.PupMenu("Note%t|This object does not have stored parametric info.")
	elif evt == EVENT_PRESET:
		askForPreset()
	elif evt == EVENT_LOADTEXT:
		loadFromText()
	elif evt == EVENT_SAVETEXT:
		saveAsText()

def gui():              # the function to draw the screen
	global equations, ranges, theName, selObj, canUpdate, titleString
	global EVENT_NONE, EVENT_EXIT, EVENT_GENERATE, EVENT_UPDATE, EVENT_SETTINGS, EVENT_GETFROM, EVENT_PRESET, EVENT_LOADTEXT, EVENT_SAVETEXT
	BGL.glClearColor(0,0,0.4,1)
	BGL.glClear(BGL.GL_COLOR_BUFFER_BIT)
	BGL.glColor3f(1,1,1)
	numranges = len(ranges)
	y = (numranges * 25) + 25
	for r in ranges:
		BGL.glRasterPos2i(10, y+7)
		Draw.Text("Range " + r.name + ":")
		r.min  = Draw.Number("min", EVENT_SETTINGS, 70, y, 100, 20, r.min.val, -10000.0, r.max.val - 0.0001)
		r.step = Draw.Number("steps", EVENT_SETTINGS, 180, y, 100, 20, r.step.val, 1, 1024)
		r.max  = Draw.Number("max", EVENT_SETTINGS, 290, y, 100, 20, r.max.val, r.min.val + 0.0001, 10000.0)
		r.wrap = Draw.Toggle("wrap "+r.name, EVENT_SETTINGS, 400, y, 55, 20, r.wrap.val)
		y -= 25

	numequations = len(equations)
	y = ((numranges + numequations) * 25) + 30
	for eq in equations:
		BGL.glRasterPos2i(42, y+7)
		Draw.Text(eq.name + " =")
		eq.value = Draw.String("", EVENT_SETTINGS, 70, y, 385, 20, eq.value.val, 256)
		y -= 25

	y = ((numranges + numequations) * 25) + 65
	BGL.glRasterPos2i(25, y+7)
	Draw.Text("Name:")
	theName = Draw.String("", EVENT_SETTINGS, 70, y, 140, 20, theName.val, 50)
	Draw.Button("Load text...", EVENT_LOADTEXT, 270,y,80,20)
	Draw.Button("Save as text", EVENT_SAVETEXT, 355,y,100,20)

	y = ((numranges + numequations) * 25) + 100
	Draw.Button("NEW object", EVENT_GENERATE, 65,10,90,20)
	objs = Blender.Object.GetSelected()
	if len(objs) == 1:
		if ((selObj == objs[0]) and (canUpdate != 0)):
			Draw.Button("UPDATE object", EVENT_UPDATE, 160,10,120,20)
		if (objs[0].getType() == 'Mesh'):
			Draw.Button("Reload settings", EVENT_GETFROM, 285,10,100,20)
	Draw.Button("Presets...", EVENT_PRESET, 390,10,65,20)

	Draw.Button("Exit", EVENT_EXIT, 10,10,35,20)
	BGL.glRasterPos2i(10, y)
	Draw.Text(titleString)

################
# MAIN PROGRAM #
################
prepareGUI()
objs = Blender.Object.GetSelected()
if len(objs) == 1:
	selObj = objs[0]
	if (selObj.getType() != 'Mesh'):
		selObj = None
	else:
		readSettings(selObj)
Draw.Register(gui, event, button_event)  # registering the 3 callbacks