# -*- indent-tabs-mode: t -*-

#!BPY

"""
Name: 'Cal3D'
Blender: 241
Group: 'Export'
Tip: 'Export armature/bone/mesh/action data to the Cal3D format.'
"""

# blender2cal3D.py
# Copyright (C) 2003-2004 Jean-Baptiste LAMY -- jibalamy@free.fr
# Copyright (C) 2004 Matthias Braun -- matze@braunis.de
#
# 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


__version__ = "0.14"
__author__  = "Jean-Baptiste 'Jiba' Lamy"
__email__   = "jibalamy@free.fr"
__url__     = "Soya3d's homepage http://home.gna.org/oomadness/en/soya/"
__bpydoc__  = """This script is a Blender => Cal3D converter.
(See http://blender.org and http://cal3d.sourceforge.net)

USAGE

To install it, place the script in your $HOME/.blender/scripts directory.
Then open the File->Export->Cal3d menu. And select the filename of the .cfg file.
The exporter will create a set of other files with same prefix (ie. bla.cfg, bla.xsf,
bla_Action1.xaf, bla_Action2.xaf, ...).
You should be able to open the .cfg file in cal3d_miniviewer.


NOT (YET) SUPPORTED

	- Rotation, translation, or stretching Blender objects is still quite
		buggy, so AVOID MOVING / ROTATING / RESIZING OBJECTS (either mesh or armature) !
		Instead, edit the object (with tab), select all points / bones (with "a"),
		and move / rotate / resize them.
	- no support for exporting springs yet
	- no support for exporting material colors (most games should only use images
		I think...)


KNOWN ISSUES

	- Cal3D versions <=0.9.1 have a bug where animations aren't played when the root bone
		is not animated
	- Cal3D versions <=0.9.1 have a bug where objects that aren't influenced by any bones
		are not drawn (fixed in Cal3D CVS)


NOTES

It requires a very recent version of Blender (tested with 2.41).

Build a model follows a few rules:
	- Use only a single armature
	- Use only a single rootbone (Cal3D doesn't support floating bones)
	- Use only locrot keys (Cal3D doesn't support bone's size change)
	- Don't try to create child/parent constructs in blender object, that gets exported
		incorrectly at the moment
	- Objects or animations whose names start by "_" are not exported (hidden object)

It can be run in batch mode, as following :
		blender model.blend -P blender2cal3d.py --blender2cal3d FILENAME=model.cfg EXPORT_FOR_SOYA=1
You can pass as many parameters as you want at the end, "EXPORT_FOR_SOYA=1" is just an
exemple. The parameters are the same than below.


Logging is sent through python logger instance. you can control verbosity by changing
log.set_level(DEBUG|WARNING|ERROR|CRITICAL). to print to it use log.debug(),
log.warning(), log.error() and log.critical(). the logger breaks normal operation
by printing _all_ info messages. ( log.info() ). Output is sent to stdout and to
a blender text.

Psyco support, turned off by default. can speed up export by 25% on my tests.
Turned off by default.

hotshot_export function to see profile. just replace export with hotshot export 
to see.

Any vertices found without influence are placed into a vertex group called 
"_no_inf". [FIXME] i need to stop this for batch more 
running under gui mode the last export dir is saved into the blender registry
and called back again.
"""


# Parameters :

# Filename to export to (if "", display a file selector dialog).
FILENAME = ""

# True (=1) to export for the Soya 3D engine
#     (http://oomadness.tuxfamily.org/en/soya).
# (=> rotate meshes and skeletons so as X is right, Y is top and -Z is front)
EXPORT_FOR_SOYA = 0

# Enables LODs computation. LODs computation is quite slow, and the algo is
# surely not optimal :-(
LODS = 0

# Scale the model (not supported by Soya).
SCALE = 1.0

# Set to 1 if you want to prefix all filename with the model name
# (e.g. knight_walk.xaf instead of walk.xaf)
PREFIX_FILE_WITH_MODEL_NAME = 0

# Set to 0 to use Cal3D binary format
XML = 1

# Configuration text buffer
CONFIG_TEXT = ""

# Allows to replace a material by another
MATERIAL_MAP = {}

# Flip the UV texture coordinates
FLIP_TEXTURE_COORDS = 1

MESSAGES = ""

# See also BASE_MATRIX below, if you want to rotate/scale/translate the model at
# the exportation.

#########################################################################################
# Code starts here.
# The script should be quite re-useable for writing another Blender animation exporter.
# Most of the hell of it is to deal with Blender's head-tail-roll bone's definition.

import sys, os, os.path, struct, math, string

try:
	import psyco
	psyco.full()
except:
	print "* Blender2Cal3D * (Psyco not found)"
	
import Blender, Blender.Scene
from Blender import Registry
from Blender.Window import DrawProgressBar
from Blender import Draw, BGL

import logging 
reload(logging)

import types

import textwrap

# HACK -- it seems that some Blender versions don't define sys.argv,
# which may crash Python if a warning occurs.
if not hasattr(sys, "argv"): sys.argv = ["???"]

# our own logger class. it works just the same as a normal logger except
# all info messages get show. 
class Logger(logging.Logger):
	def __init__(self,name,level=logging.NOTSET):
		logging.Logger.__init__(self,name,level)

		self.has_warnings=False
		self.has_errors=False
		self.has_critical=False
	
	def info(self,msg,*args,**kwargs):
		apply(self._log,(logging.INFO,msg,args),kwargs)

	def warning(self,msg,*args,**kwargs):
		logging.Logger.warning(self,msg,*args,**kwargs)
		self.has_warnings=True
	
	def error(self,msg,*args,**kwargs):
		logging.Logger.error(self,msg,*args,**kwargs)
		self.has_errors=True
	
	def critical(self,msg,*args,**kwargs):
		logging.Logger.critical(self,msg,*args,**kwargs)
		self.has_errors=True

	
# should be able to make this print to stdout in realtime and save MESSAGES
# as well. perhaps also have a log to file option
class LogHandler(logging.StreamHandler):
	def __init__(self):
		logging.StreamHandler.__init__(self,sys.stdout)
		
		if "blender2cal3d_log" not in Blender.Text.Get():
			self.outtext=Blender.Text.New("blender2cal3d_log")
		else:
			self.outtext=Blender.Text.Get('blender2cal3d_log')
			self.outtext.clear()

		self.lastmsg=''

	def emit(self,record):
		# print to stdout and  to a new blender text object

		msg=self.format(record)

		if msg==self.lastmsg:
			return 

		self.lastmsg=msg
	
		self.outtext.write("%s\n" %msg)

		logging.StreamHandler.emit(self,record)
	
		"""
		try:
			msg=self.format(record)
			if not hasattr(types,"UnicodeType"):
				self.stream.write("%s\n" % msg)
			else:
				try:
					self.stream.write("%s\n" % msg)
				except UnicodeError:
					self.stream.write("%s\n" % msg.encode("UTF-8"))

				self.flush()
		except:
			self.handleError(record)
		""" 
logging.setLoggerClass(Logger)
log=logging.getLogger('blender2cal3d')

handler=LogHandler()
formatter=logging.Formatter('%(levelname)s %(message)s')
handler.setFormatter(formatter)

