Browse Source

*** empty log message ***

Mark Mine 25 years ago
parent
commit
9b44311ec2

+ 448 - 449
direct/src/directutil/DirectCameraControl.py

@@ -1,449 +1,448 @@
-from PandaObject import *
-
-CAM_MOVE_DURATION = 1.0
-Y_AXIS = Vec3(0,1,0)
-
-class DirectCameraControl(PandaObject):
-    def __init__(self, direct):
-        # Create the grid
-        self.direct = direct
-        self.chan = direct.chan
-        self.camera = self.chan.camera
-	self.orthoViewRoll = 0.0
-	self.lastView = 0
-        self.coa = Point3(0)
-        self.coaMarker = loader.loadModel('models/misc/sphere')
-        self.coaMarker.setColor(1,0,0)
-        self.coaMarkerPos = Point3(0)
-	self.relNodePath = render.attachNewNode(NamedNode('targetNode'))
-        self.zeroBaseVec = VBase3(0)
-        self.zeroVector = Vec3(0)
-        self.centerVec = Vec3(0, 1, 0)
-        self.zeroPoint = Point3(0)
-
-    def mouseFlyStart(self, chan):
-	# Record starting mouse positions
-	self.initMouseX = chan.mouseX
-	self.initMouseY = chan.mouseY
-	# Where are we in the channel?
-        if ((abs(self.initMouseX) < 0.9) & (abs(self.initMouseY) < 0.9)):
-            # MOUSE IS IN CENTRAL REGION
-            # Hide the marker for this kind of motion
-            self.coaMarker.hide()
-            # See if the shift key is pressed
-            if (self.direct.fShift):
-                # If shift key is pressed, just perform horiz and vert pan:
-                self.spawnHPPan()
-            else:
-                # Otherwise, check for a hit point based on
-                # current mouse position
-                # And then spawn task to determine mouse mode
-                numEntries = self.direct.iRay.pickGeom(
-                    render,chan.mouseX,chan.mouseY)
-                # Filter out hidden nodes from entry list
-                indexList = []
-                for i in range(0,numEntries):
-                    entry = self.direct.iRay.cq.getEntry(i)
-                    node = entry.getIntoNode()
-                    if node.isHidden():
-                        pass
-                    else:
-                        # Not one of the widgets, use it
-                        indexList.append(i)
-                coa = Point3(0)
-                if(indexList):
-                    # Start off with first point
-                    minPt = indexList[0]
-                    # Find hit point in camera's space
-                    hitPt = self.direct.iRay.camToHitPt(minPt)
-                    coa.set(hitPt[0],hitPt[1],hitPt[2])
-                    coaDist = Vec3(coa - self.zeroPoint).length()
-                    # Check other intersection points, sorting them
-                    # TBD: Use TBS C++ function to do this
-                    if len(indexList) > 1:
-                        for i in range(1,len(indexList)):
-                            entryNum = indexList[i]
-                            hitPt = self.direct.iRay.camToHitPt(entryNum)
-                            dist = Vec3(hitPt - self.zeroPoint).length()
-                            if (dist < coaDist):
-                                coaDist = dist
-                                coa.set(hitPt[0],hitPt[1],hitPt[2])
-                                minPt = i
-
-                    # Handle case of bad coa point (too close or too far)
-                    if ((coaDist < (1.1 * self.chan.near)) |
-                        (coaDist > self.chan.far)):
-                        # Put it out in front of the camera
-                        coa.set(0,100,0)
-                        coaDist = 100
-                else:
-                    # If no intersection point:
-                    # Put coa out in front of the camera
-                    coa.set(0,100,0)
-                    coaDist = 100
-
-                # Update coa and marker
-                self.updateCoa(coa, coaDist)
-                # Now spawn task to determine mouse fly mode
-                self.determineMouseFlyMode()
-            # END MOUSE IN CENTRAL REGION
-        else:
-            # Mouse is in outer frame, spawn mouseRotateTask
-            self.spawnMouseRotateTask()
-
-    def mouseFlyStop(self):
-	taskMgr.removeTasksNamed('determineMouseFlyMode')
-	taskMgr.removeTasksNamed('manipulateCamera')
-        # Show the marker
-        self.coaMarker.show()
-        # Resize it
-        self.updateCoaMarkerSize()
-
-    def determineMouseFlyMode(self):
-        # Otherwise, determine mouse fly mode
-        t = Task.Task(self.determineMouseFlyModeTask)
-        taskMgr.spawnTaskNamed(t, 'determineMouseFlyMode')
-
-    def determineMouseFlyModeTask(self, state):
-        deltaX = self.chan.mouseX - self.initMouseX
-        deltaY = self.chan.mouseY - self.initMouseY
-        if ((abs(deltaX) < 0.1) & (abs(deltaY) < 0.1)):
-            return Task.cont
-        else:
-            if (abs(deltaY) > 0.1):
-                self.spawnHPanYZoom()
-            else:
-                self.spawnXZTranslate()
-            return Task.done
-
-    def updateCoa(self, cam2point, coaDist = None):
-        self.coa.set(cam2point[0], cam2point[1], cam2point[2])
-        if coaDist:
-            self.coaDist = coaDist
-        else:
-            self.coaDist = Vec3(self.coa - self.zeroPoint).length()
-        # Place the marker in render space
-        self.coaMarker.setPos(self.camera,self.coa)
-        # Resize it
-        self.updateCoaMarkerSize(coaDist)
-        # Record marker pos in render space
-        self.coaMarkerPos.assign(self.coaMarker.getPos())
-
-    def updateCoaMarkerSize(self, coaDist = None):
-        if not coaDist:
-            coaDist = Vec3(self.coaMarker.getPos( self.chan.camera )).length()
-        self.coaMarker.setScale(0.01 * coaDist *
-                                math.tan(deg2Rad(self.chan.fovV)))
-
-    def homeCam(self, chan):
-        chan.camera.setMat(Mat4.identMat())
-
-    def uprightCam(self, chan):
-	taskMgr.removeTasksNamed('manipulateCamera')
-        currH = chan.camera.getH()
-	chan.camera.lerpHpr(currH, 0, 0,
-                            CAM_MOVE_DURATION,
-                            other = render,
-                            blendType = 'easeInOut',
-                            task = 'manipulateCamera')
-
-    def centerCam(self, chan):
-        # Chan is a display region context
-	self.centerCamIn(chan, 1.0)
-        
-    def centerCamNow(self, chan):
-        self.centerCamIn(chan, 0.)
-
-    def centerCamIn(self, chan, t):
-        # Chan is a display region context
-	taskMgr.removeTasksNamed('manipulateCamera')
-        markerToCam = self.coaMarker.getPos( chan.camera )
-	dist = Vec3(markerToCam - self.zeroPoint).length()
-	scaledCenterVec = self.centerVec * dist
-	delta = markerToCam - scaledCenterVec
-	self.relNodePath.setPosHpr(chan.camera, Point3(0), Point3(0))
-	chan.camera.lerpPos(Point3(delta),
-                            CAM_MOVE_DURATION,
-                            other = self.relNodePath,
-                            blendType = 'easeInOut',
-                            task = 'manipulateCamera')
-
-    def zoomCam(self, chan, zoomFactor, t):
-	taskMgr.removeTasksNamed('manipulateCamera')
-	# Find a point zoom factor times the current separation
-        # of the widget and cam
-        zoomPtToCam = self.coaMarker.getPos(chan.camera) * zoomFactor
-	# Put a target nodePath there
-	self.relNodePath.setPos(chan.camera, zoomPtToCam)
-	# Move to that point
-	chan.camera.lerpPos(self.zeroPoint,
-                            CAM_MOVE_DURATION,
-                            other = self.relNodePath,
-                            blendType = 'easeInOut',
-                            task = 'manipulateCamera')
-        
-    def SpawnMoveToView(self, chan, view):
-        # Kill any existing tasks
-        taskMgr.removeTasksNamed('manipulateCamera')
-        # 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.relNodePath.setPosHpr(self.coaMarker, self.zeroBaseVec,
-                                   hprOffset)
-        # Scale center vec by current distance to target
-        offsetDistance = Vec3(chan.camera.getPos(self.relNodePath) - 
-                              self.zeroPoint).length()
-        scaledCenterVec = self.centerVec * (-1.0 * offsetDistance)
-        # Now put the relNodePath at that point
-        self.relNodePath.setPosHpr(self.relNodePath,
-                                   scaledCenterVec,
-                                   self.zeroBaseVec)
-        # Record view for next time around
-        self.lastView = view
-        chan.camera.lerpPosHpr(self.zeroPoint,
-                               VBase3(0,0,self.orthoViewRoll),
-                               CAM_MOVE_DURATION,
-                               other = self.relNodePath,
-                               blendType = 'easeInOut',
-                               task = 'manipulateCamera')
-
-        
-    def swingCamAboutWidget(self, chan, degrees, t):
-        # Remove existing camera manipulation task
-	taskMgr.removeTasksNamed('manipulateCamera')
-	
-	# Coincident with widget
-        self.relNodePath.setPos(self.coaMarker, self.zeroPoint)
-	# But aligned with render space
-	self.relNodePath.setHpr(self.zeroPoint)
-
-	parent = self.camera.getParent()
-	self.camera.wrtReparentTo(self.relNodePath)
-
-	manipTask = self.relNodePath.lerpHpr(VBase3(degrees,0,0),
-                                             CAM_MOVE_DURATION,
-                                             blendType = 'easeInOut',
-                                             task = 'manipulateCamera')
-        # Upon death, reparent Cam to parent
-        manipTask.parent = parent
-        manipTask.uponDeath = self.reparentCam
-
-    def reparentCam(self, state):
-        self.camera.wrtReparentTo(state.parent)
-
-    def spawnHPanYZoom(self):
-        # Kill any existing tasks
-	taskMgr.removeTasksNamed('manipulateCamera')
-        # hide the marker
-        self.coaMarker.hide()
-        # Negate vec to give it the correct sense for mouse motion below
-        targetVector = self.coa * -1
-        t = Task.Task(self.HPanYZoomTask)
-        t.targetVector = targetVector
-        taskMgr.spawnTaskNamed(t, 'manipulateCamera')
-
-    def HPanYZoomTask(self,state):
-        targetVector = state.targetVector
-        distToMove = targetVector * self.chan.mouseDeltaY
-        self.camera.setPosHpr(self.camera,
-                              distToMove[0],
-                              distToMove[1],
-                              distToMove[2],
-                              (0.5 * self.chan.mouseDeltaX *
-                               self.chan.fovH),
-                              0.0, 0.0)
-        return Task.cont
-
-
-    def spawnXZTranslateOrHPPan(self):
-        # Kill any existing tasks
-	taskMgr.removeTasksNamed('manipulateCamera')
-        # Hide the marker
-        self.coaMarker.hide()
-        t = Task.Task(self.XZTranslateOrHPPanTask)
-        t.scaleFactor = (self.coaDist / self.chan.near)
-        taskMgr.spawnTaskNamed(t, 'manipulateCamera')
-
-    def XZTranslateOrHPPanTask(self, state):
-        if self.direct.fShift:
-            self.camera.setHpr(self.camera,
-                               (0.5 * self.chan.mouseDeltaX *
-                                self.chan.fovH),
-                               (-0.5 * self.chan.mouseDeltaY *
-                                self.chan.fovV),
-                               0.0)
-        else:
-            self.camera.setPos(self.camera,
-                               (-0.5 * self.chan.mouseDeltaX *
-                                self.chan.nearWidth *
-                                state.scaleFactor),
-                               0.0,
-                               (-0.5 * self.chan.mouseDeltaY *
-                                self.chan.nearHeight *
-                                state.scaleFactor))
-        return Task.cont
-
-    def spawnXZTranslate(self):
-        # Kill any existing tasks
-	taskMgr.removeTasksNamed('manipulateCamera')
-        # Hide the marker
-        self.coaMarker.hide()
-        t = Task.Task(self.XZTranslateTask)
-        t.scaleFactor = (self.coaDist / self.chan.near)
-        taskMgr.spawnTaskNamed(t, 'manipulateCamera')
-
-    def XZTranslateTask(self,state):
-        self.camera.setPos(self.camera,
-                           (-0.5 * self.chan.mouseDeltaX *
-                            self.chan.nearWidth *
-                            state.scaleFactor),
-                           0.0,
-                           (-0.5 * self.chan.mouseDeltaY *
-                            self.chan.nearHeight *
-                            state.scaleFactor))
-        return Task.cont
-
-    def spawnMouseRotateTask(self):
-        # Kill any existing tasks
-	taskMgr.removeTasksNamed('manipulateCamera')
-        # Set at markers position in render coordinates
-	self.relNodePath.setPos(self.coaMarkerPos)
-	self.relNodePath.setHpr(self.camera, self.zeroPoint)
-        t = Task.Task(self.mouseRotateTask)
-	t.wrtMat = self.camera.getMat( self.relNodePath )
-        taskMgr.spawnTaskNamed(t, 'manipulateCamera')
-
-    def mouseRotateTask(self, state):
-        wrtMat = state.wrtMat
-        self.relNodePath.setHpr(self.relNodePath,
-                                (-0.5 * self.chan.mouseDeltaX * 180.0),
-                                (0.5 * self.chan.mouseDeltaY * 180.0),
-                                0.0)
-        self.camera.setMat(self.relNodePath, wrtMat)
-        return Task.cont
-
-    def spawnHPPan(self):
-        # Kill any existing tasks
-	taskMgr.removeTasksNamed('manipulateCamera')
-        # Hide the marker
-        self.coaMarker.hide()
-        t = Task.Task(self.HPPanTask)
-        taskMgr.spawnTaskNamed(t, 'manipulateCamera')
-
-    def HPPanTask(self, state):
-        self.camera.setHpr(self.camera,
-                           (0.5 * self.chan.mouseDeltaX *
-                            self.chan.fovH),
-                           (-0.5 * self.chan.mouseDeltaY *
-                            self.chan.fovV),
-                           0.0)
-        return Task.cont
-
-    def fitOnWidget(self):
-        # Fit the node on the screen
-        
-        # stop any ongoing tasks
-        taskMgr.removeTasksNamed('manipulateCamera')
-
-        # How big is the node?
-        nodeScale = self.direct.widget.scalingNode.getScale(render)
-        maxScale = max(nodeScale[0],nodeScale[1],nodeScale[2])
-        maxDim = min(self.chan.nearWidth, self.chan.nearHeight)
-
-        # At what distance does the object fill 30% of the screen?
-        # Assuming radius of 1 on widget
-        camY = self.chan.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 = self.direct.widget.getPos(self.camera)
-    
-        # How far do you move the camera to be this distance from the node?
-        deltaMove = vWidget2Camera - centerVec
-    
-        # Move a target there
-        self.relNodePath.setPos(self.camera, deltaMove)
-
-	parent = self.camera.getParent()
-	self.camera.wrtReparentTo(self.relNodePath)
-	fitTask = self.camera.lerpPos(Point3(0,0,0),
-                                      CAM_MOVE_DURATION,
-                                      blendType = 'easeInOut',
-                                      task = 'manipulateCamera')
-        # Upon death, reparent Cam to parent
-        fitTask.parent = parent
-        fitTask.uponDeath = self.reparentCam                                
-
-    def enableMouseFly(self):
-	self.enableMouseInteraction()
-	self.enableHotKeys()
-        self.coaMarker.reparentTo(render)
-
-    def enableMouseInteraction(self):
-	# disable C++ fly interface
-	base.disableMouse()
-	# Accept middle mouse events
-	self.accept('handleMouse2', self.mouseFlyStart, [self.chan])
-	self.accept('handleMouse2Up', self.mouseFlyStop)
-
-    def enableHotKeys(self):
-        t = CAM_MOVE_DURATION
-	self.accept('u', self.uprightCam, [self.chan])
-	self.accept('c', self.centerCamIn, [self.chan, 0.5])
-	self.accept('h', self.homeCam, [self.chan])
-        self.accept('f', self.fitOnWidget)
-        for i in range(1,9):
-            self.accept(`i`, self.SpawnMoveToView, [self.chan, i])
-	self.accept('9', self.swingCamAboutWidget, [self.chan, -90.0, t])
-	self.accept('0', self.swingCamAboutWidget, [self.chan,  90.0, t])
-	self.accept('`', self.removeManipulateCameraTask)
-	self.accept('=', self.zoomCam, [self.chan, 0.5, t])
-	self.accept('+', self.zoomCam, [self.chan, 0.5, t])
-	self.accept('-', self.zoomCam, [self.chan, -2.0, t])
-	self.accept('_', self.zoomCam, [self.chan, -2.0, t])
-
-    def disableMouseFly(self):
-        # Hide the marker
-        self.coaMarker.reparentTo(hidden)
-	# Ignore middle mouse events
-	self.ignore('handleMouse2')
-	self.ignore('handleMouse2Up')
-	self.ignore('u')
-	self.ignore('c')
-	self.ignore('h')
-        self.ignore('f')
-        for i in range(0,10):
-            self.ignore(`i`)
-	self.ignore('=')
-	self.ignore('+')
-	self.ignore('-')
-	self.ignore('_')
-        self.ignore('`')
-
-    def removeManipulateCameraTask(self):
-        taskMgr.removeTasksNamed('manipulateCamera')
-
+from PandaObject import *
+
+CAM_MOVE_DURATION = 1.0
+Y_AXIS = Vec3(0,1,0)
+
+class DirectCameraControl(PandaObject):
+    def __init__(self):
+        # Create the grid
+        self.chan = direct.chan
+        self.camera = self.chan.camera
+	self.orthoViewRoll = 0.0
+	self.lastView = 0
+        self.coa = Point3(0)
+        self.coaMarker = loader.loadModel('models/misc/sphere')
+        self.coaMarker.setColor(1,0,0)
+        self.coaMarkerPos = Point3(0)
+	self.relNodePath = render.attachNewNode(NamedNode('targetNode'))
+        self.zeroBaseVec = VBase3(0)
+        self.zeroVector = Vec3(0)
+        self.centerVec = Vec3(0, 1, 0)
+        self.zeroPoint = Point3(0)
+
+    def mouseFlyStart(self, chan):
+	# Record starting mouse positions
+	self.initMouseX = chan.mouseX
+	self.initMouseY = chan.mouseY
+	# Where are we in the channel?
+        if ((abs(self.initMouseX) < 0.9) & (abs(self.initMouseY) < 0.9)):
+            # MOUSE IS IN CENTRAL REGION
+            # Hide the marker for this kind of motion
+            self.coaMarker.hide()
+            # See if the shift key is pressed
+            if (direct.fShift):
+                # If shift key is pressed, just perform horiz and vert pan:
+                self.spawnHPPan()
+            else:
+                # Otherwise, check for a hit point based on
+                # current mouse position
+                # And then spawn task to determine mouse mode
+                numEntries = direct.iRay.pickGeom(
+                    render,chan.mouseX,chan.mouseY)
+                # Filter out hidden nodes from entry list
+                indexList = []
+                for i in range(0,numEntries):
+                    entry = direct.iRay.cq.getEntry(i)
+                    node = entry.getIntoNode()
+                    if node.isHidden():
+                        pass
+                    else:
+                        # Not one of the widgets, use it
+                        indexList.append(i)
+                coa = Point3(0)
+                if(indexList):
+                    # Start off with first point
+                    minPt = indexList[0]
+                    # Find hit point in camera's space
+                    hitPt = direct.iRay.camToHitPt(minPt)
+                    coa.set(hitPt[0],hitPt[1],hitPt[2])
+                    coaDist = Vec3(coa - self.zeroPoint).length()
+                    # Check other intersection points, sorting them
+                    # TBD: Use TBS C++ function to do this
+                    if len(indexList) > 1:
+                        for i in range(1,len(indexList)):
+                            entryNum = indexList[i]
+                            hitPt = direct.iRay.camToHitPt(entryNum)
+                            dist = Vec3(hitPt - self.zeroPoint).length()
+                            if (dist < coaDist):
+                                coaDist = dist
+                                coa.set(hitPt[0],hitPt[1],hitPt[2])
+                                minPt = i
+
+                    # Handle case of bad coa point (too close or too far)
+                    if ((coaDist < (1.1 * self.chan.near)) |
+                        (coaDist > self.chan.far)):
+                        # Put it out in front of the camera
+                        coa.set(0,100,0)
+                        coaDist = 100
+                else:
+                    # If no intersection point:
+                    # Put coa out in front of the camera
+                    coa.set(0,100,0)
+                    coaDist = 100
+
+                # Update coa and marker
+                self.updateCoa(coa, coaDist)
+                # Now spawn task to determine mouse fly mode
+                self.determineMouseFlyMode()
+            # END MOUSE IN CENTRAL REGION
+        else:
+            # Mouse is in outer frame, spawn mouseRotateTask
+            self.spawnMouseRotateTask()
+
+    def mouseFlyStop(self):
+	taskMgr.removeTasksNamed('determineMouseFlyMode')
+	taskMgr.removeTasksNamed('manipulateCamera')
+        # Show the marker
+        self.coaMarker.show()
+        # Resize it
+        self.updateCoaMarkerSize()
+
+    def determineMouseFlyMode(self):
+        # Otherwise, determine mouse fly mode
+        t = Task.Task(self.determineMouseFlyModeTask)
+        taskMgr.spawnTaskNamed(t, 'determineMouseFlyMode')
+
+    def determineMouseFlyModeTask(self, state):
+        deltaX = self.chan.mouseX - self.initMouseX
+        deltaY = self.chan.mouseY - self.initMouseY
+        if ((abs(deltaX) < 0.1) & (abs(deltaY) < 0.1)):
+            return Task.cont
+        else:
+            if (abs(deltaY) > 0.1):
+                self.spawnHPanYZoom()
+            else:
+                self.spawnXZTranslate()
+            return Task.done
+
+    def updateCoa(self, cam2point, coaDist = None):
+        self.coa.set(cam2point[0], cam2point[1], cam2point[2])
+        if coaDist:
+            self.coaDist = coaDist
+        else:
+            self.coaDist = Vec3(self.coa - self.zeroPoint).length()
+        # Place the marker in render space
+        self.coaMarker.setPos(self.camera,self.coa)
+        # Resize it
+        self.updateCoaMarkerSize(coaDist)
+        # Record marker pos in render space
+        self.coaMarkerPos.assign(self.coaMarker.getPos())
+
+    def updateCoaMarkerSize(self, coaDist = None):
+        if not coaDist:
+            coaDist = Vec3(self.coaMarker.getPos( self.chan.camera )).length()
+        self.coaMarker.setScale(0.01 * coaDist *
+                                math.tan(deg2Rad(self.chan.fovV)))
+
+    def homeCam(self, chan):
+        chan.camera.setMat(Mat4.identMat())
+
+    def uprightCam(self, chan):
+	taskMgr.removeTasksNamed('manipulateCamera')
+        currH = chan.camera.getH()
+	chan.camera.lerpHpr(currH, 0, 0,
+                            CAM_MOVE_DURATION,
+                            other = render,
+                            blendType = 'easeInOut',
+                            task = 'manipulateCamera')
+
+    def centerCam(self, chan):
+        # Chan is a display region context
+	self.centerCamIn(chan, 1.0)
+        
+    def centerCamNow(self, chan):
+        self.centerCamIn(chan, 0.)
+
+    def centerCamIn(self, chan, t):
+        # Chan is a display region context
+	taskMgr.removeTasksNamed('manipulateCamera')
+        markerToCam = self.coaMarker.getPos( chan.camera )
+	dist = Vec3(markerToCam - self.zeroPoint).length()
+	scaledCenterVec = self.centerVec * dist
+	delta = markerToCam - scaledCenterVec
+	self.relNodePath.setPosHpr(chan.camera, Point3(0), Point3(0))
+	chan.camera.lerpPos(Point3(delta),
+                            CAM_MOVE_DURATION,
+                            other = self.relNodePath,
+                            blendType = 'easeInOut',
+                            task = 'manipulateCamera')
+
+    def zoomCam(self, chan, zoomFactor, t):
+	taskMgr.removeTasksNamed('manipulateCamera')
+	# Find a point zoom factor times the current separation
+        # of the widget and cam
+        zoomPtToCam = self.coaMarker.getPos(chan.camera) * zoomFactor
+	# Put a target nodePath there
+	self.relNodePath.setPos(chan.camera, zoomPtToCam)
+	# Move to that point
+	chan.camera.lerpPos(self.zeroPoint,
+                            CAM_MOVE_DURATION,
+                            other = self.relNodePath,
+                            blendType = 'easeInOut',
+                            task = 'manipulateCamera')
+        
+    def SpawnMoveToView(self, chan, view):
+        # Kill any existing tasks
+        taskMgr.removeTasksNamed('manipulateCamera')
+        # 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.relNodePath.setPosHpr(self.coaMarker, self.zeroBaseVec,
+                                   hprOffset)
+        # Scale center vec by current distance to target
+        offsetDistance = Vec3(chan.camera.getPos(self.relNodePath) - 
+                              self.zeroPoint).length()
+        scaledCenterVec = self.centerVec * (-1.0 * offsetDistance)
+        # Now put the relNodePath at that point
+        self.relNodePath.setPosHpr(self.relNodePath,
+                                   scaledCenterVec,
+                                   self.zeroBaseVec)
+        # Record view for next time around
+        self.lastView = view
+        chan.camera.lerpPosHpr(self.zeroPoint,
+                               VBase3(0,0,self.orthoViewRoll),
+                               CAM_MOVE_DURATION,
+                               other = self.relNodePath,
+                               blendType = 'easeInOut',
+                               task = 'manipulateCamera')
+
+        
+    def swingCamAboutWidget(self, chan, degrees, t):
+        # Remove existing camera manipulation task
+	taskMgr.removeTasksNamed('manipulateCamera')
+	
+	# Coincident with widget
+        self.relNodePath.setPos(self.coaMarker, self.zeroPoint)
+	# But aligned with render space
+	self.relNodePath.setHpr(self.zeroPoint)
+
+	parent = self.camera.getParent()
+	self.camera.wrtReparentTo(self.relNodePath)
+
+	manipTask = self.relNodePath.lerpHpr(VBase3(degrees,0,0),
+                                             CAM_MOVE_DURATION,
+                                             blendType = 'easeInOut',
+                                             task = 'manipulateCamera')
+        # Upon death, reparent Cam to parent
+        manipTask.parent = parent
+        manipTask.uponDeath = self.reparentCam
+
+    def reparentCam(self, state):
+        self.camera.wrtReparentTo(state.parent)
+
+    def spawnHPanYZoom(self):
+        # Kill any existing tasks
+	taskMgr.removeTasksNamed('manipulateCamera')
+        # hide the marker
+        self.coaMarker.hide()
+        # Negate vec to give it the correct sense for mouse motion below
+        targetVector = self.coa * -1
+        t = Task.Task(self.HPanYZoomTask)
+        t.targetVector = targetVector
+        taskMgr.spawnTaskNamed(t, 'manipulateCamera')
+
+    def HPanYZoomTask(self,state):
+        targetVector = state.targetVector
+        distToMove = targetVector * self.chan.mouseDeltaY
+        self.camera.setPosHpr(self.camera,
+                              distToMove[0],
+                              distToMove[1],
+                              distToMove[2],
+                              (0.5 * self.chan.mouseDeltaX *
+                               self.chan.fovH),
+                              0.0, 0.0)
+        return Task.cont
+
+
+    def spawnXZTranslateOrHPPan(self):
+        # Kill any existing tasks
+	taskMgr.removeTasksNamed('manipulateCamera')
+        # Hide the marker
+        self.coaMarker.hide()
+        t = Task.Task(self.XZTranslateOrHPPanTask)
+        t.scaleFactor = (self.coaDist / self.chan.near)
+        taskMgr.spawnTaskNamed(t, 'manipulateCamera')
+
+    def XZTranslateOrHPPanTask(self, state):
+        if direct.fShift:
+            self.camera.setHpr(self.camera,
+                               (0.5 * self.chan.mouseDeltaX *
+                                self.chan.fovH),
+                               (-0.5 * self.chan.mouseDeltaY *
+                                self.chan.fovV),
+                               0.0)
+        else:
+            self.camera.setPos(self.camera,
+                               (-0.5 * self.chan.mouseDeltaX *
+                                self.chan.nearWidth *
+                                state.scaleFactor),
+                               0.0,
+                               (-0.5 * self.chan.mouseDeltaY *
+                                self.chan.nearHeight *
+                                state.scaleFactor))
+        return Task.cont
+
+    def spawnXZTranslate(self):
+        # Kill any existing tasks
+	taskMgr.removeTasksNamed('manipulateCamera')
+        # Hide the marker
+        self.coaMarker.hide()
+        t = Task.Task(self.XZTranslateTask)
+        t.scaleFactor = (self.coaDist / self.chan.near)
+        taskMgr.spawnTaskNamed(t, 'manipulateCamera')
+
+    def XZTranslateTask(self,state):
+        self.camera.setPos(self.camera,
+                           (-0.5 * self.chan.mouseDeltaX *
+                            self.chan.nearWidth *
+                            state.scaleFactor),
+                           0.0,
+                           (-0.5 * self.chan.mouseDeltaY *
+                            self.chan.nearHeight *
+                            state.scaleFactor))
+        return Task.cont
+
+    def spawnMouseRotateTask(self):
+        # Kill any existing tasks
+	taskMgr.removeTasksNamed('manipulateCamera')
+        # Set at markers position in render coordinates
+	self.relNodePath.setPos(self.coaMarkerPos)
+	self.relNodePath.setHpr(self.camera, self.zeroPoint)
+        t = Task.Task(self.mouseRotateTask)
+	t.wrtMat = self.camera.getMat( self.relNodePath )
+        taskMgr.spawnTaskNamed(t, 'manipulateCamera')
+
+    def mouseRotateTask(self, state):
+        wrtMat = state.wrtMat
+        self.relNodePath.setHpr(self.relNodePath,
+                                (-0.5 * self.chan.mouseDeltaX * 180.0),
+                                (0.5 * self.chan.mouseDeltaY * 180.0),
+                                0.0)
+        self.camera.setMat(self.relNodePath, wrtMat)
+        return Task.cont
+
+    def spawnHPPan(self):
+        # Kill any existing tasks
+	taskMgr.removeTasksNamed('manipulateCamera')
+        # Hide the marker
+        self.coaMarker.hide()
+        t = Task.Task(self.HPPanTask)
+        taskMgr.spawnTaskNamed(t, 'manipulateCamera')
+
+    def HPPanTask(self, state):
+        self.camera.setHpr(self.camera,
+                           (0.5 * self.chan.mouseDeltaX *
+                            self.chan.fovH),
+                           (-0.5 * self.chan.mouseDeltaY *
+                            self.chan.fovV),
+                           0.0)
+        return Task.cont
+
+    def fitOnWidget(self):
+        # Fit the node on the screen
+        
+        # stop any ongoing tasks
+        taskMgr.removeTasksNamed('manipulateCamera')
+
+        # How big is the node?
+        nodeScale = direct.widget.scalingNode.getScale(render)
+        maxScale = max(nodeScale[0],nodeScale[1],nodeScale[2])
+        maxDim = min(self.chan.nearWidth, self.chan.nearHeight)
+
+        # At what distance does the object fill 30% of the screen?
+        # Assuming radius of 1 on widget
+        camY = self.chan.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 = direct.widget.getPos(self.camera)
+    
+        # How far do you move the camera to be this distance from the node?
+        deltaMove = vWidget2Camera - centerVec
+    
+        # Move a target there
+        self.relNodePath.setPos(self.camera, deltaMove)
+
+	parent = self.camera.getParent()
+	self.camera.wrtReparentTo(self.relNodePath)
+	fitTask = self.camera.lerpPos(Point3(0,0,0),
+                                      CAM_MOVE_DURATION,
+                                      blendType = 'easeInOut',
+                                      task = 'manipulateCamera')
+        # Upon death, reparent Cam to parent
+        fitTask.parent = parent
+        fitTask.uponDeath = self.reparentCam                                
+
+    def enableMouseFly(self):
+	self.enableMouseInteraction()
+	self.enableHotKeys()
+        self.coaMarker.reparentTo(render)
+
+    def enableMouseInteraction(self):
+	# disable C++ fly interface
+	base.disableMouse()
+	# Accept middle mouse events
+	self.accept('handleMouse2', self.mouseFlyStart, [self.chan])
+	self.accept('handleMouse2Up', self.mouseFlyStop)
+
+    def enableHotKeys(self):
+        t = CAM_MOVE_DURATION
+	self.accept('u', self.uprightCam, [self.chan])
+	self.accept('c', self.centerCamIn, [self.chan, 0.5])
+	self.accept('h', self.homeCam, [self.chan])
+        self.accept('f', self.fitOnWidget)
+        for i in range(1,9):
+            self.accept(`i`, self.SpawnMoveToView, [self.chan, i])
+	self.accept('9', self.swingCamAboutWidget, [self.chan, -90.0, t])
+	self.accept('0', self.swingCamAboutWidget, [self.chan,  90.0, t])
+	self.accept('`', self.removeManipulateCameraTask)
+	self.accept('=', self.zoomCam, [self.chan, 0.5, t])
+	self.accept('+', self.zoomCam, [self.chan, 0.5, t])
+	self.accept('-', self.zoomCam, [self.chan, -2.0, t])
+	self.accept('_', self.zoomCam, [self.chan, -2.0, t])
+
+    def disableMouseFly(self):
+        # Hide the marker
+        self.coaMarker.reparentTo(hidden)
+	# Ignore middle mouse events
+	self.ignore('handleMouse2')
+	self.ignore('handleMouse2Up')
+	self.ignore('u')
+	self.ignore('c')
+	self.ignore('h')
+        self.ignore('f')
+        for i in range(0,10):
+            self.ignore(`i`)
+	self.ignore('=')
+	self.ignore('+')
+	self.ignore('-')
+	self.ignore('_')
+        self.ignore('`')
+
+    def removeManipulateCameraTask(self):
+        taskMgr.removeTasksNamed('manipulateCamera')
+

