Interactive/SimpleSpaceNavigatorCustom.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.
##

# Showcase add-in example of a device controller that interfaces
# with the 3Dconnexion Space Navigator.
# Main class SimpleSpaceNavigatorCustom() initializes the add-in
# and receives messages from the Space Navigator.  This class
# then sends messages to SimpleSpaceNavigatorCustomInterpreter()
# for interpreting the output. The sensor output is converted
# to Showcase mouse keys and then button down and button up
# events are created and dispatched into the Showcase
# event queue.
#
# NOTE: 
# This controller requires several important
# python modules:
# 1. comtypes-0.2.1
# 2. ctypes
# Both modules are provided with your
# installation module
#

#
# Supported functionality:
# - Left button will Fit To View(3Dconnexion Panel button setting
#   must be 'Fit')
# - Pan left/right
# - Pan up/down
# - Zoom up/down
# - Tilt up/down
# - Spin left/right
#

__all__ = ["instantiate"]

# Python distribution imports
import threading
import math

# NOTE: Additional Python imports not found in
# Showcase distribution
import comtypes.client as client
from ctypes import *

# Showcase imports
from MessageInterpreter import MessageInterpreter
from MessageRegistry    import theMessageRegistry
from RTFapi             import Event, EventRef
from UserCustomization  import UserCustomBase, CustomInfo

# Set to True to for some debug prints
WantEventInformation = False

#
# Register messages
#

CUSTOM_NAV_KEY_EVENT_Doc = \
[(
"""
Keyboard navigation event.
"""
),
[[("upEvent"),("True if button up, False otherwise.")],
 [("keyCode"),("Integer key code.")]
 ]
]

CUSTOM_NAV_SENSOR_EVENT_Doc = \
[(
"""
Sensor navigation event.
"""
),
[[("rotateX"),("rotateX")],
 [("rotateY"),("rotateY")],
 [("rotateZ"),("rotateZ")],
 [("rotateAngle"),("rotateAngle")],
 [("translateX"),("translateX")],
 [("translateY"),("translateY")],
 [("translateZ"),("translateZ")],
 [("translateLength"),("translateLength")],
 ]
]

theMessageRegistry.register("CUSTOM_NAV_KEY_EVENT",
    (bool,int), 0, CUSTOM_NAV_KEY_EVENT_Doc)
theMessageRegistry.register("CUSTOM_NAV_SENSOR_EVENT",
    (float,float,float,float,float,float,float,float), 0, CUSTOM_NAV_SENSOR_EVENT_Doc)

#
# Windows structures required. Defined using
# ctypes
#
class POINT(Structure):
    _fields_ = [("x", c_long),
        ("y", c_long)]

class MSG(Structure):
    _fields_ = [("hWnd"     ,   c_ulong),
                ("message"  ,   c_uint),
                ("wParam"   ,   c_ulong),
                ("lParam"   ,   c_ulong),
                ("time"     ,   c_ulong),
                ("pt"       ,   POINT)]


class CustomControllerThread(threading.Thread): 

    def __init__(self, listener):
        threading.Thread.__init__(self)
        self.__listener = listener

    def GetMessage(self):
        user32 = windll.user32
        msg = MSG()
        user32.GetMessageW(byref(msg), None, 0, 0)
        return msg

    def DispatchMessage(self,msg):
        user32 = windll.user32
        user32.DispatchMessageW(byref(msg))

    def run(self):
        # Start event loop
        while self.__listener.continueListening():
            msg = self.GetMessage()
            self.DispatchMessage(msg)