log.addHandler(handler)
# set this to minimum output level. eg. logging.DEBUG, logging.WARNING, logging.ERROR
# logging.CRITICAL. logging.INFO will make little difference as these always get 
# output'd
log.setLevel(logging.WARNING)

log.info("Starting...")

# transforms a blender to a cal3d quaternion notation (x,y,z,w)
def blender2cal3dquat(q):
	return [q.x, q.y, q.z, q.w]

def quaternion2matrix(q):
	xx = q[0] * q[0]
	yy = q[1] * q[1]
	zz = q[2] * q[2]
	xy = q[0] * q[1]
	xz = q[0] * q[2]
	yz = q[1] * q[2]
	wx = q[3] * q[0]
	wy = q[3] * q[1]
	wz = q[3] * q[2]
	return [[1.0 - 2.0 * (yy + zz),       2.0 * (xy + wz),       2.0 * (xz - wy), 0.0],
					[      2.0 * (xy - wz), 1.0 - 2.0 * (xx + zz),       2.0 * (yz + wx), 0.0],
					[      2.0 * (xz + wy),       2.0 * (yz - wx), 1.0 - 2.0 * (xx + yy), 0.0],
					[0.0                  , 0.0                  , 0.0                  , 1.0]]

# def matrix2quaternion(m):
#   s = math.sqrt(abs(m[0][0] + m[1][1] + m[2][2] + m[3][3]))
#   if s == 0.0:
#     x = abs(m[2][1] - m[1][2])
#     y = abs(m[0][2] - m[2][0])
#     z = abs(m[1][0] - m[0][1])
#     if   (x >= y) and (x >= z): return 1.0, 0.0, 0.0, 0.0
#     elif (y >= x) and (y >= z): return 0.0, 1.0, 0.0, 0.0
#     else:                       return 0.0, 0.0, 1.0, 0.0
#   return quaternion_normalize([
#     -(m[2][1] - m[1][2]) / (2.0 * s),
#     -(m[0][2] - m[2][0]) / (2.0 * s),
#     -(m[1][0] - m[0][1]) / (2.0 * s),
#     0.5 * s,
#     ])

def matrix2quaternion(m):
	t = m[0][0] + m[1][1] + m[2][2] + m[3][3]
	if t > 0.00000001:
		s = math.sqrt(t) * 2
		return quaternion_normalize([
			-(m[2][1] - m[1][2]) / s,
			-(m[0][2] - m[2][0]) / s,
			-(m[1][0] - m[0][1]) / s,
			0.25 * s,
		])
	else:
		if (m[0][0] > m[1][1]) and (m[0][0] > m[2][2]):
			s = math.sqrt(1.0 + m[0][0] - m[1][1] - m[2][2]) * 2
			return quaternion_normalize([
				0.25 * s,
				-(m[1][0] + m[0][1]) / s,
				-(m[0][2] + m[2][0]) / s,
				-(m[2][1] - m[1][2]) / s,
				])
		elif m[1][1] > m[2][2]:
			s = math.sqrt(1.0 + m[1][1] - m[0][0] - m[2][2]) * 2
			return quaternion_normalize([
				-(m[1][0] + m[0][1]) / s,
				0.25 * s,
				-(m[2][1] + m[1][2]) / s,
				-(m[0][2] - m[2][0]) / s,
				])
		else:
			s = math.sqrt(1.0 + m[2][2] - m[0][0] - m[1][1]) * 2
			return quaternion_normalize([
				-(m[0][2] + m[2][0]) / s,
				-(m[2][1] + m[1][2]) / s,
				0.25 * s,
				-(m[1][0] - m[0][1]) / s,
				])

def quaternion_normalize(q):
	l = math.sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3])
	return q[0] / l, q[1] / l, q[2] / l, q[3] / l

# multiplies 2 quaternions in x,y,z,w notation
def quaternion_multiply(q1, q2):
	return [
		q2[3] * q1[0] + q2[0] * q1[3] + q2[1] * q1[2] - q2[2] * q1[1],
		q2[3] * q1[1] + q2[1] * q1[3] + q2[2] * q1[0] - q2[0] * q1[2],
		q2[3] * q1[2] + q2[2] * q1[3] + q2[0] * q1[1] - q2[1] * q1[0],
		q2[3] * q1[3] - q2[0] * q1[0] - q2[1] * q1[1] - q2[2] * q1[2],
		]

def matrix_translate(m, v):
	m[3][0] += v[0]
	m[3][1] += v[1]
	m[3][2] += v[2]
	return m

def matrix_multiply(b, a):
	return [ [
		a[0][0] * b[0][0] + a[0][1] * b[1][0] + a[0][2] * b[2][0],
		a[0][0] * b[0][1] + a[0][1] * b[1][1] + a[0][2] * b[2][1],
		a[0][0] * b[0][2] + a[0][1] * b[1][2] + a[0][2] * b[2][2],
		0.0,
		], [
		a[1][0] * b[0][0] + a[1][1] * b[1][0] + a[1][2] * b[2][0],
		a[1][0] * b[0][1] + a[1][1] * b[1][1] + a[1][2] * b[2][1],
		a[1][0] * b[0][2] + a[1][1] * b[1][2] + a[1][2] * b[2][2],
		0.0,
		], [
		a[2][0] * b[0][0] + a[2][1] * b[1][0] + a[2][2] * b[2][0],
		a[2][0] * b[0][1] + a[2][1] * b[1][1] + a[2][2] * b[2][1],
		a[2][0] * b[0][2] + a[2][1] * b[1][2] + a[2][2] * b[2][2],
		 0.0,
		], [
		a[3][0] * b[0][0] + a[3][1] * b[1][0] + a[3][2] * b[2][0] + b[3][0],
		a[3][0] * b[0][1] + a[3][1] * b[1][1] + a[3][2] * b[2][1] + b[3][1],
		a[3][0] * b[0][2] + a[3][1] * b[1][2] + a[3][2] * b[2][2] + b[3][2],
		1.0,
		] ]

def matrix_invert(m):
	det = (m[0][0] * (m[1][1] * m[2][2] - m[2][1] * m[1][2])
			 - m[1][0] * (m[0][1] * m[2][2] - m[2][1] * m[0][2])
			 + m[2][0] * (m[0][1] * m[1][2] - m[1][1] * m[0][2]))
	if det == 0.0: return None
	det = 1.0 / det
	r = [ [
			det * (m[1][1] * m[2][2] - m[2][1] * m[1][2]),
		- det * (m[0][1] * m[2][2] - m[2][1] * m[0][2]),
			det * (m[0][1] * m[1][2] - m[1][1] * m[0][2]),
			0.0,
		], [
		- det * (m[1][0] * m[2][2] - m[2][0] * m[1][2]),
			det * (m[0][0] * m[2][2] - m[2][0] * m[0][2]),
		- det * (m[0][0] * m[1][2] - m[1][0] * m[0][2]),
			0.0
		], [
			det * (m[1][0] * m[2][1] - m[2][0] * m[1][1]),
		- det * (m[0][0] * m[2][1] - m[2][0] * m[0][1]),
			det * (m[0][0] * m[1][1] - m[1][0] * m[0][1]),
			0.0,
		] ]
	r.append([
		-(m[3][0] * r[0][0] + m[3][1] * r[1][0] + m[3][2] * r[2][0]),
		-(m[3][0] * r[0][1] + m[3][1] * r[1][1] + m[3][2] * r[2][1]),
		-(m[3][0] * r[0][2] + m[3][1] * r[1][2] + m[3][2] * r[2][2]),
		1.0,
		])
	return r