+ 2 - 5
direct/src/directutil/DirectGrid.py

@@ -2,14 +2,11 @@ from PandaObject import *
 from DirectGeometry import *
 
 class DirectGrid(NodePath,PandaObject):
-    def __init__(self, direct):
+    def __init__(self):
         # Initialize superclass
         NodePath.__init__(self)
         self.assign(hidden.attachNewNode( NamedNode('DirectGrid')))
 
-        # Record handle to direct session
-        self.direct = direct
-
 	# Load up grid parts to initialize grid object
 	# Polygon used to mark grid plane
 	self.gridBack = loader.loadModel('models/misc/gridBack')
@@ -60,7 +57,7 @@ class DirectGrid(NodePath,PandaObject):
 
     def selectGridBackParent(self, nodePath):
         if nodePath.getName() == 'GridBack':
-            self.direct.select(self)
+            direct.select(self)
 
     def updateGrid(self):
 	# Update grid lines based upon current grid spacing and grid size

+ 912 - 915
direct/src/directutil/DirectManipulation.py

@@ -1,915 +1,912 @@
-from PandaObject import *
-from DirectGeometry import *
-
-MANIPULATION_MOVE_DELAY = 0.65
-VISIBLE_DISCS = ['x-disc-visible', 'y-disc-visible', 'z-disc-visible']
-
-class DirectManipulationControl(PandaObject):
-    def __init__(self, direct):
-        # Create the grid
-        self.direct = direct
-        self.chan = direct.chan
-        self.camera = self.chan.camera
-        self.objectHandles = ObjectHandles(direct)
-        self.hitPt = Point3(0)
-        self.prevHit = Vec3(0)
-        self.rotationCenter = Point3(0)
-        self.initScaleMag = 1
-        self.refNodePath = render.attachNewNode(NamedNode('refNodePath'))
-        self.hitPtDist = 0
-        self.constraint = None
-        self.rotateAxis = 'x'
-        self.lastCrankAngle = 0
-        self.fSetCoa = 0
-        self.fHitInit = 1
-        self.fWidgetTop = 0
-        self.fFreeManip = 1
-        self.fScaling = 1
-        self.mode = None
-
-    def manipulationStart(self, chan):
-        # Start out in select mode
-        self.mode = 'select'
-        # Check for a widget hit point
-        numEntries = self.direct.iRay.pickWidget(
-            render,chan.mouseX,chan.mouseY)
-        # Did we hit a widget?
-        if(numEntries):
-            # Yes!
-            # Entry 0 is the closest hit point if multiple hits
-            minPt = 0
-            # Find hit point in camera's space
-            self.hitPt = self.direct.iRay.camToHitPt(minPt)
-            self.hitPtDist = Vec3(self.hitPt - ZERO_POINT).length()
-            # Get the associated collision queue object
-            entry = self.direct.iRay.cq.getEntry(minPt)
-            # Extract the node
-            node = entry.getIntoNode()
-            # Constraint determined by nodes name
-            self.constraint = node.getName()
-        else:
-            # Nope, off the widget, no constraint
-            self.constraint = None
-        # Check to see if we are moving the object
-        # We are moving the object if we either wait long enough
-        """
-        taskMgr.spawnTaskNamed(
-            Task.doLater(MANIPULATION_MOVE_DELAY,
-                         Task.Task(self.switchToMoveMode),
-                         'manip-switch-to-move'),
-            'manip-move-wait')
-        """
-        # Or if we move far enough
-        self.moveDir = None
-        watchMouseTask = Task.Task(self.watchMouseTask)
-        watchMouseTask.initX = self.chan.mouseX
-        watchMouseTask.initY = self.chan.mouseY
-        taskMgr.spawnTaskNamed(watchMouseTask, 'manip-watch-mouse')
-
-    def switchToMoveMode(self, state):
-        taskMgr.removeTasksNamed('manip-watch-mouse')
-        self.mode = 'move'
-        self.manipulateObject()
-        return Task.done
-
-    def watchMouseTask(self, state):
-        if (((abs (state.initX - self.chan.mouseX)) > 0.01) |
-            ((abs (state.initY - self.chan.mouseY)) > 0.01)):
-            taskMgr.removeTasksNamed('manip-move-wait')
-            taskMgr.removeTasksNamed('manip-switch-to-move')
-            self.mode = 'move'
-            self.manipulateObject()
-            return Task.done
-        else:
-            return Task.cont
-
-    def manipulationStop(self):
-        taskMgr.removeTasksNamed('manipulateObject')
-        taskMgr.removeTasksNamed('manip-move-wait')
-        taskMgr.removeTasksNamed('manip-switch-to-move')
-        taskMgr.removeTasksNamed('manip-watch-mouse')
-        # depending on flag.....
-        if self.mode == 'select':
-            # Check for object under mouse
-            numEntries = self.direct.iRay.pickGeom(
-                render,self.chan.mouseX,self.chan.mouseY)
-            # Pick out the closest object that isn't a widget
-            index = -1
-            for i in range(0,numEntries):
-                entry = self.direct.iRay.cq.getEntry(i)
-                node = entry.getIntoNode()
-                if node.isHidden():
-                    pass
-                # Is it a named node?, If so, see if it has a name
-                elif issubclass(node.__class__, NamedNode):
-                    name = node.getName()
-                    if name in VISIBLE_DISCS:
-                        pass
-                    else:
-                        index = i
-                        break
-                else:
-                    # Not hidden and not one of the widgets, use it
-                    index = i
-            # Did we hit an object?
-            if(index >= 0):
-                # Yes!
-                # Find hit point in camera's space
-                self.hitPt = self.direct.iRay.camToHitPt(index)
-                self.hitPtDist = Vec3(self.hitPt - ZERO_POINT).length()
-                # Find the node path from the node found above
-                nodePath = render.findPathDownTo(node)
-                # Select it
-                self.direct.select(nodePath)
-            else:
-                self.direct.deselectAll()
-        else:
-            self.manipulateObjectCleanup()
-
-    def manipulateObjectCleanup(self):
-        if self.fScaling:
-            # We had been scaling, need to reset object handles
-            self.objectHandles.transferObjectHandlesScale()
-            self.fScaling = 0
-        if self.fSetCoa:
-            self.objectHandles.manipModeColor()
-        self.direct.selected.highlightAll()
-        self.objectHandles.showAllHandles()
-        self.objectHandles.hideGuides()
-        # Restart followSelectedNodePath task
-        if self.direct.selected.last:
-            self.spawnFollowSelectedNodePathTask()
-        messenger.send('manipulateObjectCleanup')
-
-    def spawnFollowSelectedNodePathTask(self):
-        # Where are the object handles relative to the selected object
-        pos = VBase3(0)
-        hpr = VBase3(0)
-        decomposeMatrix(self.direct.selected.last.mCoa2Dnp,
-                        VBase3(0), hpr, pos, CSDefault)
-        # Create the task
-        t = Task.Task(self.followSelectedNodePathTask)
-        # Update state variables
-        t.pos = pos
-        t.hpr = hpr
-        t.base = self.direct.selected.last
-        # Spawn the task
-        taskMgr.spawnTaskNamed(t, 'followSelectedNodePath')
-
-    def followSelectedNodePathTask(self, state):
-        self.direct.widget.setPosHpr(state.base, state.pos, state.hpr)
-        return Task.cont
-
-    def enableManipulation(self):
-	# Accept mouse events
-	self.accept('handleMouse1', self.manipulationStart, [self.chan])
-	self.accept('handleMouse1Up', self.manipulationStop)
-        self.enableHotKeys()
-
-    def enableHotKeys(self):
-        self.accept(
-            '.', self.objectHandles.multiplyScalingFactorBy, [2.0])
-        self.accept(
-            ',', self.objectHandles.multiplyScalingFactorBy, [0.5])
-        self.accept('F', self.objectHandles.growToFit)
-
-    def disableManipulation(self):
-	# Ignore middle mouse events
-	self.ignore('handleMouse1')
-	self.ignore('handleMouse1Up')
-        self.disableHotKeys()
-
-    def disableHotKeys(self):
-        self.ignore('.')
-        self.ignore(',')
-        self.ignore('F')
-
-    def removeManipulateObjectTask(self):
-        taskMgr.removeTasksNamed('manipulateObject')
-
-    def manipulateObject(self):
-        # Only do this if something is selected
-        if self.direct.selected:
-            # Remove the task to keep the widget attached to the object
-            taskMgr.removeTasksNamed('followSelectedNodePath')
-            # and the task to highlight the widget
-            taskMgr.removeTasksNamed('highlightWidgetTask')
-            # Set manipulation flag
-            self.fManip = 1
-            # Update object handles visibility
-            self.objectHandles.showGuides()
-            self.objectHandles.hideAllHandles()
-            self.objectHandles.showHandle(self.constraint)
-            if self.fSetCoa:
-                self.objectHandles.coaModeColor()
-
-            # Record relationship between selected nodes and widget
-            self.direct.selected.getWrtAll()
-
-            # hide the bbox of the selected objects during interaction
-            self.direct.selected.dehighlightAll()
-
-            """
-            # Push the undo dcs for the selected objects
-            self.direct.undo.push(
-                (self.direct.selected, 'dcs'))
-            """
-            # Manipulate the real object with the constraint
-            # The constraint is passed as the name of the node 
-            self.spawnManipulateObjectTask()
-
-    def spawnManipulateObjectTask(self):
-        # reset hit-pt flag
-        self.fHitInit = 1
-        # record initial offset between widget and camera
-        t = Task.Task(self.manipulateObjectTask)
-        taskMgr.spawnTaskNamed(t, 'manipulateObject')
-
-    def manipulateObjectTask(self, state):
-
-        if self.constraint:
-            type = self.constraint[2:]
-            if type == 'post':
-                self.xlate1D()
-            elif type == 'disc':
-                self.xlate2D()
-            elif type == 'ring':
-                self.rotate1D()
-        elif self.fFreeManip:
-            if self.fScaling & (not self.direct.fAlt):
-                # We had been scaling and changed modes,
-                # reset object handles
-                self.objectHandles.transferObjectHandlesScale()
-                self.fScaling = 0
-            if self.direct.fControl:
-                self.rotate2D()
-            elif self.direct.fAlt:
-                self.fScaling = 1
-                self.scale3D()
-            elif self.direct.fShift:
-                self.xlateCamXY()
-            else:
-                self.xlateCamXZ()
-        else:
-            # MRM: Needed, more elegant fallback
-            return Task.cont
-
-        if self.fSetCoa:
-            # Update coa based on current widget position
-            self.direct.selected.last.mCoa2Dnp.assign(
-                self.direct.widget.getMat(self.direct.selected.last)
-                )
-        else:
-            # Move the objects with the widget
-            self.direct.selected.moveWrtWidgetAll()
-            
-        # Continue
-        return Task.cont
-
-    def xlate1D(self):
-        # Constrained 1D Translation along widget axis
-        # Compute nearest hit point along axis and try to keep
-        # that point as close to the current mouse position as possible
-        # what point on the axis is the mouse pointing at?
-        self.hitPt.assign(self.objectHandles.getAxisIntersectPt(
-            self.constraint[:1]))
-        # use it to see how far to move the widget
-        if self.fHitInit:
-            # First time through, just record that point
-            self.fHitInit = 0
-            self.prevHit.assign(self.hitPt)
-        else:
-            # Move widget to keep hit point as close to mouse as possible
-            offset = self.hitPt - self.prevHit
-            self.direct.widget.setPos(self.direct.widget, offset)
-
-    def xlate2D(self):
-        # Constrained 2D (planar) translation
-        # Compute point of intersection of ray from eyepoint through cursor
-        # to one of the three orthogonal planes on the widget.
-        # This point tracks all subsequent mouse movements
-        self.hitPt.assign(self.objectHandles.getWidgetIntersectPt(
-            self.direct.widget, self.constraint[:1]))
-        # use it to see how far to move the widget
-        if self.fHitInit:
-            # First time through just record hit point
-            self.fHitInit = 0
-            self.prevHit.assign(self.hitPt)
-        else:
-	    offset = self.hitPt - self.prevHit
-            self.direct.widget.setPos(self.direct.widget, offset)
-
-
-    def xlateCamXZ(self):
-        """Constrained 2D motion parallel to the camera's image plane
-        This moves the object in the camera's XZ plane"""
-        # reset fHitInit
-        # (in case we later switch to another manipulation mode)
-        #self.fHitInit = 1
-        # Where is the widget relative to current camera view
-        vWidget2Camera = self.direct.widget.getPos(self.camera)
-        x = vWidget2Camera[0]
-        y = vWidget2Camera[1]
-        z = vWidget2Camera[2]
-        # Move widget (and objects) based upon mouse motion
-        # Scaled up accordingly based upon widget distance
-        chan = self.chan
-        self.direct.widget.setX(
-            self.camera,
-            x + 0.5 * chan.mouseDeltaX * chan.nearWidth * (y/chan.near))
-        self.direct.widget.setZ(
-            self.camera,
-            z + 0.5 * chan.mouseDeltaY * chan.nearHeight * (y/chan.near))
-
-    def xlateCamXY(self):
-        """Constrained 2D motion perpendicular to camera's image plane
-        This moves the object in the camera's XY plane"""
-        # Now, where is the widget relative to current camera view
-        vWidget2Camera = self.direct.widget.getPos(self.camera)
-        # If this is first time around, record initial y distance
-        if self.fHitInit:
-            self.fHitInit = 0
-            # Record widget offset along y
-            self.initY = vWidget2Camera[1]
-        # Extract current values
-        x = vWidget2Camera[0]
-        y = vWidget2Camera[1]
-        z = vWidget2Camera[2]
-        # Move widget (and objects) based upon mouse motion
-        # Scaled up accordingly based upon widget distance
-        chan = self.chan
-        self.direct.widget.setPos(
-            self.camera,
-            x + 0.5 * chan.mouseDeltaX * chan.nearWidth * (y/chan.near),
-            y + self.initY * chan.mouseDeltaY,
-            z)
-    
-    def getCrankAngle(self):
-        # Used to compute current angle of mouse (relative to the widget's
-        # origin) in screen space
-        x = self.chan.mouseX - self.rotationCenter[0]
-        y = self.chan.mouseY - self.rotationCenter[2]
-        return (180 + rad2Deg(math.atan2(y,x)))
-
-    def widgetCheck(self,type):
-        # Utility to see if we are looking at the top or bottom of
-        # a 2D planar widget or if we are looking at a 2D planar widget
-        # edge on
-        # Based upon angle between view vector from eye through the
-        # widget's origin and one of the three principle axes
-        axis = self.constraint[:1]
-        # First compute vector from eye through widget origin
-        mWidget2Cam = self.direct.widget.getMat(self.camera)
-        # And determine where the viewpoint is relative to widget
-        pos = VBase3(0)
-        decomposeMatrix(mWidget2Cam, VBase3(0), VBase3(0), pos,
-                        CSDefault)
-        widgetDir = Vec3(pos)
-        widgetDir.normalize()
-        # Convert specified widget axis to view space
-        if axis == 'x':
-            widgetAxis = Vec3(mWidget2Cam.xformVec(X_AXIS))
-        elif axis == 'y':
-            widgetAxis = Vec3(mWidget2Cam.xformVec(Y_AXIS))
-        elif axis == 'z':
-            widgetAxis = Vec3(mWidget2Cam.xformVec(Z_AXIS))
-        widgetAxis.normalize()
-        if type == 'top?':
-            # Check sign of angle between two vectors
-            return (widgetDir.dot(widgetAxis) < 0.)
-        elif type == 'edge?':
-            # Checking to see if we are viewing edge-on
-            # Check angle between two vectors
-            return(abs(widgetDir.dot(widgetAxis)) < .2)
-
-    def getWidgetsNearProjectionPoint(self):
-        # Find the position of the projection of the specified node path
-        # on the near plane
-        widgetOrigin = self.direct.widget.getPos(self.camera)
-        # project this onto near plane
-        return widgetOrigin * (self.chan.near / widgetOrigin[1])
-
-    def getScreenXY(self):
-        # Where does the widget's projection fall on the near plane
-        nearVec = self.getWidgetsNearProjectionPoint()
-        # Clamp these coordinates to visible screen
-        nearX = self.clamp(nearVec[0], self.chan.left, self.chan.right)
-        nearY = self.clamp(nearVec[2], self.chan.bottom, self.chan.top)
-        # What percentage of the distance across the screen is this?
-        percentX = (nearX - self.chan.left)/self.chan.nearWidth
-        percentY = (nearY - self.chan.bottom)/self.chan.nearHeight
-        # Map this percentage to the same -1 to 1 space as the mouse
-        screenXY = Vec3((2 * percentX) - 1.0,nearVec[1],(2 * percentY) - 1.0)
-        # Return the resulting value
-        return screenXY
-
-    def rotate1D(self):
-        # Constrained 1D rotation about the widget's main axis (X,Y, or Z)
-        # Rotation depends upon circular motion of the mouse about the
-        # projection of the widget's origin on the image plane
-        # A complete circle about the widget results in a change in
-        # orientation of 360 degrees.
-
-        # First initialize hit point/rotation angle
-        if self.fHitInit:
-            self.fHitInit = 0
-            self.rotateAxis = self.constraint[:1]
-            self.fWidgetTop = self.widgetCheck('top?')
-            self.rotationCenter = self.getScreenXY()
-            self.lastCrankAngle = self.getCrankAngle()
-            
-        # Rotate widget based on how far cursor has swung around origin
-        newAngle = self.getCrankAngle()
-        deltaAngle = self.lastCrankAngle - newAngle
-        if self.fWidgetTop:
-            deltaAngle = -1 * deltaAngle
-        if self.rotateAxis == 'x':
-            self.direct.widget.setP(self.direct.widget, deltaAngle)
-        elif self.rotateAxis == 'y':
-            self.direct.widget.setR(self.direct.widget, -deltaAngle)
-        elif self.rotateAxis == 'z':
-            self.direct.widget.setH(self.direct.widget, deltaAngle)
-        # Record crank angle for next time around
-        self.lastCrankAngle = newAngle
-
-    def relHpr(self, base, h, p, r):
-        # Compute widget2newWidget relative to base coordinate system
-        mWidget2Base = self.direct.widget.getMat(base)
-        mBase2NewBase = Mat4()
-        mBase2NewBase.composeMatrix(
-            UNIT_VEC, VBase3(h,p,r), ZERO_VEC,
-            CSDefault)
-        mBase2Widget = base.getMat(self.direct.widget)
-        mWidget2Parent = self.direct.widget.getMat()
-        # Compose the result
-        resultMat = mWidget2Base * mBase2NewBase
-        resultMat = resultMat * mBase2Widget
-        resultMat = resultMat * mWidget2Parent
-        # Extract and apply the hpr
-        hpr = Vec3(0)
-        decomposeMatrix(resultMat, VBase3(), hpr, VBase3(),
-                        CSDefault)
-        self.direct.widget.setHpr(hpr)
-
-    def rotate2D(self):
-        # Virtual trackball or arcball rotation of widget
-        # Rotation method depends upon variable dd-want-arcball
-        # Default is virtual trackball (handles 1D rotations better)
-        self.fHitInit = 1
-        tumbleRate = 360
-        # Mouse motion edge to edge of channel results in one full turn
-        self.relHpr(self.camera,
-                    self.chan.mouseDeltaX * tumbleRate,
-                    -self.chan.mouseDeltaY * tumbleRate,
-                    0)
-
-    def scale3D(self):
-        # Scale the selected node based upon up down mouse motion
-        # Mouse motion from edge to edge results in a factor of 4 scaling
-        # From midpoint to edge doubles or halves objects scale
-        if self.fHitInit:
-            self.fHitInit = 0
-            self.refNodePath.setPos(self.direct.widget, 0, 0, 0)
-            self.refNodePath.setHpr(self.camera, 0, 0, 0)
-            self.initScaleMag = Vec3(
-                self.objectHandles.getWidgetIntersectPt(
-                self.refNodePath, 'y')).length()
-            # record initial scale
-            self.initScale = self.direct.widget.getScale()
-        # Begin
-        # Scale factor is ratio current mag with init mag
-        currScale = (
-            self.initScale *
-            (self.objectHandles.getWidgetIntersectPt(
-            self.refNodePath, 'y').length() /
-             self.initScaleMag)
-            )
-        self.direct.widget.setScale(currScale)
-        
-    def clamp(self, val, min, max):
-        if val < min:
-            return min
-        elif val > max:
-            return max
-        else:
-            return val
-
-
-class ObjectHandles(NodePath,PandaObject):
-    def __init__(self,direct):
-        # Record pointer to direct object
-        self.direct = direct
-        # Initialize the superclass
-        NodePath.__init__(self)
-
-        # Load up object handles model and assign it to self
-        self.assign(loader.loadModel('models/misc/objectHandles'))
-        self.node().setName('objectHandles')
-        self.scalingNode = self.getChild(0)
-        self.scalingNode.node().setName('ohScalingNode')
-        self.ohScalingFactor = 1.0
-        # To avoid recreating a vec every frame
-        self.hitPt = Vec3(0)
-        # Get a handle on the components
-        self.xHandles = self.find('**/X')
-        self.xPostGroup = self.xHandles.find('**/x-post-group')
-        self.xPostCollision = self.xHandles.find('**/x-post')
-        self.xRingGroup = self.xHandles.find('**/x-ring-group')
-        self.xRingCollision = self.xHandles.find('**/x-ring')
-        self.xDiscGroup = self.xHandles.find('**/x-disc-group')
-        self.xDisc = self.xHandles.find('**/x-disc-visible')
-        self.xDiscCollision = self.xHandles.find('**/x-disc')
-        
-        self.yHandles = self.find('**/Y')
-        self.yPostGroup = self.yHandles.find('**/y-post-group')
-        self.yPostCollision = self.yHandles.find('**/y-post')
-        self.yRingGroup = self.yHandles.find('**/y-ring-group')
-        self.yRingCollision = self.yHandles.find('**/y-ring')
-        self.yDiscGroup = self.yHandles.find('**/y-disc-group')
-        self.yDisc = self.yHandles.find('**/y-disc-visible')
-        self.yDiscCollision = self.yHandles.find('**/y-disc')
-        
-        self.zHandles = self.find('**/Z')
-        self.zPostGroup = self.zHandles.find('**/z-post-group')
-        self.zPostCollision = self.zHandles.find('**/z-post')
-        self.zRingGroup = self.zHandles.find('**/z-ring-group')
-        self.zRingCollision = self.zHandles.find('**/z-ring')
-        self.zDiscGroup = self.zHandles.find('**/z-disc-group')
-        self.zDisc = self.zHandles.find('**/z-disc-visible')
-        self.zDiscCollision = self.zHandles.find('**/z-disc')
-
-        # Adjust visiblity, colors, and transparency
-        self.xPostCollision.hide()
-        self.xRingCollision.hide()
-        self.xDisc.setColor(1,0,0,.2)
-        self.yPostCollision.hide()
-        self.yRingCollision.hide()
-        self.yDisc.setColor(0,1,0,.2)
-        self.zPostCollision.hide()
-        self.zRingCollision.hide()
-        self.zDisc.setColor(0,0,1,.2)
-        # Augment geometry with lines
-        self.createObjectHandleLines()
-        # Create long markers to help line up in world
-        self.createGuideLines()
-        self.hideGuides()
-
-    def coaModeColor(self):
-        self.setColor(.5,.5,.5,1)
-
-    def manipModeColor(self):
-        self.clearColor()
-
-    def enableHandles(self, handles):
-        if type(handles) == types.ListType:
-            for handle in handles:
-                self.enableHandle(handle)
-        elif handles == 'x':
-            self.enableHandles(['x-post','x-ring','x-disc'])
-        elif handles == 'y':
-            self.enableHandles(['y-post','y-ring','y-disc'])
-        elif handles == 'z':
-            self.enableHandles(['z-post','z-ring','z-disc'])
-        elif handles == 'post':
-            self.enableHandles(['x-post','y-post','z-post'])
-        elif handles == 'ring':
-            self.enableHandles(['x-ring','y-ring','z-ring'])
-        elif handles == 'disc':
-            self.enableHandles(['x-disc','y-disc','z-disc'])
-        elif handles == 'all':
-            self.enableHandles(['x-post','x-ring','x-disc',
-                                'y-post','y-ring','y-disc',
-                                'z-post','z-ring','z-disc'])
-
-    def enableHandle(self, handle):
-        if handle == 'x-post':
-            self.xPostGroup.reparentTo(self.xHandles)
-        elif handle == 'x-ring':
-            self.xRingGroup.reparentTo(self.xHandles)
-        elif handle == 'x-disc':
-            self.xDiscGroup.reparentTo(self.xHandles)
-        if handle == 'y-post':
-            self.yPostGroup.reparentTo(self.yHandles)
-        elif handle == 'y-ring':
-            self.yRingGroup.reparentTo(self.yHandles)
-        elif handle == 'y-disc':
-            self.yDiscGroup.reparentTo(self.yHandles)
-        if handle == 'z-post':
-            self.zPostGroup.reparentTo(self.zHandles)
-        elif handle == 'z-ring':
-            self.zRingGroup.reparentTo(self.zHandles)
-        elif handle == 'z-disc':
-            self.zDiscGroup.reparentTo(self.zHandles)
-
-    def disableHandles(self, handles):
-        if type(handles) == types.ListType:
-            for handle in handles:
-                self.disableHandle(handle)
-        elif handles == 'x':
-            self.disableHandles(['x-post','x-ring','x-disc'])
-        elif handles == 'y':
-            self.disableHandles(['y-post','y-ring','y-disc'])
-        elif handles == 'z':
-            self.disableHandles(['z-post','z-ring','z-disc'])
-        elif handles == 'post':
-            self.disableHandles(['x-post','y-post','z-post'])
-        elif handles == 'ring':
-            self.disableHandles(['x-ring','y-ring','z-ring'])
-        elif handles == 'disc':
-            self.disableHandles(['x-disc','y-disc','z-disc'])
-        elif handles == 'all':
-            self.disableHandles(['x-post','x-ring','x-disc',
-                                 'y-post','y-ring','y-disc',
-                                 'z-post','z-ring','z-disc'])
-
-    def disableHandle(self, handle):
-        if handle == 'x-post':
-            self.xPostGroup.reparentTo(hidden)
-        elif handle == 'x-ring':
-            self.xRingGroup.reparentTo(hidden)
-        elif handle == 'x-disc':
-            self.xDiscGroup.reparentTo(hidden)
-        if handle == 'y-post':
-            self.yPostGroup.reparentTo(hidden)
-        elif handle == 'y-ring':
-            self.yRingGroup.reparentTo(hidden)
-        elif handle == 'y-disc':
-            self.yDiscGroup.reparentTo(hidden)
-        if handle == 'z-post':
-            self.zPostGroup.reparentTo(hidden)
-        elif handle == 'z-ring':
-            self.zRingGroup.reparentTo(hidden)
-        elif handle == 'z-disc':
-            self.zDiscGroup.reparentTo(hidden)
-
-    def showAllHandles(self):
-        self.xPost.show()
-        self.xRing.show()
-        self.xDisc.show()
-        self.yPost.show()
-        self.yRing.show()
-        self.yDisc.show()
-        self.zPost.show()
-        self.zRing.show()
-        self.zDisc.show()
-
-    def hideAllHandles(self):
-        self.xPost.hide()
-        self.xRing.hide()
-        self.xDisc.hide()
-        self.yPost.hide()
-        self.yRing.hide()
-        self.yDisc.hide()
-        self.zPost.hide()
-        self.zRing.hide()
-        self.zDisc.hide()
-
-    def showHandle(self, handle):
-        if handle == 'x-post':
-            self.xPost.show()
-        elif handle == 'x-ring':
-            self.xRing.show()
-        elif handle == 'x-disc':
-            self.xDisc.show()
-        elif handle == 'y-post':
-            self.yPost.show()
-        elif handle == 'y-ring':
-            self.yRing.show()
-        elif handle == 'y-disc':
-            self.yDisc.show()
-        elif handle == 'z-post':
-            self.zPost.show()
-        elif handle == 'z-ring':
-            self.zRing.show()
-        elif handle == 'z-disc':
-            self.zDisc.show()
-
-    def showGuides(self):
-        self.guideLines.show()
-
-    def hideGuides(self):
-        self.guideLines.hide()
-
-    def setScalingFactor(self, scaleFactor):
-        self.ohScalingFactor = scaleFactor
-        self.scalingNode.setScale(self.ohScalingFactor)
-
-    def getScalingFactor(self):
-        return self.scalingNode.getScale()
-
-    def transferObjectHandlesScale(self):
-        # see how much object handles have been scaled
-        ohs = self.getScale()
-        sns = self.scalingNode.getScale()
-        # Transfer this to the scaling node
-        self.scalingNode.setScale(
-            ohs[0] * sns[0],
-            ohs[1] * sns[1],
-            ohs[2] * sns[2])
-        self.setScale(1)
-
-    def multiplyScalingFactorBy(self, factor):
-        taskMgr.removeTasksNamed('resizeObjectHandles')
-        sf = self.ohScalingFactor = self.ohScalingFactor * factor
-        self.scalingNode.lerpScale(sf,sf,sf, 0.5,
-                                   blendType = 'easeInOut',
-                                   task = 'resizeObjectHandles')
-
-    def growToFit(self):
-        taskMgr.removeTasksNamed('resizeObjectHandles')
-        # Increase handles scale until they cover 30% of the min dimension
-        pos = self.direct.widget.getPos(self.direct.camera)
-        minDim = min(self.direct.chan.nearWidth, self.direct.chan.nearHeight)
-        sf = 0.15 * minDim * (pos[1]/self.direct.chan.near)
-        self.ohScalingFactor = sf
-        self.scalingNode.lerpScale(sf,sf,sf, 0.5,
-                                   blendType = 'easeInOut',
-                                   task = 'resizeObjectHandles')
-
-    def createObjectHandleLines(self):
-        # X post
-        self.xPost = self.xPostGroup.attachNewNode(NamedNode('x-post-visible'))
-	lines = LineNodePath(self.xPost)
-	lines.setColor(VBase4(1,0,0,1))
-	lines.setThickness(5)
-	lines.moveTo(0,0,0)
-        lines.drawTo(1.5,0,0)
-        lines.create()
-	lines = LineNodePath(self.xPost)
-	lines.setColor(VBase4(1,0,0,1))
-	lines.setThickness(1.5)
-	lines.moveTo(0,0,0)
-        lines.drawTo(-1.5,0,0)
-        lines.create()
-        
-	# X ring
-        self.xRing = self.xRingGroup.attachNewNode(NamedNode('x-ring-visible'))
-	lines = LineNodePath(self.xRing)
-	lines.setColor(VBase4(1,0,0,1))
-	lines.setThickness(3)
-	lines.moveTo(0,1,0)
-        for ang in range(15, 370, 15):
-            lines.drawTo(0,
-                          math.cos(deg2Rad(ang)),
-                          math.sin(deg2Rad(ang)))
-        lines.create()
-        
-        # Y post
-        self.yPost = self.yPostGroup.attachNewNode(NamedNode('y-post-visible'))
-	lines = LineNodePath(self.yPost)
-	lines.setColor(VBase4(0,1,0,1))
-	lines.setThickness(5)
-	lines.moveTo(0,0,0)
-        lines.drawTo(0,1.5,0)
-        lines.create()
-	lines = LineNodePath(self.yPost)
-	lines.setColor(VBase4(0,1,0,1))
-	lines.setThickness(1.5)
-	lines.moveTo(0,0,0)
-        lines.drawTo(0,-1.5,0)
-        lines.create()
-        
-	# Y ring
-        self.yRing = self.yRingGroup.attachNewNode(NamedNode('y-ring-visible'))
-	lines = LineNodePath(self.yRing)
-	lines.setColor(VBase4(0,1,0,1))
-	lines.setThickness(3)
-	lines.moveTo(1,0,0)
-        for ang in range(15, 370, 15):
-            lines.drawTo(math.cos(deg2Rad(ang)),
-                          0,
-                          math.sin(deg2Rad(ang)))
-        lines.create()
-
-        # Z post
-        self.zPost = self.zPostGroup.attachNewNode(NamedNode('z-post-visible'))
-	lines = LineNodePath(self.zPost)
-	lines.setColor(VBase4(0,0,1,1))
-	lines.setThickness(5)
-	lines.moveTo(0,0,0)
-        lines.drawTo(0,0,1.5)
-        lines.create()
-	lines = LineNodePath(self.zPost)
-	lines.setColor(VBase4(0,0,1,1))
-	lines.setThickness(1.5)
-	lines.moveTo(0,0,0)
-        lines.drawTo(0,0,-1.5)
-        lines.create()
-        
-	# Z ring
-        self.zRing = self.zRingGroup.attachNewNode(NamedNode('z-ring-visible'))
-	lines = LineNodePath(self.zRing)
-	lines.setColor(VBase4(0,0,1,1))
-	lines.setThickness(3)
-	lines.moveTo(1,0,0)
-        for ang in range(15, 370, 15):
-            lines.drawTo(math.cos(deg2Rad(ang)),
-                          math.sin(deg2Rad(ang)),
-                          0)
-        lines.create()
-
-    def createGuideLines(self):
-        self.guideLines = self.attachNewNode(NamedNode('guideLines'))
-        # X guide lines
-	lines = LineNodePath(self.guideLines)
-	lines.setColor(VBase4(1,0,0,1))
-	lines.setThickness(0.5)
-	lines.moveTo(-500,0,0)
-        lines.drawTo(500,0,0)
-        lines.create()
-        lines.node().setName('x-guide')
-
-        # Y guide lines
-	lines = LineNodePath(self.guideLines)
-	lines.setColor(VBase4(0,1,0,1))
-	lines.setThickness(0.5)
-	lines.moveTo(0,-500,0)
-        lines.drawTo(0,500,0)
-        lines.create()
-        lines.node().setName('y-guide')
-
-        # Z guide lines
-	lines = LineNodePath(self.guideLines)
-	lines.setColor(VBase4(0,0,1,1))
-	lines.setThickness(0.5)
-	lines.moveTo(0,0,-500)
-        lines.drawTo(0,0,500)
-        lines.create()
-        lines.node().setName('z-guide')
-
-    def getAxisIntersectPt(self, axis):
-        # Calc the xfrom from camera to widget
-        mCam2Widget = self.direct.camera.getMat(self.direct.widget)
-        lineDir = Vec3(mCam2Widget.xformVec(self.direct.chan.nearVec))
-        lineDir.normalize()
-        # And determine where the viewpoint is relative to widget
-        lineOrigin = VBase3(0)
-        decomposeMatrix(mCam2Widget, VBase3(0), VBase3(0), lineOrigin,
-                        CSDefault)
-        # Now see where this hits the plane containing the 1D motion axis.
-        # Pick the intersection plane most normal to the intersection ray
-        # by comparing lineDir with plane normals.  The plane with the
-        # largest dotProduct is most "normal"
-        if axis == 'x':
-            if (abs(lineDir.dot(Y_AXIS)) > abs(lineDir.dot(Z_AXIS))):
-                self.hitPt.assign(
-                    planeIntersect(lineOrigin, lineDir, ORIGIN, Y_AXIS))
-            else:
-                self.hitPt.assign(
-                    planeIntersect(lineOrigin, lineDir, ORIGIN, Z_AXIS))
-            # We really only care about the nearest point on the axis
-            self.hitPt.setY(0)
-            self.hitPt.setZ(0)
-        elif axis == 'y':
-            if (abs(lineDir.dot(X_AXIS)) > abs(lineDir.dot(Z_AXIS))):
-                self.hitPt.assign(
-                    planeIntersect(lineOrigin, lineDir, ORIGIN, X_AXIS))
-            else:
-                self.hitPt.assign(
-                    planeIntersect(lineOrigin, lineDir, ORIGIN, Z_AXIS))
-            # We really only care about the nearest point on the axis
-            self.hitPt.setX(0)
-            self.hitPt.setZ(0)
-        elif axis == 'z':
-            if (abs(lineDir.dot(X_AXIS)) > abs(lineDir.dot(Y_AXIS))):
-                self.hitPt.assign(
-                    planeIntersect(lineOrigin, lineDir, ORIGIN, X_AXIS))
-            else:
-                self.hitPt.assign(
-                    planeIntersect(lineOrigin, lineDir, ORIGIN, Y_AXIS))
-            # We really only care about the nearest point on the axis
-            self.hitPt.setX(0)
-            self.hitPt.setY(0)
-        return self.hitPt
-
-    def getWidgetIntersectPt(self, nodePath, plane):
-        # Find out the point of interection of the ray passing though the mouse
-        # with the plane containing the 2D xlation or 1D rotation widgets
-
-        # Calc the xfrom from camera to the nodePath
-        mCam2NodePath = self.direct.camera.getMat(nodePath)
-    
-        # And determine where the viewpoint is relative to widget
-        lineOrigin = VBase3(0)
-        decomposeMatrix(mCam2NodePath, VBase3(0), VBase3(0), lineOrigin,
-                        CSDefault)
-        
-        # Next we find the vector from viewpoint to the widget through
-        # the mouse's position on near plane.
-        # This defines the intersection ray
-        lineDir = Vec3(mCam2NodePath.xformVec(self.direct.chan.nearVec))
-        lineDir.normalize()
-        # Find the hit point
-        if plane == 'x':
-            self.hitPt.assign(planeIntersect(
-                lineOrigin, lineDir, ORIGIN, X_AXIS))
-        elif plane == 'y':
-            self.hitPt.assign(planeIntersect(
-                lineOrigin, lineDir, ORIGIN, Y_AXIS))
-        elif plane == 'z':
-            self.hitPt.assign(planeIntersect(
-                lineOrigin, lineDir, ORIGIN, Z_AXIS))
-        return self.hitPt
-
-
-
+from PandaObject import *
+from DirectGeometry import *
+
+MANIPULATION_MOVE_DELAY = 0.65
+VISIBLE_DISCS = ['x-disc-visible', 'y-disc-visible', 'z-disc-visible']
+
+class DirectManipulationControl(PandaObject):
+    def __init__(self):
+        # Create the grid
+        self.chan = direct.chan
+        self.camera = self.chan.camera
+        self.objectHandles = ObjectHandles()
+        self.hitPt = Point3(0)
+        self.prevHit = Vec3(0)
+        self.rotationCenter = Point3(0)
+        self.initScaleMag = 1
+        self.refNodePath = render.attachNewNode(NamedNode('refNodePath'))
+        self.hitPtDist = 0
+        self.constraint = None
+        self.rotateAxis = 'x'
+        self.lastCrankAngle = 0
+        self.fSetCoa = 0
+        self.fHitInit = 1
+        self.fWidgetTop = 0
+        self.fFreeManip = 1
+        self.fScaling = 1
+        self.mode = None
+
+    def manipulationStart(self, chan):
+        # Start out in select mode
+        self.mode = 'select'
+        # Check for a widget hit point
+        numEntries = direct.iRay.pickWidget(
+            render,chan.mouseX,chan.mouseY)
+        # Did we hit a widget?
+        if(numEntries):
+            # Yes!
+            # Entry 0 is the closest hit point if multiple hits
+            minPt = 0
+            # Find hit point in camera's space
+            self.hitPt = direct.iRay.camToHitPt(minPt)
+            self.hitPtDist = Vec3(self.hitPt - ZERO_POINT).length()
+            # Get the associated collision queue object
+            entry = direct.iRay.cq.getEntry(minPt)
+            # Extract the node
+            node = entry.getIntoNode()
+            # Constraint determined by nodes name
+            self.constraint = node.getName()
+        else:
+            # Nope, off the widget, no constraint
+            self.constraint = None
+        # Check to see if we are moving the object
+        # We are moving the object if we either wait long enough
+        """
+        taskMgr.spawnTaskNamed(
+            Task.doLater(MANIPULATION_MOVE_DELAY,
+                         Task.Task(self.switchToMoveMode),
+                         'manip-switch-to-move'),
+            'manip-move-wait')
+        """
+        # Or if we move far enough
+        self.moveDir = None
+        watchMouseTask = Task.Task(self.watchMouseTask)
+        watchMouseTask.initX = self.chan.mouseX
+        watchMouseTask.initY = self.chan.mouseY
+        taskMgr.spawnTaskNamed(watchMouseTask, 'manip-watch-mouse')
+
+    def switchToMoveMode(self, state):
+        taskMgr.removeTasksNamed('manip-watch-mouse')
+        self.mode = 'move'
+        self.manipulateObject()
+        return Task.done
+
+    def watchMouseTask(self, state):
+        if (((abs (state.initX - self.chan.mouseX)) > 0.01) |
+            ((abs (state.initY - self.chan.mouseY)) > 0.01)):
+            taskMgr.removeTasksNamed('manip-move-wait')
+            taskMgr.removeTasksNamed('manip-switch-to-move')
+            self.mode = 'move'
+            self.manipulateObject()
+            return Task.done
+        else:
+            return Task.cont
+
+    def manipulationStop(self):
+        taskMgr.removeTasksNamed('manipulateObject')
+        taskMgr.removeTasksNamed('manip-move-wait')
+        taskMgr.removeTasksNamed('manip-switch-to-move')
+        taskMgr.removeTasksNamed('manip-watch-mouse')
+        # depending on flag.....
+        if self.mode == 'select':
+            # Check for object under mouse
+            numEntries = direct.iRay.pickGeom(
+                render,self.chan.mouseX,self.chan.mouseY)
+            # Pick out the closest object that isn't a widget
+            index = -1
+            for i in range(0,numEntries):
+                entry = direct.iRay.cq.getEntry(i)
+                node = entry.getIntoNode()
+                if node.isHidden():
+                    pass
+                # Is it a named node?, If so, see if it has a name
+                elif issubclass(node.__class__, NamedNode):
+                    name = node.getName()
+                    if name in VISIBLE_DISCS:
+                        pass
+                    else:
+                        index = i
+                        break
+                else:
+                    # Not hidden and not one of the widgets, use it
+                    index = i
+            # Did we hit an object?
+            if(index >= 0):
+                # Yes!
+                # Find hit point in camera's space
+                self.hitPt = direct.iRay.camToHitPt(index)
+                self.hitPtDist = Vec3(self.hitPt - ZERO_POINT).length()
+                # Find the node path from the node found above
+                nodePath = render.findPathDownTo(node)
+                # Select it
+                direct.select(nodePath)
+            else:
+                direct.deselectAll()
+        else:
+            self.manipulateObjectCleanup()
+
+    def manipulateObjectCleanup(self):
+        if self.fScaling:
+            # We had been scaling, need to reset object handles
+            self.objectHandles.transferObjectHandlesScale()
+            self.fScaling = 0
+        if self.fSetCoa:
+            self.objectHandles.manipModeColor()
+        direct.selected.highlightAll()
+        self.objectHandles.showAllHandles()
+        self.objectHandles.hideGuides()
+        # Restart followSelectedNodePath task
+        if direct.selected.last:
+            self.spawnFollowSelectedNodePathTask()
+        messenger.send('manipulateObjectCleanup')
+
+    def spawnFollowSelectedNodePathTask(self):
+        # Where are the object handles relative to the selected object
+        pos = VBase3(0)
+        hpr = VBase3(0)
+        decomposeMatrix(direct.selected.last.mCoa2Dnp,
+                        VBase3(0), hpr, pos, CSDefault)
+        # Create the task
+        t = Task.Task(self.followSelectedNodePathTask)
+        # Update state variables
+        t.pos = pos
+        t.hpr = hpr
+        t.base = direct.selected.last
+        # Spawn the task
+        taskMgr.spawnTaskNamed(t, 'followSelectedNodePath')
+
+    def followSelectedNodePathTask(self, state):
+        direct.widget.setPosHpr(state.base, state.pos, state.hpr)
+        return Task.cont
+
+    def enableManipulation(self):
+	# Accept mouse events
+	self.accept('handleMouse1', self.manipulationStart, [self.chan])
+	self.accept('handleMouse1Up', self.manipulationStop)
+        self.enableHotKeys()
+
+    def enableHotKeys(self):
+        self.accept(
+            '.', self.objectHandles.multiplyScalingFactorBy, [2.0])
+        self.accept(
+            ',', self.objectHandles.multiplyScalingFactorBy, [0.5])
+        self.accept('F', self.objectHandles.growToFit)
+
+    def disableManipulation(self):
+	# Ignore middle mouse events
+	self.ignore('handleMouse1')
+	self.ignore('handleMouse1Up')
+        self.disableHotKeys()
+
+    def disableHotKeys(self):
+        self.ignore('.')
+        self.ignore(',')
+        self.ignore('F')
+
+    def removeManipulateObjectTask(self):
+        taskMgr.removeTasksNamed('manipulateObject')
+
+    def manipulateObject(self):
+        # Only do this if something is selected
+        if direct.selected:
+            # Remove the task to keep the widget attached to the object
+            taskMgr.removeTasksNamed('followSelectedNodePath')
+            # and the task to highlight the widget
+            taskMgr.removeTasksNamed('highlightWidgetTask')
+            # Set manipulation flag
+            self.fManip = 1
+            # Update object handles visibility
+            self.objectHandles.showGuides()
+            self.objectHandles.hideAllHandles()
+            self.objectHandles.showHandle(self.constraint)
+            if self.fSetCoa:
+                self.objectHandles.coaModeColor()
+
+            # Record relationship between selected nodes and widget
+            direct.selected.getWrtAll()
+
+            # hide the bbox of the selected objects during interaction
+            direct.selected.dehighlightAll()
+
+            """
+            # Push the undo dcs for the selected objects
+            direct.undo.push(
+                (direct.selected, 'dcs'))
+            """
+            # Manipulate the real object with the constraint
+            # The constraint is passed as the name of the node 
+            self.spawnManipulateObjectTask()
+
+    def spawnManipulateObjectTask(self):
+        # reset hit-pt flag
+        self.fHitInit = 1
+        # record initial offset between widget and camera
+        t = Task.Task(self.manipulateObjectTask)
+        taskMgr.spawnTaskNamed(t, 'manipulateObject')
+
+    def manipulateObjectTask(self, state):
+
+        if self.constraint:
+            type = self.constraint[2:]
+            if type == 'post':
+                self.xlate1D()
+            elif type == 'disc':
+                self.xlate2D()
+            elif type == 'ring':
+                self.rotate1D()
+        elif self.fFreeManip:
+            if self.fScaling & (not direct.fAlt):
+                # We had been scaling and changed modes,
+                # reset object handles
+                self.objectHandles.transferObjectHandlesScale()
+                self.fScaling = 0
+            if direct.fControl:
+                self.rotate2D()
+            elif direct.fAlt:
+                self.fScaling = 1
+                self.scale3D()
+            elif direct.fShift:
+                self.xlateCamXY()
+            else:
+                self.xlateCamXZ()
+        else:
+            # MRM: Needed, more elegant fallback
+            return Task.cont
+
+        if self.fSetCoa:
+            # Update coa based on current widget position
+            direct.selected.last.mCoa2Dnp.assign(
+                direct.widget.getMat(direct.selected.last)
+                )
+        else:
+            # Move the objects with the widget
+            direct.selected.moveWrtWidgetAll()
+            
+        # Continue
+        return Task.cont
+
+    def xlate1D(self):
+        # Constrained 1D Translation along widget axis
+        # Compute nearest hit point along axis and try to keep
+        # that point as close to the current mouse position as possible
+        # what point on the axis is the mouse pointing at?
+        self.hitPt.assign(self.objectHandles.getAxisIntersectPt(
+            self.constraint[:1]))
+        # use it to see how far to move the widget
+        if self.fHitInit:
+            # First time through, just record that point
+            self.fHitInit = 0
+            self.prevHit.assign(self.hitPt)
+        else:
+            # Move widget to keep hit point as close to mouse as possible
+            offset = self.hitPt - self.prevHit
+            direct.widget.setPos(direct.widget, offset)
+
+    def xlate2D(self):
+        # Constrained 2D (planar) translation
+        # Compute point of intersection of ray from eyepoint through cursor
+        # to one of the three orthogonal planes on the widget.
+        # This point tracks all subsequent mouse movements
+        self.hitPt.assign(self.objectHandles.getWidgetIntersectPt(
+            direct.widget, self.constraint[:1]))
+        # use it to see how far to move the widget
+        if self.fHitInit:
+            # First time through just record hit point
+            self.fHitInit = 0
+            self.prevHit.assign(self.hitPt)
+        else:
+	    offset = self.hitPt - self.prevHit
+            direct.widget.setPos(direct.widget, offset)
+
+
+    def xlateCamXZ(self):
+        """Constrained 2D motion parallel to the camera's image plane
+        This moves the object in the camera's XZ plane"""
+        # reset fHitInit
+        # (in case we later switch to another manipulation mode)
+        #self.fHitInit = 1
+        # Where is the widget relative to current camera view
+        vWidget2Camera = direct.widget.getPos(self.camera)
+        x = vWidget2Camera[0]
+        y = vWidget2Camera[1]
+        z = vWidget2Camera[2]
+        # Move widget (and objects) based upon mouse motion
+        # Scaled up accordingly based upon widget distance
+        chan = self.chan
+        direct.widget.setX(
+            self.camera,
+            x + 0.5 * chan.mouseDeltaX * chan.nearWidth * (y/chan.near))
+        direct.widget.setZ(
+            self.camera,
+            z + 0.5 * chan.mouseDeltaY * chan.nearHeight * (y/chan.near))
+
+    def xlateCamXY(self):
+        """Constrained 2D motion perpendicular to camera's image plane
+        This moves the object in the camera's XY plane"""
+        # Now, where is the widget relative to current camera view
+        vWidget2Camera = direct.widget.getPos(self.camera)
+        # If this is first time around, record initial y distance
+        if self.fHitInit:
+            self.fHitInit = 0
+            # Record widget offset along y
+            self.initY = vWidget2Camera[1]
+        # Extract current values
+        x = vWidget2Camera[0]
+        y = vWidget2Camera[1]
+        z = vWidget2Camera[2]
+        # Move widget (and objects) based upon mouse motion
+        # Scaled up accordingly based upon widget distance
+        chan = self.chan
+        direct.widget.setPos(
+            self.camera,
+            x + 0.5 * chan.mouseDeltaX * chan.nearWidth * (y/chan.near),
+            y + self.initY * chan.mouseDeltaY,
+            z)
+    
+    def getCrankAngle(self):
+        # Used to compute current angle of mouse (relative to the widget's
+        # origin) in screen space
+        x = self.chan.mouseX - self.rotationCenter[0]
+        y = self.chan.mouseY - self.rotationCenter[2]
+        return (180 + rad2Deg(math.atan2(y,x)))
+
+    def widgetCheck(self,type):
+        # Utility to see if we are looking at the top or bottom of
+        # a 2D planar widget or if we are looking at a 2D planar widget
+        # edge on
+        # Based upon angle between view vector from eye through the
+        # widget's origin and one of the three principle axes
+        axis = self.constraint[:1]
+        # First compute vector from eye through widget origin
+        mWidget2Cam = direct.widget.getMat(self.camera)
+        # And determine where the viewpoint is relative to widget
+        pos = VBase3(0)
+        decomposeMatrix(mWidget2Cam, VBase3(0), VBase3(0), pos,
+                        CSDefault)
+        widgetDir = Vec3(pos)
+        widgetDir.normalize()
+        # Convert specified widget axis to view space
+        if axis == 'x':
+            widgetAxis = Vec3(mWidget2Cam.xformVec(X_AXIS))
+        elif axis == 'y':
+            widgetAxis = Vec3(mWidget2Cam.xformVec(Y_AXIS))
+        elif axis == 'z':
+            widgetAxis = Vec3(mWidget2Cam.xformVec(Z_AXIS))
+        widgetAxis.normalize()
+        if type == 'top?':
+            # Check sign of angle between two vectors
+            return (widgetDir.dot(widgetAxis) < 0.)
+        elif type == 'edge?':
+            # Checking to see if we are viewing edge-on
+            # Check angle between two vectors
+            return(abs(widgetDir.dot(widgetAxis)) < .2)
+
+    def getWidgetsNearProjectionPoint(self):
+        # Find the position of the projection of the specified node path
+        # on the near plane
+        widgetOrigin = direct.widget.getPos(self.camera)
+        # project this onto near plane
+        return widgetOrigin * (self.chan.near / widgetOrigin[1])
+
+    def getScreenXY(self):
+        # Where does the widget's projection fall on the near plane
+        nearVec = self.getWidgetsNearProjectionPoint()
+        # Clamp these coordinates to visible screen
+        nearX = self.clamp(nearVec[0], self.chan.left, self.chan.right)
+        nearY = self.clamp(nearVec[2], self.chan.bottom, self.chan.top)
+        # What percentage of the distance across the screen is this?
+        percentX = (nearX - self.chan.left)/self.chan.nearWidth
+        percentY = (nearY - self.chan.bottom)/self.chan.nearHeight
+        # Map this percentage to the same -1 to 1 space as the mouse
+        screenXY = Vec3((2 * percentX) - 1.0,nearVec[1],(2 * percentY) - 1.0)
+        # Return the resulting value
+        return screenXY
+
+    def rotate1D(self):
+        # Constrained 1D rotation about the widget's main axis (X,Y, or Z)
+        # Rotation depends upon circular motion of the mouse about the
+        # projection of the widget's origin on the image plane
+        # A complete circle about the widget results in a change in
+        # orientation of 360 degrees.
+
+        # First initialize hit point/rotation angle
+        if self.fHitInit:
+            self.fHitInit = 0
+            self.rotateAxis = self.constraint[:1]
+            self.fWidgetTop = self.widgetCheck('top?')
+            self.rotationCenter = self.getScreenXY()
+            self.lastCrankAngle = self.getCrankAngle()
+            
+        # Rotate widget based on how far cursor has swung around origin
+        newAngle = self.getCrankAngle()
+        deltaAngle = self.lastCrankAngle - newAngle
+        if self.fWidgetTop:
+            deltaAngle = -1 * deltaAngle
+        if self.rotateAxis == 'x':
+            direct.widget.setP(direct.widget, deltaAngle)
+        elif self.rotateAxis == 'y':
+            direct.widget.setR(direct.widget, -deltaAngle)
+        elif self.rotateAxis == 'z':
+            direct.widget.setH(direct.widget, deltaAngle)
+        # Record crank angle for next time around
+        self.lastCrankAngle = newAngle
+
+    def relHpr(self, base, h, p, r):
+        # Compute widget2newWidget relative to base coordinate system
+        mWidget2Base = direct.widget.getMat(base)
+        mBase2NewBase = Mat4()
+        mBase2NewBase.composeMatrix(
+            UNIT_VEC, VBase3(h,p,r), ZERO_VEC,
+            CSDefault)
+        mBase2Widget = base.getMat(direct.widget)
+        mWidget2Parent = direct.widget.getMat()
+        # Compose the result
+        resultMat = mWidget2Base * mBase2NewBase
+        resultMat = resultMat * mBase2Widget
+        resultMat = resultMat * mWidget2Parent
+        # Extract and apply the hpr
+        hpr = Vec3(0)
+        decomposeMatrix(resultMat, VBase3(), hpr, VBase3(),
+                        CSDefault)
+        direct.widget.setHpr(hpr)
+
+    def rotate2D(self):
+        # Virtual trackball or arcball rotation of widget
+        # Rotation method depends upon variable dd-want-arcball
+        # Default is virtual trackball (handles 1D rotations better)
+        self.fHitInit = 1
+        tumbleRate = 360
+        # Mouse motion edge to edge of channel results in one full turn
+        self.relHpr(self.camera,
+                    self.chan.mouseDeltaX * tumbleRate,
+                    -self.chan.mouseDeltaY * tumbleRate,
+                    0)
+
+    def scale3D(self):
+        # Scale the selected node based upon up down mouse motion
+        # Mouse motion from edge to edge results in a factor of 4 scaling
+        # From midpoint to edge doubles or halves objects scale
+        if self.fHitInit:
+            self.fHitInit = 0
+            self.refNodePath.setPos(direct.widget, 0, 0, 0)
+            self.refNodePath.setHpr(self.camera, 0, 0, 0)
+            self.initScaleMag = Vec3(
+                self.objectHandles.getWidgetIntersectPt(
+                self.refNodePath, 'y')).length()
+            # record initial scale
+            self.initScale = direct.widget.getScale()
+        # Begin
+        # Scale factor is ratio current mag with init mag
+        currScale = (
+            self.initScale *
+            (self.objectHandles.getWidgetIntersectPt(
+            self.refNodePath, 'y').length() /
+             self.initScaleMag)
+            )
+        direct.widget.setScale(currScale)
+        
+    def clamp(self, val, min, max):
+        if val < min:
+            return min
+        elif val > max:
+            return max
+        else:
+            return val
+
+
+class ObjectHandles(NodePath,PandaObject):
+    def __init__(self):
+        # Initialize the superclass
+        NodePath.__init__(self)
+
+        # Load up object handles model and assign it to self
+        self.assign(loader.loadModel('models/misc/objectHandles'))
+        self.node().setName('objectHandles')
+        self.scalingNode = self.getChild(0)
+        self.scalingNode.node().setName('ohScalingNode')
+        self.ohScalingFactor = 1.0
+        # To avoid recreating a vec every frame
+        self.hitPt = Vec3(0)
+        # Get a handle on the components
+        self.xHandles = self.find('**/X')
+        self.xPostGroup = self.xHandles.find('**/x-post-group')
+        self.xPostCollision = self.xHandles.find('**/x-post')
+        self.xRingGroup = self.xHandles.find('**/x-ring-group')
+        self.xRingCollision = self.xHandles.find('**/x-ring')
+        self.xDiscGroup = self.xHandles.find('**/x-disc-group')
+        self.xDisc = self.xHandles.find('**/x-disc-visible')
+        self.xDiscCollision = self.xHandles.find('**/x-disc')
+        
+        self.yHandles = self.find('**/Y')
+        self.yPostGroup = self.yHandles.find('**/y-post-group')
+        self.yPostCollision = self.yHandles.find('**/y-post')
+        self.yRingGroup = self.yHandles.find('**/y-ring-group')
+        self.yRingCollision = self.yHandles.find('**/y-ring')
+        self.yDiscGroup = self.yHandles.find('**/y-disc-group')
+        self.yDisc = self.yHandles.find('**/y-disc-visible')
+        self.yDiscCollision = self.yHandles.find('**/y-disc')
+        
+        self.zHandles = self.find('**/Z')
+        self.zPostGroup = self.zHandles.find('**/z-post-group')
+        self.zPostCollision = self.zHandles.find('**/z-post')
+        self.zRingGroup = self.zHandles.find('**/z-ring-group')
+        self.zRingCollision = self.zHandles.find('**/z-ring')
+        self.zDiscGroup = self.zHandles.find('**/z-disc-group')
+        self.zDisc = self.zHandles.find('**/z-disc-visible')
+        self.zDiscCollision = self.zHandles.find('**/z-disc')
+
+        # Adjust visiblity, colors, and transparency
+        self.xPostCollision.hide()
+        self.xRingCollision.hide()
+        self.xDisc.setColor(1,0,0,.2)
+        self.yPostCollision.hide()
+        self.yRingCollision.hide()
+        self.yDisc.setColor(0,1,0,.2)
+        self.zPostCollision.hide()
+        self.zRingCollision.hide()
+        self.zDisc.setColor(0,0,1,.2)
+        # Augment geometry with lines
+        self.createObjectHandleLines()
+        # Create long markers to help line up in world
+        self.createGuideLines()
+        self.hideGuides()
+
+    def coaModeColor(self):
+        self.setColor(.5,.5,.5,1)
+
+    def manipModeColor(self):
+        self.clearColor()
+
+    def enableHandles(self, handles):
+        if type(handles) == types.ListType:
+            for handle in handles:
+                self.enableHandle(handle)
+        elif handles == 'x':
+            self.enableHandles(['x-post','x-ring','x-disc'])
+        elif handles == 'y':
+            self.enableHandles(['y-post','y-ring','y-disc'])
+        elif handles == 'z':
+            self.enableHandles(['z-post','z-ring','z-disc'])
+        elif handles == 'post':
+            self.enableHandles(['x-post','y-post','z-post'])
+        elif handles == 'ring':
+            self.enableHandles(['x-ring','y-ring','z-ring'])
+        elif handles == 'disc':
+            self.enableHandles(['x-disc','y-disc','z-disc'])
+        elif handles == 'all':
+            self.enableHandles(['x-post','x-ring','x-disc',
+                                'y-post','y-ring','y-disc',
+                                'z-post','z-ring','z-disc'])
+
+    def enableHandle(self, handle):
+        if handle == 'x-post':
+            self.xPostGroup.reparentTo(self.xHandles)
+        elif handle == 'x-ring':
+            self.xRingGroup.reparentTo(self.xHandles)
+        elif handle == 'x-disc':
+            self.xDiscGroup.reparentTo(self.xHandles)
+        if handle == 'y-post':
+            self.yPostGroup.reparentTo(self.yHandles)
+        elif handle == 'y-ring':
+            self.yRingGroup.reparentTo(self.yHandles)
+        elif handle == 'y-disc':
+            self.yDiscGroup.reparentTo(self.yHandles)
+        if handle == 'z-post':
+            self.zPostGroup.reparentTo(self.zHandles)
+        elif handle == 'z-ring':
+            self.zRingGroup.reparentTo(self.zHandles)
+        elif handle == 'z-disc':
+            self.zDiscGroup.reparentTo(self.zHandles)
+
+    def disableHandles(self, handles):
+        if type(handles) == types.ListType:
+            for handle in handles:
+                self.disableHandle(handle)
+        elif handles == 'x':
+            self.disableHandles(['x-post','x-ring','x-disc'])
+        elif handles == 'y':
+            self.disableHandles(['y-post','y-ring','y-disc'])
+        elif handles == 'z':
+            self.disableHandles(['z-post','z-ring','z-disc'])
+        elif handles == 'post':
+            self.disableHandles(['x-post','y-post','z-post'])
+        elif handles == 'ring':
+            self.disableHandles(['x-ring','y-ring','z-ring'])
+        elif handles == 'disc':
+            self.disableHandles(['x-disc','y-disc','z-disc'])
+        elif handles == 'all':
+            self.disableHandles(['x-post','x-ring','x-disc',
+                                 'y-post','y-ring','y-disc',
+                                 'z-post','z-ring','z-disc'])
+
+    def disableHandle(self, handle):
+        if handle == 'x-post':
+            self.xPostGroup.reparentTo(hidden)
+        elif handle == 'x-ring':
+            self.xRingGroup.reparentTo(hidden)
+        elif handle == 'x-disc':
+            self.xDiscGroup.reparentTo(hidden)
+        if handle == 'y-post':
+            self.yPostGroup.reparentTo(hidden)
+        elif handle == 'y-ring':
+            self.yRingGroup.reparentTo(hidden)
+        elif handle == 'y-disc':
+            self.yDiscGroup.reparentTo(hidden)
+        if handle == 'z-post':
+            self.zPostGroup.reparentTo(hidden)
+        elif handle == 'z-ring':
+            self.zRingGroup.reparentTo(hidden)
+        elif handle == 'z-disc':
+            self.zDiscGroup.reparentTo(hidden)
+
+    def showAllHandles(self):
+        self.xPost.show()
+        self.xRing.show()
+        self.xDisc.show()
+        self.yPost.show()
+        self.yRing.show()
+        self.yDisc.show()
+        self.zPost.show()
+        self.zRing.show()
+        self.zDisc.show()
+
+    def hideAllHandles(self):
+        self.xPost.hide()
+        self.xRing.hide()
+        self.xDisc.hide()
+        self.yPost.hide()
+        self.yRing.hide()
+        self.yDisc.hide()
+        self.zPost.hide()
+        self.zRing.hide()
+        self.zDisc.hide()
+
+    def showHandle(self, handle):
+        if handle == 'x-post':
+            self.xPost.show()
+        elif handle == 'x-ring':
+            self.xRing.show()
+        elif handle == 'x-disc':
+            self.xDisc.show()
+        elif handle == 'y-post':
+            self.yPost.show()
+        elif handle == 'y-ring':
+            self.yRing.show()
+        elif handle == 'y-disc':
+            self.yDisc.show()
+        elif handle == 'z-post':
+            self.zPost.show()
+        elif handle == 'z-ring':
+            self.zRing.show()
+        elif handle == 'z-disc':
+            self.zDisc.show()
+
+    def showGuides(self):
+        self.guideLines.show()
+
+    def hideGuides(self):
+        self.guideLines.hide()
+
+    def setScalingFactor(self, scaleFactor):
+        self.ohScalingFactor = scaleFactor
+        self.scalingNode.setScale(self.ohScalingFactor)
+
+    def getScalingFactor(self):
+        return self.scalingNode.getScale()
+
+    def transferObjectHandlesScale(self):
+        # see how much object handles have been scaled
+        ohs = self.getScale()
+        sns = self.scalingNode.getScale()
+        # Transfer this to the scaling node
+        self.scalingNode.setScale(
+            ohs[0] * sns[0],
+            ohs[1] * sns[1],
+            ohs[2] * sns[2])
+        self.setScale(1)
+
+    def multiplyScalingFactorBy(self, factor):
+        taskMgr.removeTasksNamed('resizeObjectHandles')
+        sf = self.ohScalingFactor = self.ohScalingFactor * factor
+        self.scalingNode.lerpScale(sf,sf,sf, 0.5,
+                                   blendType = 'easeInOut',
+                                   task = 'resizeObjectHandles')
+
+    def growToFit(self):
+        taskMgr.removeTasksNamed('resizeObjectHandles')
+        # Increase handles scale until they cover 30% of the min dimension
+        pos = direct.widget.getPos(direct.camera)
+        minDim = min(direct.chan.nearWidth, direct.chan.nearHeight)
+        sf = 0.15 * minDim * (pos[1]/direct.chan.near)
+        self.ohScalingFactor = sf
+        self.scalingNode.lerpScale(sf,sf,sf, 0.5,
+                                   blendType = 'easeInOut',
+                                   task = 'resizeObjectHandles')
+
+    def createObjectHandleLines(self):
+        # X post
+        self.xPost = self.xPostGroup.attachNewNode(NamedNode('x-post-visible'))
+	lines = LineNodePath(self.xPost)
+	lines.setColor(VBase4(1,0,0,1))
+	lines.setThickness(5)
+	lines.moveTo(0,0,0)
+        lines.drawTo(1.5,0,0)
+        lines.create()
+	lines = LineNodePath(self.xPost)
+	lines.setColor(VBase4(1,0,0,1))
+	lines.setThickness(1.5)
+	lines.moveTo(0,0,0)
+        lines.drawTo(-1.5,0,0)
+        lines.create()
+        
+	# X ring
+        self.xRing = self.xRingGroup.attachNewNode(NamedNode('x-ring-visible'))
+	lines = LineNodePath(self.xRing)
+	lines.setColor(VBase4(1,0,0,1))
+	lines.setThickness(3)
+	lines.moveTo(0,1,0)
+        for ang in range(15, 370, 15):
+            lines.drawTo(0,
+                          math.cos(deg2Rad(ang)),
+                          math.sin(deg2Rad(ang)))
+        lines.create()
+        
+        # Y post
+        self.yPost = self.yPostGroup.attachNewNode(NamedNode('y-post-visible'))
+	lines = LineNodePath(self.yPost)
+	lines.setColor(VBase4(0,1,0,1))
+	lines.setThickness(5)
+	lines.moveTo(0,0,0)
+        lines.drawTo(0,1.5,0)
+        lines.create()
+	lines = LineNodePath(self.yPost)
+	lines.setColor(VBase4(0,1,0,1))
+	lines.setThickness(1.5)
+	lines.moveTo(0,0,0)
+        lines.drawTo(0,-1.5,0)
+        lines.create()
+        
+	# Y ring
+        self.yRing = self.yRingGroup.attachNewNode(NamedNode('y-ring-visible'))
+	lines = LineNodePath(self.yRing)
+	lines.setColor(VBase4(0,1,0,1))
+	lines.setThickness(3)
+	lines.moveTo(1,0,0)
+        for ang in range(15, 370, 15):
+            lines.drawTo(math.cos(deg2Rad(ang)),
+                          0,
+                          math.sin(deg2Rad(ang)))
+        lines.create()
+
+        # Z post
+        self.zPost = self.zPostGroup.attachNewNode(NamedNode('z-post-visible'))
+	lines = LineNodePath(self.zPost)
+	lines.setColor(VBase4(0,0,1,1))
+	lines.setThickness(5)
+	lines.moveTo(0,0,0)
+        lines.drawTo(0,0,1.5)
+        lines.create()
+	lines = LineNodePath(self.zPost)
+	lines.setColor(VBase4(0,0,1,1))
+	lines.setThickness(1.5)
+	lines.moveTo(0,0,0)
+        lines.drawTo(0,0,-1.5)
+        lines.create()
+        
+	# Z ring
+        self.zRing = self.zRingGroup.attachNewNode(NamedNode('z-ring-visible'))
+	lines = LineNodePath(self.zRing)
+	lines.setColor(VBase4(0,0,1,1))
+	lines.setThickness(3)
+	lines.moveTo(1,0,0)
+        for ang in range(15, 370, 15):
+            lines.drawTo(math.cos(deg2Rad(ang)),
+                          math.sin(deg2Rad(ang)),
+                          0)
+        lines.create()
+
+    def createGuideLines(self):
+        self.guideLines = self.attachNewNode(NamedNode('guideLines'))
+        # X guide lines
+	lines = LineNodePath(self.guideLines)
+	lines.setColor(VBase4(1,0,0,1))
+	lines.setThickness(0.5)
+	lines.moveTo(-500,0,0)
+        lines.drawTo(500,0,0)
+        lines.create()
+        lines.node().setName('x-guide')
+
+        # Y guide lines
+	lines = LineNodePath(self.guideLines)
+	lines.setColor(VBase4(0,1,0,1))
+	lines.setThickness(0.5)
+	lines.moveTo(0,-500,0)
+        lines.drawTo(0,500,0)
+        lines.create()
+        lines.node().setName('y-guide')
+
+        # Z guide lines
+	lines = LineNodePath(self.guideLines)
+	lines.setColor(VBase4(0,0,1,1))
+	lines.setThickness(0.5)
+	lines.moveTo(0,0,-500)
+        lines.drawTo(0,0,500)
+        lines.create()
+        lines.node().setName('z-guide')
+
+    def getAxisIntersectPt(self, axis):
+        # Calc the xfrom from camera to widget
+        mCam2Widget = direct.camera.getMat(direct.widget)
+        lineDir = Vec3(mCam2Widget.xformVec(direct.chan.nearVec))
+        lineDir.normalize()
+        # And determine where the viewpoint is relative to widget
+        lineOrigin = VBase3(0)
+        decomposeMatrix(mCam2Widget, VBase3(0), VBase3(0), lineOrigin,
+                        CSDefault)
+        # Now see where this hits the plane containing the 1D motion axis.
+        # Pick the intersection plane most normal to the intersection ray
+        # by comparing lineDir with plane normals.  The plane with the
+        # largest dotProduct is most "normal"
+        if axis == 'x':
+            if (abs(lineDir.dot(Y_AXIS)) > abs(lineDir.dot(Z_AXIS))):
+                self.hitPt.assign(
+                    planeIntersect(lineOrigin, lineDir, ORIGIN, Y_AXIS))
+            else:
+                self.hitPt.assign(
+                    planeIntersect(lineOrigin, lineDir, ORIGIN, Z_AXIS))
+            # We really only care about the nearest point on the axis
+            self.hitPt.setY(0)
+            self.hitPt.setZ(0)
+        elif axis == 'y':
+            if (abs(lineDir.dot(X_AXIS)) > abs(lineDir.dot(Z_AXIS))):
+                self.hitPt.assign(
+                    planeIntersect(lineOrigin, lineDir, ORIGIN, X_AXIS))
+            else:
+                self.hitPt.assign(
+                    planeIntersect(lineOrigin, lineDir, ORIGIN, Z_AXIS))
+            # We really only care about the nearest point on the axis
+            self.hitPt.setX(0)
+            self.hitPt.setZ(0)
+        elif axis == 'z':
+            if (abs(lineDir.dot(X_AXIS)) > abs(lineDir.dot(Y_AXIS))):
+                self.hitPt.assign(
+                    planeIntersect(lineOrigin, lineDir, ORIGIN, X_AXIS))
+            else:
+                self.hitPt.assign(
+                    planeIntersect(lineOrigin, lineDir, ORIGIN, Y_AXIS))
+            # We really only care about the nearest point on the axis
+            self.hitPt.setX(0)
+            self.hitPt.setY(0)
+        return self.hitPt
+
+    def getWidgetIntersectPt(self, nodePath, plane):
+        # Find out the point of interection of the ray passing though the mouse
+        # with the plane containing the 2D xlation or 1D rotation widgets
+
+        # Calc the xfrom from camera to the nodePath
+        mCam2NodePath = direct.camera.getMat(nodePath)
+    
+        # And determine where the viewpoint is relative to widget
+        lineOrigin = VBase3(0)
+        decomposeMatrix(mCam2NodePath, VBase3(0), VBase3(0), lineOrigin,
+                        CSDefault)
+        
+        # Next we find the vector from viewpoint to the widget through
+        # the mouse's position on near plane.
+        # This defines the intersection ray
+        lineDir = Vec3(mCam2NodePath.xformVec(direct.chan.nearVec))
+        lineDir.normalize()
+        # Find the hit point
+        if plane == 'x':
+            self.hitPt.assign(planeIntersect(
+                lineOrigin, lineDir, ORIGIN, X_AXIS))
+        elif plane == 'y':
+            self.hitPt.assign(planeIntersect(
+                lineOrigin, lineDir, ORIGIN, Y_AXIS))
+        elif plane == 'z':
+            self.hitPt.assign(planeIntersect(
+                lineOrigin, lineDir, ORIGIN, Z_AXIS))
+        return self.hitPt
+
+
+

