#!BPY
# $Id: export-palomino.py,v 1.15 2006/08/20 16:34:17 devel Exp $
# Author:   Jim Brooks  http://www.jimbrooks.org
# Desc:     Blender Python script to convert Blender 3D data to Palomino format.
# Usage:    Put this in the Python path as set in Blender's User Preferences.
#           Edit User Preferences / File Paths / Python.
#           There's a nearby button named something like "reevaluate menu".
#           Then select File / Save Default Settings.
#           Blender should auto-magically add this script as "Palomino Exporter"
#           to its "Export" sub-menu of its Python Scripts menu.
# Notes:    Vertex Groups:
#           Blender's Vertex Groups capability is used to mark a set of vertexs
#           for use by Palomino (not in the way that was intended for Blender users)
#           such as rotating the wings of an F-14 Tomcat which requires knowing
#           which vertexs and polygons compose the wings.  Blender associates
#           a string for a set of vertexs.
#
#           Remember: Before exporting, first save the .blend file.
#           Otherwise, full data won't be exported (don't know why).
#
# LEGAL:    COPYRIGHT (C) JIM E. BROOKS 2005
#           THIS SOURCE CODE IS RELEASED UNDER THE TERMS
#           OF THE GNU GENERAL PUBLIC LICENSE (GPL).
# ==============================================================================

""" Registration info for Blender menus:
Name: 'Palomino Exporter'
Blender: 230
Group: 'Export'
Tooltip: 'Export to Palomino format'
"""

import Blender
from Blender import NMesh, Object
from Blender.BGL import *
from Blender.Draw import *
from Blender.Window import *
from Blender.Image import *

import sys, struct, string
from types import *
import os
from os import path

#================================================================================
#--------------------- 3D math (pythonized from Palomino gfx) -------------------
#================================================================================

from math import *

# Matrix index constants.
Xx = 0
Xy = 1
Xz = 2
Xs = 3

Yx = 4
Yy = 5
Yz = 6
Ys = 7

Zx = 8
Zy = 9
Zz = 10
Zs = 11

Ox = 12
Oy = 13
Oz = 14
Os = 15

# pi
M_PI = 3.14159265358979323846

def deg2rad( deg ):
    """ Return a radian from a degree. """
    return deg / 360.0 * (2.0 * M_PI)

def NewMatrix():
    m = []
    for i in range(16):
        m.append(0.0)
    return m

def PrintMatrix( m ):
    print m[Xx], m[Xy], m[Xz]
    print m[Yx], m[Yy], m[Yz]
    print m[Zx], m[Zy], m[Zz]

def IdentityMatrix( m ):
    """ Set matrix to 1:1 """
    m[Xx] = 1.0
    m[Xy] = 0.0
    m[Xz] = 0.0
    
    m[Yx] = 0.0
    m[Yy] = 1.0
    m[Yz] = 0.0
    
    m[Zx] = 0.0
    m[Zy] = 0.0
    m[Zz] = 1.0

def CopyMatrix( dest, src ):
    #dest = src[:]   # wrong
    for i in range(16):
        dest[i] = src[i]

def RotTransCoord( axis, x, y, z, m ):
    return x * m[axis*4+0] + y * m[axis*4+1] + z * m[axis*4+2] + m[12+axis]

def RotateMatrixFixed( axis, degree, m ):
    """ m (OUT)
        Rotate a matrix around a fixed axis.
        This function is suited to rotating the viewpoint/eye.
        The word 'fixed' should be clear by looking at the math.
        Eg, the value of the X coord won't be changed by a rotation around the X axis. """

    s = sin( deg2rad(degree) )
    c = cos( deg2rad(degree) )
    n = NewMatrix()
    CopyMatrix( n, m )    # dest,src

    if axis == 0:    # X axis Pitch
        
        n[ 4] = m[ 4]*c - m[ 8]*s      # Y = Ycos - Zsin
        n[ 5] = m[ 5]*c - m[ 9]*s
        n[ 6] = m[ 6]*c - m[10]*s

        n[ 8] = m[ 4]*s + m[ 8]*c      # Z = Ysin + Zcos
        n[ 9] = m[ 5]*s + m[ 9]*c
        n[10] = m[ 6]*s + m[10]*c
        
    elif axis == 1:  # Y axis Yaw
        
        n[ 0] = m[ 0]*c - m[ 8]*s      # X = Xcos - Zsin
        n[ 1] = m[ 1]*c - m[ 9]*s
        n[ 2] = m[ 2]*c - m[10]*s

        n[ 8] = m[ 0]*s + m[ 8]*c      # Z = Xsin + Zcos
        n[ 9] = m[ 1]*s + m[ 9]*c
        n[10] = m[ 2]*s + m[10]*c

    else:            # Z axis Roll
        
        n[ 0] = m[ 0]*c - m[ 4]*s       # X = Xcos - Zsin
        n[ 1] = m[ 1]*c - m[ 5]*s
        n[ 2] = m[ 2]*c - m[ 6]*s

        n[ 4] = m[ 0]*s + m[ 4]*c       # Z = Xsin + Zcos
        n[ 5] = m[ 1]*s + m[ 5]*c
        n[ 6] = m[ 2]*s + m[ 6]*c

    CopyMatrix( m, n )    # dest,src
    