def matrix_rotate_x(angle):
	cos = math.cos(math.radians(angle))
	sin = math.sin(math.radians(angle))
	return [
		[1.0,  0.0, 0.0, 0.0],
		[0.0,  cos, sin, 0.0],
		[0.0, -sin, cos, 0.0],
		[0.0,  0.0, 0.0, 1.0],
		]

def matrix_rotate_y(angle):
	cos = math.cos(math.radians(angle))
	sin = math.sin(math.radians(angle))
	return [
		[cos, 0.0, -sin, 0.0],
		[0.0, 1.0,  0.0, 0.0],
		[sin, 0.0,  cos, 0.0],
		[0.0, 0.0,  0.0, 1.0],
		]

def matrix_rotate_z(angle):
	cos = math.cos(math.radians(angle))
	sin = math.sin(math.radians(angle))
	return [
		[ cos, sin, 0.0, 0.0],
		[-sin, cos, 0.0, 0.0],
		[ 0.0, 0.0, 1.0, 0.0],
		[ 0.0, 0.0, 0.0, 1.0],
		]

def matrix_rotate(axis, angle):
	vx  = axis[0]
	vy  = axis[1]
	vz  = axis[2]
	vx2 = vx * vx
	vy2 = vy * vy
	vz2 = vz * vz
	cos = math.cos(math.radians(angle))
	sin = math.sin(math.radians(angle))
	co1 = 1.0 - cos
	return [
		[vx2 * co1 + cos,          vx * vy * co1 + vz * sin, vz * vx * co1 - vy * sin, 0.0],
		[vx * vy * co1 - vz * sin, vy2 * co1 + cos,          vy * vz * co1 + vx * sin, 0.0],
		[vz * vx * co1 + vy * sin, vy * vz * co1 - vx * sin, vz2 * co1 + cos,          0.0],
		[0.0, 0.0, 0.0, 1.0],
		]

def matrix_scale(fx, fy, fz):
	return [
		[ fx, 0.0, 0.0, 0.0],
		[0.0,  fy, 0.0, 0.0],
		[0.0, 0.0,  fz, 0.0],
		[0.0, 0.0, 0.0, 1.0],
		]
	
def point_by_matrix(p, m):
	return [p[0] * m[0][0] + p[1] * m[1][0] + p[2] * m[2][0] + m[3][0],
					p[0] * m[0][1] + p[1] * m[1][1] + p[2] * m[2][1] + m[3][1],
					p[0] * m[0][2] + p[1] * m[1][2] + p[2] * m[2][2] + m[3][2]]

def point_distance(p1, p2):
	return math.sqrt((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2 + (p2[2] - p1[2]) ** 2)

def vector_add(v1, v2):
	return [v1[0]+v2[0], v1[1]+v2[1], v1[2]+v2[2]]

def vector_sub(v1, v2):
	return [v1[0]-v2[0], v1[1]-v2[1], v1[2]-v2[2]]
		
def vector_by_matrix(p, m):
	return [p[0] * m[0][0] + p[1] * m[1][0] + p[2] * m[2][0],
					p[0] * m[0][1] + p[1] * m[1][1] + p[2] * m[2][1],
					p[0] * m[0][2] + p[1] * m[1][2] + p[2] * m[2][2]]

def vector_length(v):
	return math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])

def vector_normalize(v):
	l = math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])
	if l: return v[0] / l, v[1] / l, v[2] / l
	else: return v

def vector_dotproduct(v1, v2):
	return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2]

def vector_crossproduct(v1, v2):
	return [
		v1[1] * v2[2] - v1[2] * v2[1],
		v1[2] * v2[0] - v1[0] * v2[2],
		v1[0] * v2[1] - v1[1] * v2[0],
		]

def vector_angle(v1, v2):
	s = vector_length(v1) * vector_length(v2)
	f = vector_dotproduct(v1, v2) / s
	if f >=  1.0: return 0.0
	if f <= -1.0: return math.pi / 2.0
	return math.atan(-f / math.sqrt(1.0 - f * f)) + math.pi / 2.0

def blender_bone2matrix(head, tail, roll):
	# Convert bone rest state (defined by bone.head, bone.tail and bone.roll)
	# to a matrix (the more standard notation).
	# Taken from blenkernel/intern/armature.c in Blender source.
	# See also DNA_armature_types.h:47.
	
	target = [0.0, 1.0, 0.0]
	delta  = [tail[0] - head[0], tail[1] - head[1], tail[2] - head[2]]
	nor    = vector_normalize(delta)
	axis   = vector_crossproduct(target, nor)
	
	if vector_dotproduct(axis, axis) > 0.0000000000001:
		axis    = vector_normalize(axis)
		theta   = math.acos(vector_dotproduct(target, nor))
		bMatrix = matrix_rotate(axis, math.degrees(theta))
		
	else:
		if vector_dotproduct(target, nor) > 0.0: updown =  1.0
		else:                                    updown = -1.0
		
		# Quoted from Blender source : "I think this should work ..."
		bMatrix = [
			[updown, 0.0,    0.0, 0.0],
			[0.0,    updown, 0.0, 0.0],
			[0.0,    0.0,    1.0, 0.0],
			[0.0,    0.0,    0.0, 1.0],
			]
	
	rMatrix = matrix_rotate(nor, roll)
	return matrix_multiply(rMatrix, bMatrix)


# Hack for having the model rotated right.
# Put in BASE_MATRIX your own rotation if you need some.

BASE_MATRIX = None


# Cal3D data structures

CAL3D_VERSION = 910

NEXT_MATERIAL_ID = 0
class Material:
	def __init__(self, map_filename = None):
		self.ambient_r  = 255
		self.ambient_g  = 255
		self.ambient_b  = 255
		self.ambient_a  = 255
		self.diffuse_r  = 255
		self.diffuse_g  = 255
		self.diffuse_b  = 255
		self.diffuse_a  = 255
		self.specular_r = 255
		self.specular_g = 255
		self.specular_b = 255
		self.specular_a = 255
		self.shininess = 1.0

		log.debug("Material with name %s",map_filename)
		
		if map_filename: self.maps_filenames = [map_filename]
		else:            self.maps_filenames = []
		
		MATERIALS[map_filename] = self
		
		global NEXT_MATERIAL_ID
		self.id = NEXT_MATERIAL_ID
		NEXT_MATERIAL_ID += 1
		
	# old cal3d format
	def to_cal3d(self):
		s = "CRF\0" + struct.pack("iBBBBBBBBBBBBfi", CAL3D_VERSION, self.ambient_r, self.ambient_g, self.ambient_b, self.ambient_a, self.diffuse_r, self.diffuse_g, self.diffuse_b, self.diffuse_a, self.specular_r, self.specular_g, self.specular_b, self.specular_a, self.shininess, len(self.maps_filenames))
		for map_filename in self.maps_filenames:
			s += struct.pack("i", len(map_filename) + 1)
			s += map_filename + "\0"
		return s
 
	# new xml format
	def to_cal3d_xml(self):
		s = "<?xml version=\"1.0\"?>\n"
		s += "<HEADER MAGIC=\"XRF\" VERSION=\"%i\"/>\n" % CAL3D_VERSION
		s += "<MATERIAL NUMMAPS=\"" + str(len(self.maps_filenames)) + "\">\n"
		s += "  <AMBIENT>" + str(self.ambient_r) + " " + str(self.ambient_g) + " " + str(self.ambient_b) + " " + str(self.ambient_a) + "</AMBIENT>\n";
		s += "  <DIFFUSE>" + str(self.diffuse_r) + " " + str(self.diffuse_g) + " " + str(self.diffuse_b) + " " + str(self.diffuse_a) + "</DIFFUSE>\n";
		s += "  <SPECULAR>" + str(self.specular_r) + " " + str(self.specular_g) + " " + str(self.specular_b) + " " + str(self.specular_a) + "</SPECULAR>\n";
		s += "  <SHININESS>" + str(self.shininess) + "</SHININESS>\n";

		for map_filename in self.maps_filenames:
			s += "  <MAP>" + map_filename + "</MAP>\n";
			
		s += "</MATERIAL>\n";
				
		return s
	
