# ***** BEGIN LICENSE BLOCK *****
# Version: MPL 1.1/GPL 2.0/LGPL 2.1
#
# The contents of this file are subject to the Mozilla Public License Version
# 1.1 (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
# http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
# for the specific language governing rights and limitations under the
# License.
#
# The Original Code is the Python Computer Graphics Kit.
#
# The Initial Developer of the Original Code is Matthias Baas.
# Portions created by the Initial Developer are Copyright (C) 2004
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
#
# Alternatively, the contents of this file may be used under the terms of
# either the GNU General Public License Version 2 or later (the "GPL"), or
# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
# in which case the provisions of the GPL or the LGPL are applicable instead
# of those above. If you wish to allow use of your version of this file only
# under the terms of either the GPL or the LGPL, and not to allow others to
# use your version of this file under the terms of the MPL, indicate your
# decision by deleting the provisions above and replace them with the notice
# and other provisions required by the GPL or the LGPL. If you do not delete
# the provisions above, a recipient may use your version of this file under
# the terms of any one of the MPL, the GPL or the LGPL.
#
# ***** END LICENSE BLOCK *****
# $Id: cmds.py,v 1.12 2006/02/16 17:49:38 mbaas Exp $

## \file cmds.py
## Contains builtin and user defined commands.

# (this line separates the \file comment from the module docs)

"""Commands.
"""

import sys, os, os.path, types, string
import pluginmanager
import eventmanager
import events
from scene import getScene
import worldobject
import draw, drawtextgeom
import trimeshgeom
import polyhedrongeom
import group as group_module
import _core
from cgtypes import *

class NoImporter(Exception): pass
class NoExporter(Exception): pass



# TriMeshPolyAdapter
class TriMeshPolyAdapter:
    """Provides the PolyhedronGeom interface for a TriMeshGeom.

    This is a preliminary class that's used in the convFaceVarToVar()
    function so that TriMeshGeoms can be handled the same way than
    PolyhedronGeoms.

    Todo: This class should be stored somewhere else and probably be made
    available as a true adapter that can be used with the protocols framework.

    Todo: Most Component methods are still missing.
    """
    def __init__(self, tm):
        self.tm = tm
        
        self.verts = tm.verts
        self.verts_slot = tm.verts_slot

    def slot(self, name):
        return self.tm.slot(name)

    def boundingBox(self):
        return self.tm.boundingBox()

    def drawGL(self):
        self.tm.drawGL()

    def uniformCount(self):
        return self.tm.uniformCount()

    def vertexCount(self):
        return self.tm.vertexCount()

    def varyingCount(self):
        return self.tm.varyingCount()

    def faceVaryingCount(self):
        return self.tm.faceVaryingCount()

    def faceVertexCount(self):
        return self.tm.faceVertexCount()

    def newVariable(self, name, storage, type, multiplicity=1, user_n=0):
        self.tm.newVariable(name, storage, type, multiplicity, user_n)

    def deleteVariable(self, name):
        self.tm.deleteVariable(name)

    def deleteAllVariables(self):
        self.tm.deleteAllVariables()

    def findVariable(self, name):
        return self.tm.findVariable(name)

    def iterVariables(self):
        return self.tm.iterVariables()

    def convert(self, targetgeom):
        self.tm.convert(targetgeom)
        
    def hasPolysWithHoles(self):
        return False

    def getNumPolys(self):
        return self.tm.faces.size()

    def getNumLoops(self, poly):
        return 1

    def getNumVerts(self, poly, loop):
        return 3

    def setNumPolys(self, num):
        self.tm.faces.resize(num)

    def setNumLoops(self, poly, num):
        if num>1:
            raise RuntimeError, "Triangles cannot have holes"

    def getLoop(self, poly, loop):
        if loop==0:
            return self.tm.faces[poly]
        else:
            raise RuntimeError, "Triangles have no holes"

    def setLoop(self, poly, loop, vloop):
        if loop==0:
            self.tm.faces[poly] = vloop
        else:
            raise RuntimeError, "Triangles cannot have holes"

    def getPoly(self, poly):
        return [self.tm.faces[poly]]

    def setPoly(self, poly, polydef):
        self.tm.faces[poly] = polydef[0]
    

