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

# Go into stereo mode, specifying eye distance or automatic eye distance
# based on the focal point.  To get true stereo, it requires enabling it
# in the driver, as well as running Showcase Professional.
#

__all__ = ['StereoCustom', 'instantiate']

from MessageRegistry     import theMessageRegistry
from MessageInterpreter  import MessageInterpreter
from ActionRegistry      import theActionRegistry
from UserCustomization   import UserCustomBase, CustomInfo
from RTFapi              import Event, Display, SystemInformation
from RunInUiThread       import RunInUiThread
from DisplayUtilities    import GetCameras, GetViewStates

import math

STEREO_CAMERA_Doc = \
[(
"""
Enable or disable stereo.
"""
),
[[("enable"),("True to turn it on, False to turn it off.")],
 ]
]
theMessageRegistry.register("STEREO_CAMERA",
    (bool,), 0, STEREO_CAMERA_Doc)


STEREO_CAMERA_AUTO_ADJUST_Doc = \
[(
"""
Automatically adjust the eye distance for stereo.
"""
),
[[("autoDistanceAdjust"),("Automatically adjust the eye spacing based on distance.  If true, it is set and the factor specified is used.  If false, it is unset and the following argument is not used.")],
 [("autoDistanceAdjustFactor"),("Factor for the automatic adjustment")],
 ]
]
theMessageRegistry.register("STEREO_CAMERA_AUTO_ADJUST",
    (bool,float), 0, STEREO_CAMERA_AUTO_ADJUST_Doc)


STEREO_CAMERA_EYE_DISTANCE_Doc = \
[(
"""
Automatically turns off the automatic eye distance setting and sets it to specified value.
"""
),
[[("distance"),("The eye distance to use")],
 ]
]
theMessageRegistry.register("STEREO_CAMERA_EYE_DISTANCE",
    (bool,float), 0, STEREO_CAMERA_EYE_DISTANCE_Doc)


STEREO_CAMERA_FOCUS_DISTANCE_Doc = \
[(
"""
Set or reset the focus distance.
"""
),
[[("reset"),("If true, the distance value is ignored and instead the distance is set to half way between near and far clipping planes.")],
 [("distance"),("Focus distance to use.")],
 ]
]
theMessageRegistry.register("STEREO_CAMERA_FOCUS_DISTANCE",
    (bool,float), 0, STEREO_CAMERA_FOCUS_DISTANCE_Doc)


STEREO_ADJUST_PARAMETERS_MODE_Doc = \
[(
"""
Enable or disable eye distance adjustment for stereo. When enabled,
left mouse button will change the eye distance.
"""
),
[[("state"),("True to enable and False to disable")],
 ]
]
theMessageRegistry.register("STEREO_ADJUST_PARAMETERS_MODE",
    (bool,), 0, STEREO_ADJUST_PARAMETERS_MODE_Doc)

# -----------------------------------------------------------------------

class StereoCustom (UserCustomBase):
    def __init__(self):
        # This enables the stereo pixel format for the session
        Display.setEnableStereoFormat()

        # Stereo is only available with OpenGL; make sure this
        # is what we are running.
        SystemInformation.instance().setDisplayDevice("VirtualDeviceGL",True)

        # DirectX has trouble with Windows Aero - it can't keep up.  OpenGL
        # seems to be OK, so disable the flash on start.
        SystemInformation.instance().setDisableThemeSwitch(True)

        self.__myCallIdStereo = None
        self.__myCallIdAdjust = None
        self.__myCallIdReset = None
        self.__myFirstTime = True

        self.__myInterp = LocalInterp()

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


    def appendMenuItems(self,id,menu):
        if "View" == id:
            self.__myMenu = menu

            menu.appendSeparator()
            self.__myCallIdStereo = \
                        menu.appendCheckItem(_( 'Stereo Mode' ),
                                             self.__onCameraStereo )
            self.__myCallIdAdjust = \
                        menu.appendCheckItem(_( 'Stereo Adjust' ),
                                             self.__onCameraAdjust)

            self.__myCallIdReset = \
                        menu.appendItem(_( 'Stereo Reset' ),
                                        self.__onCameraReset)


    def enableMenuStates(self,id,enableStates):
        if "View" == id:
            stereoOn = bool(self.__myMenu.IsChecked(self.__myCallIdStereo))
            adjustOn = bool(self.__myMenu.IsChecked(self.__myCallIdAdjust))
            enableStates[self.__myCallIdStereo] = not adjustOn
            enableStates[self.__myCallIdAdjust] = stereoOn
            enableStates[self.__myCallIdReset] = stereoOn


    def resetMenuStates(self, id):
        if "View" == id:
            menuItemStates = {}
            menuItemStates[self.__myCallIdAdjust] = False
            menuItemStates[self.__myCallIdStereo] = False
            self.__myMenu.checkMenuItems(menuItemStates)

    # ------------------------------------------------
    def __onCameraStereo( self, event ):
        isOn = bool(self.__myMenu.IsChecked(self.__myCallIdStereo))
        self.__myInterp.sendMessage( "STEREO_CAMERA", (isOn, ) )
        if self.__myFirstTime:
            self.__onCameraReset( event )
            self.__myFirstTime = False
        RunInUiThread( self.__myMenu.updateMenu )

    def __onCameraAdjust( self, event ):
        isOn = bool(self.__myMenu.IsChecked(self.__myCallIdAdjust))
        self.__myInterp.sendMessage("STEREO_ADJUST_PARAMETERS_MODE", (isOn,))
        RunInUiThread( self.__myMenu.updateMenu )

    def __onCameraReset( self, event ):
        self.__myInterp.sendMessage( "STEREO_CAMERA_AUTO_ADJUST",(True,0.03) )
        self.__myInterp.sendMessage( "STEREO_CAMERA_FOCUS_DISTANCE",(True,1.0))


