bl_info = {     "name": "Export OpenRails/MSTS Shape File(.s)",
                "author": "Wayne Campbell",
                "version": (3, 4),
                "blender": (2, 6, 3),
                "location": "File > Export > OpenRails/MSTS (.s)",
                "description": "Export file to OpenRails/MSTS .S format",
                "category": "Import-Export"}
                

'''
COPYRIGHT 2016 by Wayne Campbell        

	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 3 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.

    A copy of the GNU General Public License is included in this distribution package
    or may be found here <http://www.gnu.org/licenses/>.

For complete documentation, and CONTACT info see the Instructions included in the distribution package.    

   

REVISION HISTORY

2016-01-11      Released as V 3.4
                strip periods (.) from object names
                fixes to LOD inheritance
                remember RetainNames setting
                file export dialog, default to .s extension          
2016-01-10      Testing as V 3.3
                Added RetainNames option
                Fixed bounding sphere bug
                Trimmed most numbers to 6 decimal places to reduce output file size
2016-01-10      Testing as V 3.2
                Documented hidden option to use Railworks style LOD naming
                Documented setting MAX values in Custom Properties
                Fixed AttributeError: 'NoneType' object has no attribute 'game_settings'
2016-01-09      Testing as V 3.1
                Added MipMapLODBias setting
                Fixed bug, untextured faces - AttributeError: 'NoneType' object has no attribute 'name'
                Implemented MSTS sub_object flags for sorting alpha blended faces ( not needed for OR )
                Implement MSTS sub_object flags for specularity ( not needed for OR )
                Added Alpha Sorted Transparency option to MSTS Material
                Improved descriptions and naming of MSTS Material settings
                Fixed MSTS Lighting value not being saved
                Sped up iVertexAdd, iNormalAdd, etc,  with spatial tree indexing, etc
2016-01-08      Testing as V 3.0
                Fix for WHEELS13, WHEELS23
                Changed use_shadeless, from selecting Cruciform, now done with MSTS Lighting
                Added MSTS Material panel for lighting options, etc
                Added support for Railworks style LOD naming
2015-11-11      Released as V 2
                Changed prim_state labels to be compatible with Polymaster
2013-11-01      Initial Release as V 2.6.3


TODO FUTURE
    - add a progress bar ( currently unavailable in the Blender API )
    - document or remove hidden Normals override options ( superceded by Blender's new Normals Modifier )
    - document or remove hidden UV wrapping options - Wrap, Clamp, Extend etc ( not OR compatible)
    - support undocumented MSTS capability, eg
        - double sided faces
        - bump mapping and environmental reflections
        - AddATex, SubractATex, etc and other undocumented shaders
        - zBias
        - proper use of lod_control objects to improve LOD efficiency
    - use proper blender addon packaging method
    - future OR shaders, eg light maps, bump maps, reflection maps
    - option to export texture files
    - option to compress shape file
    - options to generate .SD, .ENG, .WAG or .REF

'''

import os
from math import radians
    
import bpy
from bpy.props import StringProperty, EnumProperty, BoolProperty
import mathutils
from mathutils import *

MaxVerticesPerPrimitive = 8000     # 8000 OK 20000 Fails
MaxVerticesPerSubObject = 15000      # 15000 OK 20000 Fails
#MaxVerticesPerVertexSet = 20000    # limited by vertices per subobject
#MaxSubObjectsPerDistanceLevel =  I've done 120 subobjects without and problem
# 40,000 points loads
# 40,000 poly's spread across 12 subobjects, 120,000 vertices will load

RetainNames = False   # user option, when true, the exporter disables mesh 
                      # consolidation and hierarchy collapse optimizations

                      
#####################################       

from bpy_extras.io_utils import ExportHelper, ImportHelper  # gives access to the FileSelectParams

class ExportHelper:
    filepath = StringProperty(
            name="File Path",
            description="Filepath used for exporting the file",
            maxlen=1024,
            subtype='FILE_PATH',
            )

    # set up the FileSelectParams
    filename_ext = ".s"
    filter_glob = StringProperty(default="*.s", options={'HIDDEN'})


class MSTSExporter(bpy.types.Operator, ExportHelper):


    bl_idname = "export.msts_s"
    bl_description = 'Export to OpenRails/MSTS .s file'
    bl_label = "Export OpenRails/MSTS S"
    bl_space_type = "PROPERTIES"
    bl_region_type = "WINDOW"
    bl_options = {'UNDO'}

    filepath = StringProperty(subtype='FILE_PATH')
    
    def invoke(self, context, event):
    
        settings = context.scene.msts
        
        # set up default path
        blend_filepath = context.blend_data.filepath
        if not blend_filepath:
            blend_filepath = "untitled"
        else:
            blend_filepath = os.path.splitext(blend_filepath)[0]
        self.filepath = blend_filepath + self.filename_ext
        
        # override with last used path if it looks good
        if settings.SFilepath != "":
            lastSavedPath = os.path.abspath( bpy.path.abspath( settings.SFilepath ) )
            lastSavedFolder = os.path.split(lastSavedPath)[0]
            if os.path.exists( lastSavedFolder ):
                self.filepath = lastSavedPath

        WindowManager = context.window_manager
        WindowManager.fileselect_add(self)
        return {"RUNNING_MODAL"}
        
    def draw( self, context ):
    
        settings = context.scene.msts
        
        layout = self.layout
        
        layout.prop( self, "filepath" )
        layout.prop( settings, "RetainNames" )

    def execute(self, context):
   
        settings = context.scene.msts
        
        global RetainNames
        RetainNames = settings.RetainNames

        #Append .s
        exportPath = bpy.path.ensure_ext(self.filepath, ".s")
        settings.SFilepath = TryGetRelPath( exportPath )
        
        # Validate root object
        rootName = 'MAIN'
        if bpy.context.scene.objects.get(rootName) == None:
            # try alternate RW Naming
            rootName = FirstRWName()
            if rootName == None:
                print()
                print( "ERROR: Scene doesn't have a MAIN object")
                self.report( {'ERROR'}, "Scene doesn't have a MAIN object " )
                return {'CANCELLED' }
        if bpy.context.scene.objects[rootName].parent != None:
            print()
            print( "ERROR: MAIN object must be in the root.")
            self.report( {'ERROR'}, "ERROR: MAIN object must be in the root." )
            return {'CANCELLED' }
            
            
        #force out of edit mode
        if bpy.context.mode != 'OBJECT':
            bpy.ops.object.mode_set( mode = 'OBJECT' )
        
        #Export
        print()
        print( "EXPORTING "+rootName )
        print()
        ExportShapeFile( rootName, exportPath, True )
            
        print()
        print( "DONE" )
        self.report( {'INFO'}, "Finished OK" )
        return {"FINISHED"}


def menu_func(self, context):
    self.layout.operator(MSTSExporter.bl_idname, text="OpenRails/MSTS (.s)")

def register():
    bpy.utils.register_class( msts_material_props)  # define the new msts material properties
    bpy.utils.register_class( msts_scene_props)  
    bpy.types.Material.msts = bpy.props.PointerProperty(type=msts_material_props)   # add the properties to the material type
    bpy.types.Scene.msts = bpy.props.PointerProperty(type=msts_scene_props)
    bpy.utils.register_class( msts_material_panel )                               # add a UI panel to the Materials window
    bpy.utils.register_class( MSTSExporter )                                    # define the exporter dialog panel
    bpy.types.INFO_MT_file_export.append(menu_func)                             # add the exporter to the menu

def unregister():
    bpy.types.INFO_MT_file_export.remove(menu_func)
    bpy.utils.unregister_class( MSTSExporter )
    bpy.utils.unregister_class( msts_material_panel )
    del bpy.types.Material.msts
    bpy.utils.unregister_class( msts_scene_props)  
    bpy.utils.unregister_class( msts_material_props )

'''
************************ THE GUI *******************************
'''
        
#####################################       
# return the first object with rw naming - ie 1_1000_xxxxx, in the root of the current scene
def FirstRWName():

    for o in bpy.context.scene.objects:
        if o.parent == None:
            if IsRWName( o.name ):
                return o.name
    return None


##########################################
# turn a list into the input for a selector box
def MakeItems( list ):
    items = []
    if list != None:
        for s in list:
            items.append( ( s, s, "" ) )
    return items

    
#####################################       
# called when the material panel is changed
def UpdateMSTSMaterial( self, context ):

    if context.material != None:
        blM = context.material
        
        blM.game_settings.alpha_blend = blM.msts.Transparency
        if  blM.msts.Transparency != 'OPAQUE':
            blM.use_face_texture_alpha = True
            blM.use_transparency = True
            blM.alpha = 0
            blM.specular_alpha = 0
        else:
            blM.use_face_texture_alpha = False
            blM.use_transparency = False
            
        if blM.msts.Lighting == "EMISSIVE":
            blM.use_shadeless = True
        else:
            blM.use_shadeless = False

    
#####################################       
class msts_material_panel(bpy.types.Panel):
    """Creates a Panel in the material context of the properties editor"""
    bl_label = "MSTS Materials"
    bl_idname = "MATERIAL_PT_msts"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "material"

    def draw(self, context):   # see bpy.context for info on available context's
        
        layout = self.layout
        if context.material != None:
            mstsmaterial= context.material.msts
            game_settings = context.material.game_settings
            
            # Create a simple row.
            layout.prop(mstsmaterial, property="Transparency" )
            layout.prop(mstsmaterial, property="Lighting" )
            layout.prop(mstsmaterial, property="MipMapLODBias")  #TODO, this should be a texture 
                                                                # property, not a material property
                                                                # but here is simpler for the user to find
                                                                

#####################################       
class msts_material_props(bpy.types.PropertyGroup):
    # define custom material properties,  ie material.rw.shadername   note: these prop's will be saved in the blend file

    Transparency = bpy.props.EnumProperty(
                name="Transparency",
                description="Controls handling of alpha channel.",
                items = [ ( "OPAQUE",   "Solid Opaque",         "Alpha channel is ignored" ),
                          ( "CLIP",     "Transparency On/Off",  "Transparent if alpha value below a threshold" ),
                          ( "ALPHA",    "Alpha Blended",        "Alpha value blends from transparent up to opaque" ),
                          ( "ALPHA_SORT","Alpha Sorted",        "Alpha blending with depth sort" ) 
                        ],
                default="OPAQUE",
                update= UpdateMSTSMaterial,
                )
    
    Lighting = bpy.props.EnumProperty(
                name="Lighting",
                description="Adjusts light and shadow",
                items = [ ("NORMAL",         "Normal",       "Sun facing surfaces are lit and opposite are shaded" ),
                          ("SPECULAR25",     "Specular 25",  "Strong specular highlight" ),
                          ("SPECULAR750",    "Specular 750", "Small specular highlight" ),
                          ("FULLBRIGHT",     "Full Bright",  "Shaded surfaces appear lit" ),
                          ("HALFBRIGHT",     "Half Bright",  "Shaded surfaces appear partly lit" ),
                          ("DARK",           "Dark",         "Sun facing surfaces appear fully shaded" ),
                          ("CRUCIFORM",      "Cruciform",    "Indirect ambient lighting only" ),
                          ("EMISSIVE",       "Emissive",     "Surfaces emit light at night" )
                        ],  
                default="NORMAL",
                update= UpdateMSTSMaterial,
                )
                
    MipMapLODBias = bpy.props.FloatProperty(
                description="Controls sharpness of the image, default = -3",
                default = 0,
                soft_max = 8,
                soft_min = -8,
                precision = 1,
                step = 100
                )

                