def convFaceVarToVar(geom):
    """Convert all facevarying into varying.

    All facevarying variables are converted into varying variables and
    all facevertex variables into vertex variables.
    This is done by splitting the vertices in the mesh.
    The number of values (and the values themselves) of the facevarying
    variables do not change, only the verts and varying/vertex variables
    are modified.

    geom can be either a TriMeshGeom or a PolyhedronGeom.

    The return value is the new geom object (the original object is not
    modified).
    """

    if isinstance(geom, trimeshgeom.TriMeshGeom):
        newgeom = trimeshgeom.TriMeshGeom()
        newgeom.faces.resize(geom.faces.size())
        geom.faces.copyValues(0, geom.faces.size(), newgeom.faces, 0)
        workinggeom = TriMeshPolyAdapter(newgeom)
    elif isinstance(geom, polyhedrongeom.PolyhedronGeom):
        newgeom = polyhedrongeom.PolyhedronGeom()
        newgeom.setNumPolys(geom.getNumPolys())
        workinggeom = newgeom
        # Copy the polygons (even though the indices have to be modified
        # afterwards), so that the variables can be created
        for i in range(geom.getNumPolys()):
            poly = geom.getPoly(i)
            newgeom.setPoly(i, poly)
    else:
        raise TypeError, "geom must be a TriMeshGeom or PolyhedronGeom"

    # Allocate storage for the vertices
    workinggeom.verts.resize(geom.faceVaryingCount())

    # This is a list with (oldslot,newslot) tuples. This is later used to
    # initialize the new varying/vertex slots.
    varyingvars = [(geom.verts, newgeom.verts)]

    # Create a copy of all variables...
    for name,storage,type,mult in geom.iterVariables():

        # facevarying/facevertex will be converted to varying/vertex
        if storage==_core.VarStorage.FACEVARYING:
            newstorage = _core.VarStorage.VARYING
        elif storage==_core.VarStorage.FACEVERTEX:
            newstorage = _core.VarStorage.VERTEX
        else:
            newstorage = storage            
            
        slot = geom.slot(name)
        workinggeom.newVariable(name, newstorage, type, mult, slot.size())
        newslot = workinggeom.slot(name)
        # Original varying/vertex variables will be initialized later,
        # everything else is just a copy.
        if storage==_core.VarStorage.VARYING or storage==_core.VarStorage.VERTEX:
            varyingvars.append((slot, newslot))
        else:
            slot.copyValues(0, slot.size(), newslot, 0)


    # Initialize the varying/vertex variables and update the vertex indices
    # in the faces...
    vidx = 0
    for i in range(workinggeom.getNumPolys()):
        poly = workinggeom.getPoly(i)
        # ids: Flattened version of poly (i.e. all vertex ids)
        ids = []
        # newpoly: New polygon with new indices (sequentially numbered)
        newpoly = []
        vidx2 = vidx
        for loop in poly:
            ids += loop
            newpoly.append(range(vidx2, vidx2+len(loop)))
            vidx2 += len(loop)

        workinggeom.setPoly(i, newpoly)

        for id in ids:
            for slot,newslot in varyingvars:
                newslot[vidx] = slot[id]
            vidx += 1

    return newgeom
        

def removeIsolatedVertices(tm):

    print >>sys.stderr, 'Removing isolated vertices...'

    newgeom = trimeshgeom.TriMeshGeom()
    newgeom.faces.resize(len(tm.faces))

    # Key: Old vertex index   Value: New vertex index
    vert_used = {}
    # Index: New id   Value: Old id
    newverts = []
    for i in range(len(tm.faces)):
        ai,bi,ci = tm.faces[i]
        if ai not in vert_used:
            vert_used[ai] = len(newverts)
            newverts.append(ai)
        if bi not in vert_used:
            vert_used[bi] = len(newverts)
            newverts.append(bi)
        if ci not in vert_used:
            vert_used[ci] = len(newverts)
            newverts.append(ci)

        newgeom.faces[i] = (vert_used[ai], vert_used[bi], vert_used[ci])
            
    newgeom.verts.resize(len(newverts))

    # Set new vertices
    for i in range(len(newverts)):
        newgeom.verts[i] = tm.verts[newverts[i]]

    # Copy remaining variables...
    for name,storage,type,mult in tm.iterVariables():
        if storage==_core.VarStorage.VARYING:
            newgeom.newVariable(name, _core.VarStorage.VARYING, type, mult)
            src = tm.slot(name)
            dst = newgeom.slot(name)
            for i in range(len(newverts)):
                oldid = newverts[i]
                dst[i] = src[oldid]
        else:
            src = tm.slot(name)
            # Create slot
            if storage==_core.VarStorage.USER:
                newgeom.newVariable(name, _core.VarStorage.USER, type, mult, len(src))
            else:
                newgeom.newVariable(name, storage, type, mult)
            dst = newgeom.slot(name)
            # Copy slot
            src.copyValues(0, len(src), dst, 0)