def RotateMatrixLocal( axis, degree, m ):
    """ m (OUT)
        Rotate a local coordinate system around its own axis. """

    # Identity (1:1) matrix r.
    r = NewMatrix()
    IdentityMatrix( r )

    # Rotate matrix r.
    RotateMatrixFixed( axis, degree, r )

    # Transform r thru m.
    # Ie, thru matrix m, pass r as if it were the rotated coords (1.0, 1.0, 1.0).
    t = NewMatrix()
    t[0] = RotTransCoord(0,m[0],m[1],m[2],r)
    t[1] = RotTransCoord(1,m[0],m[1],m[2],r)
    t[2] = RotTransCoord(2,m[0],m[1],m[2],r)

    t[4] = RotTransCoord(0,m[4],m[5],m[6],r)
    t[5] = RotTransCoord(1,m[4],m[5],m[6],r)
    t[6] = RotTransCoord(2,m[4],m[5],m[6],r)

    t[8] = RotTransCoord(0,m[8],m[9],m[10],r)
    t[9] = RotTransCoord(1,m[8],m[9],m[10],r)
    t[10]= RotTransCoord(2,m[8],m[9],m[10],r)

    # m = r
    # Don't copy over bottom row which contains the origin coords.
    m[0] = t[0]
    m[1] = t[1]
    m[2] = t[2]

    m[4] = t[4]
    m[5] = t[5]
    m[6] = t[6]

    m[8] = t[8]
    m[9] = t[9]
    m[10]= t[10]

def Rotate3D( x, y, z, m ):
    """ Rotate a 3D vertex thru a matrix (transform without translating). """
    x2 = x * m[Xx] + y * m[Xy] + z * m[Xz]
    y2 = x * m[Yx] + y * m[Yy] + z * m[Yz]
    z2 = x * m[Zx] + y * m[Zy] + z * m[Zz]
    return (x2,y2,z2)

#================================================================================
#------------------------------ export code -------------------------------------
#================================================================================

class State:
    """ Global state. """

    def __init__( s ):
        # File to export to (should have ".pal" suffix).
        s.mFilename = Create( os.environ["HOME"] + "/gfx/palomino/game/pony/models/NONAME.pal" )

        # Exported model can be rotated optionally.
        s.mRotation = Create( "0.0,0.0,0.0" )

        # Exported model can be scaled optionally.
        s.mScale = Create( "1.0" )

        # 16-element matrix for rotating model.
        s.m = NewMatrix()
        
        # If mLong is true, for debugging, exports to a longer XML-like format
        # that is more readable (though this format isn't actually importable).
        s.mLong             = False

        # GUI events.
        s.EVENT_NONE        = 1
        s.EVENT_SAVE_FILE   = 2
        s.EVENT_LOAD_FILE   = 3
        s.EVENT_FORMAT      = 4

        s.warnedAboutDefaultColors = False

    def PrepareMatrix( s ):
        """ Rotate the matrix by the 3 degrees specified in the text entry widget.
            Used to rotate an exported model.
            To be called when exporting begins. """

        IdentityMatrix( s.m )

        # Extract degrees from comma-separated string: "x,y,z"
        degList = s.mRotation.val.split( "," )
        if len(degList) != 3:
            raise "degList != 3"

        # Rotate matrix around its X,Y,Z axises.
        axis = 0
        while axis < 3:
            deg = float(degList[axis])    # string-to-float
            if abs(deg) > 0.1:            # if degree isn't zero
                RotateMatrixLocal( axis, deg, s.m )
            axis = axis + 1

