Interactive/IdNavigationCustom.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.

__all__ = ["IdNavigationCustom", "instantiate"]

import math

from ActionRegistry     import theActionRegistry
from awSupportApi       import AffineMatrix, Line, Normal, Point, Vector
from MessageInterpreter import MessageInterpreter
from MessageRegistry    import theMessageRegistry
from Modes              import SelectionMode
from RTFapi             import Event
from UserCustomization  import UserCustomBase, CustomInfo
from SceneGraphUtilities import GetNodesFromIds

import ModelIO


CUSTOM_ID_NAVIGATION_ENABLE_Doc = \
[(
"""
Enable or disable custom id navigation feature. When enabled, left mouse
button will tumble the scene or selected objects. When disabled the items
being tumbled are returned to their original transform.
"""
),
[[("state"),("True to enable and False to disable")],
 ]
]

theMessageRegistry.register("CUSTOM_ID_NAVIGATION_ENABLE",
    (bool,), 0, CUSTOM_ID_NAVIGATION_ENABLE_Doc)


class IdNavigationCustom(UserCustomBase):

    __kMenuId = u"Edit"


    def __init__(self):
        self.__myMenu = None
        self.__myMenuItemId = None
        self.__myInterpreter = IdNavigationInterpreter()


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


    def appendMenuItems(self, id, menu):
        if IdNavigationCustom.__kMenuId == id:
            self.__myMenu = menu

            menu.appendSeparator()
            self.__myMenuItemId = menu.appendCheckItem(
                _("Custom ID Navigation"), self.OnIdNavigation)


    def enableMenuStates(self, id, enableStates):
        if IdNavigationCustom.__kMenuId == id:
            enableStates[self.__myMenuItemId] = True


    def resetMenuStates(self, id):
        if IdNavigationCustom.__kMenuId == id:
            menuItemStates = {}
            menuItemStates[self.__myMenuItemId] = False
            self.__myMenu.checkMenuItems(menuItemStates)


    def OnIdNavigation(self, event):
        isOn = bool(self.__myMenu.IsChecked(self.__myMenuItemId))
        self.sendMessage("CUSTOM_ID_NAVIGATION_ENABLE", (isOn,))