LightingOptions = { "NORMAL":-5,  
                    "SPECULAR25":-6, 
                    "SPECULAR750":-7,
                    "FULLBRIGHT":-8, 
                    "HALFBRIGHT":-11, 
                    "DARK":-12,
                    "CRUCIFORM":-9,     
                    "EMISSIVE":-5
                  }

class msts_scene_props(bpy.types.PropertyGroup):
    # these properties appear in the file export dialog
    
    SFilepath = StringProperty( default = "" )

    RetainNames = BoolProperty(name='Retain Names', description = 'Disables object merging optimizations', default = False )

                  
'''
This code converts from Blender data structures to MSTS data structures
from here down to the Library section, the code uses blender coordinate system unless specified as MSTS
'''

#####################################
def GetFileNameNoExtension( filepath ):  # eg filePath = '//textures\\redtile.tga'
    s = filepath.replace( '\\','/' )       # s = '//textures/redtile.tga'
    parts = s.split( '/' )              # parts = ['','','textures','redtile.tga']
    lastPart = parts[ len(parts)-1 ]        # lastPart = 'redtile.tga'
    noextension = os.path.splitext( lastPart )[0]   # noextension = 'redtile'
    return noextension
    
#####################################
# convert to path relative to this blend file if possible
# otherwise just return filepath
def TryGetRelPath( filepath ):  

    try:
        return bpy.path.relpath( filepath )
    except:
        return filepath

    
    
#####################################    
# specifies how normals are to be calculated   
class Normals:       # passed to AddFaceToSubObject to request special handling of normals
        Face = 0        # flat normals
        Smooth = 1      # smoothed ( can be overriden per face )
        Out = 3         # normals radiate out from center of model   Mesh or Material Property:  NORMALS = OUT
        Up = 4          # all normals face up   Mesh or Material Property:  NORMALS = UP
        Fillet = 5      # bevels are modified to appear rounded  Mesh or Material Property: NORMALS = FILLET
        OutX = 6        # normals radiate out to the left and right along objects x=0,z=-4 axis
        

#####################################     
# given a blender property override like NORMALS = UP, and an initialNormal, 
# return the normal state after the override is applied
# propertyString represents eg "UP"
# initialNormal eg Normals.Face
def GetNormalsOverride( propertyString, initialNormal ):
        if propertyString == 'UP':
            return Normals.Up
        elif propertyString == 'OUT':
            return Normals.Out
        elif propertyString == 'FILLET':
            return Normals.Fillet
        elif propertyString == "OUTX":
            return Normals.OutX
        return initialNormal

        
    
#####################################
def iShaderAdd( shaderName ):

    if shaderName in ExportShape.Shaders:
        iShader = ExportShape.Shaders.index(shaderName)
    else:
        iShader = len( ExportShape.Shaders )
        ExportShape.Shaders.append( shaderName )
    return iShader


#####################################
def iFilterAdd( filterName ):

    if filterName in ExportShape.Filters:
        iFilter = ExportShape.Filters.index( filterName )
    else:
        iFilter = len( ExportShape.Filters )
        ExportShape.Filters.append( filterName )
    return iFilter


#####################################
def iImageAdd( image ):
    
    if image!=None:
        fileName = GetFileNameNoExtension( image.filepath )
    else:
        fileName = 'blank'
    imageName = fileName + '.ace'

    for iImage in range(0, len( ExportShape.Images)):
        if ExportShape.Images[iImage] == imageName:
            return iImage
    
    iImage = len( ExportShape.Images)
    ExportShape.Images.append( imageName)
    return iImage

    
#####################################
def iTextureAdd( image, mipMapLODBias ):   # creates both texture and image entries

    iImage = iImageAdd( image )

    for iTexture in range( 0, len( ExportShape.Textures)):
        texture = ExportShape.Textures[iTexture]
        if texture.iImage == iImage and texture.MipMapLODBias == mipMapLODBias:
            return iTexture
    
    iTexture = len( ExportShape.Textures )
    newTexture = Texture()
    newTexture.iImage = iImage
    newTexture.iFilter = iFilterAdd( 'MipLinear' )
    newTexture.MipMapLODBias = mipMapLODBias
    ExportShape.Textures.append( newTexture )
    return iTexture

#####################################       
def UVOpsMatch( opsList,  specifierList ):       # specifiers is a list ( operation, textureAddressMode ) pairs
    
    if len( opsList ) != len( specifierList ):
        return False
    for i in range( 0, len( opsList ) ):
        eachUVOp = opsList[i]
        operation = specifierList[i][0]
        if eachUVOp.__class__ != operation: return False
        if operation == UVOpCopy and eachUVOp.TextureAddressMode != specifierList[i][1]: return False
        elif operation == UVOpReflectMapFull: continue
    return True

#####################################       
def iLightConfigAdd( uvops ):    # it a list of   ( operation, textureAddressMode ) pairs
    
    # see if its already set up
    for i in range( 0, len( ExportShape.LightConfigs ) ):
        if UVOpsMatch( ExportShape.LightConfigs[i].UVOps, uvops ):
                return i
    
    # no, so create it
    i = len( ExportShape.LightConfigs )
    newLightConfig = LightConfig()
    for eachop in uvops:
        if eachop[0] == UVOpCopy:
            newUVOp = UVOpCopy()
            newUVOp.TextureAddressMode = eachop[1]
        elif op[0] == UVOpReflectMapFull:
            newUVOp = UVOpReflectMapFull()
        else:
            raise Exception( "PROGRAM ERROR: UNDEFINED UV OPERATION" )
        newLightConfig.UVOps.append( newUVOp )
    ExportShape.LightConfigs.append( newLightConfig )
    return i
    
#####################################       
def iColorAdd( color ):   # a r g b
    global UniqueColors
    return UniqueColors.IndexOf( color )

#####################################       
def iLightMaterialAdd( lm ):  # diff, amb, spec, emmisive, power
    global UniqueLightMaterials
    return UniqueLightMaterials.IndexOf( lm )    

    
#####################################       
def iVertexStateAdd( subObject, flags, iMatrix, iLightMaterial, iLightConfig ):
    i = len( ExportShape.VertexStates )  # use the last correct entry - earlier ones will have a full vtx_set
    while i > 0:
        i -= 1
        eachVertexState = ExportShape.VertexStates[i]
        if eachVertexState.Flags == flags \
          and eachVertexState.iMatrix == iMatrix \
          and eachVertexState.iLightMaterial == iLightMaterial \
          and eachVertexState.iLightConfig == iLightConfig:
            # we found one, make sure there is room
            vertexSet = subObject.VertexSets[i]
            return i
    # we didn't find it, so add it and add a corresponding vertex set to every sub_object
    i = len( ExportShape.VertexStates )
    newVertexState = VertexState()
    newVertexState.Flags = flags
    newVertexState.iMatrix = iMatrix
    newVertexState.iLightMaterial = iLightMaterial
    newVertexState.iLightConfig = iLightConfig
    ExportShape.VertexStates.append( newVertexState )
    for lodControl in ExportShape.LodControls:
        for distanceLevel in lodControl.DistanceLevels:
            for subobject in distanceLevel.SubObjects:
                subobject.VertexSets.append( VertexSet() ) #Note: unused vertexSets are purged during write
    return i

#####################################       
def MatchList( lista, listb ):
    
    if len(lista) != len(listb):
        return False
    for i in range( 0, len(lista) ):
        if lista[i] != listb[i]:
            return False
    return True

#####################################       
def ColorWord( floats ):
    
    value = 0;
    for f in floats:
        value = value * 256
        value = value + round( f * 255 )
    return value

# PrimState Caching
LastSubObject = None
LastFaceImage = None
LastMaterial = None
LastiMatrix = -1
LastiPrimState = 0
    
#####################################       
def iPrimStateAdd( subObject, faceImage, material, iMatrix ):
    # TODO for future shaders, this should return a list of UV layers that should be included in each vertex
    
    global ExportShape
    
    # This is called for every face, so do some caching to speed this up
    global LastSubObject
    global LastFaceImage
    global LastMaterial
    global LastiMatrix
    global LastiPrimState
    
    if LastSubObject == subObject and LastFaceImage == faceImage and  LastMaterial == material and LastiMatrix == iMatrix:
            return LastiPrimState
            
    LastSubObject = subObject
    LastFaceImage = faceImage
    LastMaterial = material
    LastiMatrix = iMatrix
    
    
    # set up some defaults
    iTextures = []
    uvops = []   # s/b one for each texture
    
    mipMapLODBias = -3 #if no material, use the MSTS default
    if material != None:
        mipMapLODBias = material.msts.MipMapLODBias
   
    # Try to use the material texture slots first to populate iTextures[] and uvops[]
    if material != None and not material.use_face_texture:
        for i in range( 0, len( material.texture_slots ) ):
            if material.texture_slots[i] != None:
                slot = material.texture_slots[i]
                if slot.texture.type == 'IMAGE':
                    itex = slot.texture.type_recast()
                    iTextures.append( iTextureAdd( itex.image, mipMapLODBias ) )
                    textureAddressMode = 1 # repeat
                    if itex.extension == 'EXTEND':
                        textureAddressMode = 3   # extend edges
                    elif itex.extension == 'CLIP':
                        textureAddressMode = 4   # clamp with border
                    elif itex.use_mirror_x or itex.use_mirror_y:
                        textureAddressMode = 2   # mirror
                    uvops.append( ( UVOpCopy, textureAddressMode ) )
                    break  #For now we are allowing only one image, this may change in the future
    
    # if use_face_textures checked, or the material has no textures, then use the face texture
    if iTextures == []:
        iTextures.append( iTextureAdd( faceImage , mipMapLODBias) )
        uvops.append( ( UVOpCopy, 1 )  )
        
    # now configure options based on the material settings
    zBias = 0.0

    # Set Up Vertex Lighting
    vertexFlags = 0
    vertexLight = -5
    if material != None:

        vertexLight = LightingOptions[ material.msts.Lighting ]
        
        
    # Set up the alpha test mode    
    alphaTestMode = 1
    if material != None:
            
        if material.game_settings.alpha_blend == 'CLIP':    
            alphaTestMode = 1
        else:
            alphaTestMode = 0
            
    # Select a Shader
    if material != None:
        
        if material.msts.Lighting == "EMISSIVE":
            if material.game_settings.alpha_blend == 'OPAQUE':
                iShader = iShaderAdd( 'Tex' )
            else:
                iShader = iShaderAdd( 'BlendATex' )
        else:
            if material.game_settings.alpha_blend == 'OPAQUE':
                iShader = iShaderAdd( 'TexDiff' )
            else:
                iShader = iShaderAdd( 'BlendATexDiff' )
            
        # by default MSTS needs at least one image for these shaders, this links to 'Blank' texture
        if iTextures == []:
            # TODO Future shaders may have no textures, but for now ref. Blank.Ace
            iTextures.append( iTextureAdd( None, -3 ) )
            uvops.append( ( UVOpCopy, 1 ) )
    else:        
        iShader = iShaderAdd( 'BlendATexDiff' ) 
        iTextures.append( iTextureAdd( None, -3 ) )
        uvops.append( ( UVOpCopy, 1 ) )

    # current shaders have only one texture, so remove all the rest
    iTextures = ( iTextures[0], )
    uvops = ( uvops[0], )
    
    # and a final debug check to ensure these match
    if len( iTextures ) != len( uvops ):
        raise Exception( "PROGRAM ERROR: iTexture count must equal uvops count" )

        
    iLightConfig = iLightConfigAdd( uvops )
    
    iVertexState = iVertexStateAdd( subObject, vertexFlags, iMatrix, vertexLight, iLightConfig )
    
    
    # see if this one's already set up    
    for i in range( 0, len( ExportShape.PrimStates ) ):
        eachPrimState = ExportShape.PrimStates[i]    
        if eachPrimState.iVertexState == iVertexState \
          and eachPrimState.zBias == zBias \
          and eachPrimState.iShader == iShader \
          and eachPrimState.AlphaTestMode == alphaTestMode \
          and eachPrimState.iLightConfig == iLightConfig \
          and MatchList( eachPrimState.iTextures, iTextures ):
                LastiPrimState = i
                return i
    # we didn't find it so add it
    i = len( ExportShape.PrimStates )
    newPrimState = PrimState()
    newPrimState.Label = ExportShape.Matrices[iMatrix].Label
    if len( iTextures) > 0:
        texture = ExportShape.Textures[iTextures[0]]
        imagename = ExportShape.Images[texture.iImage]
        newPrimState.Label = newPrimState.Label + '_' + os.path.splitext( imagename)[0]
    for iTexture in iTextures:
        newPrimState.iTextures.append(iTexture)
    newPrimState.zBias = zBias
    newPrimState.iVertexState = iVertexState
    newPrimState.iShader = iShader
    newPrimState.AlphaTestMode = alphaTestMode
    newPrimState.iLightConfig = iLightConfig
    ExportShape.PrimStates.append( newPrimState )
    LastiPrimState = i
    return i
    