def ExportVertexs( ofile, mesh ):

    if not st.mLong:
        ofile.write( "<v/>\n" )                       # vertex array
        ofile.write( str(len(mesh.verts)) + "\n" )    # count of vertexs
        for v in mesh.verts:
            (x,y,z) = Rotate3D( v.co[0], v.co[1], v.co[2], st.m )
            x = x * float(st.mScale.val)
            y = y * float(st.mScale.val)
            z = z * float(st.mScale.val)
            ofile.write( str(x) + "\n" )
            ofile.write( str(y) + "\n" )
            ofile.write( str(z) + "\n" )
    else:
        for v in mesh.verts:
            ln = "<vertex> " + str(v.co[0]) + ", " + str(v.co[1]) + ", " + str(v.co[2]) + " </vertex>\n"
            ofile.write( ln )

def ExportVertexGroups( ofile, mesh ):

    # <vg name='wingLeft'>
    # 4    // count of vix
    # 12   // vix (vertex index)
    # ...  // vix...
    # </vg>

    # For each Vertex Group.
    for vgName in mesh.getVertGroupNames():
        
        # Start tag and name of Vertex Group.
        if not st.mLong:
            ofile.write( "<vg>\n" + vgName + "\n" )
        else:
            ofile.write( "<VertexGroup name='" + vgName + "'>\n" )
                
        # Write the amount of vertex indexes.
        vixCnt = len(mesh.getVertsFromGroup(vgName))
        if not st.mLong:
            ofile.write( str(vixCnt) + "\n" )
        else:
            ofile.write( "<vixCnt>" + str(vixCnt) + "</vixCnt>\n" )
            
        # Write each vix of this Vertex Group.
        for vix in mesh.getVertsFromGroup( vgName ):
            if not st.mLong:
                ofile.write( str(vix) + "\n" )
            else:
                ofile.write( "  <vix>" + str(vix) + "</vix>\n" )

        # End tag.
        if not st.mLong:
            ofile.write( "</vg>\n" )
        else:
            ofile.write( "</VertexGroup>\n" )

def ExportNormals( ofile, mesh ):

    # I'm not 100% certain of this as I can't find documentation, but apparently
    # Blender normals (face.normal) are in normal space and originate from the first vertex.
    # That is, v[0] is in world space, so by adding face.normal to it, the result
    # is a point in world space on the normal vector extending from vert[0].

    if not st.mLong:
        ofile.write( "<n/>\n" )                       # vertex array

        # Write the count of normals.
        # Count the normals which all polygons that have >= 3 vertexs.
        normalCnt = 0
        for face in mesh.faces:
            # Ignore a polygon that is a line/edge.
            if len(face.v) >= 3:
                normalCnt += 1
        ofile.write( str(normalCnt) + "\n" )    # count of normals (each face has a normal)

        # Write each normal.
        for face in mesh.faces:
            # Ignore a polygon that is a line/edge.
            if len(face.v) >= 3:
                (x,y,z) = Rotate3D( face.normal[0], face.normal[1], face.normal[2], st.m )
                ofile.write( str(-x) + "\n" )  # negate coords for Palomino
                ofile.write( str(-y) + "\n" )
                ofile.write( str(-z) + "\n" )
                ofile.write( str(face.v[0].index) + "\n" )   # vix (vertex that is normal's origin)
    else:
        for face in mesh.faces:
            indent = "  "
            # Ignore a polygon that is a line/edge.
            if len(face.v) >= 3:
                ln  = "<normal>\n"
                ln += indent + "<vertex> "
                ln += str(-face.normal[0]) + ", " + str(-face.normal[1]) + ", " + str(-face.normal[2]) + " </vertex>\n"
                ln += indent + "<vix> " + str(face.v[0].index) + " </vix>\n"
                ln += "</normal>\n"
                ofile.write( ln )