#            for i in range(len(src)):
#                dst[i] = src[i]

    return newgeom
    

# extractUniform
def extractUniform(tm, varname, value):
    """Extract a uniform variable from a TriMesh.

    The resulting mesh will only contain faces that have the
    given value.
    """

    print >>sys.stderr, 'Extracting variable "%s"...'%(varname)

    var_slot = tm.slot(varname)
    # A list representing the new faces.
    # Each entry newfaces[i] represents face i of the new mesh
    # The value newfaces[i] is the index of the old face
    newfaces = []

    # Determine the new faces...
    for i in range(len(tm.faces)):
        if var_slot[i]==value:
            newfaces.append(i)

    newgeom = trimeshgeom.TriMeshGeom()
    newgeom.faces.resize(len(newfaces))
    newgeom.verts.resize(len(tm.verts))

    # Initialize new faces...
    for i in range(len(newfaces)):
        oldid = newfaces[i]
        newgeom.faces[i] = tm.faces[oldid]

    # Copy vertices...
    tm.verts.copyValues(0, len(tm.verts), newgeom.verts, 0)
#    for i in range(len(tm.verts)):
#        newgeom.verts[i] = tm.verts[i]

    # Copy remaining variables...
    for name,storage,type,mult in tm.iterVariables():
        if storage==_core.VarStorage.UNIFORM:
            newgeom.newVariable(name, _core.VarStorage.UNIFORM, type, mult)
            src = tm.slot(name)
            dst = newgeom.slot(name)
            for i in range(len(newfaces)):
                oldid = newfaces[i]
                dst[i] = src[oldid]
        else:
            src = tm.slot(name)
            # Create slot
            if storage==_core.VarStorage.USER:
                newgeom.newVariable(name, _core.VarStorage.USER, type, mult, len(src))
            else:
                newgeom.newVariable(name, storage, type, mult)
            dst = newgeom.slot(name)
            # Copy slot
            src.copyValues(0, len(src), dst, 0)
#            for i in range(len(src)):
#                dst[i] = src[i]

    return newgeom


def _mapID(id, attrid, attr, attr_lut, newverts):
    """Determine new vertex ID.

    attr_lut is a dictionary that servers as attribute lookup table.
    Key is the vertex ID and the value is another dictionary that
    has the attribute ID as key and the new ID as value.

    \param id  Old vertex ID
    \param attrid  Attribute ID
    \param attr  Attribute value
    \param attr_lut  Attribute lookup table
    \partam newverts
    \return New vertex ID
    """
    if id not in attr_lut:
        attr_lut[id] = {}
        
    newid_lut = attr_lut[id]
    if attrid in newid_lut:
        return newid_lut[attrid]
    else:
        newid = len(newverts)
        newid_lut[attrid] = newid
        newverts.append((id, attr))
        return newid


def convMeshAttr(tm, attrname, attrfacesname=None):
    """Convert attribute type.

    attrname is the name of the attribute that should be turned into
    a "varying" attribute.
    attrfacesnames is the name of the corresponding attribute
    faces. This must be a uniform variable.

    The result is a copy of tm where the attribute attrname is a
    "varying" variable. This means each vertex has a unique attribute.
    The result has the same number of faces than tm, but the number
    of vertices is usally higher.

    \param tm TriMeshGeom
    \param attrname Attribute 
    \param attrsfacesnames Faces attribute
    \return New TriMeshGeom
    """
    if attrfacesname==None:
        attrfacesname = attrname+"faces"
        
    print >>sys.stderr, 'Converting "%s"/"%s" into "varying %s"...'%(attrname,attrfacesname,attrname)
    newgeom = trimeshgeom.TriMeshGeom()

    attrdesc = tm.findVariable(attrname)

    attr_slot = tm.slot(attrname)
    attrfaces_slot = tm.slot(attrfacesname)

    attr_lut = {}
    # A list of tuples (Old vertex ID, Attribute ID)
    # An entry newverts[i] represents vertex i of the new mesh
    newverts = []
    
    newgeom.faces.resize(len(tm.faces))
    
    # Check each face...
    for i in range(len(tm.faces)):
        ai,bi,ci = tm.faces[i]
        attrai,attrbi,attrci = attrfaces_slot[i]

        # Get the 3 attributes associated to the current face
        attra = attr_slot[attrai]
        attrb = attr_slot[attrbi]
        attrc = attr_slot[attrci]

        newai = _mapID(ai, attrai, attra, attr_lut, newverts)