MATERIALS = {}

class Mesh:
	def __init__(self, name):
		name=string.replace(name,'.','_')
		self.name      = name
		self.submeshes = []
		
		self.next_submesh_id = 0
		
	def to_cal3d(self):
		s = "CMF\0" + struct.pack("ii", CAL3D_VERSION, len(self.submeshes))
		s += "".join(map(SubMesh.to_cal3d, self.submeshes))
		return s

	def to_cal3d_xml(self):
		s = "<?xml version=\"1.0\"?>\n"
		s += "<HEADER MAGIC=\"XMF\" VERSION=\"%i\"/>\n" % CAL3D_VERSION
		s += "<MESH NUMSUBMESH=\"%i\">\n" % len(self.submeshes)
		s += "".join(map(SubMesh.to_cal3d_xml, self.submeshes))
		s += "</MESH>\n"                                                  
		return s

class SubMesh:
	def __init__(self, mesh, material):
		self.material   = material
		self.vertices   = []
		self.faces      = []
		self.nb_lodsteps = 0
		self.springs    = []
		
		self.next_vertex_id = 0
		
		self.mesh = mesh
		self.id = mesh.next_submesh_id
		mesh.next_submesh_id += 1
		mesh.submeshes.append(self)
		
	def compute_lods(self):
		"""Computes LODs info for Cal3D (there's no Blender related stuff here)."""
		
		log.info("Start LODs computation...")

		vertex2faces = {}
		for face in self.faces:
			for vertex in (face.vertex1, face.vertex2, face.vertex3):
				l = vertex2faces.get(vertex)
				if not l: vertex2faces[vertex] = [face]
				else: l.append(face)
				
		couple_treated         = {}
		couple_collapse_factor = []
		for face in self.faces:
			for a, b in ((face.vertex1, face.vertex2), (face.vertex1, face.vertex3), (face.vertex2, face.vertex3)):
				a = a.cloned_from or a
				b = b.cloned_from or b
				if a.id > b.id: a, b = b, a
				if not couple_treated.has_key((a, b)):
					# The collapse factor is simply the distance between the 2 points :-(
					# This should be improved !!
					if vector_dotproduct(a.normal, b.normal) < 0.9: continue
					couple_collapse_factor.append((point_distance(a.loc, b.loc), a, b))
					couple_treated[a, b] = 1
			
		couple_collapse_factor.sort()
		
		collapsed    = {}
		new_vertices = []
		new_faces    = []
		for factor, v1, v2 in couple_collapse_factor:
			# Determines if v1 collapses to v2 or v2 to v1.
			# We choose to keep the vertex which is on the smaller number of faces, since
			# this one has more chance of being in an extrimity of the body.
			# Though heuristic, this rule yields very good results in practice.
			if   len(vertex2faces[v1]) <  len(vertex2faces[v2]): v2, v1 = v1, v2
			elif len(vertex2faces[v1]) == len(vertex2faces[v2]):
				if collapsed.get(v1, 0): v2, v1 = v1, v2 # v1 already collapsed, try v2
				
			if (not collapsed.get(v1, 0)) and (not collapsed.get(v2, 0)):
				collapsed[v1] = 1
				collapsed[v2] = 1
				
				# Check if v2 is already colapsed
				while v2.collapse_to: v2 = v2.collapse_to
				
				common_faces = filter(vertex2faces[v1].__contains__, vertex2faces[v2])
				
				v1.collapse_to         = v2
				v1.face_collapse_count = len(common_faces)
				
				for clone in v1.clones:
					# Find the clone of v2 that correspond to this clone of v1
					possibles = []
					for face in vertex2faces[clone]:
						possibles.append(face.vertex1)
						possibles.append(face.vertex2)
						possibles.append(face.vertex3)
					clone.collapse_to = v2
					for vertex in v2.clones:
						if vertex in possibles:
							clone.collapse_to = vertex
							break
						
					clone.face_collapse_count = 0
					new_vertices.append(clone)

				# HACK -- all faces get collapsed with v1 (and no faces are collapsed with v1's
				# clones). This is why we add v1 in new_vertices after v1's clones.
				# This hack has no other incidence that consuming a little few memory for the
				# extra faces if some v1's clone are collapsed but v1 is not.
				new_vertices.append(v1)
				
				self.nb_lodsteps += 1 + len(v1.clones)
				
				new_faces.extend(common_faces)
				for face in common_faces:
					face.can_collapse = 1
					
					# Updates vertex2faces
					vertex2faces[face.vertex1].remove(face)
					vertex2faces[face.vertex2].remove(face)
					vertex2faces[face.vertex3].remove(face)
				vertex2faces[v2].extend(vertex2faces[v1])
				
		new_vertices.extend(filter(lambda vertex: not vertex.collapse_to, self.vertices))
		new_vertices.reverse() # Cal3D want LODed vertices at the end
		for i in range(len(new_vertices)): new_vertices[i].id = i
		self.vertices = new_vertices
		
		new_faces.extend(filter(lambda face: not face.can_collapse, self.faces))
		new_faces.reverse() # Cal3D want LODed faces at the end
		self.faces = new_faces
		
		log.info("LODs computed : %s vertices can be removed (from a total of %s)." % (self.nb_lodsteps, len(self.vertices)))
		
	def rename_vertices(self, new_vertices):
		"""Rename (change ID) of all vertices, such as self.vertices == new_vertices."""
		for i in range(len(new_vertices)): new_vertices[i].id = i
		self.vertices = new_vertices
		
	def to_cal3d(self):
		s =  struct.pack("iiiiii", self.material.id, len(self.vertices), len(self.faces), self.nb_lodsteps, len(self.springs), len(self.material.maps_filenames))
		s += "".join(map(Vertex.to_cal3d, self.vertices))
		s += "".join(map(Spring.to_cal3d, self.springs))
		s += "".join(map(Face  .to_cal3d, self.faces))
		return s

	def to_cal3d_xml(self):
		s = "  <SUBMESH NUMVERTICES=\"%i\" NUMFACES=\"%i\" MATERIAL=\"%i\" " % \
				(len(self.vertices), len(self.faces), self.material.id)
		s += "NUMLODSTEPS=\"%i\" NUMSPRINGS=\"%i\" NUMTEXCOORDS=\"%i\">\n" % \
				 (self.nb_lodsteps, len(self.springs),
				 len(self.material.maps_filenames))
		s += "".join(map(Vertex.to_cal3d_xml, self.vertices))
		s += "".join(map(Spring.to_cal3d_xml, self.springs))
		s += "".join(map(Face.to_cal3d_xml, self.faces))
		s += "  </SUBMESH>\n"
		return s