#####################################       
def iPrimitiveAdd( subObject, iPrimState, newVerticesCount ):
    i = len( subObject.Primitives )
    while i > 0:                                                # use the last correct entry - earlier ones could be full
        i -= 1
        primitive = subObject.Primitives[i]
        if primitive.iPrimState == iPrimState:
            if len(primitive.Triangles) * 3 + newVerticesCount > MaxVerticesPerPrimitive:  # make sure their room, 
                break                                                                                                        # or else start a new primitive
            return i
    #we didn't find it, or it was full, so add a new one
    i = len( subObject.Primitives )
    newPrimitive = Primitive()
    newPrimitive.iPrimState = iPrimState
    subObject.Primitives.append( newPrimitive )
    return i

    

#####################################       
class UniqueArray:
    
    def __init__(self, data, hash, tolerance ):
        self.data = data
        self.keys = {}
        self.hash = hash
        self.tolerance = tolerance
    
    def __getitem__(self, i):
        return self.data[i]

    def Match( self, v1, v2 ):
        for i in range( 0, len( v1 ) ):
            if abs( v1[i] - v2[i] ) > self.tolerance:
                return False
        return True
        
    def Key( self, value ):
        key = 0.0
        for v in value:
            key += v
        return round(key,self.hash)
    
    def IndexOf( self, value ):
        key = self.Key( value )
        index = self.keys.get(key,-1)
        while index != -1:
            storedValue = self.data[index]
            if self.Match( value, storedValue):
                return index
            key += .9  # in case of a hash collision, we advance the key this amount
            index = self.keys.get(key,-1)
        # we didn't find it so add it
        index = len( self.data )
        self.data.append( value )
        self.keys[ key ] = index
        return index

#####################################       
def iVertexAdd( iPoint, iNormal, iUVs, vertexSet, color1, color2 ):

    # vertexSet.iStart marks the first vertex in each mesh
    for i in range( vertexSet.iStart, len( vertexSet.Vertices ) ):
        vertex = vertexSet.Vertices[i]
        if vertex.iPoint == iPoint and vertex.iNormal == iNormal and MatchList(vertex.iUVs,iUVs) and vertex.Color1 == color1 and vertex.Color2 == color2 : 
            return i
        
    #we didnt' find it so add a new one
    vertex = Vertex()
    vertex.iPoint = iPoint
    vertex.iNormal = iNormal
    vertex.Color1 = color1
    vertex.Color2 = color2
    for iUV in iUVs:
        vertex.iUVs.append( iUV )
    iVertex = len( vertexSet.Vertices )
    vertexSet.Vertices.append( vertex )
    return iVertex  

#####################################       
def iUVPointAdd( uvPoint ):
    global UniqueUVPoints
    MSTSuvPoint =  (uvPoint[0],1-uvPoint[1])
    return UniqueUVPoints.IndexOf( MSTSuvPoint )

#####################################       
def iNormalAdd( vector ):
    global UniqueNormals
    MSTSvector =  (vector[0],vector[2],vector[1] )
    return UniqueNormals.IndexOf( MSTSvector )


#####################################       
def SubObjectFull( subObject):
    vertexCount = 0
    for eachVertexSet in subObject.VertexSets:
        vertexCount += len( eachVertexSet.Vertices )
    if vertexCount > MaxVerticesPerSubObject:
        return True
    return False
    
    
#####################################       
def SplitSubObject( subObject):
    print( '------ new subobject --------' )
    newSubObject = SubObject( subObject.DistanceLevel )
    newSubObject.Flags = subObject.Flags
    newSubObject.Priority = subObject.Priority
    for eachVertexSet in subObject.VertexSets:
        newVertexSet = VertexSet() 
        newSubObject.VertexSets.append( newVertexSet ) #unused vertex sets are purged during write
    subObject.DistanceLevel.SubObjects.append( newSubObject )
    return newSubObject

########################################
# find the last subobject that uses the specified flags
# or return None of none found    
def FindSubObject( distanceLevel, flags, priority):

    # starting at the end, look for one with the needed flags
    iLast = len( distanceLevel.SubObjects ) - 1
    while iLast >= 0:
        subObject = distanceLevel.SubObjects[iLast]
        if subObject.Flags == flags and subObject.Priority == priority:
            return subObject
        iLast -= 1
        
    return None
    

    
#####################################       
# adds to this distance_level
# choosing a sub_object based on the material used on this face
def AddFaceToDistanceLevel( distanceLevel, mesh, iFace , iMatrix, offsetMatrix, iPointOffset, normals ):

    # determine what header flags should be used with this face's material
    flags = '00000400 -1 -1 000001d2 000001c4'
    priority = 0
    if len( mesh.materials) > 0:
        meshFace = mesh.tessfaces[iFace]
        mat = mesh.materials[ meshFace.material_index ]
        if mat != None:
            if mat.game_settings.alpha_blend == 'ALPHA':
                flags = '00000400 -1 -1 000001d2 000001c4'
                priority = 1
            elif mat.game_settings.alpha_blend == 'ALPHA_SORT':
                flags = '00000500 0 0 000001d2 000001c4'
                priority = 2
            if mat.msts.Lighting.startswith( 'SPECULAR'):
                # clear bit 10
                # eg  00000500 become 00000100 and 0000400 becomes  00000000
                d5 = int(flags[5],16)
                d5 &= 0b1011
                flags = flags[0:5]+hex(d5)[2]+flags[6:]
        
    subObject = FindSubObject( distanceLevel, flags, priority)    
    
    if subObject == None:    
        # create the required subobject
        subObject = SubObject(distanceLevel)
        subObject.Flags = flags
        subObject.Priority = priority
        for eachVertexState in ExportShape.VertexStates:    # note, unused vertexsets are purged at during write
            subObject.VertexSets.append( VertexSet() )
        distanceLevel.SubObjects.append( subObject )

    elif SubObjectFull( subObject ):
        subObject = SplitSubObject( subObject)
        
    
    AddFaceToSubObject( subObject, mesh, iFace, iMatrix, offsetMatrix, iPointOffset, normals )


    
#####################################       
# adds to a sub_object in this distance_level
# chooses a sub_object based on the material used on this face
def AddFaceToSubObject( subObject, mesh, iFace , iMatrix, offsetMatrix, iPointOffset, normals ):

    meshFace = mesh.tessfaces[iFace]

    faceImage = None
    
    if len(mesh.tessface_uv_textures) > 0:
        uvLayer = mesh.tessface_uv_textures[0]
        uvFace = uvLayer.data[iFace]
        faceImage = uvFace.image
        
    if len( mesh.materials ) > 0:
        material = mesh.materials[ meshFace.material_index ]    
    else:
        material = None
        
    iPrimState = iPrimStateAdd( subObject, faceImage, material, iMatrix )  # enhance to include full material characteristics
    color1 = 0xFFFFFFFF   # vertex colors ( when vertex color layer not present )
    color2 = 0xFF000000
    
    
    if normals == Normals.Face:         # if we didn't specify a special normal mesh property
        if meshFace.use_smooth:         # Per face override for smooth normals
            normals = Normals.Smooth

    iVertexState = ExportShape.PrimStates[iPrimState].iVertexState
    vertexSet = subObject.VertexSets[iVertexState]

    iPrimitive = iPrimitiveAdd( subObject, iPrimState ,3 )
    primitive = subObject.Primitives[iPrimitive]
    
    #if scale is negative, invert winding order of triangles
    scale = offsetMatrix.to_scale()
    sign = scale.x * scale.y * scale.z
    if sign < 0:
        invert = True
    else:
        invert = False
    
    if not invert:
        if len(meshFace.vertices) == 3:
            indexSets = ( (0,2,1),  )
        elif len( meshFace.vertices ) == 4:
            indexSets = ( (0,2,1), (0,3,2) )
    else:
        if len(meshFace.vertices) == 3:
            indexSets = ( (0,1,2),  )
        elif len( meshFace.vertices ) == 4:
            indexSets = ( (0,1,2), (0,2,3) )
        
    for indexList in indexSets:
        triangle = []
        for i in indexList:
            iVert = meshFace.vertices[i]
            
            iUVs = []
            for uvLayer in mesh.tessface_uv_textures:
                uvFace = uvLayer.data[iFace]
                u = uvFace.uv_raw[i*2]
                v = uvFace.uv_raw[i*2+1]
                iUV = iUVPointAdd( (u,v) )
                iUVs.append(iUV)
            #test for missing UV's
            if iUVs == []:
                iUV = iUVPointAdd( (0,0) )
                iUVs = [iUV]
                
            if normals == Normals.Out:
                # use tree type tangent shading ( normals radiate out from center )
                normal = mesh.vertices[iVert].co
                normal = offsetMatrix.to_3x3() * normal
                normal.normalize()
            elif normals == Normals.Up:
                normal = ( 0,0,1 )
            elif normals == Normals.Smooth:
                # smooth shading uses the vertex normal
                normal = mesh.vertices[iVert].normal
                normal = offsetMatrix.to_3x3() * normal
                normal.normalize()
            elif normals == Normals.OutX:
                # radiate out from below Y axis ( ie LPSTrack100m side vegetation )
                normal = offsetMatrix.translation
                normal = Vector( (normal.x-4, 0, normal.z + 12) )
                normal.normalize()
            else:
                # flat shading uses the face normal
                normal = meshFace.normal
                normal = offsetMatrix.to_3x3() * normal
                normal.normalize()
            
            iNormal = iNormalAdd( normal )

            iPoint = iVert + iPointOffset
            if iPoint >= len( ExportShape.Points ):
                print( 'Out of range' )
            
            triangle.append( iVertexAdd( iPoint, iNormal, iUVs, vertexSet, color1, color2) ) 
            
        primitive.Triangles.append( triangle )
        # add a face normal ( used by MSTS for culling purposes )
        normal =  offsetMatrix.to_3x3() * meshFace.normal
        normal.normalize()
        primitive.iNormals.append( iNormalAdd( normal ) )
                
    
    