#        print "A: %d -> %d"%(ai,newai)
        newbi = _mapID(bi, attrbi, attrb, attr_lut, newverts)
#        print "B: %d -> %d"%(bi,newbi)
        newci = _mapID(ci, attrci, attrc, attr_lut, newverts)
#        print "C: %d -> %d"%(ci,newci)
        
        newgeom.faces[i] = (newai,newbi,newci)

    # Set the new vertices...
    numnewverts = len(newverts)
    newgeom.verts.resize(numnewverts)
    newgeom.newVariable(attrname, _core.VarStorage.VARYING, attrdesc[2], attrdesc[3])
    newattr_slot = newgeom.slot(attrname)
    for i in range(numnewverts):
        oldid,attr = newverts[i]
        newgeom.verts[i] = tm.verts[oldid]
        newattr_slot[i] = attr

    # Copy remaining variables...
    for name,storage,type,mult in tm.iterVariables():
        if name==attrname or name==attrfacesname:
            continue
        if storage==_core.VarStorage.VARYING:
            newgeom.newVariable(name, _core.VarStorage.VARYING, type, mult)
            src = tm.slot(name)
            dst = newgeom.slot(name)
            for i in range(numnewverts):
                oldid,attr = newverts[i]
                dst[i] = src[oldid]
        else:
            src = tm.slot(name)
            # Create slot
            if storage==_core.VarStorage.USER:
                newgeom.newVariable(name, _core.VarStorage.USER, type, mult, len(src))
            else:
                newgeom.newVariable(name, storage, type, mult)
            dst = newgeom.slot(name)
            # Copy slot
            src.copyValues(0, len(src), dst, 0)
#            for i in range(len(src)):
#                dst[i] = src[i]

    return newgeom

# fitCone
def fitCone(pos, obj):
    """Compute a cone that has its apex at pos and that includes obj.

    The generated cone is the minimal cone that entirely contains the
    bounding box of obj (which must be a WorldObject).
    pos is the apex of the cone given in world coordinates.
    The return value is a tuple (n,w) where n is the axis direction of the
    cone and w is the (full) angle in radians.
    """
    pos = vec3(pos)
    W = obj.worldtransform
    Winv = W.inverse()
    p = Winv*pos
    
    bb = obj.boundingBox()
    a,b,c,d,e,f,g,h = _bbCorners(bb)
    a = (a-p).normalize()
    b = (b-p).normalize()
    c = (c-p).normalize()
    d = (d-p).normalize()
    e = (e-p).normalize()
    f = (f-p).normalize()
    g = (g-p).normalize()
    h = (h-p).normalize()
    n1,w1 = _cone(a,h)
    n2,w2 = _cone(b,g)
    n3,w3 = _cone(c,f)
    n4,w4 = _cone(d,e)
    w = max(w1,w2,w3,w4)
    if w1==w:
        n = n1
    elif w2==w:
        n = n2
    elif w3==w:
        n = n3
    else:
        n = n4

    n = W.getMat3()*n
    return n,w

# _cone
def _cone(a,b):
    """Create a cone from two directions.

    a and b are two normalized directions that point along the generated
    cone on two opposite directions, i.e. the returned cone is the minmal
    cone that has the 'rays' a and b on its surface.
    The return value is a tuple (n,w) where n is the cone axis and w
    the (full) angle in radians.

    This is a helper function for the fitCone() function.
    """
    n = (a+b).normalize()
    w = a.angle(b)
    return n,w
    

# _bbCorners
def _bbCorners(bb):
    """Return the eight corners of a bounding box.

    bb is a BoundingBox object. The index of the returned points is
    such that bit 0 determines if the point is minmal or maximal in x
    direction. Bit 1 is the y direction and bit 2 the z
    direction. This means point n is opposite to point (~n)&0x7.

    This is a helper function for the fitCone() function.
    """
    a,h = bb.getBounds()
    b = vec3(h.x, a.y, a.z)
    c = vec3(a.x, h.y, a.z)
    d = vec3(h.x, h.y, a.z)
    e = vec3(a.x, a.y, h.z)
    f = vec3(h.x, a.y, h.z)
    g = vec3(a.x, h.y, h.z)
    return a,b,c,d,e,f,g,h