class Vertex:
	def __init__(self, submesh, loc, normal):
		self.loc    = loc
		self.normal = normal
		self.collapse_to         = None
		self.face_collapse_count = 0
		self.maps       = []
		self.influences = []
		self.weight = None
		
		self.cloned_from = None
		self.clones      = []
		
		self.submesh = submesh
		self.id = submesh.next_vertex_id
		submesh.next_vertex_id += 1
		submesh.vertices.append(self)
		
	def to_cal3d(self):
		if self.collapse_to: collapse_id = self.collapse_to.id
		else:                collapse_id = -1
		s =  struct.pack("ffffffii", self.loc[0], self.loc[1], self.loc[2], self.normal[0], self.normal[1], self.normal[2], collapse_id, self.face_collapse_count)
		s += "".join(map(Map.to_cal3d, self.maps))
		s += struct.pack("i", len(self.influences))
		s += "".join(map(Influence.to_cal3d, self.influences))
		if not self.weight is None: s += struct.pack("f", len(self.weight))
		return s
	
	def to_cal3d_xml(self):
		if self.collapse_to:
			collapse_id = self.collapse_to.id
		else:
			collapse_id = -1
		s = "    <VERTEX ID=\"%i\" NUMINFLUENCES=\"%i\">\n" % \
				(self.id, len(self.influences))
		s += "      <POS>%f %f %f</POS>\n" % (self.loc[0], self.loc[1], self.loc[2])
		s += "      <NORM>%f %f %f</NORM>\n" % \
				 (self.normal[0], self.normal[1], self.normal[2])
		if collapse_id != -1:
			s += "      <COLLAPSEID>%i</COLLAPSEID>\n" % collapse_id
			s += "      <COLLAPSECOUNT>%i</COLLAPSECOUNT>\n" % \
					 self.face_collapse_count
		s += "".join(map(Map.to_cal3d_xml, self.maps))
		s += "".join(map(Influence.to_cal3d_xml, self.influences))
		if not self.weight is None:
			s += "      <PHYSIQUE>%f</PHYSIQUE>\n" % len(self.weight)
		s += "    </VERTEX>\n"
		return s
 
class Map:
	def __init__(self, u, v):
		self.u = u
		self.v = v
		
	def to_cal3d(self):
		return struct.pack("ff", self.u, self.v)

	def to_cal3d_xml(self):
		return "      <TEXCOORD>%f %f</TEXCOORD>\n" % (self.u, self.v)    

class Influence:
	def __init__(self, bone, weight):
		self.bone   = bone
		self.weight = weight
		
	def to_cal3d(self):
		return struct.pack("if", self.bone.id, self.weight)

	def to_cal3d_xml(self):
		return "      <INFLUENCE ID=\"%i\">%f</INFLUENCE>\n" % \
					 (self.bone.id, self.weight)
 
class Spring:
	def __init__(self, vertex1, vertex2):
		self.vertex1 = vertex1
		self.vertex2 = vertex2
		self.spring_coefficient = 0.0
		self.main_looplength = 0.0
		
	def to_cal3d(self):
		return struct.pack("iiff", self.vertex1.id, self.vertex2.id, self.spring_coefficient, self.main_looplength)

	def to_cal3d_xml(self):
		return "    <SPRING VERTEXID=\"%i %i\" COEF=\"%f\" LENGTH=\"%f\"/>\n" % \
					 (self.vertex1.id, self.vertex2.id, self.spring_coefficient,
					 self.main_looplength)

class Face:
	def __init__(self, submesh, vertex1, vertex2, vertex3):
		self.vertex1 = vertex1
		self.vertex2 = vertex2
		self.vertex3 = vertex3
		
		self.can_collapse = 0
		
		self.submesh = submesh
		submesh.faces.append(self)
		
	def to_cal3d(self):
		return struct.pack("iii", self.vertex1.id, self.vertex2.id, self.vertex3.id)

	def to_cal3d_xml(self):
		return "    <FACE VERTEXID=\"%i %i %i\"/>\n" % \
					 (self.vertex1.id, self.vertex2.id, self.vertex3.id)
 
class Skeleton:
	def __init__(self):
		self.bones = []
		
		self.next_bone_id = 0
		
	def to_cal3d(self):
		s = "CSF\0" + struct.pack("ii", CAL3D_VERSION, len(self.bones))
		s += "".join(map(Bone.to_cal3d, self.bones))
		return s

	def to_cal3d_xml(self):
		s = "<?xml version=\"1.0\"?>\n"
		s += "<HEADER MAGIC=\"XSF\" VERSION=\"%i\"/>\n" % CAL3D_VERSION
		s += "<SKELETON NUMBONES=\"%i\">\n" % len(self.bones)
		s += "".join(map(Bone.to_cal3d_xml, self.bones))
		s += "</SKELETON>\n"
		return s

BONES = {}

class Bone:
	def __init__(self, skeleton, parent, name, loc, rot):
		self.parent = parent
		name=string.replace(name,'.','_')
		self.name   = name
		self.loc = loc
		self.rot = rot
		self.children = []
		
		self.matrix = self.local_matrix = matrix_translate(quaternion2matrix(rot), loc)
		
		if parent:
			self.matrix = matrix_multiply(parent.matrix, self.matrix)
			parent.children.append(self)
		
		# lloc and lrot are the bone => model space transformation (translation and rotation).
		# They are probably specific to Cal3D.
		m = matrix_invert(self.matrix)
		self.lloc = m[3][0], m[3][1], m[3][2]
		self.lrot = matrix2quaternion(m)
		
		self.skeleton = skeleton
		self.id = skeleton.next_bone_id
		skeleton.next_bone_id += 1
		skeleton.bones.append(self)
		
		BONES[name] = self
		
	def to_cal3d(self):
		s =  struct.pack("i", len(self.name) + 1) + self.name + "\0"
		
		# We need to negate quaternion W value, but why ?
		s += struct.pack("ffffffffffffff", self.loc[0], self.loc[1], self.loc[2], self.rot[0], self.rot[1], self.rot[2], -self.rot[3], self.lloc[0], self.lloc[1], self.lloc[2], self.lrot[0], self.lrot[1], self.lrot[2], -self.lrot[3])
		if self.parent: s += struct.pack("i", self.parent.id)
		else:           s += struct.pack("i", -1)
		s += struct.pack("i", len(self.children))
		s += "".join(map(lambda bone: struct.pack("i", bone.id), self.children))
		return s

	def to_cal3d_xml(self):
		s = "  <BONE ID=\"%i\" NAME=\"%s\" NUMCHILD=\"%i\">\n" % \
				(self.id, self.name, len(self.children))
		# We need to negate quaternion W value, but why ?
		s += "    <TRANSLATION>%f %f %f</TRANSLATION>\n" % \
				 (self.loc[0], self.loc[1], self.loc[2])
		s += "    <ROTATION>%f %f %f %f</ROTATION>\n" % \
				 (self.rot[0], self.rot[1], self.rot[2], -self.rot[3])
		s += "    <LOCALTRANSLATION>%f %f %f</LOCALTRANSLATION>\n" % \
				 (self.lloc[0], self.lloc[1], self.lloc[2])
		s += "    <LOCALROTATION>%f %f %f %f</LOCALROTATION>\n" % \
				 (self.lrot[0], self.lrot[1], self.lrot[2], -self.lrot[3])
		if self.parent:
			s += "    <PARENTID>%i</PARENTID>\n" % self.parent.id
		else:
			s += "    <PARENTID>%i</PARENTID>\n" % -1
		s += "".join(map(lambda bone: "    <CHILDID>%i</CHILDID>\n" % bone.id,
				 self.children))
		s += "  </BONE>\n"
		return s

