from direct.showbase.DirectObject import DirectObject
from DirectUtil import *
from DirectGeometry import *
from DirectGlobals import *
from DirectSelection import SelectionRay
from direct.interval.IntervalGlobal import Sequence, Func
from direct.directnotify import DirectNotifyGlobal
from direct.task import Task

CAM_MOVE_DURATION = 1.2
COA_MARKER_SF = 0.0075
Y_AXIS = Vec3(0, 1, 0)

class DirectCameraControl(DirectObject):
    
    notify = DirectNotifyGlobal.directNotify.newCategory('DirectCameraControl')
    
    def __init__(self):
        # Create the grid
        self.startT = 0.0
        self.startF = 0
        self.orthoViewRoll = 0.0
        self.lastView = 0
        self.coa = Point3(0, 100, 0)
        self.coaMarker = loader.loadModel('models/misc/sphere')
        self.coaMarker.setName('DirectCameraCOAMarker')
        self.coaMarker.setTransparency(1)
        self.coaMarker.setColor(1, 0, 0, 0)
        self.coaMarker.setPos(0, 100, 0)
        useDirectRenderStyle(self.coaMarker)
        self.coaMarkerPos = Point3(0)
        self.coaMarkerColorIval = None
        self.fLockCOA = 0
        self.nullHitPointCount = 0
        self.cqEntries = []
        self.coaMarkerRef = base.direct.group.attachNewNode('coaMarkerRef')
        self.camManipRef = base.direct.group.attachNewNode('camManipRef')
        self.switchDirBelowZero = True
        self.manipulateCameraTask = None
        self.manipulateCameraInterval = None

        t = CAM_MOVE_DURATION
        self.actionEvents = [
            ['DIRECT-mouse1', self.mouseRotateStart],
            ['DIRECT-mouse1Up', self.mouseDollyStop],
            ['DIRECT-mouse2', self.mouseFlyStart],
            ['DIRECT-mouse2Up', self.mouseFlyStop],
            ['DIRECT-mouse3', self.mouseDollyStart],
            ['DIRECT-mouse3Up', self.mouseDollyStop],
            ]

        # [gjeon] moved all of the hotkeys to single place for easy remapping
##         self.keyEvents = [
##             ['c', self.centerCamIn, 0.5],
##             ['f', self.fitOnWidget],                  # Note: This function doesn't work as intended
##             ['h', self.homeCam],
##             ['shift-v', self.toggleMarkerVis],          
##             ['m', self.moveToFit],                      # Note: This function doesn't work as intended; the object dissappears and screen flashes
##             ['n', self.pickNextCOA],
##             ['u', self.orbitUprightCam],
##             ['shift-u', self.uprightCam],
##             [repr(1), self.spawnMoveToView, 1],
##             [repr(2), self.spawnMoveToView, 2],
##             [repr(3), self.spawnMoveToView, 3],
##             [repr(4), self.spawnMoveToView, 4],
##             [repr(5), self.spawnMoveToView, 5],
##             [repr(6), self.spawnMoveToView, 6],
##             [repr(7), self.spawnMoveToView, 7],
##             [repr(8), self.spawnMoveToView, 8],
##             ['9', self.swingCamAboutWidget, -90.0, t],
##             ['0', self.swingCamAboutWidget,  90.0, t],
##             ['`', self.removeManipulateCameraTask],
##             ['=', self.zoomCam, 0.5, t],
##             ['+', self.zoomCam, 0.5, t],
##             ['-', self.zoomCam, -2.0, t],
##             ['_', self.zoomCam, -2.0, t],
##             ]
        
        self.keyEvents = [
            ['DIRECT-centerCamIn', self.centerCamIn, 0.5],
            ['DIRECT-fitOnWidget', self.fitOnWidget],                  # Note: This function doesn't work as intended
            ['DIRECT-homeCam', self.homeCam],
            ['DIRECT-toggleMarkerVis', self.toggleMarkerVis],          
            ['DIRECT-moveToFit', self.moveToFit],                      # Note: This function doesn't work as intended; the object dissappears and screen flashes
            ['DIRECT-pickNextCOA', self.pickNextCOA],
            ['DIRECT-orbitUprightCam', self.orbitUprightCam],
            ['DIRECT-uprightCam', self.uprightCam],
            ['DIRECT-spwanMoveToView-1', self.spawnMoveToView, 1],
            ['DIRECT-spwanMoveToView-2', self.spawnMoveToView, 2],
            ['DIRECT-spwanMoveToView-3', self.spawnMoveToView, 3],
            ['DIRECT-spwanMoveToView-4', self.spawnMoveToView, 4],
            ['DIRECT-spwanMoveToView-5', self.spawnMoveToView, 5],
            ['DIRECT-spwanMoveToView-6', self.spawnMoveToView, 6],
            ['DIRECT-spwanMoveToView-7', self.spawnMoveToView, 7],
            ['DIRECT-spwanMoveToView-8', self.spawnMoveToView, 8],
            ['DIRECT-swingCamAboutWidget-0', self.swingCamAboutWidget, -90.0, t],
            ['DIRECT-swingCamAboutWidget-1', self.swingCamAboutWidget,  90.0, t],
            ['DIRECT-removeManipulateCameraTask', self.removeManipulateCameraTask],
            ['DIRECT-zoomInCam', self.zoomCam, 0.5, t],
            ['DIRECT-zoomOutCam', self.zoomCam, -2.0, t],
            ]
        # set this to true to prevent the camera from rolling
        self.lockRoll = False
        # NIK - flag to determine whether to use maya camera controls
        self.useMayaCamControls = 0
        self.altDown = 0
        self.perspCollPlane = None # [gjeon] used for new LE
        self.perspCollPlane2 = None # [gjeon] used for new LE
        
    def toggleMarkerVis(self):