#####################################       
def ChildOf( ancestor, object ):
    if object.parent == None:
        return False
    if object.parent == ancestor: 
        return True
    if ChildOf( ancestor, object.parent ):
        return True
    return False
 
#####################################
def ConstructMatrix( ancestor, object ):
    # construct a cumulative transfer matrix from object to ancestor
    if object == ancestor:
        return mathutils.Matrix.Translation((0,0,0))
    if object.parent == None:
        return object.matrix_local
    if object.parent == ancestor:
        return object.matrix_local
    return ConstructMatrix( ancestor, object.parent ) * object.matrix_local

#####################################
def IsRWName( name ):

    # return true if eg 1_1000_xxxxx
    if len( name ) < 8 :  
        return False
    if not name[0].isdigit():
        return False
    if name[1] != '_':
        return False
    if not name[2].isdigit():
        return False
    if not name[3].isdigit():
        return False
    if not name[4].isdigit():
        return False
    if not name[5].isdigit():
        return False
    if name[6] != '_':
        return False
    
    return True
    
#####################################
def IsRWFirstLODName( name ):

    # return true if eg 1_1000_xxxxx
    if len( name ) < 1 :  
        return False
    if not name[0] == '1':
        return False
    return IsRWName( name )

#####################################
def IsAnimated( nodeObject ):

    #if its one of the automatically animated parts
    mstsName = MSTSName( nodeObject.name)
    animatedParts = ('BOGIE1','BOGIE2','WHEELS11','WHEELS12','WHEELS13','WHEELS21','WHEELS22','WHEELS23' )
    if animatedParts.count( mstsName.upper() ) > 0: return True    
    #or it has some animation defined
    if nodeObject.animation_data != None:
        if nodeObject.animation_data.action != None:
            fcurves = nodeObject.animation_data.action.fcurves 
            if len(fcurves) > 0:
                return True
    return False

#####################################
def IsRetained( nodeObject ):
    # return true if we should retain this level of hierarchy else collapse it down
    
    if RetainNames:
        # all except a MAIN attached to a SceneCenter
        return not ( SceneCenter != None and nodeObject in SceneCenter.children )
    else:
        # retain only the animated nodes
        return IsAnimated( nodeObject)

#####################################
def MergeNode( nodeObject, parent ):
    
    for eachNode in nodeObject.children:
        if IsRetained( eachNode ):
            BuildHierarchyFrom( eachNode, parent )
        else:
            MergeNode( eachNode, parent )

#####################################
def BuildHierarchyFrom( nodeObject, parent ):
    global hierarchy
    global hierarchyObjects
    
    index = len(hierarchy)
    hierarchy.append( parent )
    hierarchyObjects.append( nodeObject )
    MergeNode( nodeObject, index )    
    


#####################################
def MSTSNameFromRW( name ):

    name = name[7:]  # remove the 1_0200_ stuff
    
    lower = name.lower()
    
    if lower == "bo01": name = "BOGIE1"
    if lower == "bo02": name = "BOGIE2"
    if lower == "bo01wh01": name = "WHEELS11"
    if lower == "bo01wh02": name = "WHEELS12"
    if lower == "bo01wh03": name = "WHEELS13"
    if lower == "bo02wh01": name = "WHEELS21"
    if lower == "bo02wh02": name = "WHEELS22"
    if lower == "bo02wh03": name = "WHEELS23"

    return name
    
#####################################
def MSTSName( name ):

    if IsRWName( name ):   # support Railworks style naming
        name = MSTSNameFromRW( name )
    
    name = name.replace( '.','_')
    return name    


#####################################
def CreateMSTSMatrices():
    global ExportShape
    global hierarchy
    global hierarchyObjects

    ExportShape.Matrices = []
    for i in range( 0, len( hierarchy ) ):
        thisNodeObject = hierarchyObjects[i]
        mstsMatrix = MSTSMatrix() 
        mstsMatrix.Label = MSTSName(thisNodeObject.name)
        if hierarchy[i] != -1:
            parentNodeObject = hierarchyObjects[hierarchy[i]]
            blenderMatrix = ConstructMatrix( parentNodeObject, thisNodeObject )
            
            mstsMatrix.M11 = blenderMatrix[0][0]  # note coordinate conversion and use of new 2.62 indexing
            mstsMatrix.M13 = blenderMatrix[1][0]
            mstsMatrix.M12 = blenderMatrix[2][0]

            mstsMatrix.M31 = blenderMatrix[0][1]
            mstsMatrix.M33 = blenderMatrix[1][1]
            mstsMatrix.M32 = blenderMatrix[2][1]

            mstsMatrix.M21 = blenderMatrix[0][2]
            mstsMatrix.M23 = blenderMatrix[1][2]
            mstsMatrix.M22 = blenderMatrix[2][2]

            mstsMatrix.M41 = blenderMatrix[0][3]
            mstsMatrix.M43 = blenderMatrix[1][3]
            mstsMatrix.M42 = blenderMatrix[2][3]
    
        ExportShape.Matrices.append( mstsMatrix ) 

#####################################
# based on Custom Properties
def InViewingRange( objectMin, objectMax, levelMin ):

    if objectMax <= levelMin+0.001:  return False   # there's a lower detail LOD that would be better fit
    if objectMin >  levelMin+0.001:  return False   # there's a higher detail LOD that would be better fit
    return True
        
        
        
#####################################
# based on Custom Properties 
def InDistanceLevelProperties( meshObject, levelMin ):
    
    objectMin = meshObject.get('DMIN',0)
    objectMax = meshObject.get('DMAX',100000 )
    return InViewingRange( objectMin, objectMax, levelMin)
        
#####################################
# based on Railworks Naming
def InDistanceLevelRWNaming( meshObject, levelMin ):

    # this assumes successive LODs are children of the previous LOD
    objectMin = 0
    if meshObject.name[0] != '1':
        if meshObject.parent != None and IsRWName( meshObject.parent.name ): 
            objectMin = int( meshObject.parent.name[2:6] )
        else:
            print( "WARNING - RW LOD hierarchy incorrect for: " + meshObject.name)
        
    objectMax = int( meshObject.name[2:6] )
    
    return InViewingRange( objectMin, objectMax, levelMin)

    
#####################################
def InDistanceLevel( meshObject, levelMin ):

    if meshObject.type == 'PROXY':  # the SceneCenterProxy object
        return True
    if IsRWName( meshObject.name ):  # support Railworks style LOD naming
        return InDistanceLevelRWNaming( meshObject, levelMin)
    elif 'DMIN' in meshObject or 'DMAX' in meshObject:   # support Custom Properties
        return InDistanceLevelProperties( meshObject, levelMin)
    elif meshObject.parent != None:     # otherwise inherit from parent
        return InDistanceLevel( meshObject.parent, levelMin )
    else:                # if no restrictions, then it must be visible
        return True

#####################################
# create points for each vertex in the mesh
# return an offset into the shape's point table 
def AddMeshVertexPoints( mesh, offsetMatrix ):

    iPointOffset = len( ExportShape.Points )
    for v in mesh.vertices:
        blenderPoint = offsetMatrix * v.co
        mstspoint = (blenderPoint[0],blenderPoint[2],blenderPoint[1] )
        ExportShape.Points.append( mstspoint )

    return iPointOffset

#####################################
def HasGeometry( object ):
    
    # make sure its not a camera, light etc that do not have geometry
    return object.type in ['MESH','CURVE','SURFACE','META','FONT' ]
   

#####################################
def AddObjectGeometry( distanceLevel, object, iHierarchy, relativeMatrix ):
    
    print( '    ',object.name )

    # make sure its not a camera, light etc that do not have geometry
    if HasGeometry( object ):
   
        # this provides hints to short cut the search for matching vertices
        for eachSubObject in distanceLevel.SubObjects:
            for eachVertexSet in eachSubObject.VertexSets:
                eachVertexSet.iStart = len( eachVertexSet.Vertices )
        
        mesh = object.to_mesh(bpy.context.scene,True,'PREVIEW')
        print( '              v=', len(mesh.vertices) )
        iPointOffset = AddMeshVertexPoints( mesh, relativeMatrix )
        mesh.calc_tessface()
        
        # evaluate any special handling for normals
        #   Note: these may be overriden 'per face' in AddFaceToSubObject
        normals = Normals.Face
        meshProperty = object.data.get( 'NORMALS', '' )
        if meshProperty == 'UP':
            normals = Normals.Up
        elif meshProperty == 'OUT':
            normals = Normals.Out
        elif meshProperty == 'FILLET':
            normals = Normals.Fillet
        elif meshProperty == "OUTX":
            normals = Normals.OutX
            
        if normals == Normals.Fillet:
            # preprocess normals for filleted appearance
            # verts in a flat face will use the face normal
            # verts in a smoothed face use the normal of the adjacent flat face
            for meshFace in mesh.tessfaces:
                if not meshFace.use_smooth:
                    for iVert in meshFace.vertices:
                        mesh.vertices[iVert].normal = meshFace.normal
            # now handle them like standard smoothed normals 
            normals = Normals.Face
        
        for iFace in range( 0, len( mesh.tessfaces ) ):
            AddFaceToDistanceLevel( distanceLevel, mesh, iFace, iHierarchy, relativeMatrix, iPointOffset, normals )
    
#####################################################
# add them to the distance level if they are in range
def AddObjects( distanceLevel, nodeObject, iHierarchy ):
    global ExportShape
    global hierarchyObjects
    
    rootObject = hierarchyObjects[iHierarchy]
    relativeMatrix =  ConstructMatrix( rootObject, nodeObject )

    if nodeObject.is_duplicator:
        settings=[] # save draw_percentage settings for later restore
        for eachParticleSystem in nodeObject.particle_systems:
            settings.append( eachParticleSystem.settings.draw_percentage )
            eachParticleSystem.settings.draw_percentage = 100
        bpy.context.scene.update()
        nodeObject.dupli_list_create( bpy.context.scene )
        for eachDuplicate in nodeObject.dupli_list:
            dupliObject = eachDuplicate.object
            if InDistanceLevel( dupliObject, distanceLevel.Start ):
                AddObjectGeometry( distanceLevel, dupliObject, iHierarchy, rootObject.matrix_world.inverted() * eachDuplicate.matrix  )        
        nodeObject.dupli_list_clear()    
        # restore draw_percentage settings
        i = 0
        for eachParticleSystem in nodeObject.particle_systems:
            eachParticleSystem.settings.draw_percentage = settings[i]
            i += 1
        bpy.context.scene.update()
    
    else:  # add geometry
        if InDistanceLevel( nodeObject, distanceLevel.Start ):
            AddObjectGeometry( distanceLevel, nodeObject, iHierarchy, relativeMatrix )
        
    for eachChild in nodeObject.children:
       if not IsRetained( eachChild ):
            # do all objects attached to this retained level in the hierarchy
            AddObjects( distanceLevel, eachChild, iHierarchy )
        

            