class IdNavigationInterpreter(MessageInterpreter):

    __kRotateStartKeyMappingId = "RotateStart"
    __kRotateKeyMappingId = "Rotate"

    class NodeData:
        def __init__(self, node):
            self.node = node
            self.undoMatrix = None
            self.startMatrix = None

    def __init__(self):
        MessageInterpreter.__init__(self)

        self.__myDisplay = None
        self.__myLastPosition = (0.0, 0.0)

        self.__targets = None

        self.__myAzimuth = 0.0
        self.__myElevation = 0.0

        #   Rotate key mappings.
        #
        keyMappings = (
          (Event.BUTTON_LMB, (), True, Event.BUTTONDOWN, "MyRotateStart"),
        )
        theActionRegistry.registerList(IdNavigationInterpreter.__kRotateStartKeyMappingId, keyMappings)

        keyMappings = (
          (Event.BUTTON_LMB, (), False, Event.BUTTONUP, "MyRotateStop")
        , (Event.BUTTON_None, (), False, Event.MOVE, "MyRotate")
        )
        theActionRegistry.registerList(IdNavigationInterpreter.__kRotateKeyMappingId, keyMappings)


    def getPosition(self, event):
        x = event.getValue(Event.kNormalizedWinX)
        y = event.getValue(Event.kNormalizedWinY)
        return x, y


    def MyRotateStart(self, event):
        if self.__myDisplay is None or \
            self.__myTargets is None:
            return False

        self.__myLastPosition = self.getPosition(event)
        theActionRegistry.addSetToStack(IdNavigationInterpreter.__kRotateKeyMappingId)
        theActionRegistry.activateSet(IdNavigationInterpreter.__kRotateKeyMappingId, True)

        for target in self.__myTargets:
            target.startMatrix = AffineMatrix(1.0)
            target.node.getMatrix( target.startMatrix )

        self.__myAzimuth = 0.0
        self.__myElevation = 0.0

        return True


    def MyRotate(self, event):
        x, y = self.getPosition(event)
        lastX, lastY = self.__myLastPosition

        dx = x - lastX
        dy = y - lastY

        self.__myLastPosition = self.getPosition(event)
        
        rotationMatrix = self.__computeMatrix(dx, dy)

        for target in self.__myTargets:
            matrix = AffineMatrix(rotationMatrix)
            matrix.preMult(target.startMatrix)
            target.node.setMatrix(matrix)

        return True


    def __computeMatrix(self, dx, dy):

        matrix = AffineMatrix(1.0)

        if self.__myDisplay is None:
            return matrix

        # Map the mouse movement into changes in azimuth and elevation
        
        azimuthDegrees = dx * 180.0
        azimuthRadians = math.radians(azimuthDegrees)
        elevationDegrees = dy * 90.0
        elevationRadians = math.radians(elevationDegrees)

        self.__myAzimuth += azimuthRadians
        self.__myElevation += elevationRadians
        halfpi = math.pi * 0.5
        if self.__myElevation < -halfpi:
            self.__myElevation = -halfpi + 0.0001
        if self.__myElevation > halfpi:
            self.__myElevation = halfpi - 0.0001

        # Compute axes to rotate around based on camera position
        # TODO camera tilt not currently being taken in account

        camera = self.__myDisplay.getCamera()
        assert(camera)
        
        pos = Point()
        coi = Point()
        up = Vector()
        camera.getView(pos, coi, up)
        
        view = Vector(coi[0] - pos[0], coi[1] - pos[1], coi[2] - pos[2])
        side = up.cross(view)

        azimuthAxis = Normal( 0.0, 0.0, 1.0 )
        elevationAxis = Normal(side[0], side[1], side[2])

        # Tumble around the chosen pivot point

        elevationAxis = Line(coi, elevationAxis)
        azimuthAxis   = Line(coi, azimuthAxis)
        matrix.rotate(self.__myElevation, elevationAxis)
        matrix.rotate(self.__myAzimuth, azimuthAxis)

        return matrix


    def MyRotateStop(self, event):
        self.__myLastPosition = self.getPosition(event)
        theActionRegistry.removeSetFromStack(IdNavigationInterpreter.__kRotateKeyMappingId)
        theActionRegistry.activateSet(IdNavigationInterpreter.__kRotateKeyMappingId, False)
        return True


    def APPLICATION_CLOSE_SCENE(self, message):
        self.__myTargets = None
        self.__myLastPosition = (0.0, 0.0)
        theActionRegistry.removeSetFromStack(IdNavigationInterpreter.__kRotateKeyMappingId)
        theActionRegistry.removeSetFromStack(IdNavigationInterpreter.__kRotateStartKeyMappingId)


    def CUSTOM_ID_NAVIGATION_ENABLE(self, message):
        (isOn,) = message.data

        if self.__myDocument is None:
            return

        models = self.__myDocument.get(ModelIO.id)
        if models is None:
            return

        if isOn:
            if models.selected.empty():
                self.__myTargets = ( IdNavigationInterpreter.NodeData(models.root), )
            else:
                self.__myTargets = tuple( IdNavigationInterpreter.NodeData(node)
                                          for node in GetNodesFromIds( models.selected.asTuple() ) )

            # Save the initial matrices of the targets for undo purposes when exiting this mode
            for target in self.__myTargets:
                target.undoMatrix = AffineMatrix(1.0)
                target.node.getMatrix( target.undoMatrix )

            self.sendMessage("SELECT", ((), SelectionMode.kReplace), requestUndo=False)

            theActionRegistry.addSetToStack(IdNavigationInterpreter.__kRotateStartKeyMappingId)
        else:
            theActionRegistry.removeSetFromStack(IdNavigationInterpreter.__kRotateStartKeyMappingId)

            # Restore the targets to their original state
            for target in self.__myTargets:
                target.node.setMatrix( target.undoMatrix )
                target.undoMatrix = None
            self.__myTargets = None

        theActionRegistry.activateSet(IdNavigationInterpreter.__kRotateStartKeyMappingId, isOn)


    def SET_DISPLAY(self, message):
        (self.__myDisplay,) = message.data


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


def instantiate():
    return IdNavigationCustom()


def info():
    customInfo = CustomInfo()
    customInfo.vendor = 'Autodesk'
    customInfo.version = '1.0'
    customInfo.api = '2013'
    customInfo.shortInfo = "Enable or disable custom id navigation feature."
    customInfo.longInfo = \
"""When enabled, left mouse button will tumble the scene or selected objects. \
When disabled the items being tumbled are returned to their original transform.
"""
    return customInfo