##        if base.direct.cameraControl.coaMarker.isHidden():
##            base.direct.cameraControl.coaMarker.show()
##        else:
##            base.direct.cameraControl.coaMarker.hide()
            
        if self.coaMarker.isHidden():
            self.coaMarker.show()
        else:
            self.coaMarker.hide()

    def mouseRotateStart(self, modifiers):
        if self.useMayaCamControls and modifiers == 4:          # alt is pressed - use maya controls
            # base.direct.pushUndo([base.direct.camera])        # Wasteful use of undo
            self.spawnMouseRotateTask()

    def mouseDollyStart(self, modifiers):
        if self.useMayaCamControls and modifiers == 4: # alt is pressed - use maya controls
            # Hide the marker for this kind of motion
            self.coaMarker.hide()
            # Record time of start of mouse interaction
            self.startT= globalClock.getFrameTime()
            self.startF = globalClock.getFrameCount()
            # If the cam is orthogonal, spawn differentTask
            if hasattr(base.direct, "manipulationControl") and base.direct.manipulationControl.fMultiView and\
               base.direct.camera.getName() != 'persp':
                self.spawnOrthoZoom()
            else:
                # Start manipulation
                self.spawnHPanYZoom()

    def __stopManipulateCamera(self):
        if self.manipulateCameraTask:
            taskMgr.remove(self.manipulateCameraTask)
            self.manipulateCameraTask = None
            
        if self.manipulateCameraInterval:
            self.manipulateCameraInterval.finish()
            self.manipulateCameraInterval = None

    def __startManipulateCamera(self, func = None, task = None, ival = None):
        self.__stopManipulateCamera()
        if func:
            assert(task is None)
            task = Task.Task(func)
        if task:
            self.manipulateCameraTask = taskMgr.add(task, 'manipulateCamera')
        if ival:
            ival.start()
            self.manipulateCameraInterval = ival
            
    def mouseDollyStop(self):
        self.__stopManipulateCamera()

    def mouseFlyStart(self, modifiers):
        # Record undo point
        # base.direct.pushUndo([base.direct.camera])            # Wasteful use of undo
        if self.useMayaCamControls and modifiers == 4:          # alt is down, use maya controls
            # Hide the marker for this kind of motion
            self.coaMarker.hide()
            # Record time of start of mouse interaction
            self.startT= globalClock.getFrameTime()
            self.startF = globalClock.getFrameCount()
            # Start manipulation
            # If the cam is orthogonal, spawn differentTask
            if hasattr(base.direct, "manipulationControl") and base.direct.manipulationControl.fMultiView and\
               base.direct.camera.getName() != 'persp':
                self.spawnOrthoTranslate()
            else:
                self.spawnXZTranslate()
            self.altDown = 1
        elif not self.useMayaCamControls:
            # Where are we in the display region?
            if ((abs(base.direct.dr.mouseX) < 0.9) and (abs(base.direct.dr.mouseY) < 0.9)):
                # MOUSE IS IN CENTRAL REGION
                # Hide the marker for this kind of motion
                self.coaMarker.hide()
                # Record time of start of mouse interaction
                self.startT= globalClock.getFrameTime()
                self.startF = globalClock.getFrameCount()
                # Start manipulation
                self.spawnXZTranslateOrHPanYZoom()
                # END MOUSE IN CENTRAL REGION
            else:
                if ((abs(base.direct.dr.mouseX) > 0.9) and
                    (abs(base.direct.dr.mouseY) > 0.9)):
                    # Mouse is in corners, spawn roll task
                    self.spawnMouseRollTask()
                else:
                    # Mouse is in outer frame, spawn mouseRotateTask
                    self.spawnMouseRotateTask()
        if not modifiers == 4:
            self.altDown = 0

    def mouseFlyStop(self):
        self.__stopManipulateCamera()
        stopT = globalClock.getFrameTime()
        deltaT = stopT - self.startT
        stopF = globalClock.getFrameCount()
        deltaF = stopF - self.startF
        ## No reason this shouldn't work with Maya cam on
        # if not self.useMayaCamControls and (deltaT <= 0.25) or (deltaF <= 1):

        # Do this when not trying to manipulate camera
        if not self.altDown and len(base.direct.selected.getSelectedAsList()) == 0:
            # Check for a hit point based on
            # current mouse position
            # Allow intersection with unpickable objects
            # And then spawn task to determine mouse mode
            # Don't intersect with hidden or backfacing objects
            skipFlags = SKIP_HIDDEN | SKIP_BACKFACE
            # Skip camera (and its children), unless control key is pressed
            skipFlags |= SKIP_CAMERA * (1 - base.getControl())
            self.computeCOA(base.direct.iRay.pickGeom(skipFlags = skipFlags))
            # Record reference point
            self.coaMarkerRef.iPosHprScale(base.cam)
            # Record entries
            self.cqEntries = []
            for i in range(base.direct.iRay.getNumEntries()):
                self.cqEntries.append(base.direct.iRay.getEntry(i))
        # Show the marker
        self.coaMarker.show()
        # Resize it
        self.updateCoaMarkerSize()
        
    def mouseFlyStartTopWin(self):
        print "Moving mouse 2 in new window"
        #altIsDown = base.getAlt()
        #if altIsDown:
        #    print "Alt is down"

    def mouseFlyStopTopWin(self):
        print "Stopping mouse 2 in new window"

    def spawnXZTranslateOrHPanYZoom(self):
        # Kill any existing tasks
        self.__stopManipulateCamera()
        # Spawn the new task
        t = Task.Task(self.XZTranslateOrHPanYZoomTask)
        # For HPanYZoom
        t.zoomSF = Vec3(self.coaMarker.getPos(base.direct.camera)).length()
        self.__startManipulateCamera(task = t)

    def spawnXZTranslateOrHPPan(self):
        # Kill any existing tasks
        self.__stopManipulateCamera()
        # Spawn new task
        self.__startManipulateCamera(func = self.XZTranslateOrHPPanTask)

    def spawnXZTranslate(self):
        # Kill any existing tasks
        self.__stopManipulateCamera()
        # Spawn new task
        self.__startManipulateCamera(func = self.XZTranslateTask)

    def spawnOrthoTranslate(self):
        # Kill any existing tasks
        self.__stopManipulateCamera()
        # Spawn new task
        self.__startManipulateCamera(func = self.OrthoTranslateTask)

    def spawnHPanYZoom(self):
        # Kill any existing tasks
        self.__stopManipulateCamera()
        # Spawn new task
        t = Task.Task(self.HPanYZoomTask)
        t.zoomSF = Vec3(self.coaMarker.getPos(base.direct.camera)).length()
        self.__startManipulateCamera(task = t)

    def spawnOrthoZoom(self):
        # Kill any existing tasks
        self.__stopManipulateCamera()
        # Spawn new task
        t = Task.Task(self.OrthoZoomTask)
        self.__startManipulateCamera(task = t)        

    def spawnHPPan(self):
        # Kill any existing tasks
        self.__stopManipulateCamera()
        # Spawn new task
        self.__startManipulateCamera(func = self.HPPanTask)

    def XZTranslateOrHPanYZoomTask(self, state):
        if base.direct.fShift:
            return self.XZTranslateTask(state)
        else:
            return self.HPanYZoomTask(state)

    def XZTranslateOrHPPanTask(self, state):
        if base.direct.fShift:
            # Panning action
            return self.HPPanTask(state)
        else:
            # Translation action
            return self.XZTranslateTask(state)

    def XZTranslateTask(self, state):
        coaDist = Vec3(self.coaMarker.getPos(base.direct.camera)).length()
        xlateSF = (coaDist / base.direct.dr.near)
        base.direct.camera.setPos(base.direct.camera,
                             (-0.5 * base.direct.dr.mouseDeltaX *
                              base.direct.dr.nearWidth *
                              xlateSF),
                             0.0,
                             (-0.5 * base.direct.dr.mouseDeltaY *
                              base.direct.dr.nearHeight *
                              xlateSF))
        return Task.cont

    def OrthoTranslateTask(self, state):
        # create ray from the camera to detect 3d position
        iRay = SelectionRay(base.direct.camera)
        iRay.collider.setFromLens(base.direct.camNode, base.direct.dr.mouseX, base.direct.dr.mouseY)
        #iRay.collideWithBitMask(1)
        iRay.collideWithBitMask(BitMask32.bit(21))
        iRay.ct.traverse(base.direct.grid)

        entry = iRay.getEntry(0)
        hitPt = entry.getSurfacePoint(entry.getFromNodePath())
        iRay.collisionNodePath.removeNode()
        del iRay
        if hasattr(state, 'prevPt'):
            base.direct.camera.setPos(base.direct.camera, (state.prevPt - hitPt))
        state.prevPt = hitPt
        return Task.cont

    def HPanYZoomTask(self, state):
        # If the cam is orthogonal, don't rotate or zoom.
        if (hasattr(base.direct.cam.node(), "getLens") and
            base.direct.cam.node().getLens().__class__.__name__ == "OrthographicLens"):
            return

        if base.direct.fControl:
            moveDir = Vec3(self.coaMarker.getPos(base.direct.camera))
            # If marker is behind camera invert vector
            if moveDir[1] < 0.0:
                moveDir.assign(moveDir * -1)
            moveDir.normalize()
        else:
            moveDir = Vec3(Y_AXIS)

        if self.useMayaCamControls : # use maya controls
            moveDir.assign(moveDir * ((base.direct.dr.mouseDeltaX -1.0 * base.direct.dr.mouseDeltaY) 
                                    * state.zoomSF))
            hVal = 0.0
        else:
            moveDir.assign(moveDir * (-1.0 * base.direct.dr.mouseDeltaY *
                                        state.zoomSF))
            if base.direct.dr.mouseDeltaY > 0.0:
                moveDir.setY(moveDir[1] * 1.0)

            hVal = 0.5 * base.direct.dr.mouseDeltaX * base.direct.dr.fovH

        base.direct.camera.setPosHpr(base.direct.camera,
                                moveDir[0],
                                moveDir[1],
                                moveDir[2],
                                hVal,
                                0.0, 0.0)
        if (self.lockRoll == True):
            # flatten roll
            base.direct.camera.setR(0)

        return Task.cont

    def OrthoZoomTask(self, state):
        filmSize = base.direct.camNode.getLens().getFilmSize()
        factor = (base.direct.dr.mouseDeltaX -1.0 * base.direct.dr.mouseDeltaY) * 0.1
        x = base.direct.dr.getWidth()
        y = base.direct.dr.getHeight()
        base.direct.dr.orthoFactor -= factor
        if base.direct.dr.orthoFactor < 0:
            base.direct.dr.orthoFactor = 0.0001
        base.direct.dr.updateFilmSize(x, y)
        return Task.cont
    
    def HPPanTask(self, state):
        base.direct.camera.setHpr(base.direct.camera,
                             (0.5 * base.direct.dr.mouseDeltaX *
                              base.direct.dr.fovH),
                             (-0.5 * base.direct.dr.mouseDeltaY *
                              base.direct.dr.fovV),
                             0.0)
        return Task.cont

    def spawnMouseRotateTask(self):
        # Kill any existing tasks
        self.__stopManipulateCamera()
        if self.perspCollPlane:
            iRay = SelectionRay(base.direct.camera)
            iRay.collider.setFromLens(base.direct.camNode, 0.0, 0.0)
            iRay.collideWithBitMask(1)

            if base.direct.camera.getPos().getZ() >=0:
                iRay.ct.traverse(self.perspCollPlane)
            else:
                iRay.ct.traverse(self.perspCollPlane2)                

            if iRay.getNumEntries() > 0:
                entry = iRay.getEntry(0)
                hitPt = entry.getSurfacePoint(entry.getFromNodePath())

                # create a temp nodePath to get the position
                np = NodePath('temp')
                np.setPos(base.direct.camera, hitPt)
                self.coaMarkerPos = np.getPos()
                np.remove()
                self.coaMarker.setPos(self.coaMarkerPos)

            iRay.collisionNodePath.removeNode()
            del iRay

        # Set at markers position in render coordinates
        self.camManipRef.setPos(self.coaMarkerPos)
        self.camManipRef.setHpr(base.direct.camera, ZERO_POINT)
        t = Task.Task(self.mouseRotateTask)
        if abs(base.direct.dr.mouseX) > 0.9:
            t.constrainedDir = 'y'
        else:
            t.constrainedDir = 'x'
        self.__startManipulateCamera(task = t)

    def mouseRotateTask(self, state):
        # If the cam is orthogonal, don't rotate.
        if (hasattr(base.direct.cam.node(), "getLens") and
            base.direct.cam.node().getLens().__class__.__name__ == "OrthographicLens"):
            return
        # If moving outside of center, ignore motion perpendicular to edge
        if ((state.constrainedDir == 'y') and (abs(base.direct.dr.mouseX) > 0.9)):
            deltaX = 0
            deltaY = base.direct.dr.mouseDeltaY
        elif ((state.constrainedDir == 'x') and (abs(base.direct.dr.mouseY) > 0.9)):
            deltaX = base.direct.dr.mouseDeltaX
            deltaY = 0
        else:
            deltaX = base.direct.dr.mouseDeltaX
            deltaY = base.direct.dr.mouseDeltaY
        if base.direct.fShift:
            base.direct.camera.setHpr(base.direct.camera,
                                 (deltaX * base.direct.dr.fovH),
                                 (-deltaY * base.direct.dr.fovV),
                                 0.0)
            if (self.lockRoll == True):
                # flatten roll
                base.direct.camera.setR(0)
            self.camManipRef.setPos(self.coaMarkerPos)
            self.camManipRef.setHpr(base.direct.camera, ZERO_POINT)
        else:
            if base.direct.camera.getPos().getZ() >=0 or not self.switchDirBelowZero:
                dirX = -1
            else:
                dirX = 1

            wrt = base.direct.camera.getTransform(self.camManipRef)
            self.camManipRef.setHpr(self.camManipRef,
                                    (dirX * deltaX * 180.0),
                                    (deltaY * 180.0),
                                    0.0)

            if (self.lockRoll == True):
                # flatten roll
                self.camManipRef.setR(0)
            base.direct.camera.setTransform(self.camManipRef, wrt)
        return Task.cont

    def spawnMouseRollTask(self):
        # Kill any existing tasks
        self.__stopManipulateCamera()
        # Set at markers position in render coordinates
        self.camManipRef.setPos(self.coaMarkerPos)
        self.camManipRef.setHpr(base.direct.camera, ZERO_POINT)
        t = Task.Task(self.mouseRollTask)
        t.coaCenter = getScreenXY(self.coaMarker)
        t.lastAngle = getCrankAngle(t.coaCenter)
        # Store the camera/manipRef offset transform
        t.wrt = base.direct.camera.getTransform(self.camManipRef)
        self.__startManipulateCamera(task = t)

    def mouseRollTask(self, state):
        wrt = state.wrt
        angle = getCrankAngle(state.coaCenter)
        deltaAngle = angle - state.lastAngle
        state.lastAngle = angle
        self.camManipRef.setHpr(self.camManipRef, 0, 0, deltaAngle)
        if (self.lockRoll == True):
            # flatten roll
            self.camManipRef.setR(0)
        base.direct.camera.setTransform(self.camManipRef, wrt)
        return Task.cont

    def lockCOA(self):
        self.fLockCOA = 1
        base.direct.message('COA Lock On')

    def unlockCOA(self):
        self.fLockCOA = 0
        base.direct.message('COA Lock Off')

    def toggleCOALock(self):
        self.fLockCOA = 1 - self.fLockCOA
        if self.fLockCOA:
            base.direct.message('COA Lock On')
        else:
            base.direct.message('COA Lock Off')

    def pickNextCOA(self):
        """ Cycle through collision handler entries """
        if self.cqEntries:
            # Get next entry and rotate entries
            entry = self.cqEntries[0]
            self.cqEntries = self.cqEntries[1:] + self.cqEntries[:1]
            # Filter out object's under camera
            nodePath = entry.getIntoNodePath()
            if base.direct.camera not in nodePath.getAncestors():
                # Compute new hit point
                hitPt = entry.getSurfacePoint(entry.getFromNodePath())
                # Move coa marker to new point
                self.updateCoa(hitPt, ref = self.coaMarkerRef)
            else:
                # Remove offending entry
                self.cqEntries = self.cqEntries[:-1]
                self.pickNextCOA()

    def computeCOA(self, entry):
        coa = Point3(0)
        dr = base.direct.drList.getCurrentDr()
        if self.fLockCOA:
            # COA is locked, use existing point
            # Use existing point
            coa.assign(self.coaMarker.getPos(base.direct.camera))
            # Reset hit point count
            self.nullHitPointCount = 0
        elif entry:
            # Got a hit point (hit point is in camera coordinates)
            # Set center of action
            hitPt = entry.getSurfacePoint(entry.getFromNodePath())
            hitPtDist = Vec3(hitPt).length()
            coa.assign(hitPt)
            # Handle case of bad coa point (too close or too far)
            if ((hitPtDist < (1.1 * dr.near)) or
                (hitPtDist > dr.far)):
                # Just use existing point
                coa.assign(self.coaMarker.getPos(base.direct.camera))
            # Reset hit point count
            self.nullHitPointCount = 0
        else:
            # Increment null hit point count
            self.nullHitPointCount = (self.nullHitPointCount + 1) % 7
            # No COA lock and no intersection point
            # Use a point out in front of camera
            # Distance to point increases on multiple null hit points
            # MRM: Would be nice to be able to control this
            # At least display it
            dist = pow(10.0, self.nullHitPointCount)
            base.direct.message('COA Distance: ' + repr(dist))
            coa.set(0, dist, 0)
        # Compute COA Dist
        coaDist = Vec3(coa - ZERO_POINT).length()
        if coaDist < (1.1 * dr.near):
            coa.set(0, 100, 0)
            coaDist = 100
        # Update coa and marker
        self.updateCoa(coa, coaDist = coaDist)

    def updateCoa(self, ref2point, coaDist = None, ref = None):
        self.coa.set(ref2point[0], ref2point[1], ref2point[2])
        if not coaDist:
            coaDist = Vec3(self.coa - ZERO_POINT).length()
        # Place the marker in render space
        if ref == None:
            # KEH: use the current display region
            # ref = base.cam
            ref = base.direct.drList.getCurrentDr().cam
        self.coaMarker.setPos(ref, self.coa)
        pos = self.coaMarker.getPos()
        self.coaMarker.setPosHprScale(pos, Vec3(0), Vec3(1))
        # Resize it
        self.updateCoaMarkerSize(coaDist)
        # Record marker pos in render space
        self.coaMarkerPos.assign(self.coaMarker.getPos())

    def updateCoaMarkerSizeOnDeath(self):
        # Needed because tasks pass in state as first arg
        self.updateCoaMarkerSize()

    def updateCoaMarkerSize(self, coaDist = None):
        if not coaDist:
            coaDist = Vec3(self.coaMarker.getPos(base.direct.camera)).length()
        # Nominal size based on default 30 degree vertical FOV
        # Need to adjust size based on distance and current FOV
        sf = COA_MARKER_SF * coaDist * (base.direct.drList.getCurrentDr().fovV/30.0)
        if sf == 0.0:
            sf = 0.1
        self.coaMarker.setScale(sf)
        # Lerp color to fade out
        if self.coaMarkerColorIval:
            self.coaMarkerColorIval.finish()
        self.coaMarkerColorIval = Sequence(
            Func(self.coaMarker.unstash),
            self.coaMarker.colorInterval(1.5, Vec4(1, 0, 0, 0),
                                         startColor = Vec4(1, 0, 0, 1),
                                         blendType = 'easeInOut'),
            Func(self.coaMarker.stash)
            )
        self.coaMarkerColorIval.start()

    def homeCam(self):
        # Record undo point
        base.direct.pushUndo([base.direct.camera])
        base.direct.camera.reparentTo(render)
        base.direct.camera.clearMat()
        # Resize coa marker
        self.updateCoaMarkerSize()

    def uprightCam(self):
        self.__stopManipulateCamera()
        # Record undo point
        base.direct.pushUndo([base.direct.camera])
        # Pitch camera till upright
        currH = base.direct.camera.getH()
        ival = base.direct.camera.hprInterval(CAM_MOVE_DURATION,
                                              (currH, 0, 0),
                                              other = render,
                                              blendType = 'easeInOut',
                                              name = 'manipulateCamera')
        self.__startManipulateCamera(ival = ival)

    def orbitUprightCam(self):
        self.__stopManipulateCamera()
        # Record undo point
        base.direct.pushUndo([base.direct.camera])
        # Transform camera z axis to render space
        mCam2Render = Mat4(Mat4.identMat()) # [gjeon] fixed to give required argument
        mCam2Render.assign(base.direct.camera.getMat(render))
        zAxis = Vec3(mCam2Render.xformVec(Z_AXIS))
        zAxis.normalize()
        # Compute rotation angle needed to upright cam
        orbitAngle = rad2Deg(math.acos(CLAMP(zAxis.dot(Z_AXIS), -1, 1)))
        # Check angle
        if orbitAngle < 0.1:
            # Already upright
            return
        # Compute orthogonal axis of rotation
        rotAxis = Vec3(zAxis.cross(Z_AXIS))
        rotAxis.normalize()
        # Find angle between rot Axis and render X_AXIS
        rotAngle = rad2Deg(math.acos(CLAMP(rotAxis.dot(X_AXIS), -1, 1)))
        # Determine sign or rotation angle
        if rotAxis[1] < 0:
            rotAngle *= -1
        # Position ref CS at coa marker with xaxis aligned with rot axis
        self.camManipRef.setPos(self.coaMarker, Vec3(0))
        self.camManipRef.setHpr(render, rotAngle, 0, 0)
        # Reparent Cam to ref Coordinate system
        parent = base.direct.camera.getParent()
        base.direct.camera.wrtReparentTo(self.camManipRef)
        # Rotate ref CS to final orientation
        ival = self.camManipRef.hprInterval(CAM_MOVE_DURATION,
                                            (rotAngle, orbitAngle, 0),
                                            other = render,
                                            blendType = 'easeInOut')
        ival = Sequence(ival, Func(self.reparentCam, parent),
                        name = 'manipulateCamera')
        self.__startManipulateCamera(ival = ival)

    def centerCam(self):
        self.centerCamIn(1.0)

    def centerCamNow(self):
        self.centerCamIn(0.)

    def centerCamIn(self, t):
        self.__stopManipulateCamera()
        # Record undo point
        base.direct.pushUndo([base.direct.camera])
        # Determine marker location
        markerToCam = self.coaMarker.getPos(base.direct.camera)
        dist = Vec3(markerToCam - ZERO_POINT).length()
        scaledCenterVec = Y_AXIS * dist
        delta = markerToCam - scaledCenterVec
        self.camManipRef.setPosHpr(base.direct.camera, Point3(0), Point3(0))
        ival = base.direct.camera.posInterval(CAM_MOVE_DURATION,
                                              Point3(delta),
                                              other = self.camManipRef,
                                              blendType = 'easeInOut')
        ival = Sequence(ival, Func(self.updateCoaMarkerSizeOnDeath),
                        name = 'manipulateCamera')
        self.__startManipulateCamera(ival = ival)


    def zoomCam(self, zoomFactor, t):
        self.__stopManipulateCamera()
        # Record undo point
        base.direct.pushUndo([base.direct.camera])
        # Find a point zoom factor times the current separation
        # of the widget and cam
        zoomPtToCam = self.coaMarker.getPos(base.direct.camera) * zoomFactor
        # Put a target nodePath there
        self.camManipRef.setPos(base.direct.camera, zoomPtToCam)
        # Move to that point
        ival = base.direct.camera.posInterval(CAM_MOVE_DURATION,
                                              ZERO_POINT,
                                              other = self.camManipRef,
                                              blendType = 'easeInOut')
        ival = Sequence(ival, Func(self.updateCoaMarkerSizeOnDeath),
                        name = 'manipulateCamera')
        self.__startManipulateCamera(ival = ival)

    def spawnMoveToView(self, view):
        # Kill any existing tasks
        self.__stopManipulateCamera()
        # Record undo point
        base.direct.pushUndo([base.direct.camera])
        # Calc hprOffset
        hprOffset = VBase3()
        if view == 8:
            # Try the next roll angle
            self.orthoViewRoll = (self.orthoViewRoll + 90.0) % 360.0
            # but use the last view
            view = self.lastView
        else:
            self.orthoViewRoll = 0.0
        # Adjust offset based on specified view
        if view == 1:
            hprOffset.set(180., 0., 0.)
        elif view == 2:
            hprOffset.set(0., 0., 0.)
        elif view == 3:
            hprOffset.set(90., 0., 0.)
        elif view == 4:
            hprOffset.set(-90., 0., 0.)
        elif view == 5:
            hprOffset.set(0., -90., 0.)
        elif view == 6:
            hprOffset.set(0., 90., 0.)
        elif view == 7:
            hprOffset.set(135., -35.264, 0.)
        # Position target
        self.camManipRef.setPosHpr(self.coaMarker, ZERO_VEC,
                                   hprOffset)
        # Scale center vec by current distance to target
        offsetDistance = Vec3(base.direct.camera.getPos(self.camManipRef) -
                              ZERO_POINT).length()
        scaledCenterVec = Y_AXIS * (-1.0 * offsetDistance)
        # Now put the camManipRef at that point
        self.camManipRef.setPosHpr(self.camManipRef,
                                   scaledCenterVec,
                                   ZERO_VEC)
        # Record view for next time around
        self.lastView = view
        ival = base.direct.camera.posHprInterval(CAM_MOVE_DURATION,
                                                 pos = ZERO_POINT,
                                                 hpr = VBase3(0, 0, self.orthoViewRoll),
                                                 other = self.camManipRef,
                                                 blendType = 'easeInOut')
        ival = Sequence(ival, Func(self.updateCoaMarkerSizeOnDeath),
                        name = 'manipulateCamera')
        self.__startManipulateCamera(ival = ival)


    def swingCamAboutWidget(self, degrees, t):
        # Remove existing camera manipulation task
        self.__stopManipulateCamera()

        # Record undo point
        base.direct.pushUndo([base.direct.camera])

        # Coincident with widget
        self.camManipRef.setPos(self.coaMarker, ZERO_POINT)
        # But aligned with render space
        self.camManipRef.setHpr(ZERO_POINT)

        parent = base.direct.camera.getParent()
        base.direct.camera.wrtReparentTo(self.camManipRef)

        ival = self.camManipRef.hprInterval(CAM_MOVE_DURATION,
                                            VBase3(degrees, 0, 0),
                                            blendType = 'easeInOut')
        ival = Sequence(ival, Func(self.reparentCam, parent),
                        name = 'manipulateCamera')
        self.__startManipulateCamera(ival = ival)

    def reparentCam(self, parent):
        base.direct.camera.wrtReparentTo(parent)
        self.updateCoaMarkerSize()

    def fitOnWidget(self, nodePath = 'None Given'):
        # Fit the node on the screen
        # stop any ongoing tasks
        self.__stopManipulateCamera()
        # How big is the node?
        nodeScale = base.direct.widget.scalingNode.getScale(render)
        maxScale = max(nodeScale[0], nodeScale[1], nodeScale[2])
        maxDim = min(base.direct.dr.nearWidth, base.direct.dr.nearHeight)

        # At what distance does the object fill 30% of the screen?
        # Assuming radius of 1 on widget
        camY = base.direct.dr.near * (2.0 * maxScale)/(0.3 * maxDim)

        # What is the vector through the center of the screen?
        centerVec = Y_AXIS * camY

        # Where is the node relative to the viewpoint
        vWidget2Camera = base.direct.widget.getPos(base.direct.camera)

        # How far do you move the camera to be this distance from the node?
        deltaMove = vWidget2Camera - centerVec
        
        # Move a target there
        try:
            self.camManipRef.setPos(base.direct.camera, deltaMove)
        except Exception:
            self.notify.debug

        parent = base.direct.camera.getParent()
        base.direct.camera.wrtReparentTo(self.camManipRef)
        ival = base.direct.camera.posInterval(CAM_MOVE_DURATION,
                                              Point3(0, 0, 0),
                                              blendType = 'easeInOut')
        ival = Sequence(ival, Func(self.reparentCam, parent),
                        name = 'manipulateCamera')
        self.__startManipulateCamera(ival = ival)


    def moveToFit(self):
        # How big is the active widget?
        widgetScale = base.direct.widget.scalingNode.getScale(render)
        maxScale = max(widgetScale[0], widgetScale[1], widgetScale[2])
        # At what distance does the widget fill 50% of the screen?
        camY = ((2 * base.direct.dr.near * (1.5 * maxScale)) /
                min(base.direct.dr.nearWidth, base.direct.dr.nearHeight))
        # Find a point this distance along the Y axis
        # MRM: This needs to be generalized to support non uniform frusta
        centerVec = Y_AXIS * camY
        # Before moving, record the relationship between the selected nodes
        # and the widget, so that this can be maintained
        base.direct.selected.getWrtAll()
        # Push state onto undo stack
        base.direct.pushUndo(base.direct.selected)
        # Remove the task to keep the widget attached to the object
        taskMgr.remove('followSelectedNodePath')
        # Spawn a task to keep the selected objects with the widget
        taskMgr.add(self.stickToWidgetTask, 'stickToWidget')
        # Spawn a task to move the widget
        ival = base.direct.widget.posInterval(CAM_MOVE_DURATION,
                                              Point3(centerVec),
                                              other = base.direct.camera,
                                              blendType = 'easeInOut')
        ival = Sequence(ival, Func(lambda: taskMgr.remove('stickToWidget')),
                        name = 'moveToFit')
        ival.start()

    def stickToWidgetTask(self, state):
        # Move the objects with the widget
        base.direct.selected.moveWrtWidgetAll()
        # Continue
        return Task.cont

    def enableMouseFly(self, fKeyEvents = 1):
        # disable C++ fly interface
        base.disableMouse()
        # Enable events
        for event in self.actionEvents:
            self.accept(event[0], event[1], extraArgs = event[2:])
        if fKeyEvents:
            for event in self.keyEvents:
                self.accept(event[0], event[1], extraArgs = event[2:])
        # Show marker
        self.coaMarker.reparentTo(base.direct.group)

    def disableMouseFly(self):
        # Hide the marker
        self.coaMarker.reparentTo(hidden)
        # Ignore events
        for event in self.actionEvents:
            self.ignore(event[0])
        for event in self.keyEvents:
            self.ignore(event[0])
        # Kill tasks
        self.removeManipulateCameraTask()
        taskMgr.remove('stickToWidget')
        base.enableMouse()

    def removeManipulateCameraTask(self):
        self.__stopManipulateCamera()