def ExportPolygons( ofile, mesh ):

    if not st.mLong:
        # For each polygon.
        for face in mesh.faces:
            # Ignore a polygon that is a line/edge.
            if len(face.v) > 2:
                ofile.write( "<p>\n" )                   # polygon start

                # Export RGBA as 8-bit values (takes less space than floats).
                # Blender provides a list of colors for each vertex.
                # Just use the color of the first vertex (assume all colors are the same).
                # Palomino doesn't do shading (interpolate vertex colors) currently.
                ofile.write( "<pr/>\n" )
                try:
                    color = face.col[0]
                    ofile.write( str(color.r) + "\n" )
                    ofile.write( str(color.g) + "\n" )
                    ofile.write( str(color.b) + "\n" )
                    ofile.write( str(color.a) + "\n" )
                except:
                    # Apparently this polygon wasn't assigned a color yet.
                    ofile.write( "128\n" )
                    ofile.write( "128\n" )
                    ofile.write( "128\n" )
                    ofile.write( "255\n" )
                    if not st.warnedAboutDefaultColors:
                        st.warnedAboutDefaultColors = True
                        print "WARNING: Using default colors for a polygon(s)."
                        print "WARNING: Apparently no color was assigned yet to the polygon(s)."

                # Export array of vertex indexs.
                ofile.write( "<pv/>\n" )                 # polygon vertex indexs
                ofile.write( str(len(face.v)) + "\n" )   # count of vertex indexs
                # For each vertex index.
                for v in face.v:
                    ofile.write( str(v.index) + "\n" )

                ofile.write( "</p>\n" )                   # polygon end
    else:
        indent = "  "
        # For each polygon.
        for face in mesh.faces:
            # Ignore a polygon that is a line/edge.
            if len(face.v) > 2:
                ofile.write( "<polygon>\n" )

                # For each vertex index (vix).
                ln = indent + "<vix> "
                i = 0
                for v in face.v:
                    ln += str(v.index)
                    if i < len(face.v) - 1:    # omit comma after last vertex index
                        ln += ","
                    i = i + 1
                ln += " </vix>\n"
                ofile.write( ln )

                ofile.write( "</polygon>\n" )

def ExportFile( filename ):
    """ Export Blender data to Palomino file format. """

    # Open file to export.  Add .xml suffix if appropriate.
    filename = filename[:]    # modify copy
    if st.mLong and filename.find( ".xml" ) == -1:
        filename += ".xml"
    ofile = open( filename, "w" )

    # Get the currently selected object.
    meshObj = Blender.Object.GetSelected()
    if len( meshObj ) == 0:
        ofile.write( "ERROR: must select a mesh" )
        result = Blender.Draw.PupMenu( "Select an Object to export%t|OK" )
        return
        
    # Check if it's a mesh object.
    if meshObj[0].getType() != "Mesh":
        ofile.write( "ERROR: must select a mesh to output" )
        ofile.write( "Found: " + str(meshObj[0].getType()) )
        result = Blender.Draw.PupMenu( "Selected Object to export must be a mesh ?%t|OK" )
        return
    
    # Get mesh data.
    mesh = meshObj[0].getData()

    # Build the <info/> tag.
    blenderVer = Blender.Get( "version" )
    modelName  = "?"
    try:
        # Plan A: try to get model name from Blender.
        modelName = meshObj.GetName()
    except:
        # Plan B: derive model name from export filename minus ".pal" suffix.
        if filename.find( ".pal" ) != -1:
            modelName = os.path.basename( filename )
            modelName = modelName[:-4]
    if modelName == "?":
        print "Warning: Can't determine name of this 3D model"
    if os.environ["USER"] == "jimb":
        author = "Jim Brooks"
    else:
        author = os.environ["USER"]
    infoTag  = "<info ver='1.0' type='Palomino 3D model' "
    infoTag += "name='" + modelName + "' "
    infoTag += "author='" + author + "' "
    infoTag += "modelProg='Blender' modelProgVer='" + str(blenderVer) + "' "
    infoTag += "/>\n"
    ofile.write( infoTag )

    # "<ex>" means for this export script itself.

    # Write model rotation tag (if other than default).
    if st.mRotation.val != "0.0,0.0,0.0":
        ofile.write( "<ex rot='" + st.mRotation.val + "'/>\n" )

    # Write model scale tag (if other than default).
    if st.mScale.val != "1.0":
        ofile.write( "<ex scale='" + st.mScale.val + "'/>\n" )

    st.PrepareMatrix()

    # Call subroutines.
    ExportVertexs( ofile, mesh )
    ExportNormals( ofile, mesh )
    ExportPolygons( ofile, mesh )
    ExportVertexGroups( ofile, mesh )

    print "Exported to " + str(filename)

    ofile.close()