#
# Class that binds Showcase and Controller events
# together.
# Controller events are turned into button events
# and then dispatched. A filter is used to find
# the most significant controller event and then
# run that.
# A timer is used to terminate the current event
# if the idle is too long.
#
class SimpleSpaceNavigatorCustomInterpreter(MessageInterpreter):

    # Indices for Controller data
    kRotationX = 0
    kRotationY = 1
    kRotationZ = 2
    kTranslationX = 3
    kTranslationY = 4
    kTranslationZ = 5
    kRotationAngle = 6
    kLength = 7
    kNone = 8

    # Indices for Showcase button press information
    kPanLeft = 0
    kPanRight = 1
    kPanUp = 2
    kPanDown = 3
    kZoomIn = 4
    kZoomOut = 5
    kTiltUp = 6 
    kTiltDown = 7 
    kSpinLeft = 8 
    kSpinRight = 9
    kRollLeft = 10
    kRollRight = 11

    # In seconds
    kTimerDelay = .25

    def __init__(self,uiWindow,eventQueue):
        MessageInterpreter.__init__(self)

        self.__hasDocument = False
        self.__uiWindow = uiWindow
        self.__eventQueue = eventQueue
        self.__transformInformation = [0,0,0, 0,0,0, 0,0]
        self.__cameraButtonMap =    {
            self.kPanLeft  : Event.BUTTON_KP_Left,
            self.kPanRight : Event.BUTTON_KP_Right,
            self.kPanUp    : Event.BUTTON_KP_Page_Up,
            self.kPanDown  : Event.BUTTON_KP_Page_Down,
            self.kZoomIn   : Event.BUTTON_Up,
            self.kZoomOut  : Event.BUTTON_Down,
            self.kTiltUp   : Event.BUTTON_Page_Up,
            self.kTiltDown : Event.BUTTON_Page_Down,
            self.kSpinLeft : Event.BUTTON_Left,
            self.kSpinRight: Event.BUTTON_Right,
            self.kRollLeft : Event.BUTTON_Unknown,
            self.kRollRight: Event.BUTTON_Unknown
            }
        self.__lastButtonKeycode = None
        self.__threadTimer = None
        self.__stickToLastMovement = True

    # Showcase message
    def SET_DOCUMENT(self, message):
        self.__hasDocument = True

    # Showcase message
    def APPLICATION_CLOSE_SCENE( self, message ):
        self.__hasDocument = False

    # Custom message
    def CUSTOM_NAV_KEY_EVENT(self,message):
        (keyUp,keyCode) = message.data
        if self.__documentOpen():
            if keyUp and keyCode == Event.BUTTON_D:
                self.sendMessage( 'FIT_TO_VIEW', ( True, ) )

    # Custom message
    def CUSTOM_NAV_SENSOR_EVENT(self,message):
        ( rotX, rotY, rotZ, rotAngle, X, Y, Z, Length ) = message.data
        if WantEventInformation:
            print "Rotation (%g,%g,%g) Angle %g Translation (%g,%g,%g) Length %g" %\
              (rotX, rotY, rotZ, rotAngle, X, Y, Z, Length)
        self.__handleSensorInput( rotX, rotY, rotZ, rotAngle, X, Y, Z, Length )

    def __handleSensorInput( self, rotX, rotY, rotZ, rotAngle, X, Y, Z, Length ):

        if self.__documentOpen():
            self.__saveTransformationInformation(rotX, rotY, rotZ, rotAngle, X, Y, Z, Length)
            (rotateIndex,rotateComponentValue,translateIndex,translateComponentValue) = self.__highPassFilterOnRotateTranslate()

            inputHandled = False
            if self.__stickToLastMovement:
                if self.__lastButtonKeycode is not None:
                    self.__handleNewButtonDownMessage(self.__lastButtonKeycode )
                    inputHandled = True

            if not inputHandled:
                if translateIndex == self.kTranslationX:
                    self.__doPanRightOrLeft( translateComponentValue )
                elif translateIndex == self.kTranslationY:
                    self.__doPanUpOrDown( translateComponentValue )
                elif translateIndex == self.kTranslationZ:
                    self.__doZoomInOrOut( translateComponentValue )
                else:
                    if rotateIndex == self.kRotationX:
                        self.__doTilt( rotateComponentValue )
                    elif rotateIndex == self.kRotationY:
                        self.__doSpin( rotateComponentValue )
                    elif rotateIndex == self.kRotationZ:
                        self.__doRoll( rotateComponentValue )
                    else:
                        self.__endPress()

            if self.__threadTimer is not None:
                    self.__threadTimer.cancel()
                    del self.__threadTimer
            self.__threadTimer = threading.Timer( self.kTimerDelay, self.__endPress )
            self.__threadTimer.start()

    #
    def __endPress(self):
        if self.__lastButtonKeycode is not None:
            self.__dispatchEvent( Event.BUTTONUP,self.__lastButtonKeycode) 
            self.__lastButtonKeycode = None

    #
    def __handleNewButtonDownMessage( self, buttonAction ):
        if self.__lastButtonKeycode !=  buttonAction:
            self.__endPress()
        self.__dispatchEvent( Event.BUTTONDOWN,buttonAction) 
        self.__lastButtonKeycode = buttonAction

    # Called from message handler to perform Showcase update
    def __doPanRightOrLeft(self, translateX ):
        if WantEventInformation:
            print "\tpan left/right: %g" % translateX

        buttonAction = self.__cameraButtonMap[ self.kPanLeft ]
        if translateX < 0:
              buttonAction = self.__cameraButtonMap[ self.kPanRight ]
        self.__handleNewButtonDownMessage( buttonAction )

    # Called from message handler to perform Showcase update
    def __doPanUpOrDown(self, translateY ):
        if WantEventInformation:
            print "\tpan up/down: %g" % translateY
        buttonAction = self.__cameraButtonMap[ self.kPanUp ]
        if translateY < 0:
              buttonAction = self.__cameraButtonMap[self.kPanDown ]  
        self.__handleNewButtonDownMessage( buttonAction )

    # Called from message handler to perform Showcase update
    def __doZoomInOrOut(self, translateZ ):
        if WantEventInformation:
            print "\tzoom in/out: %g" % translateZ
        buttonAction = self.__cameraButtonMap[ self.kZoomOut ]
        if translateZ < 0:
            buttonAction = self.__cameraButtonMap[ self.kZoomIn ]
        self.__handleNewButtonDownMessage( buttonAction )

    # Called from message handler to perform Showcase update
    def __doTilt(self, rotateX ):
        if WantEventInformation:
            print "\ttilt: %g" % rotateX
        buttonAction = self.__cameraButtonMap[ self.kTiltUp ]
        if rotateX < 0:
              buttonAction = self.__cameraButtonMap[ self.kTiltDown ]  
        self.__handleNewButtonDownMessage( buttonAction )

    # Called from message handler to perform Showcase update
    def __doSpin(self, rotateY ):
        if WantEventInformation:
            print "\tspin: %g" % rotateY
        buttonAction = self.__cameraButtonMap[ self.kSpinLeft ]
        if rotateY < 0:
              buttonAction = self.__cameraButtonMap[ self.kSpinRight ]  
        self.__handleNewButtonDownMessage( buttonAction )

    # Called from message handler to perform Showcase update
    def __doRoll(self, rotateZ ):
        if WantEventInformation:
            print "\troll : %g" % rotateZ

    # Utility method: return true if there is a document
    def __documentOpen(self):
         return self.__hasDocument

    # Utility method: stores the transform information
    def __saveTransformationInformation(self, rotX, rotY, rotZ, rotAngle, X, Y, Z, Length):
        self.__transformInformation[self.kRotationX] = rotX
        self.__transformInformation[self.kRotationY] = rotY
        self.__transformInformation[self.kRotationZ] = rotZ
        self.__transformInformation[self.kTranslationX] = X
        self.__transformInformation[self.kTranslationY] = Y
        self.__transformInformation[self.kTranslationZ] = Z
        self.__transformInformation[self.kRotationAngle] = rotAngle
        self.__transformInformation[self.kLength] = Length

    # Utility method: extract largest absolute value component
    # of rotate or translate.  Allows isolating movements of
    # the controller.
    def __highPassFilterOnRotateTranslate(self):

        def applyFilterToComponents(startPosition,endPosition):
            #
            for i in range(startPosition, endPosition+1):
                if self.__transformInformation[i] != 0:
                    break
            else:
                return (self.kNone,0)

            #
            maxAbsPosition = startPosition
            for i in range(startPosition+1, endPosition+1):
                currentMaxAbsValue = math.fabs( self.__transformInformation[i] )
                if currentMaxAbsValue > math.fabs( self.__transformInformation[maxAbsPosition] ):
                    maxAbsPosition = i
            return (maxAbsPosition,self.__transformInformation[maxAbsPosition])

        translateResult = applyFilterToComponents( self.kTranslationX, self.kTranslationZ )
        rotateResult = applyFilterToComponents( self.kRotationX, self.kRotationZ )
        return rotateResult + translateResult

    # Utility method: queue a Showcase event
    def __dispatchEvent(self, direction, key):
        if self.__eventQueue:
            event = Event(direction, key)
            eventRef = EventRef(event)
            self.__eventQueue.queueEvent(event)