# delete
def delete(objs):
    """Delete objects from the scene.

    Currently, this only works on WorldObjects...
    """
    objs = worldObjects(objs)
    for obj in objs:
        obj.parent.removeChild(obj)
        

# convertToTriMesh
def convertToTriMesh(objs):
    """Convert one or more objects into triangle meshes.
    """
    objs = worldObjects(objs)
    for obj in objs:
        tm = trimeshgeom.TriMeshGeom()
        obj.geom.convert(tm)
        obj.geom = tm

# convertToPolyhedron
def convertToPolyhedron(objs):
    """Convert one or more objects into polyhedron objects.
    """
    objs = worldObjects(objs)
    for obj in objs:
        pg = polyhedrongeom.PolyhedronGeom()
        obj.geom.convert(pg)
        obj.geom = pg

# setupObjectNames
def setupObjectNames():
    """Create a string that can be executed to 'import' all scene names.
    """
    res = ""
    valid_chars = string.ascii_letters + "_" + string.digits
    for obj in getScene().item("WorldRoot").iterChilds():
        varname = ""
        for c in obj.name:
            if c not in valid_chars:
                c = "_"
            varname += c
        if varname=="":
            continue
        if varname[0] in string.digits:
            varname = "_"+varname[1:]
        res += '%s = worldObject("%s")\n'%(varname, obj.name)
    return res

# importDefaultPlugins
def importDefaultPlugins():
    """Import the default plugins.

    The plugin files/directories specified by the CGKIT_PLUGIN_PATH
    environment variable (if it exists) are imported.
    The function already outputs error messages and returns a list
    of plugin descriptors.
    """
    s = os.environ.get("CGKIT_PLUGIN_PATH", "")
    plugins = splitPaths(s)
        
    descs = pluginmanager.importPlugins(plugins)
    
    for desc in descs:
        if desc.status!=pluginmanager.STATUS_OK:
            sys.stderr.write(70*"-"+"\n")
            sys.stderr.write('ERROR: Loading plugin "%s" failed:\n'%os.path.basename(desc.filename))
            sys.stderr.write("\n"+desc.traceback)
            sys.stderr.write(70*"-"+"\n")

    return descs
            
# splitPaths
def splitPaths(paths):
    """Split a string containing paths into the individual paths.
    
    The paths can either be separated by ':' or ';'. Windows drive
    letters are maintained, even when ':' is used as separator.

    Example:

    \code
    >>> splitPaths("&:c:\\shaders:c:\\more_shaders;")
    ['&', 'c:\\shaders', 'c:\\more_shaders']    
    \endcode
    
    \param paths (\c str) Paths string
    \return A list with individual path strings.
    """
        
    a = paths.split(";")
    chunks = []
    for s in a:
        if s!="":
            chunks += s.split(":")

    pathlist = []
    while len(chunks)>0:
        # Is this item only a path drive?
        if len(chunks[0])==1 and chunks[0] in string.letters:
            pathlist.append(":".join(chunks[:2]))
            chunks = chunks[2:]
        else:
            pathlist.append(chunks[0])
            chunks = chunks[1:]

    return map(lambda x: x.strip(), pathlist)

# group
def group(*children, **keyargs):
    """Group several world objects together.

    All non keyword arguments somehow refer to world objects that will
    all be grouped together. An argument may either be a WorldObject,
    the name of a world object or as a sequence of world objects or names.
    The name of the new group may be given via the \a name keyword
    argument.
    The return value is the new Group object.
    
    \param name (\c str) Name of the group
    \param children The world objects or their names.
    """

    # Check the key arguments
    for k in keyargs:
        if k not in ["name"]:
            raise TypeError, "Unknown keyword argument: '%s'"%k

    # "Flatten" the children list...
    childs = []
    for c in children:
        if isinstance(c, types.StringTypes):
            childs += [c]
        else:
            # Is c a sequence?
            try:
                childs += list(c)
            except TypeError:
                # obviously not...
                childs += [c]
                
    childs = map(worldObject, childs)
    name = keyargs.get("name", "group")
    grp = group_module.Group(name=name, childs=childs)
    return grp
        