class Animation:
	def __init__(self, name, duration = 0.0):
		name=string.replace(name,'.','_')
		self.name     = name
		self.duration = duration
		self.tracks   = {} # Map bone names to tracks
		
	def to_cal3d(self):
		s = "CAF\0" + struct.pack("ifi", CAL3D_VERSION, self.duration, len(self.tracks))
		s += "".join(map(Track.to_cal3d, self.tracks.values()))
		return s

	def to_cal3d_xml(self):
		s = "<?xml version=\"1.0\"?>\n"
		s += "<HEADER MAGIC=\"XAF\" VERSION=\"%i\"/>\n" % CAL3D_VERSION
		s += "<ANIMATION DURATION=\"%f\" NUMTRACKS=\"%i\">\n" % \
				 (self.duration, len(self.tracks))                            
		s += "".join(map(Track.to_cal3d_xml, self.tracks.values()))
		s += "</ANIMATION>\n"
		return s                                                          
 
class Track:
	def __init__(self, animation, bone):
		self.bone      = bone
		self.keyframes = []
		
		self.animation = animation
		animation.tracks[bone.name] = self
		
	def to_cal3d(self):
		s = struct.pack("ii", self.bone.id, len(self.keyframes))
		s += "".join(map(KeyFrame.to_cal3d, self.keyframes))
		return s

	def to_cal3d_xml(self):
		s = "  <TRACK BONEID=\"%i\" NUMKEYFRAMES=\"%i\">\n" % \
				(self.bone.id, len(self.keyframes))
		s += "".join(map(KeyFrame.to_cal3d_xml, self.keyframes))
		s += "  </TRACK>\n"
		return s
		
class KeyFrame:
	def __init__(self, track, time, loc, rot):
		self.time = time
		self.loc  = loc
		self.rot  = rot
		
		self.track = track
		track.keyframes.append(self)
		
	def to_cal3d(self):
		# We need to negate quaternion W value, but why ?
		return struct.pack("ffffffff", self.time, self.loc[0], self.loc[1], self.loc[2], self.rot[0], self.rot[1], self.rot[2], -self.rot[3])
	
	def to_cal3d_xml(self):
		s = "    <KEYFRAME TIME=\"%f\">\n" % self.time
		s += "      <TRANSLATION>%f %f %f</TRANSLATION>\n" % \
				 (self.loc[0], self.loc[1], self.loc[2])
		# We need to negate quaternion W value, but why ?
		s += "      <ROTATION>%f %f %f %f</ROTATION>\n" % \
				 (self.rot[0], self.rot[1], self.rot[2], -self.rot[3])
		s += "    </KEYFRAME>\n"
		return s                                                      
	