#
# Showcase add-in initialization class. Sets up
# objects that will be needed later.
#
class SimpleSpaceNavigatorCustom(UserCustomBase):
    def __init__(self):
        self.__myMenu = None
        self.__myMenuItemId = None
        self.__keyboard = None
        self.__sensor = None
        self.__exit = False
        self.__types =   {
                           0    :   "Unknown",
                           6    :   "SpaceNavigator",
                           4    :   "SpaceExplorer",
                           25   :   "SpaceTraveller",
                           29   :   "SpacePilot"
                        }

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

    # Overridden
    def inputDevices( self, uiWindow, eventQueue ):
        self.__myInterpreter = SimpleSpaceNavigatorCustomInterpreter( uiWindow, eventQueue )
        self.__initializeInputDevice()

    def __initializeInputDevice(self):
        try:
            device = client.CreateObject("TDxInput.Device")
        except:
            return
        if device.Type in self.__types:
                print self.__types[device.Type],
        else:
                print "Unknown Device",

        if not device.Connect():
           print ": Connected"

        # Keyboard listener
        self.__keyboard = device.Keyboard
        client.GetEvents(self.__keyboard, self)

        # Sensor listener
        self.__sensor = device.Sensor
        client.GetEvents(self.__sensor, self)

        # Start listening/dispatch thread
        deviceThread = CustomControllerThread(self)
        deviceThread.start()

    def continueListening(self):
        return not self.__exit

    def KeyDown(self, this, code):
        if WantEventInformation:
            print "SpaceNavigator.onKeyPress(%s)" % code
        self.sendMessage( 'CUSTOM_NAV_KEY_EVENT', ( True,code ) )

    def KeyUp(self, this, code):
        if WantEventInformation:
            print "SpaceNavigator.onKeyRelease(%s) " % code
        self.sendMessage( 'CUSTOM_NAV_KEY_EVENT', ( True,code ) )

    def SensorInput(self, this, inval=None):       
        rotX = self.__sensor.Rotation.X
        rotY = self.__sensor.Rotation.Y
        rotZ = self.__sensor.Rotation.Z
        rotAngle = self.__sensor.Rotation.Angle
        X = self.__sensor.Translation.X
        Y = self.__sensor.Translation.Y
        Z = self.__sensor.Translation.Z
        Length = self.__sensor.Translation.Length

        self.sendMessage( "CUSTOM_NAV_SENSOR_EVENT", (rotX, rotY, rotZ, rotAngle, X, Y, Z, Length) )

#
# Main entry point called from Showcase
#
def instantiate():
    return SimpleSpaceNavigatorCustom()


def info():
    customInfo = CustomInfo()
    customInfo.vendor = 'Autodesk'
    customInfo.version = '3.0'
    customInfo.api = '2013'
    customInfo.shortInfo = "Add-in of a device controller interfacing with 3Dconnexion Space Navigator."
    customInfo.longInfo = \
"""The sensor output is converted to Showcase mouse keys and then button down and button up events \
are created and dispatched into the Showcase event queue.

Supported functionality:
- Left button will Fit To View (3Dconnexion Panel button setting must be 'Fit')
- Pan left/right
- Pan up/down
- Zoom up/down
- Tilt up/down
- Spin left/right
"""
    return customInfo