Interactive/MessageByLabelsCustom.py

# (C) Copyright 2012 Autodesk, Inc.  All rights reserved.
#
# Use of this software is subject to the terms of the Autodesk license
# agreement provided at the time of installation or download, or which
# otherwise accompanies this software in either electronic or hard copy
# form.

from UserCustomization      import UserCustomBase, CustomInfo
from MessageInterpreter     import MessageInterpreter
from Message                import Message
from MessageRegistry        import theMessageRegistry
from MessageDocs            import *
from Messaging              import printDebug
from SceneGraphUtilities    import CollectNodes

import AlternativeIO
import ModelIO
import ShotIO
import StoryboardIO
import BehaviorIO
import EnvironmentIO

from EnvironmentLibrary     import EnvironmentLibrary

def instantiate():
    return MessageLabelCustomization()

class MessageLabelCustomization(UserCustomBase):
    """
    Registers a bunch of helper messages which allow users to send messages
    by label instead of IDs. Mostly used for sending messages from HTML pages.
    """
    def __init__(self):
        self.__myInterpreter = MessageLabelInterpreter()

    def getInterpreter(self, isInteractive):
        if isInteractive:
            return self.__myInterpreter
        return  None


class MessageLabelInterpreter(MessageInterpreter):
    # Bitmask identifiers for each type
    kAlternativeSetId = 1
    kAlternativeId = 2
    kNodeId = 4
    kSlideId = 8
    kShotId = 16
    kBehaviorId = 32
    kEnvironmentId = 64
    kList = 65536

    kLabelSuffix = '_BYLABEL'

    def __init__(self):
        MessageInterpreter.__init__(self)
        self.__myDocument = None
        self.__myAlternatives = None
        self.__myModels = None

        self.__myIsActive = True

        # Create the list of messages indicating which parameters need to be converted.
        #
        self.__myMessages = {
              "ALTERNATIVE_DELETE_SET"      : (self.kAlternativeSetId, )
            , "ALTERNATIVE_RENAME_SET"      : (self.kAlternativeSetId, 0)
            , "ALTERNATIVE_CREATE_DATA"     : (self.kAlternativeSetId, 0, 0, 0, 0)
            , "ALTERNATIVE_CREATE"          : (self.kAlternativeSetId, 0, 0, 0, 0)
            , "ALTERNATIVE_DELETE"          : (self.kAlternativeSetId, self.kAlternativeId)
            , "ALTERNATIVE_RENAME"          : (self.kAlternativeSetId, self.kAlternativeId, 0)
            , "ALTERNATIVE_DUPLICATE"       : (self.kAlternativeSetId, self.kAlternativeId, 0)
            , "ALTERNATIVE_ADD_ITEMS"       : (self.kAlternativeSetId, self.kAlternativeId, self.kNodeId | self.kList)
            , "ALTERNATIVE_REMOVE_ITEMS"    : (self.kAlternativeSetId, self.kAlternativeId, self.kNodeId | self.kList)
            , "ALTERNATIVE_SET_IMAGE"       : (self.kAlternativeSetId, self.kAlternativeId, 0)

            , "BEHAVIOR_START"              : (self.kBehaviorId, 0)
            , "BEHAVIOR_STOP"               : (self.kBehaviorId, 0)
            , "BEHAVIORS_LINK"              : (self.kBehaviorId | self.kList, )
            , "BEHAVIORS_UNLINK"            : (self.kBehaviorId | self.kList, )
            , "BEHAVIOR_SELECT_ASSIGNED"    : (self.kBehaviorId, 0)
            , "BEHAVIOR_PARAMETER_CHANGE"   : (self.kBehaviorId, 0, 0)
            , "BEHAVIOR_ADD_NODES"          : (self.kBehaviorId, self.kNodeId | self.kList)
            , "BEHAVIOR_REMOVE_NODES"       : (self.kBehaviorId, self.kNodeId | self.kList)
            , "BEHAVIOR_ENABLE"             : (self.kBehaviorId, 0)
            , "BEHAVIOR_SHOW_PROPERTIES"    : (self.kBehaviorId, )
            , "BEHAVIOR_UI_PROPERTIES"      : (self.kBehaviorId, )
            , "BEHAVIOR_UI_CONTROLS"        : (self.kBehaviorId, 0)
            , "BEHAVIOR_SAVE_THUMBNAIL"     : (self.kBehaviorId, )

            , "ENVIRONMENT_LOAD"            : (self.kEnvironmentId, )
            , "ENVIRONMENT_RENAME"          : (self.kEnvironmentId, 0)
            , "ENVIRONMENT_UPDATE_LIST"     : (self.kEnvironmentId | self.kList, )
            , "ENVIRONMENT_CHANGE_BACKDROP_IMAGE"       : (self.kEnvironmentId | self.kList, 0, 0)
            , "ENVIRONMENT_CHANGE_BACKPLATE_PARAMETERS" : (self.kEnvironmentId | self.kList, 0)
            , "ENVIRONMENT_CHANGE_LIGHTING_PARAMETERS"  : (self.kEnvironmentId | self.kList, 0, 0, 0, 0)

            , "SHOT_EDIT"               : (self.kShotId, )
            , "SHOT_DELETE"             : (self.kShotId, )
            , "SHOT_RENAME"             : (self.kShotId, 0)
            , "SHOT_CHANGE_TYPE"        : (self.kShotId, 0, 0)
            , "SHOT_SET_PARAMETERS"     : (self.kShotId, 0, 0)
            , "SHOT_SAVE_THUMBNAIL"     : (self.kShotId, 0)
            , "SHOT_REFRESH_THUMBNAIL"  : (self.kShotId, )
            , "SHOT_MOVE_TO_KEYFRAME"   : (self.kShotId, )
            , "SHOT_POSITION_AFTER"     : (self.kShotId, self.kShotId)
            , "SHOT_UPDATE_DIALOG_DATA" : (self.kShotId, )
            , "SHOT_SHOW_STAGESHOTS"    : (self.kShotId, )

            , "SLIDE_DELETE"                : (self.kSlideId | self.kList, )
            , "SLIDE_DUPLICATE"             : (self.kSlideId | self.kList, )
            , "SLIDE_ITEM_ADD"              : (0, self.kSlideId, 0, 0)
            , "SLIDE_ITEM_POSITION_CHANGE"  : (self.kSlideId, 0, 0, 0)
            , "SLIDE_ITEM_REMOVE"           : (self.kSlideId, 0, 0)
            , "SLIDE_POSITION_CHANGE"       : (self.kSlideId, 0)
            , "SLIDE_RENAME"                : (self.kSlideId, 0)
            , "SLIDE_THUMBNAIL_SAVE"        : (self.kSlideId, )
            , "SLIDE_THUMBNAIL_SET"         : (self.kSlideId, 0)

            , "DELETE"                  : (self.kNodeId | self.kList, )
            , "UNDELETE"                : (self.kNodeId | self.kList, )
            , "HIDE"                    : (self.kNodeId | self.kList, 0)
            , "HIDE_UNSELECTED"         : (self.kNodeId | self.kList, )
            , "DUPLICATE"               : (self.kNodeId | self.kList, )
            , "MIRROR_DUPLICATE"        : (0, self.kNodeId | self.kList)
            , "SHOW"                    : (self.kNodeId | self.kList, )
            , "NODE_RENAME"             : (self.kNodeId | self.kList, 0)
            , "GROUP_CREATE"            : (0, self.kNodeId, 0)
            , "GROUP_NODES"             : (self.kNodeId | self.kList, )
            , "UNGROUP_NODES"           : (self.kNodeId | self.kList, )
            , "GROUP_COLLAPSE"          : (self.kNodeId | self.kList, )
            , "GROUP_EXPAND"            : (self.kNodeId | self.kList, )

            , "ALIGN_TO_FLOOR"          : (self.kNodeId | self.kList, )
            , "SELECT"                  : (self.kNodeId | self.kList, 0)
            , "SELECT_VISIBLE"          : (self.kNodeId | self.kList, 0)
            , "MODEL_SELECT"            : (self.kNodeId | self.kList, 0)
            , "MODEL_HIGHLIGHT"         : (self.kNodeId | self.kList, )
            , "ROTATE_WORLDSPACE"       : (0, 0, 0, 0, self.kNodeId | self.kList)
            , "TRANSLATE_WORLDSPACE"    : (0, 0, 0, 0, self.kNodeId | self.kList)
            , "PIVOT_MOVE_WORLDSPACE"   : (0, 0, 0, 0, self.kNodeId | self.kList)

            , "MODEL_DELETE"                    : (self.kNodeId | self.kList, )
            , "MODEL_RENAME"                    : (self.kNodeId | self.kList, 0)
            , "MODEL_SWITCH_UP_AXIS"            : (self.kNodeId, )
            , "MODEL_SCALE"                     : (self.kNodeId | self.kList, 0)
            , "MODEL_IMPORT"                    : (self.kNodeId, 0, 0)
            , "MODEL_IMPORT_REPLACE"            : (self.kNodeId, 0, 0)
            , "MODEL_PREPARE"                   : (self.kNodeId, 0, 0)
            , "MODEL_PREPARE_START"             : (self.kNodeId, )
            , "MODEL_PREPARE_STOP"              : (self.kNodeId, )
            , "MODEL_IMPORT_CHANGE_ORIGINAL"    : (self.kNodeId, 0 )

            , "GEOMETRY_FLIP_NORMALS"       : (self.kNodeId | self.kList, 0)
            , "GEOMETRY_COPY_TRANSFORM"     : (self.kNodeId | self.kList, )
            , "GEOMETRY_PASTE_TRANSFORM"    : (self.kNodeId | self.kList, )
            , "GEOMETRY_SET_TRANSFORM"      : (self.kNodeId | self.kList, 0)
            , "GEOMETRY_UP_AXIS_SWITCH"     : (self.kNodeId | self.kList, )
            , "GEOMETRY_SCALE"              : (self.kNodeId | self.kList, 0)

            , "SHADOW_SET_RECEIVER"         : (self.kNodeId | self.kList, 0)
            , "SHADOW_SET_RECEIVER_OBJECTS" : (self.kNodeId | self.kList, 0)
            , "SHADOW_SET_CASTER_OBJECTS"   : (self.kNodeId | self.kList, 0)
            , "SHADOW_LIGHT_CREATE"         : (self.kNodeId | self.kList, )
            , "SHADOW_LIGHT_DELETE"         : (self.kNodeId, )
            , "SHADOW_RECEIVER_REPLACE"     : (self.kNodeId | self.kList, self.kNodeId | self.kList)

            , "PATCH_CURRENT_OBJECT"        : (self.kNodeId, )
            , "PATCH_OPERATION"             : (self.kNodeId, 0, 0, 0)
            , "PATCH_OPERATION_EXPLICIT"    : (self.kNodeId, 0, 0)
            , "PATCH_REQUEST_STATE"         : (self.kNodeId, )
            , "PATCH_STATE"                 : (self.kNodeId, 0, 0)

            , "APPEARANCE_PARAMETERS_CHANGE"    : (self.kNodeId | self.kList, 0, 0, 0, 0, 0)
            , "MATERIAL_COMPUTE_OCCLUSION"      : (self.kNodeId | self.kList, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
            , "TEXTURE_MATRIX_TRANSFORM"        : (self.kNodeId | self.kList, 0, 0, 0)
        }

        for id in self.__myMessages:
            self.__createMessageWrapper(id, self.__myMessages[id])


    def deactivate(self):
        # Make sure we clean up these or else we'll get memory leaks!!
        self.__myDocument = None
        self.__myModels = None
        self.__myAlternatives = None
        self.__myBehaviors = None
        self.__myStoryboard = None
        self.__myEnvironments = None


    def __createMessageWrapper(self, messageId, messageData):
        """
        Registers a message named 'MESSAGE_NAME_BYLABEL' for the specified
        messageId and adds a method to this class instance to handle the new message.
        """
        # Get the list of datatypes for the original message
        dataTypes = theMessageRegistry.dataType(messageId)
        id = messageId + self.kLabelSuffix

        if theMessageRegistry.isRegistered(id):
            printDebug(["NOTE: Message id %s already registered" % id])
            return
        if dataTypes is None:
            printDebug(["ERROR: Type info not found for message " + messageId])
            return
        if len(dataTypes) != len(messageData):
            printDebug(["ERROR: Number of arguments for message " + messageId + " does not match!"])
            printDebug(["       Expected: %d got %d" % (len(dataTypes), len(messageData))])
            return

        # Use the original message's documentation
        documentation = eval(messageId + "_Doc")
        theMessageRegistry.register( id, dataTypes, Message.kUndoable, documentation )

        # Add the message handler method to this class and map it to self.__handleMessage
        setattr(self, id, self.__handleMessage)


    def __handleMessage(self, message):
        """
        Accepts the label form of all messages listed in self.__myMessages, and then
        sends out an appropriate
        """
        # chop the suffix off of the message id
        originalMessage = message.id[:-len(self.kLabelSuffix)]
        assert(originalMessage in self.__myMessages)

        self.__processLabelMessage(originalMessage, message.data,
             self.__myMessages[originalMessage])


    def __processLabelMessage(self, messageId, data, flags):
        data = list(data)

        # process all the different types of labels
        data = self.__processAlternativeLabels(data, flags)
        data = self.__processNodeLabels(data, flags)
        data = self.__processShotLabels(data, flags)
        data = self.__processBehaviorLabels(data, flags)
        data = self.__processSlideLabels(data, flags)
        data = self.__processEnvironmentLabels(data, flags)

        dataTypes = theMessageRegistry.dataType(messageId)
        data = self.__fixDataTypes(data, dataTypes)

        printDebug(["Labels Converted:" + messageId + str(tuple(data))])
        self.sendMessage(messageId, tuple(data))


    def __fixDataTypes(self, data, dataTypes):
        """
        Finds any fields in data which aren't the right data type (as specified by
        dataTypes) and tries to cast them to the correct type.

        data - list of data fields
        dataTypes - a list of python types
        """
        for i in range(len(data)):
            if data.__class__ != dataTypes[i]:
                try:
                    data[i] = dataTypes[i](data[i])
                except:
                    printDebug(["ERROR: Cannot cast message parameter " + str(i) + "to the correct type!"])

        return data


    def __processSlideLabels(self, data, flags):
        """
        Replaces all slide labels with corresponding slide ids
        """
        for (i, flag) in enumerate(flags):
            if flag & self.kSlideId and flag & self.kList:
                slideIds = []
                for label in data[i]:
                    slideIds.append(self.__getSlideId(label))

                data[i] = slideIds

            elif flag & (self.kSlideId):
                data[i] = self.__getSlideId(data[i])

        return data


    def __getSlideId(self, slideLabel):
        for slide in self.__myStoryBoard:
            if slide.name() == slideLabel:
                return slide.id()
        return slideLabel


    def __processEnvironmentLabels(self, data, flags):
        """
        Replaces all environment labels with corresponding environment ids
        """
        for (i, flag) in enumerate(flags):
            if flag & self.kEnvironmentId and flag & self.kList:
                environmentIds = []
                for label in data[i]:
                    environmentIds.append(self.__getEnvironmentId(label))

                data[i] = environmentIds

            elif flag & self.kEnvironmentId:
                data[i] = self.__getEnvironmentId(data[i])

        return data


    def __getEnvironmentId(self, environmentLabel):
        # First look for it in the currently loaded environments
        for environment in self.__myEnvironments:
            if environment.name() == environmentLabel:
                return environment.id()

        # Then look in our environment libraries
        theLibrary = EnvironmentLibrary.instance()
        libraryList = theLibrary.getAllEnvironmentLibraries()

        for library in libraryList:
            for id in sorted(theLibrary.getEnvironmentIds(library)):
                if (theLibrary.getEnvironmentName(id) == environmentLabel):
                    return id

        return environmentLabel


    def __processShotLabels(self, data, flags):
        """
        Replaces all slide labels with corresponding slide ids
        """
        for (i, flag) in enumerate(flags):
            if flag & self.kShotId and flag & self.kList:
                shotIds = []

                for label in data[i]:
                    shotIds.append(self.__getShotId(label))

                data[i] = shotIds

            elif flag & self.kShotId:
                data[i] = self.__getShotId(data[i])

        return data


    def __getShotId(self, shotLabel):
        for shot in self.__myShots.shots:
            if shot.getLabel() == shotLabel:
                return shot.getId().getFullId()
        return shotLabel


    def __processBehaviorLabels(self, data, flags):
        """
        Replaces all behavior labels with corresponding behavior ids
        """
        for (i, flag) in enumerate(flags):
            if flag & self.kBehaviorId and flag & self.kList:
                behaviorIds = []
                for label in data[i]:
                    behaviorIds.append(self.__getBehaviorId(label))

                data[i] = behaviorIds

            elif flag & self.kBehaviorId:
                data[i] = self.__getBehaviorId(data[i])

        return data


    def __getBehaviorId(self, behaviorLabel):
        for name, behavior in self.__myBehaviors.iteritems():
            if behavior.getLabel() == behaviorLabel:
                return name
        return behaviorLabel


    def __processNodeLabels(self, data, flags):
        """
        Replaces all lists of node labels with a corresponding list of node ids.
        """
        for (i, flag) in enumerate(flags):
            if flag & self.kNodeId and flag & self.kList:
                data[i] = self.__getNodeIds(data[i])

            elif flag & self.kNodeId:
                nodeIds = self.__getNodeIds([ data[i] ])

                if len(nodeIds > 0):
                    data[i] = nodeId[0]
        return data


    def __getNodeIds(self, nodeLabels):
        """
        Given a list of node labels returns a list of all node ids that have
        one of the specified labels.
        """
        nodeIds = CollectNodes(
                    self.__myModels.root,
                    conditions=[lambda node: node.getLabel() in nodeLabels],
                    idsOnly=True
                    )

        return nodeIds


    def __processAlternativeLabels(self, data, flags):
        """
        Replaces one pair of alternative set label & alternative label
        with the corresponding alternative set name & alternative name.

        - Assumes there is only one pair of alternative set & alternative specified.
        - Works if only the alternative set label exists
        """

        if self.kAlternativeSetId not in flags:
            # this message doesn't contain any alternative labels
            return data

        setLabelIndex = -1
        altLabelIndex = -1

        # find the index for the alternative set
        for (i, flag) in enumerate(flags):
            if flag == self.kAlternativeSetId:
                setLabelIndex = i
                break

        setLabel = data[setLabelIndex]
        altLabel = None

        # We have an alternative set label, now get the alternative label
        # if there is one.
        if self.kAlternativeId in flags:
            for (i, flag) in enumerate(flags):
                if flag == self.kAlternativeId:
                    altLabelIndex = i
                    break
            altLabel = data[altLabelIndex]

            (setName, altName) = self.__getAlternativeNames(setLabel, altLabel)
            data[setLabelIndex] = setName
            data[altLabelIndex] = altName
        else:
            (setName, altName) = self.__getAlternativeNames(setLabel, None)
            data[setLabelIndex] = setName

        return data


    def __getAlternativeNames(self, setLabel, altLabel):
        """
        Given an alernative set label and an alternative label, returns
        a tuple containing the corresponding alternative set name and
        alternative name.

        Returns None for either name if the specified label is invalid.
        """
        altName = setName = None

        for alternativeSet in self.__myAlternatives:
            if alternativeSet.label == setLabel:
                setName = alternativeSet.name
                break

        if setName is None:
            return (None, None)
        if altLabel is None:
            return (setName, None)

        for alternative in alternativeSet:
            if alternative.label == altLabel:
                altName = alternative.name
                break

        return (setName, altName)


    def CONVERT_LABELS(self, message):
        (msgId, data, flags) = message.data

        # since this message could be coming in from HTML we want to make sure
        # that the tuple of flags are integers
        intflags = []

        for flag in flags:
            try:
                intflags.append( int(flag) )
            except:
                # just go with it :)
                intflags.append(0)

        self.__processLabelMessage(msgId, data, intflags)


    def APPLICATION_CLOSE_SCENE(self, message):
        self.__myAlternatives = None
        self.__myModels = None
        self.__myStoryboard = None
        self.__myBehaviors = None
        self.__myEnvironments = None
        self.__myShots = None


    def SET_DOCUMENT(self, message):
        (self.__myDocument, ) = message.data

        self.__myAlternatives = self.__myDocument.get(AlternativeIO.id)
        self.__myModels = self.__myDocument.get(ModelIO.id)
        self.__myStoryBoard = self.__myDocument.get(StoryboardIO.id)
        self.__myBehaviors = self.__myDocument.get(BehaviorIO.id)
        self.__myEnvironments = self.__myDocument.get(EnvironmentIO.id)
        self.__myShots = self.__myDocument.get(ShotIO.id)


CONVERT_LABELS_Doc = \
[(
"""
Calls the specified message after processing the specified fields as labels
and converting them into ids.

Valid data flags are:
1 - Alternative Set
2 - Alternative
4 - Node
8 - Slide
16 - Shot
32 - Behavior
64 - Environment
65536 - List

For example to call SHOT_PLAY using shot labels your parameters would be as follows:
('SHOT_PLAY', ( ('Front Wheels','Back Wheels'), False ), (65552, 0) )

Adding 16 + 65536 = 65552 implies that the parameter is a list of shots lables to be converted to ids.
"""
),
[[("messageId"),("The message to call")],
 [("messageData"),("A tuple containing the data for the message.")],
 [("dataFlags"),("A tuple of flags identifying which messageData fields are labels, and how to convert them.")],
 ],
]


theMessageRegistry.register(
      "CONVERT_LABELS"
    , (unicode, tuple, tuple)
    , Message.kUndoable
    , CONVERT_LABELS_Doc)


def info():
    customInfo = CustomInfo()
    customInfo.vendor = 'Autodesk'
    customInfo.version = '2.0'
    customInfo.api = '2013'
    customInfo.shortInfo = "Send messages by label intead of ID."
    customInfo.longInfo = \
"""Register a bunch of helper messages for users to send messages by label instead of ID. Mostly \
used for sending messages from HTML pages.
"""
    return customInfo