def export(filename):
	global MESSAGES
	
	if EXPORT_FOR_SOYA:
		global BASE_MATRIX
		BASE_MATRIX = matrix_rotate_x(-90.0)
		
	# Get the scene
	scene = Blender.Scene.getCurrent()
	
	# ---- Export skeleton (=armature) ----------------------------------------
	
	if Blender.mode == 'interactive': DrawProgressBar(0.0,'Exporting skeleton...')

	skeleton = Skeleton()

	foundarmature = False
	for obj in Blender.Scene.GetCurrent().getChildren(): #Blender.Object.Get():
		data = obj.getData()
		if type(data) is not Blender.Types.ArmatureType:
			continue
		
		if foundarmature == True:
			log.error("Found multiple armatures! '" + obj.getName() + "' ignored.\n")
			continue

		foundarmature = True
		matrix = obj.getMatrix()
		if BASE_MATRIX:
			matrix = matrix_multiply(BASE_MATRIX, matrix)
		
		def treat_bone(b, parent = None):
			head = b.head["BONESPACE"]
			tail = b.tail["BONESPACE"]
			
			# Turns the Blender's head-tail-roll notation into a quaternion
			quat = matrix2quaternion(blender_bone2matrix(head, tail, b.roll["BONESPACE"]))
			
			if parent:
				# Compute the translation from the parent bone's head to the child
				# bone's head, in the parent bone coordinate system.
				# The translation is parent_tail - parent_head + child_head,
				# but parent_tail and parent_head must be converted from the parent's parent
				# system coordinate into the parent system coordinate.
				
				parent_invert_transform = matrix_invert(quaternion2matrix(parent.rot))
				parent_head = vector_by_matrix(parent.head, parent_invert_transform)
				parent_tail = vector_by_matrix(parent.tail, parent_invert_transform)
				
				
				
				#ploc = vector_add(head, b.getLoc())
				parentheadtotail = vector_sub(parent_tail, parent_head)
				# hmm this should be handled by the IPos, but isn't for non-animated
				# bones which are transformed in the pose mode...
				#loc = vector_add(ploc, parentheadtotail)
				#rot = quaternion_multiply(blender2cal3dquat(b.getQuat()), quat)
				loc = parentheadtotail
				rot = quat
				
				log.debug("Parented Bone: %s",b.name)
				
				bone = Bone(skeleton, parent, b.name, loc, rot)
			else:
				# Apply the armature's matrix to the root bones
				head = point_by_matrix(head, matrix)
				tail = point_by_matrix(tail, matrix)
				quat = matrix2quaternion(matrix_multiply(matrix, quaternion2matrix(quat))) # Probably not optimal
				
				# loc = vector_add(head, b.getLoc())
				# rot = quaternion_multiply(blender2cal3dquat(b.getQuat()), quat)
				loc = head
				rot = quat
				
				log.debug("Non Parented Bone: %s",b.name)

				# Here, the translation is simply the head vector
				bone = Bone(skeleton, None, b.name, loc, rot)
				
			bone.head = head
			bone.tail = tail
			
			if b.hasChildren():
				for child in b.children:
					treat_bone(child, bone)
					
		foundroot = False
		for b in data.bones.values():
			# child bones are handled in treat_bone
			if b.parent != None:
				continue
			if foundroot == True:
				log.warning("Warning: Found multiple root-bones, this may not be supported in cal3d.")
				#print "Ignoring bone '" + b.name + "' and it's childs."
				#continue
				
			treat_bone(b)
			foundroot = True

	# ---- Export Mesh data ---------------------------------------------------

	if Blender.mode == 'interactive': DrawProgressBar(0.3,'Exporting meshes...')
	
	meshes = []
	
	for obj in Blender.Scene.GetCurrent().getChildren(): #Blender.Object.Get():
		data = obj.getData()
		if (type(data) is Blender.Types.NMeshType) and data.faces:
			mesh_name = obj.getName()
			if mesh_name[0]=='_': continue
			
			log.debug("Mesh: %s",mesh_name)
			
			mesh = Mesh(mesh_name)
			meshes.append(mesh)
			
			matrix = obj.getMatrix()
			if BASE_MATRIX:
				matrix = matrix_multiply(BASE_MATRIX, matrix)
				
			faces = data.faces
			while faces:
				image          = faces[0].image
				if image:
					image_filename = image and image.filename
					image_name     = os.path.splitext(os.path.basename(image_filename))[0]
					if MATERIAL_MAP.has_key(image_name):
						image_filename2 = os.path.join(os.path.dirname(image_filename), MATERIAL_MAP[image_name] + os.path.splitext(image_filename)[1])
					else: image_filename2 = image_filename
					material       = MATERIALS.get(image_filename2) or Material(image_filename2)
					outputuv       = len(material.maps_filenames) > 0
				else:
					image_filename  = "default"
					image_name      = ""
					image_filename2 = ""
					material        = MATERIALS.get("default") or Material("default")
					outputuv        = len(material.maps_filenames) > 0
					
				# TODO add material color support here
				
				submesh  = SubMesh(mesh, material)
				vertices = {}
				for face in faces[:]:
					if (face.image and face.image.filename) == image_filename:
						faces.remove(face)
						
						if not face.smooth:
							try:
								p1 = face.v[0].co
								p2 = face.v[1].co
								p3 = face.v[2].co
							except IndexError:
								log.error("You have faces with less that three verticies!")
								continue

							normal = vector_normalize(vector_by_matrix(vector_crossproduct(
								[p3[0] - p2[0], p3[1] - p2[1], p3[2] - p2[2]],
								[p1[0] - p2[0], p1[1] - p2[1], p1[2] - p2[2]],
								), matrix))
							
						face_vertices = []
						for i in range(len(face.v)):
							vertex = vertices.get(face.v[i].index)
							if not vertex:
								coord    = point_by_matrix (face.v[i].co, matrix)
								if face.smooth:
									normal = vector_normalize(vector_by_matrix(face.v[i].no, matrix))
								vertex   = vertices[face.v[i].index] = Vertex(submesh, coord, normal)
								
								influences = data.getVertexInfluences(face.v[i].index)
								# should this really be a warning? (well currently enabled,
								# because blender has some bugs where it doesn't return
								# influences in python api though they are set, and because
								# cal3d<=0.9.1 had bugs where objects without influences
								# aren't drawn.
								if not influences:
									log.error("A vertex of object '%s' has no influences.\n(This occurs on objects placed in an invisible layer, you can fix it by using a single layer)\n. The vertex has been added to a vertex group called _no_inf" % obj.getName())
									if '_no_inf' not in data.getVertGroupNames():
										data.addVertGroup('_no_inf')

									data.assignVertsToGroup('_no_inf',[face.v[i].index],0.5,'add')
								
								# sum of influences is not always 1.0 in Blender ?!?!
								sum = 0.0
								for bone_name, weight in influences:
									sum += weight
								
								for bone_name, weight in influences:
									bone_name=string.replace(bone_name,'.','_')
									if bone_name=='':
										log.critical('Found bone with no name which influences %s' % obj.getName())
										continue
									if bone_name not in BONES:
										log.error("Couldn't find bone '%s' which influences object '%s'.\n" % (bone_name, obj.getName()))
										continue
									vertex.influences.append(Influence(BONES[bone_name], weight / sum))
									
							elif not face.smooth:
								# We cannot share vertex for non-smooth faces, since Cal3D does not
								# support vertex sharing for 2 vertices with different normals.
								# => we must clone the vertex.
								
								old_vertex = vertex
								vertex = Vertex(submesh, vertex.loc, normal)
								vertex.cloned_from = old_vertex
								vertex.influences = old_vertex.influences
								old_vertex.clones.append(vertex)
								
							if data.hasFaceUV():
								uv = [face.uv[i][0], face.uv[i][1]]
								if FLIP_TEXTURE_COORDS: uv[1] = -uv[1]
								
								if not vertex.maps:
									if outputuv: vertex.maps.append(Map(*uv))
								elif (vertex.maps[0].u != uv[0]) or (vertex.maps[0].v != uv[1]):
									# This vertex can be shared for Blender, but not for Cal3D !!!
									# Cal3D does not support vertex sharing for 2 vertices with
									# different UV texture coodinates.
									# => we must clone the vertex.
									
									for clone in vertex.clones:
										if (clone.maps[0].u == uv[0]) and (clone.maps[0].v == uv[1]):
											vertex = clone
											break
									else: # Not yet cloned...
										old_vertex = vertex
										vertex = Vertex(submesh, vertex.loc, vertex.normal)
										vertex.cloned_from = old_vertex
										vertex.influences = old_vertex.influences
										if outputuv: vertex.maps.append(Map(*uv))
										old_vertex.clones.append(vertex)
										
							face_vertices.append(vertex)
							
						# Split faces with more than 3 vertices
						for i in range(1, len(face.v) - 1):
							Face(submesh, face_vertices[0], face_vertices[i], face_vertices[i + 1])
							
				# Computes LODs info
				if LODS:
					submesh.compute_lods()
				
	# ---- Export animations --------------------------------------------------

	if Blender.mode == 'interactive': DrawProgressBar(0.7,'Exporting animations...')

	ANIMATIONS = {}

	for a in Blender.Armature.NLA.GetActions().iteritems():
		animation_name = a[0]
		
		log.debug( "Animation: %s",animation_name)
		
		animation = Animation(animation_name)
		animation.duration = 0.0

		for b in a[1].getAllChannelIpos().iteritems():
			bone_name = string.replace(b[0],'.','_')
			if bone_name not in BONES:
				log.error("No Bone '" + bone_name + "' defined (from Animation '" + animation_name + "' ?!?\n")
				continue                                            

			bone = BONES[bone_name]

			track = Track(animation, bone)
			track.finished = 0
			animation.tracks[bone_name] = track

			ipo = b[1]
			
			times = []
			
			# SideNote: MatzeB: Ipo.getCurve(curvename) is broken in blender 2.33 and
			# below if the Ipo comes from an Action, so only use Ipo.getCurves()!
			# also blender upto 2.33a had a bug where IpoCurve.evaluate was not
			# exposed to the python interface :-/
			
			#run 1: we need to find all time values where we need to produce keyframes
			for curve in ipo.getCurves():
				curve_name = curve.getName()

				if curve_name not in ["QuatW", "QuatX", "QuatY", "QuatZ", "LocX", "LocY", "LocZ"]:
					log.error("Curve type %s not supported in Action '%s' Bone '%s'.\n"\
										% (curve_name, animation_name, bone_name))
				
				for p in curve.getPoints():
					time = p.getPoints() [0]
					if time not in times:
						times.append(time)
			
			times.sort()

			# run2: now create keyframes
			for time in times:
				cal3dtime = (time-1) / 25.0 # assume 25FPS by default
				if cal3dtime > animation.duration:
					animation.duration = cal3dtime
				trans = [0, 0, 0]
				quat  = [0, 0, 0, 0]
				
				for curve in ipo.getCurves():
					val = curve.evaluate(time)
					if curve.getName() == "LocX": trans[0] = val
					if curve.getName() == "LocY": trans[1] = val
					if curve.getName() == "LocZ": trans[2] = val
					if curve.getName() == "QuatW": quat[3] = val
					if curve.getName() == "QuatX": quat[0] = val
					if curve.getName() == "QuatY": quat[1] = val
					if curve.getName() == "QuatZ": quat[2] = val
					log.debug('Curve: %s' % curve.getName())

				if quat==[0,0,0,0]:
					log.critical('You are using just Loc keys. You must use LocRot keys instead.')
					continue
				
				transt = vector_by_matrix(trans, bone.local_matrix)
					
				loc = vector_add(bone.loc, transt)
				rot = quaternion_multiply(quat, bone.rot)
				
				try:
					rot = quaternion_normalize(rot)
				except:
					import traceback
					log.error('quaternion_normalize failed')

				KeyFrame(track, cal3dtime, loc, rot)
				
		if animation.duration <= 0:
			log.warning("Ignoring Animation '" + animation_name + \
									"': duration is 0.")
			continue
		ANIMATIONS[animation_name] = animation
		
	# Save all data
	if filename.endswith(".cfg"):
		filename = os.path.splitext(filename)[0]
	BASENAME = os.path.basename(filename)         
	DIRNAME  = os.path.dirname(filename)

	try: os.makedirs(DIRNAME)
	except: pass
	
	if PREFIX_FILE_WITH_MODEL_NAME: PREFIX = BASENAME + "_"
	else:                           PREFIX = ""
	if XML: FORMAT_PREFIX = "x"; encode = lambda x: x.to_cal3d_xml()
	else:   FORMAT_PREFIX = "c"; encode = lambda x: x.to_cal3d()
	#print DIRNAME + " - " + BASENAME
	
	cfg = open(os.path.join(DIRNAME, BASENAME + ".cfg"), "wb")
	print >> cfg, "# Cal3D model exported from Blender with blender2cal3d.py"
	print >> cfg

	if SCALE != 1.0:
		print >> cfg, "scale=%s" % SCALE
		print >> cfg
		
	filename = BASENAME + "." + FORMAT_PREFIX + "sf"
	open(os.path.join(DIRNAME, filename), "wb").write(encode(skeleton))
	print >> cfg, "skeleton=%s" % filename
	print >> cfg
	
	for animation in ANIMATIONS.values():
		if not animation.name.startswith("_"):
			if animation.duration: # Cal3D does not support animation with only one state
				filename = PREFIX + animation.name + "." + FORMAT_PREFIX + "af"
				open(os.path.join(DIRNAME, filename), "wb").write(encode(animation))
				print >> cfg, "animation=%s" % filename
				
	print >> cfg
	
	for mesh in meshes:
		if not mesh.name.startswith("_"):
			filename = PREFIX + mesh.name + "." + FORMAT_PREFIX + "mf"
			open(os.path.join(DIRNAME, filename), "wb").write(encode(mesh))
			print >> cfg, "mesh=%s" % filename
	print >> cfg
	
	materials = MATERIALS.values()
	materials.sort(lambda a, b: cmp(a.id, b.id))
	for material in materials:
		if material.maps_filenames:
			filename = PREFIX + os.path.splitext(os.path.basename(material.maps_filenames[0]))[0] + "." + FORMAT_PREFIX + "rf"
		else:
			filename = PREFIX + "plain." + FORMAT_PREFIX + "rf"
		open(os.path.join(DIRNAME, filename), "wb").write(encode(material))
		print >> cfg, "material=%s" % filename
	print >> cfg
	
	try:
		glob_params = Blender.Text.get("soya_params").asLines()
		for glob_param in glob_params:
			print >> cfg, glob_param
	except: pass
	
	if CONFIG_TEXT:
		for line in Blender.Text.get(CONFIG_TEXT).asLines():
			if not line.startswith("material_"):
				print >> cfg, line
				
				
	# Remove soya cached data -- they need to be re-computed, since the model have changed
	for filename in os.listdir(DIRNAME):
		if filename.startswith("neighbors"):
			os.remove(os.path.join(DIRNAME, filename))
			
	log.info("Saved to '%s.cfg'" % BASENAME)
	log.info("Done.")
	
	if Blender.mode == 'interactive': DrawProgressBar(1.0,'Done!')
	