# ungroup
def ungroup(group):
    """Break up a group in its individual components.

    \param group (\c WorldObject or str) Group object
    """
    group = worldObject(group)
    if group.geom!=None:
        raise ValueError, 'Object "%s" is not a mere group. Remove the geometry before ungrouping.'%group.name

    # Move the children up one level
    newparent = group.parent
    for child in list(group.iterChilds()):
        link(child, newparent)

    # Remove the group
    newparent.removeChild(group)
        

# replaceMaterial
def replaceMaterial(name, newmat):
    """Iterate over all world objects and replace a material with a new one.

    \param name (\c str) The name of an already assigned material
    \param newmat (\c Material) The new material
    """
    for obj in getScene().walkWorld():
        # Check all materials...
        for i in range(obj.getNumMaterials()):
            mat = obj.getMaterial(i)
            if mat==None:
                continue
            if mat.name==name:
                obj.setMaterial(newmat, i)
    

# link
def link(childs, parent=None, relative=False):
    """Link all childs to parent.

    The function modifies the name of a child object if there would
    be a clash with an existing object under the new parent.

    \param childs (\c list or WorldObject) A single WorldObject or a sequence of WorldObjects
    \param parent (\c WorldObject) Parent object or None (=unlink)
    \param relative (\c bool) If True the local child transform is not modified (i.e. it is interpreted as a relative position)
    """

    # Check if childs is a sequence (other than a string)...
    try:
        len(childs)
        if isinstance(childs, types.StringTypes):
            is_sequence = False
        else:
            is_sequence = True
    except:
        is_sequence = False

    if not is_sequence:
        childs = [childs]

    # No parent given? Then use the world root (=unlink)
    if parent==None:
        parent = getScene().worldRoot()
    else:
        parent = worldObject(parent)

    # Relink all childs...
    for child in childs:
        child = worldObject(child)
        oldparent = child.parent
        if oldparent!=None:
            oldparent.removeChild(child)
        if not relative:
            Lp1 = oldparent.worldtransform
            Lp2 = parent.worldtransform
            child.transform = Lp2.inverse()*Lp1*child.transform
        child.name = parent.makeChildNameUnique(child.name) 
        parent.addChild(child)
    

# drawClear
def drawClear():
    """Clear all drawing objects."""
    # Clear all markers and lines
    try:
        drw = getScene().worldObject("Global_Draw")
        drw.clear()
    except KeyError:
        pass

    # Clear all texts
    try:
        drw = getScene().worldObject("Global_DrawText")
        drw.geom.clear()
    except KeyError:
        pass
    
    
# drawMarker
def drawMarker(pos, color=(1,1,1), size=1):
    """Draw a marker.

    \param pos Marker position 
    \param color Marker color
    \param size Marker size (radius)
    """
    try:
        drw = getScene().worldObject("Global_Draw")
    except KeyError:
        drw = draw.Draw(name="Global_Draw")
    drw.marker(pos, color, size)

# drawLine
def drawLine(pos1, pos2, color=(1,1,1), size=1):
    """Draw a line.

    \param pos1 First line point
    \param pos2 Second line point
    \param color Line color
    \param size Line width
    """
    try:
        drw = getScene().worldObject("Global_Draw")
    except KeyError:
        drw = draw.Draw(name="Global_Draw")
    drw.line(pos1, pos2, color, size)

# drawText
def drawText(pos, txt, font=None, color=(1,1,1)):
    """Draw a text string.

    font can be one of the constants defined in GLUT:

    - GLUT_BITMAP_8_BY_13
    - GLUT_BITMAP_9_BY_15 (default)
    - GLUT_BITMAP_TIMES_ROMAN_10
    - GLUT_BITMAP_TIMES_ROMAN_24
    - GLUT_BITMAP_HELVETICA_10
    - GLUT_BITMAP_HELVETICA_12
    - GLUT_BITMAP_HELVETICA_18

    \param pos Text position (3D)
    \param txt The actual text
    \param font A GLUT font constant
    \param color Text color    
    """
    try:
        drw = getScene().worldObject("Global_DrawText")
    except KeyError:
        drw = worldobject.WorldObject(name="Global_DrawText")
        drw.geom = drawtextgeom.DrawTextGeom()
    drw.geom.addText(pos, txt, font, color)

# listWorld
def listWorld():
    """List the contents of the world as a tree.
    """
    scn = getScene()
    print "Root"
    _listWorld(scn.worldRoot(), "")