+ 379 - 380
direct/src/directutil/DirectSelection.py

@@ -1,380 +1,379 @@
-from PandaObject import *
-from DirectGeometry import *
-from DirectSelection import *
-
-
-class DirectNodePath(NodePath):
-    # A node path augmented with info, bounding box, and utility methods
-    def __init__(self, nodePath):
-        # Initialize the superclass
-        NodePath.__init__(self)
-        self.assign(nodePath)
-        # Get a reasonable name
-        self.name = self.getName()
-        # Create a bounding box
-        self.bbox = DirectBoundingBox(self)
-        center = self.bbox.getCenter()
-        # Create matrix to hold the offset between the nodepath
-        # and its center of action (COA)
-        self.mCoa2Dnp = Mat4()
-        self.mCoa2Dnp.assign(Mat4.identMat())
-        # self.mCoa2Dnp.setRow(3, Vec4(center[0], center[1], center[2], 1))
-        # Transform from nodePath to widget
-        self.mDnp2Widget = Mat4()
-        self.mDnp2Widget.assign(Mat4.identMat())
-
-    def highlight(self):
-        self.bbox.show()
-
-    def dehighlight(self):
-        self.bbox.hide()
-
-    def getCenter(self):
-        return self.bbox.getCenter()
-
-    def getRadius(self):
-        return self.bbox.getRadius()
-
-    def getMin(self):
-        return self.bbox.getMin()
-
-    def getMax(self):
-        return self.bbox.getMax()
-
-    def __repr__(self):
-        return ('NodePath:\t%s\n' % self.name)
-
-
-class SelectedNodePaths(PandaObject):
-    def __init__(self,direct):
-        self.direct = direct
-        self.selectedDict = {}
-        self.deselectedDict = {}
-        self.last = None
-
-    def select(self, nodePath, fMultiSelect = 0):
-	# Do nothing if nothing selected
-        if not nodePath:
-            print 'Nothing selected!!'
-            return None
-        
-	# Reset selected objects and highlight if multiSelect is false
-        if not fMultiSelect:
-            self.deselectAll()
-
-        # Get this pointer
-        id = nodePath.id()
-        # First see if its already in the selected dictionary
-        dnp = self.selectedDict.get(id, None)
-        # If so, we're done
-        if not dnp:
-            # See if it is in the deselected dictionary
-            dnp = self.deselectedDict.get(id, None)
-            if dnp:
-                # It has been previously selected:
-                # Show its bounding box
-                dnp.highlight()
-                # Remove it from the deselected dictionary
-                del(self.deselectedDict[id])
-            else:
-                # Didn't find it, create a new selectedNodePath instance
-                dnp = DirectNodePath(nodePath)
-                # Show its bounding box
-                dnp.highlight()
-            # Add it to the selected dictionary
-            self.selectedDict[dnp.id()] = dnp
-        # And update last
-        self.last = dnp
-        return dnp
-
-    def deselect(self, nodePath):
-        # Get this pointer
-        id = nodePath.id()
-        # See if it is in the selected dictionary
-        dnp = self.selectedDict.get(id, None)
-        if dnp:
-            # It was selected:
-            # Hide its bounding box
-            dnp.dehighlight()
-            # Remove it from the selected dictionary
-            del(self.selectedDict[id])
-            # And keep track of it in the deselected dictionary
-            self.deselectedDict[id] = dnp
-        return dnp
-
-    def selectedAsList(self):
-        list = []
-        for key in self.selectedDict.keys():
-            list.append(self.selectedDict[key])
-        return list
-
-    def __getitem__(self,index):
-        return self.selectedAsList()[index]
-
-    def deselectedAsList(self):
-        list = []
-        for key in self.deselectedDict.keys():
-            list.append(self.deselectedDict[key])
-        return list
-
-    def forEachSelectedNodePathDo(self, func):
-        duplicateKeys = self.selectedDict.keys()[:]
-        for key in duplicateKeys:
-            func(self.selectedDict[key])
-
-    def forEachDeselectedNodePathDo(self, func):
-        duplicateKeys = self.deselectedDict.keys()[:]
-        for key in duplicateKeys:
-            func(self.deselectedDict[key])
-
-    def getWrtAll(self):
-        self.forEachSelectedNodePathDo(self.getWrt)
-
-    def getWrt(self, nodePath):
-        nodePath.mDnp2Widget.assign(nodePath.getMat(self.direct.widget))
-
-    def moveWrtWidgetAll(self):
-        self.forEachSelectedNodePathDo(self.moveWrtWidget)
-
-    def moveWrtWidget(self, nodePath):
-        nodePath.setMat(self.direct.widget, nodePath.mDnp2Widget)
-
-    def deselectAll(self):
-        self.forEachSelectedNodePathDo(self.deselect)
-
-    def highlightAll(self):
-        self.forEachSelectedNodePathDo(DirectNodePath.highlight)
-
-    def dehighlightAll(self):
-        self.forEachSelectedNodePathDo(DirectNodePath.dehighlight)
-
-    def removeSelected(self):
-	selected = self.dnp.last
-        if selected:
-            selected.remove()
-        
-    def removeAll(self):
-	# Remove all selected nodePaths from the Scene Graph
-        self.forEachSelectedNodePathDo(NodePath.remove)
-
-    def toggleVizSelected(self):
-	selected = self.dnp.last
-        # Toggle visibility of selected node paths
-        if selected:
-            selected.toggleViz()
-
-    def toggleVizAll(self):
-        # Toggle viz for all selected node paths
-        self.forEachSelectedNodePathDo(NodePath.toggleViz)
-
-    def isolateSelected(self):
-	selected = self.dnp.last
-        if selected:
-            selected.isolate()
-
-    def getDirectNodePath(self, nodePath):
-        # Get this pointer
-        id = nodePath.id()
-        # First check selected dict
-        dnp = self.selectedDict.get(id, None)
-        if dnp:
-            return dnp
-        # Otherwise return result of deselected search
-        return self.selectedDict.get(id, None)
-
-    def getNumSelected(self):
-        return len(self.selectedDict.keys())
-
-
-class DirectBoundingBox:
-    def __init__(self, nodePath):
-        # Record the node path
-        self.nodePath = nodePath
-        # Compute bounds, min, max, etc.
-        self.computeBounds()
-        # Generate the bounding box
-        self.lines = self.createBBoxLines()
-
-    def computeBounds(self):
-        self.bounds = self.nodePath.getBounds()
-        if self.bounds.isEmpty():
-            self.center = Point3(0)
-            self.radius = 1.0
-        else:
-            self.center = self.bounds.getCenter()
-            self.radius = self.bounds.getRadius()
-        self.min = Point3(self.center - Point3(self.radius))
-        self.max = Point3(self.center + Point3(self.radius))
-        
-    def createBBoxLines(self):
-        # Create a line segments object for the bbox
-        lines = LineNodePath(hidden)
-        lines.node().setName('bboxLines')
-        lines.setColor( VBase4( 1., 0., 0., 1. ) )
-	lines.setThickness( 0.5 )
-
-        minX = self.min[0]
-        minY = self.min[1]
-        minZ = self.min[2]
-        maxX = self.max[0]
-        maxY = self.max[1]
-        maxZ = self.max[2]
-        
-        # Bottom face
-	lines.moveTo( minX, minY, minZ )
-	lines.drawTo( maxX, minY, minZ )
-	lines.drawTo( maxX, maxY, minZ )
-	lines.drawTo( minX, maxY, minZ )
-	lines.drawTo( minX, minY, minZ )
-
-	# Front Edge/Top face
-	lines.drawTo( minX, minY, maxZ )
-	lines.drawTo( maxX, minY, maxZ )
-	lines.drawTo( maxX, maxY, maxZ )
-	lines.drawTo( minX, maxY, maxZ )
-	lines.drawTo( minX, minY, maxZ )
-
-	# Three remaining edges
-	lines.moveTo( maxX, minY, minZ )
-	lines.drawTo( maxX, minY, maxZ )
-	lines.moveTo( maxX, maxY, minZ )
-	lines.drawTo( maxX, maxY, maxZ )
-	lines.moveTo( minX, maxY, minZ )
-	lines.drawTo( minX, maxY, maxZ )
-
-        # Create and return bbox lines
-	lines.create()
-        return lines
-
-    def updateBBoxLines(self):
-        ls = self.lines.lineSegs
-        
-        minX = self.min[0]
-        minY = self.min[1]
-        minZ = self.min[2]
-        maxX = self.max[0]
-        maxY = self.max[1]
-        maxZ = self.max[2]
-        
-        # Bottom face
-	ls.setVertex( 0, minX, minY, minZ )
-	ls.setVertex( 1, maxX, minY, minZ )
-	ls.setVertex( 2, maxX, maxY, minZ )
-	ls.setVertex( 3, minX, maxY, minZ )
-	ls.setVertex( 4, minX, minY, minZ )
-
-	# Front Edge/Top face
-	ls.setVertex( 5, minX, minY, maxZ )
-	ls.setVertex( 6, maxX, minY, maxZ )
-	ls.setVertex( 7, maxX, maxY, maxZ )
-	ls.setVertex( 8, minX, maxY, maxZ )
-	ls.setVertex( 9, minX, minY, maxZ )
-
-	# Three remaining edges
-	ls.setVertex( 10, maxX, minY, minZ )
-	ls.setVertex( 11, maxX, minY, maxZ )
-	ls.setVertex( 12, maxX, maxY, minZ )
-	ls.setVertex( 13, maxX, maxY, maxZ )
-	ls.setVertex( 14, minX, maxY, minZ )
-	ls.setVertex( 15, minX, maxY, maxZ )
-
-    def getBounds(self):
-        # Get a node path's bounds
-        nodeBounds = self.nodePath.node().getBound()
-        for child in self.nodePath.getChildrenAsList():
-            nodeBounds.extendBy(child.getBottomArc().getBound())
-            return nodeBounds.makeCopy()
-
-    def show(self):
-        self.lines.reparentTo(self.nodePath)
-
-    def hide(self):
-        self.lines.reparentTo(hidden)
-        
-    def getCenter(self):
-        return self.center
-
-    def getRadius(self):
-        return self.radius
-
-    def getMin(self):
-        return self.min
-
-    def getMax(self):
-        return self.max
-
-    def vecAsString(self, vec):
-        return '%.2f %.2f %.2f' % (vec[0], vec[1], vec[2])
-
-    def __repr__(self):
-        return (`self.__class__` + 
-                '\nNodePath:\t%s\n' % self.nodePath.getName() +
-                'Min:\t\t%s\n' % self.vecAsString(self.min) +
-                'Max:\t\t%s\n' % self.vecAsString(self.max) +
-                'Center:\t\t%s\n' % self.vecAsString(self.center) +
-                'Radius:\t\t%.2f' % self.radius
-                )
-
-
-class SelectionRay:
-    def __init__(self, camera):
-        # Record the camera associated with this selection ray
-        self.camera = camera
-        # Create a collision node
-        self.rayCollisionNodePath = camera.attachNewNode( CollisionNode() )
-        # Don't pay the penalty of drawing this collision ray
-        self.rayCollisionNodePath.hide()
-        self.rayCollisionNode = self.rayCollisionNodePath.node()
-        # Intersect with geometry to begin with
-        self.collideWithGeom()
-        # Create a collision ray
-        self.ray = CollisionRay()
-        # Add the ray to the collision Node
-        self.rayCollisionNode.addSolid( self.ray )
-        # Create a queue to hold the collision results
-        self.cq = CollisionHandlerQueue()
-        self.numEntries = 0
-        # And a traverser to do the actual collision tests
-        self.ct = CollisionTraverser( RenderRelation.getClassType() )
-        # Let the traverser know about the queue and the collision node
-        self.ct.addCollider(self.rayCollisionNode, self.cq )
-
-    def pickGeom(self, targetNodePath, mouseX, mouseY):
-        self.collideWithGeom()
-        return self.pick(targetNodePath, mouseX, mouseY)
-
-    def pickWidget(self, targetNodePath, mouseX, mouseY):
-        self.collideWithWidget()
-        return self.pick(targetNodePath, mouseX, mouseY)
-
-    def pick(self, targetNodePath, mouseX, mouseY):
-        # Determine ray direction based upon the mouse coordinates
-        # Note! This has to be a cam object (of type ProjectionNode)
-        self.ray.setProjection( base.cam.node(), mouseX, mouseY )
-        self.ct.traverse( targetNodePath.node() )
-        self.numEntries = self.cq.getNumEntries()
-        self.cq.sortEntries()
-        return self.numEntries
-
-    def collideWithGeom(self):
-        self.rayCollisionNode.setIntoCollideMask(BitMask32().allOff())
-        self.rayCollisionNode.setFromCollideMask(BitMask32().allOff())
-        self.rayCollisionNode.setCollideGeom(1)
-
-    def collideWithWidget(self):
-        self.rayCollisionNode.setIntoCollideMask(BitMask32().allOff())
-        mask = BitMask32()
-        mask.setWord(0x80000000)
-        self.rayCollisionNode.setFromCollideMask(mask)
-        self.rayCollisionNode.setCollideGeom(0)
-
-    def objectToHitPt(self, index):
-        return self.cq.getEntry(index).getIntoIntersectionPoint()
-
-    def camToHitPt(self, index):
-        # Get the specified entry
-        entry = self.cq.getEntry(index)
-        hitPt = entry.getIntoIntersectionPoint()
-        # Convert point from object local space to camera space
-        return entry.getInvWrtSpace().xformPoint(hitPt)
-
+from PandaObject import *
+from DirectGeometry import *
+from DirectSelection import *
+
+
+class DirectNodePath(NodePath):
+    # A node path augmented with info, bounding box, and utility methods
+    def __init__(self, nodePath):
+        # Initialize the superclass
+        NodePath.__init__(self)
+        self.assign(nodePath)
+        # Get a reasonable name
+        self.name = self.getName()
+        # Create a bounding box
+        self.bbox = DirectBoundingBox(self)
+        center = self.bbox.getCenter()
+        # Create matrix to hold the offset between the nodepath
+        # and its center of action (COA)
+        self.mCoa2Dnp = Mat4()
+        self.mCoa2Dnp.assign(Mat4.identMat())
+        # self.mCoa2Dnp.setRow(3, Vec4(center[0], center[1], center[2], 1))
+        # Transform from nodePath to widget
+        self.mDnp2Widget = Mat4()
+        self.mDnp2Widget.assign(Mat4.identMat())
+
+    def highlight(self):
+        self.bbox.show()
+
+    def dehighlight(self):
+        self.bbox.hide()
+
+    def getCenter(self):
+        return self.bbox.getCenter()
+
+    def getRadius(self):
+        return self.bbox.getRadius()
+
+    def getMin(self):
+        return self.bbox.getMin()
+
+    def getMax(self):
+        return self.bbox.getMax()
+
+    def __repr__(self):
+        return ('NodePath:\t%s\n' % self.name)
+
+
+class SelectedNodePaths(PandaObject):
+    def __init__(self):
+        self.selectedDict = {}
+        self.deselectedDict = {}
+        self.last = None
+
+    def select(self, nodePath, fMultiSelect = 0):
+	# Do nothing if nothing selected
+        if not nodePath:
+            print 'Nothing selected!!'
+            return None
+        
+	# Reset selected objects and highlight if multiSelect is false
+        if not fMultiSelect:
+            self.deselectAll()
+
+        # Get this pointer
+        id = nodePath.id()
+        # First see if its already in the selected dictionary
+        dnp = self.selectedDict.get(id, None)
+        # If so, we're done
+        if not dnp:
+            # See if it is in the deselected dictionary
+            dnp = self.deselectedDict.get(id, None)
+            if dnp:
+                # It has been previously selected:
+                # Show its bounding box
+                dnp.highlight()
+                # Remove it from the deselected dictionary
+                del(self.deselectedDict[id])
+            else:
+                # Didn't find it, create a new selectedNodePath instance
+                dnp = DirectNodePath(nodePath)
+                # Show its bounding box
+                dnp.highlight()
+            # Add it to the selected dictionary
+            self.selectedDict[dnp.id()] = dnp
+        # And update last
+        self.last = dnp
+        return dnp
+
+    def deselect(self, nodePath):
+        # Get this pointer
+        id = nodePath.id()
+        # See if it is in the selected dictionary
+        dnp = self.selectedDict.get(id, None)
+        if dnp:
+            # It was selected:
+            # Hide its bounding box
+            dnp.dehighlight()
+            # Remove it from the selected dictionary
+            del(self.selectedDict[id])
+            # And keep track of it in the deselected dictionary
+            self.deselectedDict[id] = dnp
+        return dnp
+
+    def selectedAsList(self):
+        list = []
+        for key in self.selectedDict.keys():
+            list.append(self.selectedDict[key])
+        return list
+
+    def __getitem__(self,index):
+        return self.selectedAsList()[index]
+
+    def deselectedAsList(self):
+        list = []
+        for key in self.deselectedDict.keys():
+            list.append(self.deselectedDict[key])
+        return list
+
+    def forEachSelectedNodePathDo(self, func):
+        duplicateKeys = self.selectedDict.keys()[:]
+        for key in duplicateKeys:
+            func(self.selectedDict[key])
+
+    def forEachDeselectedNodePathDo(self, func):
+        duplicateKeys = self.deselectedDict.keys()[:]
+        for key in duplicateKeys:
+            func(self.deselectedDict[key])
+
+    def getWrtAll(self):
+        self.forEachSelectedNodePathDo(self.getWrt)
+
+    def getWrt(self, nodePath):
+        nodePath.mDnp2Widget.assign(nodePath.getMat(direct.widget))
+
+    def moveWrtWidgetAll(self):
+        self.forEachSelectedNodePathDo(self.moveWrtWidget)
+
+    def moveWrtWidget(self, nodePath):
+        nodePath.setMat(direct.widget, nodePath.mDnp2Widget)
+
+    def deselectAll(self):
+        self.forEachSelectedNodePathDo(self.deselect)
+
+    def highlightAll(self):
+        self.forEachSelectedNodePathDo(DirectNodePath.highlight)
+
+    def dehighlightAll(self):
+        self.forEachSelectedNodePathDo(DirectNodePath.dehighlight)
+
+    def removeSelected(self):
+	selected = self.dnp.last
+        if selected:
+            selected.remove()
+        
+    def removeAll(self):
+	# Remove all selected nodePaths from the Scene Graph
+        self.forEachSelectedNodePathDo(NodePath.remove)
+
+    def toggleVizSelected(self):
+	selected = self.dnp.last
+        # Toggle visibility of selected node paths
+        if selected:
+            selected.toggleViz()
+
+    def toggleVizAll(self):
+        # Toggle viz for all selected node paths
+        self.forEachSelectedNodePathDo(NodePath.toggleViz)
+
+    def isolateSelected(self):
+	selected = self.dnp.last
+        if selected:
+            selected.isolate()
+
+    def getDirectNodePath(self, nodePath):
+        # Get this pointer
+        id = nodePath.id()
+        # First check selected dict
+        dnp = self.selectedDict.get(id, None)
+        if dnp:
+            return dnp
+        # Otherwise return result of deselected search
+        return self.selectedDict.get(id, None)
+
+    def getNumSelected(self):
+        return len(self.selectedDict.keys())
+
+
+class DirectBoundingBox:
+    def __init__(self, nodePath):
+        # Record the node path
+        self.nodePath = nodePath
+        # Compute bounds, min, max, etc.
+        self.computeBounds()
+        # Generate the bounding box
+        self.lines = self.createBBoxLines()
+
+    def computeBounds(self):
+        self.bounds = self.nodePath.getBounds()
+        if self.bounds.isEmpty():
+            self.center = Point3(0)
+            self.radius = 1.0
+        else:
+            self.center = self.bounds.getCenter()
+            self.radius = self.bounds.getRadius()
+        self.min = Point3(self.center - Point3(self.radius))
+        self.max = Point3(self.center + Point3(self.radius))
+        
+    def createBBoxLines(self):
+        # Create a line segments object for the bbox
+        lines = LineNodePath(hidden)
+        lines.node().setName('bboxLines')
+        lines.setColor( VBase4( 1., 0., 0., 1. ) )
+	lines.setThickness( 0.5 )
+
+        minX = self.min[0]
+        minY = self.min[1]
+        minZ = self.min[2]
+        maxX = self.max[0]
+        maxY = self.max[1]
+        maxZ = self.max[2]
+        
+        # Bottom face
+	lines.moveTo( minX, minY, minZ )
+	lines.drawTo( maxX, minY, minZ )
+	lines.drawTo( maxX, maxY, minZ )
+	lines.drawTo( minX, maxY, minZ )
+	lines.drawTo( minX, minY, minZ )
+
+	# Front Edge/Top face
+	lines.drawTo( minX, minY, maxZ )
+	lines.drawTo( maxX, minY, maxZ )
+	lines.drawTo( maxX, maxY, maxZ )
+	lines.drawTo( minX, maxY, maxZ )
+	lines.drawTo( minX, minY, maxZ )
+
+	# Three remaining edges
+	lines.moveTo( maxX, minY, minZ )
+	lines.drawTo( maxX, minY, maxZ )
+	lines.moveTo( maxX, maxY, minZ )
+	lines.drawTo( maxX, maxY, maxZ )
+	lines.moveTo( minX, maxY, minZ )
+	lines.drawTo( minX, maxY, maxZ )
+
+        # Create and return bbox lines
+	lines.create()
+        return lines
+
+    def updateBBoxLines(self):
+        ls = self.lines.lineSegs
+        
+        minX = self.min[0]
+        minY = self.min[1]
+        minZ = self.min[2]
+        maxX = self.max[0]
+        maxY = self.max[1]
+        maxZ = self.max[2]
+        
+        # Bottom face
+	ls.setVertex( 0, minX, minY, minZ )
+	ls.setVertex( 1, maxX, minY, minZ )
+	ls.setVertex( 2, maxX, maxY, minZ )
+	ls.setVertex( 3, minX, maxY, minZ )
+	ls.setVertex( 4, minX, minY, minZ )
+
+	# Front Edge/Top face
+	ls.setVertex( 5, minX, minY, maxZ )
+	ls.setVertex( 6, maxX, minY, maxZ )
+	ls.setVertex( 7, maxX, maxY, maxZ )
+	ls.setVertex( 8, minX, maxY, maxZ )
+	ls.setVertex( 9, minX, minY, maxZ )
+
+	# Three remaining edges
+	ls.setVertex( 10, maxX, minY, minZ )
+	ls.setVertex( 11, maxX, minY, maxZ )
+	ls.setVertex( 12, maxX, maxY, minZ )
+	ls.setVertex( 13, maxX, maxY, maxZ )
+	ls.setVertex( 14, minX, maxY, minZ )
+	ls.setVertex( 15, minX, maxY, maxZ )
+
+    def getBounds(self):
+        # Get a node path's bounds
+        nodeBounds = self.nodePath.node().getBound()
+        for child in self.nodePath.getChildrenAsList():
+            nodeBounds.extendBy(child.getBottomArc().getBound())
+            return nodeBounds.makeCopy()
+
+    def show(self):
+        self.lines.reparentTo(self.nodePath)
+
+    def hide(self):
+        self.lines.reparentTo(hidden)
+        
+    def getCenter(self):
+        return self.center
+
+    def getRadius(self):
+        return self.radius
+
+    def getMin(self):
+        return self.min
+
+    def getMax(self):
+        return self.max
+
+    def vecAsString(self, vec):
+        return '%.2f %.2f %.2f' % (vec[0], vec[1], vec[2])
+
+    def __repr__(self):
+        return (`self.__class__` + 
+                '\nNodePath:\t%s\n' % self.nodePath.getName() +
+                'Min:\t\t%s\n' % self.vecAsString(self.min) +
+                'Max:\t\t%s\n' % self.vecAsString(self.max) +
+                'Center:\t\t%s\n' % self.vecAsString(self.center) +
+                'Radius:\t\t%.2f' % self.radius
+                )
+
+
+class SelectionRay:
+    def __init__(self, camera):
+        # Record the camera associated with this selection ray
+        self.camera = camera
+        # Create a collision node
+        self.rayCollisionNodePath = camera.attachNewNode( CollisionNode() )
+        # Don't pay the penalty of drawing this collision ray
+        self.rayCollisionNodePath.hide()
+        self.rayCollisionNode = self.rayCollisionNodePath.node()
+        # Intersect with geometry to begin with
+        self.collideWithGeom()
+        # Create a collision ray
+        self.ray = CollisionRay()
+        # Add the ray to the collision Node
+        self.rayCollisionNode.addSolid( self.ray )
+        # Create a queue to hold the collision results
+        self.cq = CollisionHandlerQueue()
+        self.numEntries = 0
+        # And a traverser to do the actual collision tests
+        self.ct = CollisionTraverser( RenderRelation.getClassType() )
+        # Let the traverser know about the queue and the collision node
+        self.ct.addCollider(self.rayCollisionNode, self.cq )
+
+    def pickGeom(self, targetNodePath, mouseX, mouseY):
+        self.collideWithGeom()
+        return self.pick(targetNodePath, mouseX, mouseY)
+
+    def pickWidget(self, targetNodePath, mouseX, mouseY):
+        self.collideWithWidget()
+        return self.pick(targetNodePath, mouseX, mouseY)
+
+    def pick(self, targetNodePath, mouseX, mouseY):
+        # Determine ray direction based upon the mouse coordinates
+        # Note! This has to be a cam object (of type ProjectionNode)
+        self.ray.setProjection( base.cam.node(), mouseX, mouseY )
+        self.ct.traverse( targetNodePath.node() )
+        self.numEntries = self.cq.getNumEntries()
+        self.cq.sortEntries()
+        return self.numEntries
+
+    def collideWithGeom(self):
+        self.rayCollisionNode.setIntoCollideMask(BitMask32().allOff())
+        self.rayCollisionNode.setFromCollideMask(BitMask32().allOff())
+        self.rayCollisionNode.setCollideGeom(1)
+
+    def collideWithWidget(self):
+        self.rayCollisionNode.setIntoCollideMask(BitMask32().allOff())
+        mask = BitMask32()
+        mask.setWord(0x80000000)
+        self.rayCollisionNode.setFromCollideMask(mask)
+        self.rayCollisionNode.setCollideGeom(0)
+
+    def objectToHitPt(self, index):
+        return self.cq.getEntry(index).getIntoIntersectionPoint()
+
+    def camToHitPt(self, index):
+        # Get the specified entry
+        entry = self.cq.getEntry(index)
+        hitPt = entry.getIntoIntersectionPoint()
+        # Convert point from object local space to camera space
+        return entry.getInvWrtSpace().xformPoint(hitPt)
+