def LoadSettings( filename ):
    """ Load settings from a .pal file (eg rotation). """

    # Read .pal file.
    try:
        ifile = open( filename, "r" )
    except:
        print "WARNING: file '" + filename + "' doesn't exist"
        return
    
    # "<ex>" means for this export script itself.
    for line in ifile:

        # Load model rotation degrees (optional).
        if line.find( "<ex rot='" ) != -1:
            # Extract field containing 3 comma-separated degrees.
            # For example: <ex rot='1,0,0'/>
            # They came from the "Rotation: " text entry
            # when this script was ran previously.
            st.mRotation.val = GetWhatsQuoted( line, "<ex rot='", "'" )
            print "loaded model rotation: ", st.mRotation.val

        # Load model scale (optional).
        if line.find( "<ex scale='" ) != -1:
            # Extract field containing scale.
            # For example: <ex scale='1.0'/>
            # It came from the "Scale: " text entry
            # when this script was ran previously.
            st.mScale.val = GetWhatsQuoted( line, "<ex scale='", "'" )
            print "loaded model scale: ", st.mScale.val

    ifile.close()

def GetWhatsQuoted( line, preceding, quoteChar = "'" ):
    # Eg, returns "joe" for:
    # line = "<tag person='joe'/>"
    # preceding = "person='"    preceding's last char should be a quote char

    # Preceding's last char should be a quote char.
    if preceding[-1] != quoteChar:
        raise IndexError

    # Ensure "preceding" is on this line.
    idxPreceding = line.find( preceding )
    if idxPreceding == -1:    
        raise IndexError

    # Extract the quoted field (without quote chars).
    idxQuote1 = idxPreceding + len(preceding)
    idxQuote2 = line.find( quoteChar, idxQuote1 + 1 )
    if idxQuote2 == -1:
        raise IndexError
    return line[idxQuote1:idxQuote2]

def DrawGui():
    """ Draw GUI elements (buttons, text entry, etc) of export script. """

    glClear( GL_COLOR_BUFFER_BIT )
    glRasterPos2d( 8, 115 )

    # Title.
    if st.mLong:
        Text( "Blender-to-Palomino Exporter  (debug XML format)" )
    else:
        Text( "Blender-to-Palomino Exporter (note: must save before exporting)" )

    # String(name, event, x, y, width, height, initial, length, tooltip)
    stringPadding = 20

    # Text entry widgets (Blender "String").
    stringX = 10    # helps to build text entrys horizontally
    stringW = 400
    st.mFilename = String( "Export to: ",
                           st.EVENT_NONE,
                           stringX, 75, stringW, 20,
                           st.mFilename.val, 255,
                           "filename to export to: " )

    stringX = stringX + stringW + stringPadding
    stringW = 200
    st.mRotation = String( "Rotation: ",
                           st.EVENT_NONE,
                           stringX, 75, stringW, 20,
                           st.mRotation.val, 255,
                           "model rotation: x,y,z separated by commas" )

    stringX = stringX + stringW + stringPadding
    stringW = 120
    st.mScale   = String( "Scale: ",
                           st.EVENT_NONE,
                           stringX, 75, stringW, 20,
                           st.mScale.val, 255,
                           "scale model" )

    # Buttons.
    Button( "Export", st.EVENT_SAVE_FILE,   10, 40, 80, 20, "export to .pal file" )
    Button( "Load",   st.EVENT_LOAD_FILE,  100, 40, 80, 20, "reload settings from .pal file" )
    Toggle( "Format", st.EVENT_FORMAT,     200, 40, 80, 20, 0, "toggle production/debug .pal format" )

def EventHandler( event, val ):
    if event == QKEY and not val:
        Exit()

def EventHandler2( event ):
    if event == st.EVENT_SAVE_FILE:
        ExportFile( st.mFilename.val )
    elif event == st.EVENT_LOAD_FILE:
        LoadSettings( st.mFilename.val )
        Blender.Draw.Redraw(1)
    elif event == st.EVENT_FORMAT:
        st.mLong = not st.mLong
        Blender.Draw.Redraw(1)

# ------------------------------------------------------------------------------

st = State()
Register( DrawGui, EventHandler, EventHandler2 )