#####################################       
def AppendDistanceLevel( startDistance, distanceLimit ):
    global ExportShape
    global hierarchy
    global hierarchyObjects
    
    print( "DLEVEL"+str(distanceLimit) )
    # add the distance level
    lodControl = ExportShape.LodControls[0]                
    distanceLevel = DistanceLevel( lodControl )
    distanceLevel.Start = startDistance
    distanceLevel.Selection = distanceLimit
    distanceLevel.Hierarchy = hierarchy
    lodControl.DistanceLevels.append( distanceLevel )
    
    for i in range( 0, len( hierarchy ) ):
        nodeObject = hierarchyObjects[i]
        AddObjects( distanceLevel, nodeObject, i )

    distanceLevel.SubObjects.sort( key=lambda subObject: subObject.Priority )
    
    if len( distanceLevel.SubObjects) == 0 or len( distanceLevel.SubObjects[0].Primitives ) == 0:
        print( 'WARNING - empty distance level ',distanceLevel.Selection )
        del lodControl.DistanceLevels[ len( lodControl.DistanceLevels )-1 ]
        
#####################################       
def GetLevelDistances( mainObject ):
    
    levelDistances = []
    
    for i in range( 0,10 ):
        dlname = 'DLEVEL'+str(i)
        dldistance = mainObject.get( dlname, -1 )
        if dldistance != -1:
            levelDistances.append( dldistance )
    
    # if no DLEVEL's are specified on MAIN, then use a default
    if levelDistances == []:
        levelDistances = [2000]
    
    return levelDistances       

#####################################       
# update the global lowerBound and upperBound vectors
# for the specified mesh object
def GetMeshBounds( nodeObject, matrix ):
    global UpperBound
    global LowerBound
    
    # make sure its not a camera, light etc that do not have geometry
    if HasGeometry( nodeObject ):
    
        mesh = nodeObject.to_mesh(bpy.context.scene,True,'PREVIEW')
    
        for eachVertex in mesh.vertices:
            v = matrix * eachVertex.co
            if v.x > UpperBound.x:  UpperBound.x = v.x
            if v.y > UpperBound.y:  UpperBound.y = v.y
            if v.z > UpperBound.z:  UpperBound.z = v.z
            if v.x < LowerBound.x:  LowerBound.x = v.x
            if v.y < LowerBound.y:  LowerBound.y = v.y
            if v.z < LowerBound.z:  LowerBound.z = v.z
    

#####################################       
# update the global lowerBound and upperBound vectors
# for the specified object and its children
def GetBounds( rootObject, nodeObject ):
    global UpperBound
    global LowerBound
    
    relativeMatrix = ConstructMatrix( rootObject, nodeObject )

    if nodeObject.is_duplicator:
        nodeObject.dupli_list_create( bpy.context.scene )
        for eachDuplicate in nodeObject.dupli_list:
            dupliObject = eachDuplicate.object
            GetMeshBounds( dupliObject, rootObject.matrix_world.inverted() * eachDuplicate.matrix  )        
        nodeObject.dupli_list_clear()    
        
    
    else:
        GetMeshBounds( nodeObject, relativeMatrix )
            
    for eachChild in nodeObject.children:
        GetBounds( rootObject, eachChild )
            
    
#####################################       
# update the global BoundingRadius
# for the specified group object and its sub groups
def GetMeshBoundingRadius( nodeObject, center , matrix):
    global BoundingRadiusSquared
    
    # make sure its not a camera, light etc that do not have geometry
    if HasGeometry( nodeObject ):
    
        mesh = nodeObject.to_mesh(bpy.context.scene,True,'PREVIEW')

        for eachVertex in mesh.vertices:
            v = matrix * eachVertex.co
            v = v - center
            dSquared = v.x*v.x+v.y*v.y+v.z*v.z
            if dSquared>BoundingRadiusSquared:
                BoundingRadiusSquared = dSquared
    

#####################################       
# update the global BoundingRadius
# for the specified object and its children relative to the specified center
def GetBoundingRadius( rootObject, nodeObject, center ):
    global BoundingRadiusSquared
    
    relativeMatrix = ConstructMatrix( rootObject, nodeObject )
    
    if nodeObject.is_duplicator:
        nodeObject.dupli_list_create( bpy.context.scene )
        for eachDuplicate in nodeObject.dupli_list:
            dupliObject = eachDuplicate.object
            GetMeshBoundingRadius( dupliObject, center, rootObject.matrix_world.inverted() * eachDuplicate.matrix  )        
        nodeObject.dupli_list_clear()    
    else:
        GetMeshBoundingRadius( nodeObject, center, relativeMatrix )
            
    for eachChild in nodeObject.children:
        GetBoundingRadius( rootObject, eachChild, center )
    

#####################################       
# return a vector representing the geometric center of all the
# geometry in rootObject and its children 
# the returned vector is relative to rootObject's location
def FindCenter( rootObject ):
    global UpperBound
    global LowerBound
    
    UpperBound = mathutils.Vector( ( -100000,-100000,-100000 ))
    LowerBound = mathutils.Vector( ( 100000, 100000, 100000 ) )
    GetBounds( rootObject, rootObject )
    center = ( UpperBound + LowerBound ) / 2.0
    return center

def FindBoundingRadius( rootObject, center ):
    global BoundingRadiusSquared
    
    BoundingRadiusSquared = 0
    GetBoundingRadius( rootObject, rootObject, center )
    return sqrt( BoundingRadiusSquared )

#####################################       
def CompactPoints():
    global ExportShape
    
    oldPoints = ExportShape.Points
    ExportShape.Points = []
    uniquePoints = UniqueArray( ExportShape.Points,3,0.0001 )
    conversion = []
    
    for eachPoint in oldPoints:
       conversion.append( uniquePoints.IndexOf( eachPoint ) )
       
    for eachLODControl in ExportShape.LodControls:
        for eachDistanceLevel in eachLODControl.DistanceLevels:
            for eachSubObject in eachDistanceLevel.SubObjects:
                for eachVertexSet in eachSubObject.VertexSets:
                    for eachVertex in eachVertexSet.Vertices:
                        eachVertex.iPoint = conversion[eachVertex.iPoint]

#####################################       
def CreateEulerRotationController( iFC, fcurves ):
    
    rotationController = RotationController()

    #TODO this assumes all points are lined up at the same time
    for iKey in range(0,len(fcurves[iFC].keyframe_points)):
        key = RotationKey()
        key.Frame = fcurves[iFC].keyframe_points[iKey].co[0]
        x = fcurves[iFC+0].keyframe_points[iKey].co[1]
        y = fcurves[iFC+1].keyframe_points[iKey].co[1]  #Note coordinate conversion
        z = fcurves[iFC+2].keyframe_points[iKey].co[1]
        
        euler = mathutils.Euler( ( x,y,z ), 'XYZ' )
        quat = euler.to_quaternion()

        key.W = quat.w
        key.X = quat.x
        key.Y = quat.z
        key.Z = quat.y
        rotationController.Keys.append( key )

    return rotationController

                        
#####################################       
def CreateRotationController( iFC, fcurves ):
    
    rotationController = RotationController()

    #TODO this assumes all points are lined up at the same time
    for iKey in range(0,len(fcurves[iFC].keyframe_points)):
        key = RotationKey()
        key.Frame = fcurves[iFC].keyframe_points[iKey].co[0]
        key.W = fcurves[iFC].keyframe_points[iKey].co[1]
        key.X = fcurves[iFC+1].keyframe_points[iKey].co[1]
        key.Y = fcurves[iFC+3].keyframe_points[iKey].co[1]  #Note coordinate conversion
        key.Z = fcurves[iFC+2].keyframe_points[iKey].co[1]
        rotationController.Keys.append( key )

    return rotationController

#####################################       
def CreateLinearController( iFC, fcurves ):
    
    linearController = PositionController()

    #TODO this assumes all points are lined up on the same frame
    for iKey in range( 0, len(fcurves[iFC].keyframe_points) ):
        key = LinearKey()
        key.Frame = fcurves[iFC].keyframe_points[iKey].co[0]
        key.X = fcurves[iFC].keyframe_points[iKey].co[1]
        key.Y = fcurves[iFC+2].keyframe_points[iKey].co[1]  #Note coordinate conversion
        key.Z = fcurves[iFC+1].keyframe_points[iKey].co[1]
        linearController.Keys.append( key )
        
    return linearController
                        
#####################################       
def CreateAnimationNode( nodeObject ):
    
    animationNode = AnimationNode()
    animationNode.Label = nodeObject.name
    if nodeObject.animation_data != None:
        if nodeObject.animation_data.action != None:
            fcurves = nodeObject.animation_data.action.fcurves 
            iFC = 0
            while iFC < len( fcurves ):
                if fcurves[iFC].data_path == 'rotation_quaternion':
                    animationNode.Controllers.append( CreateRotationController( iFC, fcurves ) )
                    iFC += 4
                elif fcurves[iFC].data_path == 'rotation_euler':
                    animationNode.Controllers.append( CreateEulerRotationController( iFC, fcurves ) )
                    iFC += 3
                elif fcurves[iFC].data_path == 'location':
                    animationNode.Controllers.append( CreateLinearController( iFC, fcurves ) )
                    iFC += 3
                else:
                    print( 'Unknown controller type ',fcurves[iFC].data_path,' in ',nodeObject.name )
                    iFC += 1
                
    
    return animationNode                        
                                                        
#####################################       
SceneCenter = None        

class SceneCenterObject:    # a proxy to represent a node object at the center of the scene

    is_duplicator = False
    type = 'PROXY'  # made up for this application, not one of Blender's recognized types
    animation_data = None
    matrix_world = Matrix()

    def __init__(self , mainObject ):
        global SceneCenter
        if SceneCenter != None:
            raise Exception( "PROGRAM ERROR:  Only one scene center is allowed.")
        SceneCenter = self
        self.children = [ mainObject ]
        self.name = mainObject.name
        
    def get( self, parameter, default):
        return default
        