def _listWorld(obj, lines):
    """Helper function for the listWorld() command."""
    childs = list(obj.iterChilds())
    idx = 0
    for child in childs:
        if child.geom!=None:
            g = child.geom.__class__.__name__
        else:
            g = "-"
        print "%s+---%s (%s/%s)"%(lines,child.name,child.__class__.__name__, g)
        if idx<len(childs)-1:
            childlines = lines+"|   "
        else:
            childlines = lines+"    "
        _listWorld(child, childlines)
        idx += 1
    

# load
def load(filename, **options):
    """Load a file.

    This function loads the given file without deleting the scene,
    so the contents of the file is appended to the current scene.

    To be able to load the file there must be an appropriate import class
    (protocol: "Import") available in the plugin manager. The class is
    determined by examining the file extension. If no importer is
    found a NoImporter exception is thrown.

    Any exception generated in the importer is passed to the caller.

    \param filename (\c str) File name
    \param options Options that are passed to the import plugin
    """
    # Extract the extension (without '.') and make it lower case
    ext = os.path.splitext(filename)[1][1:].lower()

    # Find the appropriate import plugin class by comparing the extension
    for objdesc in pluginmanager.iterProtoObjects("Import"):
        if ext in objdesc.object.extension():
            break
    else:
        raise NoImporter('No import plugin found for extension "%s".'%ext)

    if not os.path.exists(filename):
        raise IOError('File "%s" does not exist.'%filename)

    # Change into the directory of the given file
    oldpath = os.getcwd()
    dir = os.path.dirname(os.path.abspath(filename))
    os.chdir(dir)

    # Import the file
    imp = objdesc.object()
    imp.importFile(os.path.basename(filename), **options)

    # Change back to the previous directory
    os.chdir(oldpath)


# save
def save(filename, **options):
    """Save the scene to a file.

    This function saves the current scene.

    To be able to save the scene there must be an appropriate export class
    (protocol: "Export") available in the plugin manager. The class is
    determined by examining the file extension. If no exporter is
    found a NoExporter exception is thrown.

    Any exception generated in the exporter is passed to the caller.

    \param filename (\c str) File name
    \param options Options that are passed to the export plugin
    """
    # Extract the extension (without '.') and make it lower case
    ext = os.path.splitext(filename)[1][1:].lower()

    # Find the appropriate export plugin class by comparing the extension
    for objdesc in pluginmanager.iterProtoObjects("Export"):
        if ext in objdesc.object.extension():
            break
    else:
        raise NoExporter('No export plugin found for extension "%s".'%ext)

    # Change into the directory of the given file
    oldpath = os.getcwd()
    dir = os.path.dirname(os.path.abspath(filename))
    os.chdir(dir)

    # Export the file
    exp = objdesc.object()
    exp.exportFile(os.path.basename(filename), **options)

    # Change back to the previous directory
    os.chdir(oldpath)

# reset
def reset():
    """Reset an animation/simulation.

    Reset effectively sets the time back to 0.
    """
    eventmanager.eventManager().event(events.RESET)
    
# worldObjects
def worldObjects(objs):
    """Return a list of world objects.

    \a objs can be a string, a world object or a sequence of strings or
    world objects. A string always referes to the world object with that
    name. The return value is always a list, even when only an individual
    object was specified as input.

    \param objs A sequence of strings/world objects or an individual string
           or world object.
    \return A list of WorldObject objects.
    """

    if isinstance(objs, types.StringTypes):
        objs = [objs]

    try:
        it = iter(objs)
    except TypeError:
        it = iter([objs])
        
    res = []
    for obj in it:
        res.append(worldObject(obj))
        
    return res

# worldObject
def worldObject(obj):
    """Return a world object.

    If \a obj is a string, the function returns the world object with
    that name. Otherwise \a obj is returned.

    \param obj An object or the name of an object.
    """
    
    if isinstance(obj, types.StringTypes):
        return getScene().worldObject(obj)
    else:
        return obj

# register
def register(*cmds):
    """Register functions in the cmds module.

    The given functions will be added to the cmds module.

    \code
    import cmds
    
    def func1():
        ...

    def func2(v):
        ...

    cmds.register(func1, func2)
            
    \endcode
    
    """
    # Add the commands to the 
    namespace = globals()
    for c in cmds:
        namespace[c.__name__] = c