# -----------------------------------------------------------------------

class LocalInterp( MessageInterpreter ):

    __kFactorStartId = "FactorStart"
    __kFactorId = "Factor"
    __kFocusStartId = "FocusStart"
    __kFocusId = "Focus"

    def __init__( self ):
        MessageInterpreter.__init__( self )
        self.__myDisplay = None
        self.__myFactor = 0.03
        self.__myFocus = 80.0
        self.__myLastPosition = (0.0, 0.0)
        self.__myFirstTime = True

        #   Factor key mappings.
        #
        keyMappings = (
          (Event.BUTTON_LMB, (), True, Event.BUTTONDOWN, "MyFactorStart"),
        )
        theActionRegistry.registerList(LocalInterp.__kFactorStartId,
                                       keyMappings)
        keyMappings = (
          (Event.BUTTON_LMB, (), False, Event.BUTTONUP, "MyFactorStop"),
          (Event.BUTTON_None, (), False, Event.MOVE, "MyFactor")
        )
        theActionRegistry.registerList(LocalInterp.__kFactorId,
                                       keyMappings)

        #   Focus key mappings.
        #
        keyMappings = (
          (Event.BUTTON_MMB, (), True, Event.BUTTONDOWN, "MyFocusStart"),
        )
        theActionRegistry.registerList(LocalInterp.__kFocusStartId,
                                       keyMappings)
        keyMappings = (
          (Event.BUTTON_MMB, (), False, Event.BUTTONUP, "MyFocusStop"),
          (Event.BUTTON_None, (), False, Event.MOVE, "MyFocus")
        )
        theActionRegistry.registerList(LocalInterp.__kFocusId,
                                       keyMappings)


    def __getMainCamera( self ):
        return self.__myDisplay.getCamera() if self.__myDisplay else None


    def SET_DISPLAY(self, message):
        (self.__myDisplay,) = message.data
        camera = self.__getMainCamera()
        if camera:
            self.__myFocus = camera.getFocusDistance()


    def APPLICATION_CLOSE_SCENE(self, message):
        theActionRegistry.removeSetFromStack(LocalInterp.__kFactorId)
        theActionRegistry.removeSetFromStack(LocalInterp.__kFactorStartId)
        self.sendMessage( "STEREO_CAMERA", (False, ) )


    def STEREO_CAMERA( self, message ):
        if self.__myDisplay is None:
            return
        (enable,) = message.data
        if enable:
            self.__myDisplay.enableStereo()
            self.__messageInfo()
        else:
            self.__myDisplay.disableStereo()


    def STEREO_CAMERA_AUTO_ADJUST( self, message ):
        (autoAdjust,factor) = message.data
        for viewstate in GetViewStates( self.__myDisplay ):
            viewstate.setStereoAutomaticEyeDistanceAdjustment( autoAdjust )
            viewstate.setStereoAutomaticEyeDistanceFactor( factor )


    def STEREO_CAMERA_EYE_DISTANCE( self, message ):
        (dist,) = message.data
        for viewstate in GetViewStates( self.__myDisplay ):
            viewstate.setStereoAutomaticEyeDistanceAdjustment( False )
            viewstate.setStereoEyeDistance( dist )


    def STEREO_CAMERA_FOCUS_DISTANCE( self, message ):
        (reset,dist) = message.data
        for camera in GetCameras( self.__myDisplay ):
            if reset:
                dist = 0.5 * (camera.getNearClip()+ camera.getFarClip())

            camera.setFocusDistance( dist )

        camera = self.__getMainCamera()
        if camera:
            self.__myFocus = camera.getFocusDistance()


    def STEREO_ADJUST_PARAMETERS_MODE(self, message):
        (isOn,) = message.data
        if isOn:
            theActionRegistry.addSetToStack(LocalInterp.__kFactorStartId)
            theActionRegistry.addSetToStack(LocalInterp.__kFocusStartId)
            self.__message()
        else:
            theActionRegistry.removeSetFromStack(LocalInterp.__kFactorStartId)
            theActionRegistry.removeSetFromStack(LocalInterp.__kFocusStartId)
        theActionRegistry.activateSet(LocalInterp.__kFactorStartId, isOn)
        theActionRegistry.activateSet(LocalInterp.__kFocusStartId, isOn)


    def NAVIGATION_POSITION(self, message):
        (nav,) = message.data
        p = nav.getPosition()
        la = nav.getLookAt()
        coi = nav.getCOI()

        # Camera and center of interest, or camera and look at?
        # d = (p[0]-coi[0],p[1]-coi[1],p[2]-coi[2])
        d = (p[0]-la[0],p[1]-la[1],p[2]-la[2])

        self.__myFocus = math.sqrt(d[0]*d[0] + d[1]*d[1] + d[2]*d[2])
        for camera in GetCameras( self.__myDisplay ):
            camera.setFocusDistance( self.__myFocus )


    def __message(self):
        self.sendMessage( "STATUSBAR_SET_TEXT",
                          ("LMB - offset (%f), MMB - focus (%f)" % \
                           (self.__myFactor, self.__myFocus),) )


    def __messageInfo(self):
        self.sendMessage( "STATUSBAR_SET_TEXT",
                          ("Offset (%f), Focus (%f)" % \
                           (self.__myFactor, self.__myFocus),) )


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


    def MyFactorStart(self, event):
        if self.__myDisplay is None:
            return False

        self.__myLastPosition = self.__getPosition(event)
        theActionRegistry.addSetToStack(LocalInterp.__kFactorId)
        theActionRegistry.activateSet(LocalInterp.__kFactorId, True)

        return True

    def MyFactor(self, event):
        x, y = self.__getPosition(event)

        lastX, lastY = self.__myLastPosition
        self.__myLastPosition = x, y

        self.__myFactor += (x - lastX) + (y - lastY)
        self.__message()
        self.sendMessage( "STEREO_CAMERA_AUTO_ADJUST", (True,self.__myFactor) )

        return True

    def MyFactorStop(self, event):
        theActionRegistry.removeSetFromStack(LocalInterp.__kFactorId)
        theActionRegistry.activateSet(LocalInterp.__kFactorId, False)
        return True


    def MyFocusStart(self, event):
        if self.__myDisplay is None:
            return False

        self.__myLastPosition = self.__getPosition(event)
        theActionRegistry.addSetToStack(LocalInterp.__kFocusId)
        theActionRegistry.activateSet(LocalInterp.__kFocusId, True)

        return True

    def MyFocus(self, event):
        x, y = self.__getPosition(event)

        lastX, lastY = self.__myLastPosition
        self.__myLastPosition = x, y

        self.__myFocus += 10*((x - lastX) + (y - lastY))
        self.__message()
        self.sendMessage("STEREO_CAMERA_FOCUS_DISTANCE",(False,self.__myFocus))

        return True

    def MyFocusStop(self, event):
        theActionRegistry.removeSetFromStack(LocalInterp.__kFocusId)
        theActionRegistry.activateSet(LocalInterp.__kFocusId, False)
        return True


# --------------------------------------------------------------------

def instantiate():
    return StereoCustom()


def info():
    customInfo = CustomInfo()
    customInfo.vendor = 'Autodesk'
    customInfo.version = '5.0'
    customInfo.api = '2013'
    customInfo.shortInfo = "View in OpenGL stereo mode."
    customInfo.longInfo = \
"""Go into stereo mode, specifying eye distance or automatic eye distance based on the focal point. \
To get true stereo, it requires enabling the support in the driver and Showcase Professional license.  It will switch Showcase into OpenGL mode, rather than the default DirectX.
"""
    return customInfo