#####################################       
def ExportShapeFile( mainNodeName, MSTSFilePath, useSceneCenter ):
    
    global ExportShape

    global UniqueUVPoints
    global UniqueNormals
    global UniqueColors
    global UniqueLightMaterials
    
    
    global hierarchy        # linked list ie [-1,    0,1,1,0 ]
    global hierarchyObjects  # related nodes [ MAIN, BOGIE1, WHEELS11, WHEELS12 .. ]
    hierarchy = []
    hierarchyObjects = []
    
    # initialize an empty shape file
    ExportShape = Shape()
    UniqueUVPoints = UniqueArray( ExportShape.UVPoints, 3, 0.0001 )
    UniqueNormals = UniqueArray( ExportShape.Normals, 2, 0.001 )
    UniqueColors = UniqueArray( ExportShape.Colors, 3, 0.0001 )
    UniqueLightMaterials = UniqueArray( ExportShape.LightMaterials, 1,1 )
    lodControl = LodControl( ExportShape )
    ExportShape.LodControls.append( lodControl )
    
    global LastSubObject
    global LastFaceImage
    global LastMaterial
    global LastiMatrix
    global LastiPrimState
    LastSubObject = None
    LastFaceImage = None
    LastMaterial = None
    LastiMatrix = -1
    LastiPrimState = 0

    global SceneCenter
    SceneCenter = None
    
    
    rootObject = bpy.data.objects[mainNodeName]
    if useSceneCenter:
        if rootObject.parent != None:
            raise Exception( "Main Object must be at root of the scene when UseSceneCenter is specified." )
        rootObject = SceneCenterObject( rootObject )  # make everything relative to the scene center
    
    BuildHierarchyFrom( rootObject, -1 )  # create global hierarchy array  
    
    CreateMSTSMatrices( )

    # create a distance level for each one specified in MAIN    
    levelDistances = GetLevelDistances( bpy.data.objects[mainNodeName] )
    startDistance = 0
    for distance in levelDistances:
        AppendDistanceLevel( startDistance, distance )
        startDistance = distance

    # export animations
    if len( bpy.data.actions ) > 0:
        animation = Animation()
        ExportShape.Animations.append(animation)
        animation.FrameCount = bpy.context.scene.frame_end
        animation.FrameRate = 30
        for eachObject in hierarchyObjects:
            animation.AnimationNodes.append( CreateAnimationNode( eachObject ) )
    
    # set up the volume sphere      
    volumeSphere = VolumeSphere()
    # center = FindCenter( rootObject )  OR doesn't display properly with a computed center, 
    center = rootObject.matrix_world.translation
    radius = FindBoundingRadius( rootObject, center )
    MSTSvector = ( center.x, center.z, center.y )
    volumeSphere.Vector = MSTSvector
    volumeSphere.Radius = radius * 1.1  # add some safety margin
    ExportShape.Volumes.append( volumeSphere )  
    
    
    print ( "POINTS = ", len( ExportShape.Points ) )
    print ( "Compacting Points" )
    CompactPoints()
    
    ExportShape.Write( MSTSFilePath )
    
    # Reporting
    polyCount = 0
    primitiveCount = 0
    vertex_sets = 0
    vertices = 0
    if len( ExportShape.LodControls ) > 0 :
        if len( ExportShape.LodControls[0].DistanceLevels ) > 0:
            for eachSubObject in ExportShape.LodControls[0].DistanceLevels[0].SubObjects:
                vertex_sets += len( eachSubObject.VertexSets )
                for vertex_set in eachSubObject.VertexSets:
                    vertices += len( vertex_set.Vertices )
                for primitive in eachSubObject.Primitives:
                    primitiveCount += 1
                    polyCount += len(primitive.Triangles)
    print ( )
    print ( "Statistics for first LOD:" )
    print ( )
    print ( "POLYCOUNT = ", polyCount ) 
    print ( "POINTS = ", len( ExportShape.Points ) )
    print ( "UV_POINTS = ", len( ExportShape.UVPoints ) )
    print ( "NORMALS = ", len( ExportShape.Normals ))
    print ( "PRIMITIVES = ", primitiveCount )
    if len( ExportShape.LodControls ) > 0 :
        if len( ExportShape.LodControls[0].DistanceLevels ) > 0:
            print ( "SUBOBJECTS = ", len( ExportShape.LodControls[0].DistanceLevels[0].SubObjects ) )
        else:
            print ( "SUBOBJECTS = 0" )    
    else:
        print ( "SUBOBJECTS = 0" )    
    print ( "VERTEXSETS = ", vertex_sets )    
    print ( "VERTICES = ", vertices )
    print ( )
    print ( "IMAGES = " )
    for eachImage in ExportShape.Images:
        print( "   ",eachImage )
    
    return                  


'''  LIBRARY FUNCTIONS    
All code below uses the MSTS coordinate system.
 
Structure matches the MSTS .s file with the following 
top level name substitutions:

VolumeSphere
Matrix
VertexState
Texture
PrimState
Vertex
Primitive
VertexSet
SubObject
DistanceLevel
LodControl
RotationKey
TCBRotationKey
LinearKey
PositionController
RotationController
AnimationNode
Animation

This structure matches the MSTS .s file with the following exceptions

Add vertices to vertex_sets, not subobject.vertices as in MSTS. 
It not needed to populate the sub_object_header data.  

    Both of the above are generated from the underlying data on write.

'''

import codecs
import bpy

import math
from math import *


    
####################################        
class STFWriter:
####################################        
#
# Writes an MSTS structured unicode text file   
# 
        
        
        def __init__( self, filename):
                self.f = codecs.open(filename, 'w', encoding='utf-16')
                
        def WriteLine( self, string ):
                self.f.write( string )
                self.f.write( '\r\n' )
                
        def Write( self, string ):
                self.f.write( string )
                
        def Close(self ):
                self.f.close()
                
                

                                
########################################
class VolumeSphere:
    
        def __init__( self ):
            self.Vector = ( 0.0,0.0,0.0 )
            self.Radius = 100.0
    
        def Write( self, stf ):
            stf.WriteLine( '        vol_sphere (' )
            stf.WriteLine( '            vector ( {0} {1} {2} ) {3}'.format( self.Vector[0],self.Vector[1],self.Vector[2],self.Radius))
            stf.WriteLine( '        )' )

                
                
########################################
class MSTSMatrix:
    
        def __init__( self ):
            self.Label = 'MAIN'
            self.M11 = 1.0
            self.M12 = 0.0
            self.M13 = 0.0
            self.M21 = 0.0
            self.M22 = 1.0
            self.M23 = 0.0
            self.M31 = 0.0
            self.M32 = 0.0
            self.M33 = 1.0
            self.M41 = 0.0
            self.M42 = 0.0
            self.M43 = 0.0
            

        def Write( self, stf ):
            stf.WriteLine( '        matrix {0} ( {1} {2} {3} {4} {5} {6} {7} {8} {9} {10} {11} {12} )'.format( self.Label,self.M11,self.M12,self.M13,self.M21,self.M22,self.M23,self.M31,self.M32,self.M33,self.M41,self.M42,self.M43 ))
        
    
########################################
class VertexState:

    def __init__( self ):
        self.Flags = 0
        self.iMatrix = 0
        self.iLightMaterial = -5
        self.iLightConfig = 0       

    def Write( self, stf ):
        stf.WriteLine( '        vtx_state ( {0:08X} {1} {2} {3} 00000002 )'.format( self.Flags, self.iMatrix, self.iLightMaterial, self.iLightConfig ))
        
        
    
########################################
class Texture:

    def __init__(self):
        self.iImage = 0
        self.iFilter = 0
        self.MipMapLODBias = 0 # often -3 in MSTS

        
    def Write( self, stf ):
        stf.WriteLine( '        texture ( {0} {1} {2} ff000000 )'.format( self.iImage,self.iFilter,self.MipMapLODBias))
        
########################################
class UVOpCopy:   # uv_op_copy

    def __init__( self ):
        self.TextureAddressMode = 0     #TexAddrMode
        self.SourceUVIndex = 0          #SrcUVIdx 
        
    def Write( self, stf ):
        stf.WriteLine( '                uv_op_copy ( {0} {1} )'\
                .format( self.TextureAddressMode, self.SourceUVIndex ) )

########################################
class UVOpReflectMapFull:   #uv_op_reflectmap        ==> :uint,TexAddrMode .

    def __init__( self ):
        self.TextureAddressMode = 0     #TexAddrMode
        
    def Write( self, stf ):
        stf.WriteLine( '                uv_op_reflectmapfull ( {0} )'\
                .format( self.TextureAddressMode ) )

########################################
class LightConfig:       #light_model_cfg         ==> :dword,flags :uv_ops .

    def __init__( self ):
        self.UVOps = []
        
    def Write( self, stf ):
        stf.WriteLine( '		light_model_cfg ( 00000000' )
        count = len( self.UVOps )
        stf.WriteLine( '			uv_ops ( {0}'.format( count ) )
        for i in range( 0, count ):
            self.UVOps[i].Write( stf )
        stf.WriteLine( '			)' )
        stf.WriteLine( '		)' )

        
########################################
class PrimState:
    
                    # eg        prim_state (    00000000 0
                    #                                        tex_idxs ( 1 0 ) 0 0 0 0 1
                    #                                   )

    def __init__( self ):
        self.Label = ''
        self.iShader = 0
        self.iTextures = []
        self.ZBias = 0.0
        self.iVertexState = 0
        self.AlphaTestMode = 1
        self.iLightConfig = 0
        self.ZBufMode = 1       
        
 
    def Write( self,stf ):
        stf.WriteLine( '        prim_state {0} ( 00000000 {1}'.format( self.Label,self.iShader) )
        stf.Write( '            tex_idxs ( {0}'.format(len(self.iTextures)) )
        for eachiTexture in self.iTextures:
            stf.Write( ' {0}'.format( eachiTexture ) )
        stf.WriteLine(' ) {0} {1} {2} {3} {4}'.format( self.ZBias,self.iVertexState, self.AlphaTestMode, self.iLightConfig, self.ZBufMode ) )
        stf.WriteLine( '        )' )
            

    
########################################
class Vertex:

        # eg vertex ( 00000000 0 0 ffffffff ff000000
        #        vertex_uvs ( 1 0 )
        #               )
        
        def __init__( self ):
                self.iPoint = 0
                self.iNormal = 0
                self.iUVs = []  #TODO multiple UV's
                self.Color1 = 0xffffffff
                self.Color2 = 0xff000000

       
        def Write( self, stf ):
                stf.WriteLine( '                                vertex ( 00000000 {0} {1} {2:08X} {3:08X}'.format(self.iPoint,self.iNormal,self.Color1, self.Color2 ) )
                stf.Write( '                                    vertex_uvs ( {0}'.format( len(self.iUVs) ) )
                for eachiUV in self.iUVs:
                    stf.Write( ' {0}'.format(eachiUV))
                stf.WriteLine( ' )' )
                stf.WriteLine( '                                )')

        

########################################