class BlenderGui:
	def __init__(self):
		text="""A log has been written to a blender text window. Change this window type to 
a text window and you will be able to select the file."""

		text=textwrap.wrap(text,40)

		text+=['']
		
		if log.has_critical:
			text+=['There were critical errors!!!!']

		elif log.has_errors:
			text+=['There were errors!']

		elif log.has_warnings:
			text+=['There were warnings']
		
		# add any more text before here
		text.reverse()

		self.msg=text

		Blender.Draw.Register(self.gui, self.event, self.button_event)
	
	def gui(self,):
		quitbutton = Blender.Draw.Button("Exit", 1, 0, 0, 100, 20, "Close Window")
		
		y=35

		for line in self.msg:
			BGL.glRasterPos2i(10,y)
			Blender.Draw.Text(line)
			y+=15
		
	def event(self,evt, val):
		if evt == Blender.Draw.ESCKEY:
			Blender.Draw.Exit()
			return

	def button_event(self,evt):
		if evt == 1:
			Blender.Draw.Exit()
			return

def hotshot_export(filename):
	import hotshot,hotshot.stats
	prof=hotshot.Profile('blender2cal3d.prof')
	print prof.runcall(export,filename)
	prof.close()
	stats=hotshot.stats.load('blender2cal3d.prof')
	stats.strip_dirs()
	stats.sort_stats('time','calls')
	stats.print_stats()

# Main script
def fs_callback(filename):
	save_to_registry(filename)
	#hotshot_export(filename)
	export(filename)
	BlenderGui()

def save_to_registry(filename):
	dir,name=os.path.split(filename)
	d={'default_path':dir,
		}
	
	log.info('storing %s to registry' % str(d))

	Registry.SetKey('blender2cal3d',d)

def get_from_registry():
	d=Registry.GetKey('blender2cal3d')
	if d:
		log.info('got %s from registry' % str(d))
		return d['default_path']
	else:
		return ''

# Check for batch mode
if "--blender2cal3d" in sys.argv:
	args = sys.argv[sys.argv.index("--blender2cal3d") + 1:]
	for arg in args:
		attr, val = arg.split("=")
		try: val = int(val)
		except:
			try: val = float(val)
			except: pass
		globals()[attr] = val
		
	if CONFIG_TEXT:
		for line in Blender.Text.get(CONFIG_TEXT).asLines():
			if line.startswith("material_"):
				old, new = line[9:].split("=")
				MATERIAL_MAP[old] = new
				log.info("Maps material %s to %s" % (old, new))
				
	export(FILENAME)
	Blender.Quit()
	
else:
	if FILENAME: fs_callback(FILENAME)
	else:
		defaultname = Blender.Get("filename")
		
		if defaultname.endswith(".blend"):
			defaultname = defaultname[0:len(defaultname)-len(".blend")] + ".cfg"
	 
		dir,name=os.path.split(defaultname)
	 
		lastpath=get_from_registry()
		defaultname=os.path.join(lastpath,name)

		Blender.Window.FileSelector(fs_callback, "Cal3D Export", defaultname)