+ 8 - 4
direct/src/directutil/DirectSession.py

@@ -5,10 +5,14 @@ from DirectSelection import *
 from DirectGrid import *
 from DirectGeometry import *
 import OnscreenText
+import __builtin__
 
 class DirectSession(PandaObject):
 
     def __init__(self):
+        # Establish a global pointer to the direct object early on
+        # so dependant classes can access it in their code
+        __builtin__.direct = self
         self.contextList = []
         self.iRayList = []
         for camera in base.cameraList:
@@ -17,14 +21,14 @@ class DirectSession(PandaObject):
         self.chan = self.getChanData(0)
         self.camera = base.cameraList[0]
 
-        self.cameraControl = DirectCameraControl(self)
-        self.manipulationControl = DirectManipulationControl(self)
+        self.cameraControl = DirectCameraControl()
+        self.manipulationControl = DirectManipulationControl()
         self.useObjectHandles()
-        self.grid = DirectGrid(self)
+        self.grid = DirectGrid()
         self.grid.disable()
 
         # Initialize the collection of selected nodePaths
-        self.selected = SelectedNodePaths(self)
+        self.selected = SelectedNodePaths()
 
         self.readout = OnscreenText.OnscreenText( '', 0.1, -0.95 )
         # self.readout.textNode.setCardColor(0.5, 0.5, 0.5, 0.5)