class Primitive:

        def __init__( self ):
                self.iPrimState = 0
                self.Triangles = []
                self.iNormals = []
        
            
        def Write( self, stf, indexOffset ):
                stf.WriteLine( '                                indexed_trilist (' )
                
                stf.Write( '                                    vertex_idxs ( {0} '.format( len(self.Triangles) * 3) )
                linecount = 0
                for eachTriangle in self.Triangles:
                        for eachIndex in eachTriangle:
                                stf.Write( '{0} '.format( eachIndex + indexOffset ) )
                                linecount += 1
                                if linecount > 100:
                                        linecount = 0
                                        stf.WriteLine( '' )
                                        stf.Write( '                                    ')
                stf.WriteLine( ')') #vertex_idxs
                
                stf.Write( '                                    normal_idxs ( {0} '.format( len(self.Triangles)) )
                linecount = 0
                for i in self.iNormals:
                            stf.Write( '{0} 3 '.format( i ) )
                            linecount += 1
                            if linecount > 100:
                                    linecount = 0
                                    stf.WriteLine( '' )
                                    stf.Write( '                                    ')
                stf.WriteLine( ')') #normal_idxs
                
                stf.Write( '                                    flags ( {0} '.format( len(self.Triangles)) )
                linecount = 0
                for i in self.iNormals:
                            stf.Write( '00000000 ' )
                            linecount += 1
                            if linecount > 100:
                                    linecount = 0
                                    stf.WriteLine( '' )
                                    stf.Write( '                                    ')
                stf.WriteLine( ')') #flags

                stf.WriteLine( '                                )') #indexed_trilist
                
        
########################################
class GeometryInfoPerMatrix:
    
        def __init__( self, ShapeVertexStatesCount ):
            self.PrimitivesCount = 0
            self.TrianglesCount = 0
            self.VerticesCount = 0
            self.VertexStatesCount = 0
            self.VertexStatesUsed = []
            for i in range(0,ShapeVertexStatesCount):
                self.VertexStatesUsed.append( False )
                
                
########################################
class VertexSet:
    
        def __init__( self ):
                self.Vertices = []
                self.iStart = 0

        
########################################
class SubObject:
    
    
        def __init__( self, parent ):
                self.VertexSets = []
                self.Primitives = []
                self.DistanceLevel = parent
                self.Flags = '00000400 -1 -1 000001d2 000001c4'
                self.Priority = 0       # sub_objects are sorted by this number, 0 comes first
    
        
        def Write( self, stf ):
                stf.WriteLine( '                        sub_object (' )
                self.WriteSubObjectHeader( stf )
                self.WriteVertices( stf )
                self.WriteVertexSets( stf )
                self.WritePrimitives( stf )
                stf.WriteLine( '                        )') #sub_object
                return          


        def WriteSubObjectHeader( self, stf ):
                stf.WriteLine( '                            sub_object_header ( ' + self.Flags )
                self.WriteSubObjectGeometryInfo( stf )
                self.WriteSubObjectShaders( stf )
                self.WriteSubObjectLightConfigs( stf )
                stf.WriteLine( '                            )')
                

        def WriteSubObjectGeometryInfo( self, stf ):
                shape = self.DistanceLevel.LodControl.Shape
                # Set up data collection array and initialize
                matrixInfo = []
                for i in range(0,len(shape.Matrices) ):
                        matrixInfo.append( GeometryInfoPerMatrix( len(shape.VertexStates) ))
                #Scan all geometry and accumulate the statistics per matrix
                for primitive in self.Primitives:
                        primState = shape.PrimStates[primitive.iPrimState]
                        vertexState = shape.VertexStates[primState.iVertexState]
                        iMatrix = vertexState.iMatrix
                        matrixInfo[iMatrix].PrimitivesCount += 1
                        matrixInfo[iMatrix].TrianglesCount += len(primitive.Triangles)
                        matrixInfo[iMatrix].VerticesCount += len(primitive.Triangles) * 3
                        matrixInfo[iMatrix].VertexStatesUsed[primState.iVertexState] = True
                # Calculate VertexStates (txLightCmds) per matrix
                for i in range( 0, len(matrixInfo) ):
                        matrixInfo[i].VertexStatesCount = 0
                        for b in matrixInfo[i].VertexStatesUsed:
                                if b : matrixInfo[i].VertexStatesCount += 1
                # Now update the summary data
                # Calculate FaceNormals
                faceNormalsCount = 0
                for primitive in self.Primitives:
                        faceNormalsCount += len(primitive.Triangles)
                # Calculate number of vtx_states used by primitives in this sub_objects  
                vertexStatesCount = 0
                for eachVertexSet in self.VertexSets:
                    if len(eachVertexSet.Vertices)>0:
                        vertexStatesCount += 1
                # Calculate number of vertices (VertIdxs) in all the vertex_idxs statements ( 3 x FaceNormals )
                verticesCount = faceNormalsCount * 3
                # Calculate Trilists - number of indexed_trilist statements ( in all the files I've seen, all primitives are trilists )
                trilistsCount = len(self.Primitives)
                # UNKNOWN use - NodeTxLightCmds, LineListIdxs, NodeXTrilistIdxs, LineLists, PtLists , NodeXTrilists - always seems to be 0
                stf.WriteLine( '                                geometry_info ( {0} {1} 0 {2} 0 0 {3} 0 0 0'.format(faceNormalsCount,vertexStatesCount,verticesCount,trilistsCount))
                # Write geometry nodes
                # Create a new empty geometry node map and geometry nodes array
                geometryNodeMap = []
                geometryNodeCount = 0
                for i in range( 0, len( shape.Matrices) ):
                        geometryNodeMap.append(-1)  # initialize to -1's
                        if matrixInfo[i].PrimitivesCount > 0:
                                geometryNodeCount += 1
                # Populate the new  geometry_node_map and write out the geometry_nodes
                stf.WriteLine( '                                    geometry_nodes ( {0}'.format(geometryNodeCount))
                iGeometryNode = 0
                for iMatrix in range( 0, len(shape.Matrices)):
                        matrix_info = matrixInfo[iMatrix]
                        if matrix_info.PrimitivesCount > 0:
                                stf.WriteLine( '                                        geometry_node ( {0} 0 0 0 0'.format(matrix_info.VertexStatesCount))
                                stf.WriteLine( '                                            cullable_prims ( {0} {1} {2} )'.format(matrix_info.PrimitivesCount,matrix_info.TrianglesCount,matrix_info.VerticesCount))
                                stf.WriteLine( '                                        )')
                                geometryNodeMap[iMatrix] = iGeometryNode
                                iGeometryNode += 1
                stf.WriteLine( '                                    )')
                # Write geometry node map
                stf.Write( '                                    geometry_node_map ( {0} '.format(len(shape.Matrices)))
                for iGeometryNode in geometryNodeMap:
                        stf.Write( '{0} '.format( iGeometryNode) )
                stf.WriteLine( ')' ) #geometry_node_map
                stf.WriteLine( '                                )') #geometry_info
                return
                
        def WriteSubObjectShaders( self, stf ):
                shape = self.DistanceLevel.LodControl.Shape
                subObjectShaders = set()
                for eachPrimitive in self.Primitives:
                        primState = shape.PrimStates[eachPrimitive.iPrimState]
                        subObjectShaders |= set([primState.iShader])
                stf.Write( '                                subobject_shaders ( {0} '.format(len(subObjectShaders) ) )
                for iShader in subObjectShaders:
                        stf.Write( '{0} '.format( iShader ) )
                stf.WriteLine( ')' )
            
        def WriteSubObjectLightConfigs( self, stf ):
                shape = self.DistanceLevel.LodControl.Shape
                subObjectLightConfigs = set()
                for primitive in self.Primitives:
                        primState = shape.PrimStates[primitive.iPrimState]
                        subObjectLightConfigs |= set([primState.iLightConfig])
                stf.Write( '                                subobject_light_cfgs ( {0} '.format(len(subObjectLightConfigs) ) )
                for iLightConfig in subObjectLightConfigs:
                        stf.Write( '{0} '.format( iLightConfig ) )
                stf.WriteLine( ')' )  # TODO most files have an extra 0 after this bracket (is it needed? )

            
        def WriteVertices( self, stf ):  # and set start location for vertex_set's
                # count the vertices
                count = 0
                for vertexSet in self.VertexSets:
                        count += len( vertexSet.Vertices )
                stf.WriteLine( '                            vertices ( {0}'.format(count))
                # write out the vertices
                iStart = 0
                for vertexSet in self.VertexSets:
                    vertexSet.iStart = iStart
                    for vertex in vertexSet.Vertices:
                        vertex.Write( stf )
                    iStart += len( vertexSet.Vertices )
                stf.WriteLine( '                            )') # vertices
                
                
        def WriteVertexSets( self, stf ):
                # count the vertex sets
                count = 0
                for vertexSet in self.VertexSets:
                        if len( vertexSet.Vertices ) > 0:
                                count += 1
                stf.WriteLine( '                            vertex_sets ( {0}'.format(count) ) 

                # write out the vertex sets
                for i in range( 0, len( self.VertexSets ) ):
                        vertexSet = self.VertexSets[i]
                        if len( vertexSet.Vertices ) > 0:
                                stf.WriteLine( '                                vertex_set ( {0} {1} {2} )'.format(i,vertexSet.iStart,len(vertexSet.Vertices)))
                
                stf.WriteLine( '                            )' ) #vertex_sets
                return
            
                
        def WritePrimitives( self,stf ):
                shape = self.DistanceLevel.LodControl.Shape
                print ("Writing primitives")
                # determine count
                count = 0
                iPrimState = -1
                for eachPrimitive in self.Primitives:
                        count += 1
                        if eachPrimitive.iPrimState != iPrimState:
                                iPrimState = eachPrimitive.iPrimState
                                count += 1
                stf.WriteLine( '                            primitives ( {0}'.format(count))
                
                # and write out the primitives
                iPrimState = -1
                for eachPrimitive in self.Primitives:
                        if eachPrimitive.iPrimState != iPrimState:
                                iPrimState = eachPrimitive.iPrimState
                                stf.WriteLine( '                                prim_state_idx ( {0} )'.format( iPrimState ) )
                        primState = shape.PrimStates[iPrimState]
                        vertexSet = self.VertexSets[ primState.iVertexState]
                        eachPrimitive.Write( stf, vertexSet.iStart )
                        
                stf.WriteLine( '                            )' ) # primitives
                        
                




#######################################

class DistanceLevel:
        
        #hierarchy
        #dlevel_selection
        
        def __init__(self,parent):
                self.LodControl = parent
                self.SubObjects = []
                self.Start = 0          # starting distance for this level eg 0 to 200 
                self.Selection = 0      # maximum distance this distance level is visible
                self.Hierarchy = []

              
        def Write( self, stf ):
                stf.WriteLine( '                distance_level (' )
                print ("Writing distance level")
                
                self.WriteHeader( stf )
                
                count = len( self.SubObjects )
                stf.WriteLine( '                    sub_objects ( {0}'.format(count))
                for i in range( 0,count):
                    self.SubObjects[i].Write(stf)
                stf.WriteLine( '                    )')

                stf.WriteLine( '                )')
                
        def WriteHeader( self, stf ):
                stf.WriteLine( '                    distance_level_header (' )
                stf.WriteLine( '                        dlevel_selection ( {0} )'.format( self.Selection ) )
                
                count = len( self.Hierarchy )
                stf.Write( '                        hierarchy ( {0} '.format( count ) )
                for i in range( 0,count ):
                    stf.Write( '{0} '.format( self.Hierarchy[i] ) )
                stf.WriteLine( ')')
                
                stf.WriteLine( '                    )' )


########################################