+ 11 - 10
direct/src/directutil/DirectSessionGlobal.py

@@ -1,10 +1,11 @@
-from ShowBaseGlobal import *
-
-# If specified in the user's Configrc, create the direct session
-if base.wantDIRECT:
-    from DirectSession import *
-    direct = base.direct = DirectSession()
-else:
-    # Otherwise set the values to None
-    direct = base.direct = None
-
+from ShowBaseGlobal import *
+import __builtin__
+
+# If specified in the user's Configrc, create the direct session
+if base.wantDIRECT:
+    from DirectSession import *
+    __builtin__.direct = base.direct = DirectSession()
+else:
+    # Otherwise set the values to None
+    __builtin__.direct = base.direct = None
+

+ 10 - 9
direct/src/showbase/ShowBaseGlobal.py

@@ -1,15 +1,16 @@
 """instantiate global ShowBase object"""
 
 from ShowBase import *
+import __builtin__
 
-base = ShowBase()
+__builtin__.base = ShowBase()
 
 # Make some global aliases for convenience
-render2d = base.render2d
-render = base.render
-hidden = base.hidden
-camera = base.camera
-loader = base.loader
-ostream = Notify.out()
-run = base.run
-tkroot = base.tkroot
+__builtin__.render2d = base.render2d
+__builtin__.render = base.render
+__builtin__.hidden = base.hidden
+__builtin__.camera = base.camera
+__builtin__.loader = base.loader
+__builtin__.ostream = Notify.out()
+__builtin__.run = base.run
+__builtin__.tkroot = base.tkroot