class LodControl:
    
        def __init__( self, parent ):
                self.Shape = parent
                self.DistanceLevels = []

           
        def Write( self, stf ):
                stf.WriteLine( '        lod_control (' )
                stf.WriteLine( '            distance_levels_header ( 0 )' )
                
                count = len( self.DistanceLevels )
                stf.WriteLine( '            distance_levels ( {0}'.format(count))
                for i in range(0,count):
                    self.DistanceLevels[i].Write(stf)
                stf.WriteLine( '            )') 
                
                stf.WriteLine( '        )')

########################################

class RotationKey:   #key
    
    def __init__(self ):
        self.Frame = 0
        self.X = 0
        self.Y = 0
        self.Z = 0
        self.W = 0
    
    
    def Write( self, stf ):
        
        stf.WriteLine( '                            slerp_rot ( {0} {1} {2} {3} {4} )'\
                .format( int(self.Frame), round(self.X,8), round(self.Y,8), round(self.Z,8), round(self.W,8) ))  # note frame must be an int for ffeditc_unicode to compress it properly


########################################

class TCBRotationKey:    #key
    
    def __init__(self ):
        self.Frame = 0
        self.X = 0
        self.Y = 0
        self.Z = 0
        self.W = 0
        self.Tension =  0
        self.Continuity =  0
        self.Bias =  0
        self.In =  0
        self.Out =  0
        

    def Write( self, stf ):
        
        stf.WriteLine( '                            tcb_key ( {0} {1} {2} {3} {4} {5} {6} {7} {8} {9} )'\
                .format( int(self.Frame), self.X, self.Y, self.Z, self.W, self.Tension, self.Continuity, self.Bias, self.In, self.Out ))

########################################

class LinearKey:   # key
    
    def __init__(self ):
        self.Frame = 0
        self.X = 0
        self.Y = 0
        self.Z = 0

        
    def Write( self, stf ):
        
        stf.WriteLine( '                            linear_key ( {0} {1} {2} {3} )'\
                .format( int(self.Frame), round(self.X,8), round(self.Y,8),round( self.Z,8) ))

########################################

class PositionController:   # controller
    
    def __init__( self ):
        self.Keys = []


    def Write( self, stf ):
        
        stf.WriteLine('                     linear_pos ( {0}'.format(len(self.Keys)))
        for eachKey in self.Keys:
            eachKey.Write( stf )
        stf.WriteLine('                     )')
                
        

########################################

class RotationController:     # controller
    
    def __init__( self ):
        self.Keys = []


    def Write( self, stf ):
        
        stf.WriteLine('                     tcb_rot ( {0}'.format(len(self.Keys)))
        for eachKey in self.Keys:
            eachKey.Write( stf )
        stf.WriteLine('                     )')

########################################

class AnimationNode:
    
        def __init__( self ):
            self.Label = None
            self.Controllers = []
            
                
        def Write( self, stf ):
            stf.WriteLine( '                anim_node {0} ('.format( self.Label ) )
            stf.WriteLine( '                    controllers ( {0}'.format( len(self.Controllers) ) )
            for eachController in self.Controllers:
                eachController.Write( stf )
            stf.WriteLine( '                    )' )
            stf.WriteLine( '                )' )

########################################

class Animation:
    
        def __init__( self ):
            self.FrameCount = 0
            self.FrameRate = 30
            self.AnimationNodes = []
        
                
        def Write( self, stf ):
            stf.WriteLine( '        animation ( {0} {1}'.format( int(self.FrameCount), self.FrameRate) )
            stf.WriteLine( '            anim_nodes ( {0}'.format( len(self.AnimationNodes) ) )
            for eachAnimationNode in self.AnimationNodes:
                eachAnimationNode.Write( stf )
            stf.WriteLine( '            )')
            stf.WriteLine( '        )' )


########################################

class Shape:

        
        def __init__( self ):
                self.Volumes = []
                self.Shaders = []
                self.Filters = []
                self.Points = []
                self.UVPoints = []
                self.Normals = []
                self.Matrices = []
                self.Images = []
                self.Textures = []
                self.Colors = []
                self.LightMaterials = []
                self.LightConfigs = []
                self.VertexStates = []
                self.PrimStates = []
                self.LodControls = []
                self.Animations = []
                
                    
                
        def Write( self, filepath ):
                stf = STFWriter( filepath )
                stf.WriteLine( 'SIMISA@@@@@@@@@@JINX0s1t______\r\n' )
                stf.WriteLine( 'shape (' )
                stf.WriteLine( '    shape_header ( 00000000 00000000 )' )
                self.WriteVolumes( stf )                
                self.WriteShaders( stf )
                self.WriteFilters( stf )
                self.WritePoints( stf )
                self.WriteUVPoints( stf )
                self.WriteNormals( stf )
                self.WriteSortVectors( stf )
                self.WriteColours( stf )
                self.WriteMatrices( stf )
                self.WriteImages(stf)
                self.WriteTextures(stf )
                self.WriteLightMaterials(stf)
                self.WriteLightConfigs( stf )
                self.WriteVertexStates(stf)
                self.WritePrimStates(stf)
                self.WriteLodControls(stf)
                self.WriteAnimations(stf)
                stf.WriteLine( ')' )
                stf.Close()

        def WriteVolumes( self, stf ):
                print ("Writing volumes")
                count = len( self.Volumes )
                stf.WriteLine( '    volumes ( {0}'.format(count) )  
                for i in range( 0,count):
                    self.Volumes[i].Write( stf )
                stf.WriteLine( '    )' )

        def WriteShaders( self, stf ):
                print ("Writing shader names")
                count = len( self.Shaders )
                stf.WriteLine( '    shader_names ( {0}'.format( count ))
                for i in range( 0, count ):
                    stf.WriteLine( '        named_shader ( {0} )'.format( self.Shaders[i] ))
                stf.WriteLine( '    )')

        def WriteFilters( self, stf ):
                print ("Writing texture filter names")
                count = len( self.Filters )
                stf.WriteLine( '    texture_filter_names ( {0}'.format(count))
                for i in range( 0, count ):
                    stf.WriteLine( '        named_filter_mode ( {0} )'.format( self.Filters[i]))
                stf.WriteLine( '    )')
                
        
        def WritePoints( self, stf ):
                print ("Writing points")
                count =  len( self.Points )
                stf.WriteLine( '    points ( {0}'.format(count) )
                for i in range( 0, count ):
                    p = self.Points[i]
                    stf.WriteLine( '        point ( {0} {1} {2} )'.format(round(p[0],6),round(p[1],6),round(p[2],6) ) )
                stf.WriteLine( '    )' )

        
        def WriteUVPoints( self, stf ):
                print ("Writing uv points")
                count =  len( self.UVPoints )
                stf.WriteLine( '    uv_points ( {0}'.format(count) )
                for i in range( 0, count ):
                    p = self.UVPoints[i]
                    stf.WriteLine( '        uv_point ( {0} {1} )'.format(round(p[0],6),round(p[1],6)) )
                stf.WriteLine( '    )' )
                
        
        def WriteNormals( self, stf ):
                print ("Writing normals")
                count =  len( self.Normals )
                stf.WriteLine( '    normals ( {0}'.format(count) )
                for i in range( 0, count ):
                    p = self.Normals[i]
                    stf.WriteLine( '        vector ( {0} {1} {2} )'.format(round(p[0],6),round(p[1],6),round(p[2],6) ) )
                stf.WriteLine( '    )' )

        def WriteSortVectors( self, stf ):
                stf.WriteLine( '    sort_vectors ( 1' )
                stf.WriteLine( '    	vector ( 0 0 0 )' )
                stf.WriteLine( '    )' )
                
                
        def WriteMatrices( self, stf ):
                print ("Writing matrices")
                count = len( self.Matrices )
                stf.WriteLine( '    matrices ( {0}'.format( count ) )
                for i in range( 0,count):
                    self.Matrices[i].Write( stf )
                stf.WriteLine( '    )' )        
                
        def WriteImages( self, stf ):
                print ("Writing image names")
                count = len( self.Images )
                stf.WriteLine( '    images ( {0}'.format(count))
                for i in range(0,count):
                    imageFileName = self.Images[i]
                    if imageFileName.count( ' ' ) == 0:   #only use quotes when spaces exist in name for SVIEW compatibility
                        stf.WriteLine( '        image ( {0} )'.format( imageFileName ) )
                    else:
                        stf.WriteLine( '        image ( "{0}" )'.format( imageFileName ) )
                stf.WriteLine( '    )' )

        def WriteTextures( self, stf ):
                print ("Writing textures")
                count = len( self.Textures )
                stf.WriteLine( '    textures ( {0}'.format(count))
                for i in range(0,count):
                    self.Textures[i].Write(stf)
                stf.WriteLine( '    )' )
                
        def WriteColours( self, stf ):
                count = len( self.Colors )
                stf.WriteLine( '    colours ( {0}'.format( count) )
                for i in range( 0, count ):
                    c = self.Colors[i]              # a r g b
                    stf.WriteLine( '        colour ( {0} {1} {2} {3} )'.format( c[0],c[1],c[2],c[3] ) )
                stf.WriteLine( '    )' )

        def WriteLightMaterials( self, stf ):
                count = len( self.LightMaterials )
                stf.WriteLine( '    light_materials ( {0}'.format( count) )
                for i in range( 0, count ):
                    m = self.LightMaterials[i]
                    stf.WriteLine( '        light_material ( 00000000 {0} {1} {2} {3} {4} )'.format( m[0],m[1],m[2],m[3],m[4] ) )
                stf.WriteLine( '    )' )
                
                
        def WriteLightConfigs( self, stf ):
                print( "Writing light configs" )
                count = len( self.LightConfigs )
                stf.WriteLine( '    light_model_cfgs ( {0}'.format(count)) 
                for i in range( 0,count ):
                    self.LightConfigs[i].Write(stf)
                stf.WriteLine( '    )')
                
                
        def WriteVertexStates( self, stf ):
                print ("Writing vertex states")
                count = len( self.VertexStates )
                stf.WriteLine( '    vtx_states ( {0}'.format(count))
                for i in range( 0,count):
                    self.VertexStates[i].Write( stf )
                stf.WriteLine( '    )')

                
        def WritePrimStates( self, stf ):
                print ("Writing prim states")
                count = len( self.PrimStates )
                stf.WriteLine( '    prim_states ( {0}'.format(count))
                for i in range( 0,count ):
                    self.PrimStates[i].Write(stf)
                stf.WriteLine( '    )')

        def WriteLodControls(self, stf ):
                count = len( self.LodControls )
                stf.WriteLine( '    lod_controls ( {0}'.format( count ) )
                for i in range(0,count):
                    self.LodControls[i].Write(stf)
                stf.WriteLine( '    )')


               
        def WriteAnimations(self, stf ):
                count = len( self.Animations )
                stf.WriteLine( '    animations ( {0}'.format( count ) )
                for i in range(0,count):
                    self.Animations[i].Write(stf)
                stf.WriteLine( '    )')
 
'''
************************* end library *******************************
'''             
 


if __name__ == "__main__":
   unregister()
   register()

#   print( "START" )
#   ExportShapeFile( 'MAIN', r'C:\MSTS\GLOBAL\SHAPES\LPSTrack100m.s', True ) #r'c:\users\wayne\desktop\out.s' ) #
#   print( "DONE" )
  
