Browse Source

*** empty log message ***

Mark Mine 25 years ago
parent
commit
2524515269

+ 14 - 7
direct/src/directdevices/DirectDeviceManager.py

@@ -96,19 +96,26 @@ class DirectAnalogs(AnalogNode, PandaObject):
     def disable(self):
     def disable(self):
         self.nodePath.reparentTo(base.dataUnused)
         self.nodePath.reparentTo(base.dataUnused)
     
     
-    def normalize(self, val, min = -1, max = -1):
+    def normalize(self, val, minVal = -1, maxVal = -1):
+        # First record sign
+        if val < 0:
+            sign = -1
+        else:
+            sign = 1
+        # Zero out values in deadband
+        val = sign * max(abs(val) - ANALOG_DEADBAND, 0.0)
+        # Now clamp value between minVal and maxVal
         val = CLAMP(val, ANALOG_MIN, ANALOG_MAX)
         val = CLAMP(val, ANALOG_MIN, ANALOG_MAX)
-        if abs(val) < ANALOG_DEADBAND:
-            val = 0.0
-        return ((max - min) * ((val - ANALOG_MIN) / ANALOG_RANGE)) + min
+        return (((maxVal - minVal) * ((val - ANALOG_MIN) / ANALOG_RANGE))
+                + minVal)
     
     
-    def normalizeChannel(self, chan, min = -1, max = 1):
+    def normalizeChannel(self, chan, minVal = -1, maxVal = 1):
         try:
         try:
             if (chan == 2) | (chan == 6):
             if (chan == 2) | (chan == 6):
                 # These channels have reduced range
                 # These channels have reduced range
-                return self.normalize(self[chan] * 3.0, min, max)
+                return self.normalize(self[chan] * 3.0, minVal, maxVal)
             else:
             else:
-                return self.normalize(self[chan], min, max)
+                return self.normalize(self[chan], minVal, maxVal)
         except IndexError:
         except IndexError:
             return 0.0
             return 0.0
 
 

+ 27 - 41
direct/src/directdevices/DirectJoybox.py

@@ -30,22 +30,23 @@ class DirectJoybox(PandaObject):
     joyboxCount = 0
     joyboxCount = 0
     xyzMultiplier = 1.0
     xyzMultiplier = 1.0
     hprMultiplier = 1.0
     hprMultiplier = 1.0
-    def __init__(self, nodePath = direct.camera):
+    def __init__(self, device = 'CerealBox', nodePath = direct.camera):
         # See if device manager has been initialized
         # See if device manager has been initialized
         if direct.deviceManager == None:
         if direct.deviceManager == None:
             direct.deviceManager = DirectDeviceManager()
             direct.deviceManager = DirectDeviceManager()
         # Set name
         # Set name
         self.name = 'Joybox-' + `DirectJoybox.joyboxCount`
         self.name = 'Joybox-' + `DirectJoybox.joyboxCount`
         # Get buttons and analogs
         # Get buttons and analogs
-        self.device = base.config.GetString('joybox-device', 'CerealBox')
+        self.device = device
         self.analogs = direct.deviceManager.createAnalogs(self.device)
         self.analogs = direct.deviceManager.createAnalogs(self.device)
         self.buttons = direct.deviceManager.createButtons(self.device)
         self.buttons = direct.deviceManager.createButtons(self.device)
         self.aList = [0,0,0,0,0,0,0,0]
         self.aList = [0,0,0,0,0,0,0,0]
         self.bList = [0,0,0,0,0,0,0,0]
         self.bList = [0,0,0,0,0,0,0,0]
         # For joybox fly mode
         # For joybox fly mode
-        self.mapping = [L_LEFT_RIGHT, L_FWD_BACK, L_TWIST,
-                        R_TWIST, R_FWD_BACK, R_LEFT_RIGHT]
-        self.modifier = [1,1,1,1,1,1]
+        # Default is joe mode
+        self.mapping = [R_LEFT_RIGHT, R_FWD_BACK, L_FWD_BACK,
+                        R_TWIST, L_TWIST, NULL_AXIS]
+        self.modifier = [1,1,1,-1,-1,0]
         # Initialize time
         # Initialize time
         self.lastTime = globalClock.getTime()
         self.lastTime = globalClock.getTime()
         # Record node path
         # Record node path
@@ -58,7 +59,7 @@ class DirectJoybox(PandaObject):
         # List of functions to cycle through
         # List of functions to cycle through
         self.modeList = [self.joeMode, self.driveMode]
         self.modeList = [self.joeMode, self.driveMode]
         # Pick initial mode
         # Pick initial mode
-        self.updateFunc = self.joeFly
+        self.updateFunc = self.joyboxFly
         self.modeName = 'Joe Mode'
         self.modeName = 'Joe Mode'
         # Button registry
         # Button registry
         self.addButtonEvents()
         self.addButtonEvents()
@@ -173,35 +174,8 @@ class DirectJoybox(PandaObject):
         self.showMode(self.modeName)
         self.showMode(self.modeName)
         self.enable()
         self.enable()
         
         
-    def joeMode(self):
-        self.setMode(self.joeFly, 'Joe Mode')
-    
-    def joeFly(self):
-        hprScale = (self.analogs.normalizeChannel(L_SLIDE, 0.1, 200) *
-                    DirectJoybox.hprMultiplier)
-        posScale = (self.analogs.normalizeChannel(R_SLIDE, 0.1, 100) *
-                    DirectJoybox.xyzMultiplier)
-        # XYZ
-        x = self.aList[R_LEFT_RIGHT]
-        y = self.aList[R_FWD_BACK]
-        if self.bList[L_STICK]:
-            z = 0.0
-        else:
-            z = self.aList[L_FWD_BACK]
-        pos = Vec3(x,y,z) * (posScale * self.deltaTime)
-        # HPR
-        h = -1 * self.aList[R_TWIST]
-        if self.bList[L_STICK]:
-            p = -1 * self.aList[L_FWD_BACK]
-        else:
-            p = 0.0
-        r = 0.0
-        hpr = Vec3(h,p,r) * (hprScale * self.deltaTime)
-        # Move node path
-        self.nodePath.setPosHpr(self.nodePath, pos, hpr)
-
     def joyboxFly(self):
     def joyboxFly(self):
-        hprScale = (self.analogs.normalizeChannel(L_SLIDE, 0.1, 200) *
+        hprScale = (self.analogs.normalizeChannel(L_SLIDE, 0.1, 100) *
                     DirectJoybox.hprMultiplier)
                     DirectJoybox.hprMultiplier)
         posScale = (self.analogs.normalizeChannel(R_SLIDE, 0.1, 100) *
         posScale = (self.analogs.normalizeChannel(R_SLIDE, 0.1, 100) *
                     DirectJoybox.xyzMultiplier)
                     DirectJoybox.xyzMultiplier)
@@ -223,18 +197,30 @@ class DirectJoybox(PandaObject):
         # Move node path
         # Move node path
         self.nodePath.setPosHpr(self.nodePath, pos, hpr)
         self.nodePath.setPosHpr(self.nodePath, pos, hpr)
 
 
+    def joeMode(self):
+        self.mapping = [R_LEFT_RIGHT, R_FWD_BACK, L_FWD_BACK,
+                        R_TWIST, L_TWIST, NULL_AXIS]
+        self.modifier = [1,1,1,-1,-1,0]
+        self.setMode(self.joyboxFly, 'Joe Mode')
+
+    def driveMode(self):
+        self.mapping = [L_LEFT_RIGHT, R_FWD_BACK, R_TWIST,
+                        R_LEFT_RIGHT, L_FWD_BACK, NULL_AXIS]
+        self.modifier = [1,1,-1,-1,-1,0]
+        self.setMode(self.joyboxFly, 'Drive Mode')
+
+    def lookAtMode(self):
+        self.mapping = [R_LEFT_RIGHT, R_TWIST, R_FWD_BACK,
+                        L_LEFT_RIGHT, L_FWD_BACK, NULL_AXIS]
+        self.modifier = [1,1,1,-1,1,0]
+        self.setMode(self.joyboxFly, 'Look At Mode')
+
     def demoMode(self):
     def demoMode(self):
         self.mapping = [R_LEFT_RIGHT, R_FWD_BACK, L_FWD_BACK,
         self.mapping = [R_LEFT_RIGHT, R_FWD_BACK, L_FWD_BACK,
                         R_TWIST, NULL_AXIS, NULL_AXIS]
                         R_TWIST, NULL_AXIS, NULL_AXIS]
         self.modifier = [1,1,1,-1,0,0]
         self.modifier = [1,1,1,-1,0,0]
         self.setMode(self.joyboxFly, 'Demo Mode')
         self.setMode(self.joyboxFly, 'Demo Mode')
 
 
-    def driveMode(self):
-        self.mapping = [L_LEFT_RIGHT, R_FWD_BACK, L_FWD_BACK,
-                        R_LEFT_RIGHT, NULL_AXIS, NULL_AXIS]
-        self.modifier = [1,1,1,-1,0,0]
-        self.setMode(self.joyboxFly, 'Drive Mode')
-
     def hprXyzMode(self):
     def hprXyzMode(self):
         self.mapping = [R_LEFT_RIGHT, R_FWD_BACK, R_TWIST,
         self.mapping = [R_LEFT_RIGHT, R_FWD_BACK, R_TWIST,
                         L_TWIST, L_FWD_BACK, L_LEFT_RIGHT]
                         L_TWIST, L_FWD_BACK, L_LEFT_RIGHT]
@@ -257,7 +243,7 @@ class DirectJoybox(PandaObject):
         self.setMode(self.orbitFly, 'Orbit Mode')
         self.setMode(self.orbitFly, 'Orbit Mode')
 
 
     def orbitFly(self):
     def orbitFly(self):
-        hprScale = (self.analogs.normalizeChannel(L_SLIDE, 0.1, 200) *
+        hprScale = (self.analogs.normalizeChannel(L_SLIDE, 0.1, 100) *
                     DirectJoybox.hprMultiplier)
                     DirectJoybox.hprMultiplier)
         posScale = (self.analogs.normalizeChannel(R_SLIDE, 0.1, 100) *
         posScale = (self.analogs.normalizeChannel(R_SLIDE, 0.1, 100) *
                     DirectJoybox.xyzMultiplier)
                     DirectJoybox.xyzMultiplier)

+ 5 - 0
direct/src/directtools/DirectSession.py

@@ -54,6 +54,11 @@ class DirectSession(PandaObject):
         # Create a vrpn client vrpn-server or default
         # Create a vrpn client vrpn-server or default
         if base.config.GetBool('want-vrpn', 0):
         if base.config.GetBool('want-vrpn', 0):
             self.deviceManager = DirectDeviceManager()
             self.deviceManager = DirectDeviceManager()
+            # Automatically create any devices specified in config file
+            joybox = base.config.GetString('vrpn-joybox-device', '')
+            if joybox:
+                from DirectJoybox import *
+                self.joybox = DirectJoybox(joybox)
         else:
         else:
             self.deviceManager = None
             self.deviceManager = None
 
 

+ 564 - 129
direct/src/tkpanels/MopathRecorder.py

@@ -41,12 +41,30 @@ class MopathRecorder(AppShell, PandaObject):
         
         
         self.initialiseoptions(MopathRecorder)
         self.initialiseoptions(MopathRecorder)
 
 
+        self.selectRecordNodePathNamed('init')
+        self.selectPlaybackNodePathNamed('init')
+
     def appInit(self):
     def appInit(self):
         # Dictionary of widgets
         # Dictionary of widgets
         self.widgetDict = {}
         self.widgetDict = {}
         self.variableDict = {}
         self.variableDict = {}
         # Initialize state
         # Initialize state
-        self.tempCS = direct.group.attachNewNode('mopathRecorderTempCS')
+        self.recorderNodePath = direct.group.attachNewNode(self['name'])
+        self.tempCS = self.recorderNodePath.attachNewNode(
+            'mopathRecorderTempCS')
+        self.playbackMarker = loader.loadModel('models/directmodels/happy')
+        self.playbackMarker.reparentTo(self.recorderNodePath)
+        # For node path selectors
+        self.recNodePathDict = {}
+        self.recNodePathDict['marker'] = self.playbackMarker
+        self.recNodePathDict['camera'] = direct.camera
+        self.recNodePathDict['widget'] = direct.widget
+        self.recNodePathNames = ['marker', 'camera', 'widget', 'selected']
+        self.pbNodePathDict = {}
+        self.pbNodePathDict['marker'] = self.playbackMarker
+        self.pbNodePathDict['camera'] = direct.camera
+        self.pbNodePathDict['widget'] = direct.widget
+        self.pbNodePathNames = ['marker', 'camera', 'widget', 'selected']
         # Count of point sets recorded
         # Count of point sets recorded
         self.pointSet = []
         self.pointSet = []
         self.pointSetDict = {}
         self.pointSetDict = {}
@@ -54,18 +72,33 @@ class MopathRecorder(AppShell, PandaObject):
         self.pointSetName = self['name'] + '-ps-' + `self.pointSetCount`
         self.pointSetName = self['name'] + '-ps-' + `self.pointSetCount`
         # Hook to start/stop recording
         # Hook to start/stop recording
         self.startStopHook = 'f6'
         self.startStopHook = 'f6'
+        self.keyframeHook = 'f12'
         # Curve fitter object
         # Curve fitter object
+        self.fHasPoints = 0
+        self.startPos = Point3(0)
         self.xyzCurveFitter = CurveFitter()
         self.xyzCurveFitter = CurveFitter()
         self.hprCurveFitter = CurveFitter()
         self.hprCurveFitter = CurveFitter()
-        # Curve objects
-        self.nurbsCurve = NurbsCurve()
-        self.nurbsCurveDrawer = NurbsCurveDrawer(self.nurbsCurve)
-        self.curveNodePath = None
+        # Curve variables
         # Number of ticks per parametric unit
         # Number of ticks per parametric unit
         self.numTicks = 1
         self.numTicks = 1
         # Number of segments to represent each parametric unit
         # Number of segments to represent each parametric unit
         # This just affects the visual appearance of the curve
         # This just affects the visual appearance of the curve
         self.numSegs = 2
         self.numSegs = 2
+        # The nurbs curves
+        self.xyzNurbsCurve = None
+        self.hprNurbsCurve = None
+        # Curve drawers
+        self.nurbsCurveDrawer = NurbsCurveDrawer(NurbsCurve())
+        self.nurbsCurveDrawer.setNumSegs(self.numSegs)
+        self.curveNodePath = self.recorderNodePath.attachNewNode(
+            self.nurbsCurveDrawer.getGeomNode())
+        # Playback variables
+        self.playbackTime = 0.0
+        self.loopPlayback = 1
+        # Sample variables
+        self.fEven = 0
+        self.desampleFrequency = 1
+        self.numSamples = 100
         # Set up event hooks
         # Set up event hooks
         self.undoEvents = [('undo', self.undoHook),
         self.undoEvents = [('undo', self.undoHook),
                            ('pushUndo', self.pushUndoHook),
                            ('pushUndo', self.pushUndoHook),
@@ -158,7 +191,7 @@ class MopathRecorder(AppShell, PandaObject):
             self.undoButton['state'] = 'normal'
             self.undoButton['state'] = 'normal'
         else:
         else:
             self.undoButton['state'] = 'disabled'
             self.undoButton['state'] = 'disabled'
-        self.undoButton.pack(side = 'left', expand = 0)
+        self.undoButton.pack(side = LEFT, expand = 0)
         self.bind(self.undoButton, 'Undo last operation')
         self.bind(self.undoButton, 'Undo last operation')
 
 
         self.redoButton = Button(self.menuFrame, text = 'Redo',
         self.redoButton = Button(self.menuFrame, text = 'Redo',
@@ -167,14 +200,13 @@ class MopathRecorder(AppShell, PandaObject):
             self.redoButton['state'] = 'normal'
             self.redoButton['state'] = 'normal'
         else:
         else:
             self.redoButton['state'] = 'disabled'
             self.redoButton['state'] = 'disabled'
-        self.redoButton.pack(side = 'left', expand = 0)
+        self.redoButton.pack(side = LEFT, expand = 0)
         self.bind(self.redoButton, 'Redo last operation')
         self.bind(self.redoButton, 'Redo last operation')
 
 
         # Create notebook pages
         # Create notebook pages
         self.mainNotebook = Pmw.NoteBook(interior)
         self.mainNotebook = Pmw.NoteBook(interior)
         self.mainNotebook.pack(fill = BOTH, expand = 1)
         self.mainNotebook.pack(fill = BOTH, expand = 1)
         self.recordPage = self.mainNotebook.add('Record')
         self.recordPage = self.mainNotebook.add('Record')
-        self.cvPage = self.mainNotebook.add('CV Controls')
         self.refinePage = self.mainNotebook.add('Refine')
         self.refinePage = self.mainNotebook.add('Refine')
         # Put this here so it isn't called right away
         # Put this here so it isn't called right away
         self.mainNotebook['raisecommand'] = self.updateInfo
         self.mainNotebook['raisecommand'] = self.updateInfo
@@ -184,85 +216,179 @@ class MopathRecorder(AppShell, PandaObject):
                             borderwidth = 2)
                             borderwidth = 2)
         label = Label(recordFrame, text = 'RECORD PATH',
         label = Label(recordFrame, text = 'RECORD PATH',
                       font=('MSSansSerif', 12, 'bold'))
                       font=('MSSansSerif', 12, 'bold'))
-        label.pack(fill = 'x')
+        label.pack(fill = X)
         # Recording Buttons
         # Recording Buttons
-        frame = Frame(recordFrame)
-        self.createCheckbutton(frame, 'Recording', 'Recording Path',
-                               'On: path is being recorded',
-                               self.toggleRecord, 0,
-                               side = 'left')
-        self.getWidget('Recording', 'Recording Path').configure(
-            foreground = 'Red', relief = RAISED, borderwidth = 2,
-            anchor = CENTER)
-        self.getWidget('Recording', 'Recording Path').pack(
-            fill = 'x', expand = 1)
-        self.createLabeledEntry(frame, 'Recording', 'Start/Stop Hook',
-                                'Hook used to start/stop recording',
-                                initialValue = self.startStopHook,
-                                command = self.setStartStopHook)
-        self.setStartStopHook(None)
-        frame.pack(fill = 'x', expand = 1)
-        recordFrame.pack(fill = 'x')
+        # Record node path
+        self.gridFrame = Frame(recordFrame)        
+        self.recNodePathMenu = Pmw.ComboBox(
+            self.gridFrame, labelpos = W, label_text = 'Record Node Path:',
+            entry_width = 20,
+            selectioncommand = self.selectRecordNodePathNamed,
+            scrolledlist_items = self.recNodePathNames)
+        self.recNodePathMenu.selectitem('camera')
+        self.recNodePathMenuEntry = (
+            self.recNodePathMenu.component('entryfield_entry'))
+        self.recNodePathMenuBG = (
+            self.recNodePathMenuEntry.configure('background')[3])
+        self.recNodePathMenu.grid(row = 0, col = 0, sticky = NSEW)
+        self.bind(self.recNodePathMenu,
+                  'Select node path to track when recording a new curve')
+        # Record type
+        self.recordType = StringVar()
+        self.recordType.set('Continuous')
+        widget = self.createRadiobutton(
+            self.gridFrame, 'left',
+            'Recording', 'Continuous Recording',
+            ('New point added to curve fitter every frame'),
+            self.recordType, 'Continuous', self.setRecordType,
+            expand = 1)
+        widget['anchor'] = 'center'
+        widget.pack_forget()
+        widget.grid(row = 1, col = 0, sticky = NSEW)
+        widget = self.createRadiobutton(
+            self.gridFrame, 'left',
+            'Recording', 'Keyframe Recording',
+            ('Add new point to curve fitter by pressing keyframe button'),
+            self.recordType, 'Keyframe', self.setRecordType,
+            expand = 1)
+        widget['anchor'] = 'center'
+        widget.pack_forget()
+        widget.grid(row = 1, col = 1, sticky = NSEW)
+        # Record button
+        widget = self.createCheckbutton(
+            self.gridFrame, 'Recording', 'Recording Path',
+            'On: path is being recorded', self.toggleRecord, 0,
+            side = LEFT, fill = BOTH, expand = 1)
+        widget.pack_forget()
+        widget.grid(row=2, column=0, sticky = NSEW)
+        widget.configure(foreground = 'Red', relief = RAISED, borderwidth = 2,
+                         anchor = CENTER, width = 10)
+        widget = self.createButton(self.gridFrame, 'Recording', 'Add Key Frame',
+                                   'Add Keyframe To Current Path',
+                                   self.addKeyframe,
+                                   side = LEFT, expand = 1)
+        widget.configure(state = 'disabled', width = 24)
+        widget.pack_forget()
+        widget.grid(row=2, column=1, sticky = NSEW)
+        # Keyframe button
+        # Hook
+        widget = self.createLabeledEntry(
+            self.gridFrame, 'Recording', 'Record Hook',
+            'Hook used to start/stop recording',
+            initialValue = self.startStopHook,
+            command = self.setStartStopHook)[0]
+        label['width'] = 14
+        self.setStartStopHook()
+        widget.pack_forget()
+        widget.grid(row=3, column=0, sticky = NSEW)
+        widget = self.createLabeledEntry(
+            self.gridFrame, 'Recording', 'Keyframe Hook',
+            'Hook used to add a new keyframe',
+            initialValue = self.keyframeHook,
+            command = self.setKeyframeHook)[0]
+        label['width'] = 14
+        self.setKeyframeHook()
+        widget.pack_forget()
+        widget.grid(row=3, column=1, sticky = NSEW)
+        self.gridFrame.pack(fill = X, expand = 1)
+        # This gets the widgets to spread out
+        self.gridFrame.grid_columnconfigure(1,weight = 1)        
+        recordFrame.pack(fill = X, pady = 2)
         # Playback controls
         # Playback controls
         playbackFrame = Frame(self.recordPage, relief = SUNKEN,
         playbackFrame = Frame(self.recordPage, relief = SUNKEN,
                               borderwidth = 2)
                               borderwidth = 2)
         Label(playbackFrame, text = 'PLAYBACK CONTROLS',
         Label(playbackFrame, text = 'PLAYBACK CONTROLS',
-              font=('MSSansSerif', 12, 'bold')).pack(fill = 'x')
-        self.createEntryScale(playbackFrame, 'Playback', 'Time',
-                              'Set current playback time',
-                              resolution = 0.01,
-                              command = self.setPlaybackTime)
+              font=('MSSansSerif', 12, 'bold')).pack(fill = X)
+        # Playback modifiers
         frame = Frame(playbackFrame)
         frame = Frame(playbackFrame)
-        self.createButton(frame, 'Playback', 'Stop', 'Stop playback',
-                          self.stopPlayback, side = 'left', expand = 1)
-        self.createButton(frame, 'Playback', 'Play', 'Start playback',
-                          self.startPlayback, side = 'left', expand = 1)
-        self.createButton(frame, 'Playback', 'Pause', 'Pause playback',
-                          self.pausePlayback, side = 'left', expand = 1)
-        frame.pack(fill = 'x', expand = 1)
-        playbackFrame.pack(fill = 'x')
-
-        ## CV PAGE ##
-        cvFrame = Frame(self.cvPage, relief = SUNKEN,
-                        borderwidth = 2)
-        label = Label(cvFrame, text = 'CV CONTROLS',
-                      font=('MSSansSerif', 12, 'bold'))
-        label.pack(fill = 'x')
-        self.createEntryScale(cvFrame, 'CV Controls', 'Delta Pos',
-                              'Position threshold between selected points',
-                              resolution = 0.01,
-                              command = self.setDeltaPos)
-        self.createEntryScale(cvFrame, 'CV Controls', 'Delta Hpr',
-                              'Orientation threshold between selected points',
-                              resolution = 0.01,
-                              command = self.setDeltaHpr)
-        self.createEntryScale(cvFrame, 'CV Controls', 'Delta Time',
-                              'Time threshold between selected points',
-                              resolution = 0.01,
-                              command = self.setDeltaTime)
-
-        # Constant velocity frame
-        frame = Frame(cvFrame)
-        self.createCheckbutton(frame, 'CV Controls', 'Constant Velocity',
-                               'On: Resulting path has constant velocity',
-                               self.toggleConstantVelocity, 0,
-                               side = 'left')
-        self.getWidget('CV Controls', 'Constant Velocity').configure(
-            relief = RAISED, borderwidth = 2, anchor = CENTER)
-        self.getWidget('CV Controls', 'Constant Velocity').pack(
-            fill = 'x', expand = 1)
-        self.createLabeledEntry(frame, 'CV Controls', 'Path Duration',
+        # Playback node path
+        self.pbNodePathMenu = Pmw.ComboBox(
+            frame, labelpos = W, label_text = 'Playback Node Path:',
+            entry_width = 20,
+            selectioncommand = self.selectPlaybackNodePathNamed,
+            scrolledlist_items = self.pbNodePathNames)
+        self.pbNodePathMenu.selectitem('camera')
+        self.pbNodePathMenuEntry = (
+            self.pbNodePathMenu.component('entryfield_entry'))
+        self.pbNodePathMenuBG = (
+            self.pbNodePathMenuEntry.configure('background')[3])
+        self.pbNodePathMenu.pack(side = LEFT, fill = X, expand = 1)
+        self.bind(self.pbNodePathMenu,
+                  'Select node path to fly along path during playback')
+        # Duration entry
+        self.createLabeledEntry(frame, 'Resample', 'Path Duration',
                                 'Set total curve duration',
                                 'Set total curve duration',
-                                command = self.setTotalTime)
-        frame.pack(fill = 'x', expand = 1)
-        cvFrame.pack(fill = 'x')
+                                command = self.setPathDuration)
+        frame.pack(fill = X, expand = 1)
+        frame = Frame(playbackFrame)
+        widget = self.createEntryScale(
+            frame, 'Playback', 'Time', 'Set current playback time',
+            resolution = 0.01, command = self.setPlaybackTime, side = LEFT)
+        widget.component('hull')['relief'] = RIDGE
+        # Kill playback task if drag slider
+        widget.component('scale').bind(
+            '<ButtonPress-1>', lambda e = None, s = self: s.stopPlayback())
+        self.createCheckbutton(frame, 'Playback', 'Loop',
+                               'On: loop playback',
+                               self.setLoopPlayback, self.loopPlayback,
+                               side = LEFT, fill = BOTH, expand = 0)
+        frame.pack(fill = X, expand = 1)
+        # Start stop buttons
+        frame = Frame(playbackFrame)
+        widget = self.createButton(frame, 'Playback', '<<',
+                                   'Jump to start of playback',
+                                   self.jumpToStartOfPlayback,
+                                   side = LEFT, expand = 1)
+        widget['font'] = (('MSSansSerif', 12, 'bold'))
+        widget = self.createCheckbutton(frame, 'Playback', 'Play',
+                                        'Start/Stop playback',
+                                        self.startStopPlayback, 0,
+                                        side = LEFT, fill = BOTH, expand = 1)
+        widget.configure(anchor = 'center', justify = 'center',
+                         relief = RAISED, font = ('MSSansSerif', 12, 'bold'))
+        widget = self.createButton(frame, 'Playback', '>>',
+                                   'Jump to end of playback',
+                                   self.jumpToEndOfPlayback,
+                                   side = LEFT, expand = 1)
+        widget['font'] = (('MSSansSerif', 12, 'bold'))
+        frame.pack(fill = X, expand = 1)
+        playbackFrame.pack(fill = X, pady = 2)
+        # Desample
+        desampleFrame = Frame(
+            self.recordPage, relief = SUNKEN, borderwidth = 2)
+        Label(desampleFrame, text = 'DESAMPLE CURVE',
+              font=('MSSansSerif', 12, 'bold')).pack()
+        widget = self.createEntryScale(
+            desampleFrame, 'Resample', 'Points Between Samples',
+            'Recompute curve using every nth point',
+            resolution = 1, max = 100, command = self.setDesampleFrequency)
+        widget.component('hull')['relief'] = RIDGE
+        widget.onRelease = widget.onReturnRelease = self.desampleCurve
+        desampleFrame.pack(fill = X, expand = 0, pady = 2)
+        # Resample
+        resampleFrame = Frame(
+            self.recordPage, relief = SUNKEN, borderwidth = 2)
+        label = Label(resampleFrame, text = 'RESAMPLE CURVE',
+                      font=('MSSansSerif', 12, 'bold')).pack()
+        widget = self.createEntryScale(
+            resampleFrame, 'Resample', 'Num. Samples',
+            'Number of samples in resampled curve',
+            resolution = 1, max = 1000, command = self.setNumSamples,
+            side = LEFT)
+        widget.component('hull')['relief'] = RIDGE
+        widget.onRelease = widget.onReturnRelease = self.sampleCurve
+        self.createCheckbutton(resampleFrame, 'Resample', 'Even',
+                               'On: Resulting path has constant velocity',
+                               self.setEven, self.fEven,
+                               side = LEFT, fill = BOTH, expand = 0)
+        resampleFrame.pack(fill = X, expand = 0, pady = 2)
 
 
         ## REFINE PAGE ##
         ## REFINE PAGE ##
         refineFrame = Frame(self.refinePage, relief = SUNKEN,
         refineFrame = Frame(self.refinePage, relief = SUNKEN,
                             borderwidth = 2)
                             borderwidth = 2)
         label = Label(refineFrame, text = 'REFINE CURVE',
         label = Label(refineFrame, text = 'REFINE CURVE',
                       font=('MSSansSerif', 12, 'bold'))
                       font=('MSSansSerif', 12, 'bold'))
-        label.pack(fill = 'x')
+        label.pack(fill = X)
         self.createEntryScale(refineFrame, 'Refine Page', 'From',
         self.createEntryScale(refineFrame, 'Refine Page', 'From',
                               'Begin time of refine pass',
                               'Begin time of refine pass',
                               resolution = 0.01,
                               resolution = 0.01,
@@ -271,12 +397,12 @@ class MopathRecorder(AppShell, PandaObject):
                               'Stop time of refine pass',
                               'Stop time of refine pass',
                               resolution = 0.01,
                               resolution = 0.01,
                               command = self.setRefineStop)
                               command = self.setRefineStop)
-        refineFrame.pack(fill = 'x')
+        refineFrame.pack(fill = X)
 
 
         offsetFrame = Frame(self.refinePage)
         offsetFrame = Frame(self.refinePage)
         self.createButton(offsetFrame, 'Refine Page', 'Offset',
         self.createButton(offsetFrame, 'Refine Page', 'Offset',
                           'Zero refine curve offset',
                           'Zero refine curve offset',
-                          self.resetOffset, side = 'left')
+                          self.resetOffset, side = LEFT)
         self.createLabeledEntry(offsetFrame, 'Refine Page', 'X',
         self.createLabeledEntry(offsetFrame, 'Refine Page', 'X',
                                 'Refine pass X offset', width = 3, expand = 1)
                                 'Refine pass X offset', width = 3, expand = 1)
         self.createLabeledEntry(offsetFrame, 'Refine Page', 'Y',
         self.createLabeledEntry(offsetFrame, 'Refine Page', 'Y',
@@ -289,12 +415,12 @@ class MopathRecorder(AppShell, PandaObject):
                                 'Refine pass P offset', width = 3, expand = 1)
                                 'Refine pass P offset', width = 3, expand = 1)
         self.createLabeledEntry(offsetFrame, 'Refine Page', 'R',
         self.createLabeledEntry(offsetFrame, 'Refine Page', 'R',
                                 'Refine pass R offset', width = 3, expand = 1)
                                 'Refine pass R offset', width = 3, expand = 1)
-        offsetFrame.pack(fill = 'x')
+        offsetFrame.pack(fill = X)
 
 
         frame = Frame(self.refinePage)
         frame = Frame(self.refinePage)
         self.createButton(frame, 'Refine Page', 'Speed',
         self.createButton(frame, 'Refine Page', 'Speed',
                           'Reset refine speed',
                           'Reset refine speed',
-                          self.resetRefineSpeed, side = 'left',)
+                          self.resetRefineSpeed, side = LEFT,)
 
 
         self.mainNotebook.setnaturalsize()        
         self.mainNotebook.setnaturalsize()        
         
         
@@ -337,7 +463,8 @@ class MopathRecorder(AppShell, PandaObject):
             self.ignore(event)
             self.ignore(event)
         # remove start stop hook
         # remove start stop hook
         self.ignore(self.startStopHook)
         self.ignore(self.startStopHook)
-        self.tempCS.removeNode()
+        self.ignore(self.keyframeHook)
+        self.recorderNodePath.removeNode()
 
 
     def createNewPointSet(self):
     def createNewPointSet(self):
         self.pointSet = []
         self.pointSet = []
@@ -351,11 +478,24 @@ class MopathRecorder(AppShell, PandaObject):
         names = list(listbox.get(0,'end'))
         names = list(listbox.get(0,'end'))
         names.append(self.pointSetName)
         names.append(self.pointSetName)
         scrolledList.setlist(names)
         scrolledList.setlist(names)
+        comboBox.selectitem(self.pointSetName)
         # Update count
         # Update count
         self.pointSetCount += 1
         self.pointSetCount += 1
 
 
     def selectPointSetNamed(self, name):
     def selectPointSetNamed(self, name):
         self.pointSet = self.pointSetDict.get(name, None)
         self.pointSet = self.pointSetDict.get(name, None)
+        # Reload points into curve fitter
+        # Reset curve fitters
+        self.xyzCurveFitter.reset()
+        self.hprCurveFitter.reset()
+        for time, pos, hpr in self.pointSet:
+            # Add it to the curve fitters
+            self.xyzCurveFitter.addPoint(time, pos )
+            self.hprCurveFitter.addPoint(time, hpr)
+        self.fHasPoints = 1
+        # Compute curve
+        self.computeCurves()
+    
 
 
     def setTraceVis(self):
     def setTraceVis(self):
         print self.traceVis.get()
         print self.traceVis.get()
@@ -382,17 +522,41 @@ class MopathRecorder(AppShell, PandaObject):
             self.nurbsCurveDrawer.setNumTicks(0)
             self.nurbsCurveDrawer.setNumTicks(0)
 
 
     def setMarkerVis(self):
     def setMarkerVis(self):
-        print self.markerVis.get()
+        if self.markerVis.get():
+            self.playbackMarker.reparentTo(self.recorderNodePath)
+        else:
+            self.playbackMarker.reparentTo(hidden)
         
         
-    def setStartStopHook(self, event):
+    def setStartStopHook(self, event = None):
         # Clear out old hook
         # Clear out old hook
         self.ignore(self.startStopHook)
         self.ignore(self.startStopHook)
         # Record new one
         # Record new one
-        hook = self.getVariable('Recording', 'Start/Stop Hook').get()
+        hook = self.getVariable('Recording', 'Record Hook').get()
         self.startStopHook = hook
         self.startStopHook = hook
         # Add new one
         # Add new one
         self.accept(self.startStopHook, self.toggleRecordVar)
         self.accept(self.startStopHook, self.toggleRecordVar)
 
 
+    def setKeyframeHook(self, event = None):
+        # Clear out old hook
+        self.ignoreKeyframeHook()
+        # Record new one
+        hook = self.getVariable('Recording', 'Keyframe Hook').get()
+        self.keyframeHook = hook
+
+    def acceptKeyframeHook(self):
+        # Add new one
+        self.accept(self.keyframeHook, self.addKeyframe)
+
+    def ignoreKeyframeHook(self):
+        # Clear out old hook
+        self.ignore(self.keyframeHook)
+
+    def setRecordType(self):
+        if self.recordType.get() == 'Keyframe':
+            self.getWidget('Recording', 'Add Key Frame')['state'] = 'normal'
+        else:
+            self.getWidget('Recording', 'Add Key Frame')['state'] = 'disabled'
+
     def toggleRecordVar(self):
     def toggleRecordVar(self):
         # Get recording variable
         # Get recording variable
         v = self.getVariable('Recording', 'Recording Path')
         v = self.getVariable('Recording', 'Recording Path')
@@ -410,18 +574,52 @@ class MopathRecorder(AppShell, PandaObject):
             self.hprCurveFitter.reset()
             self.hprCurveFitter.reset()
             # Create a new point set to hold raw data
             # Create a new point set to hold raw data
             self.createNewPointSet()
             self.createNewPointSet()
-            # Start new task
-            t = taskMgr.spawnMethodNamed(
-                self.recordTask, self['name'] + '-recordTask')
-            t.startTime = globalClock.getTime()
-            t.uponDeath = self.endRecordTask
+            # Continuous or keyframe?
+            if self.recordType.get() == 'Continuous':
+                # Start new task
+                t = taskMgr.spawnMethodNamed(
+                    self.recordTask, self['name'] + '-recordTask')
+                t.startTime = globalClock.getTime()
+                t.uponDeath = self.endRecordTask
+            else:
+                # Add hook
+                self.acceptKeyframeHook()
+                # Record first point
+                self.startPos = self['nodePath'].getPos()
+                self.recordPoint(0.0)
+
+            # Don't want to change record modes
+            self.getWidget('Recording', 'Continuous Recording')['state'] = (
+                'disabled')
+            self.getWidget('Recording', 'Keyframe Recording')['state'] = (
+                'disabled')
         else:
         else:
-            # Kill old task
-            taskMgr.removeTasksNamed(self['name'] + '-recordTask')
+            if self.recordType.get() == 'Continuous':
+                # Kill old task
+                taskMgr.removeTasksNamed(self['name'] + '-recordTask')
+            else:
+                # Add last point
+                self.addKeyframe()
+                # Ignore hook
+                self.ignoreKeyframeHook()
+                # Compute curve
+                self.computeCurves()
+            # Now you can change record modes
+            self.getWidget('Recording', 'Continuous Recording')['state'] = (
+                'normal')
+            self.getWidget('Recording', 'Keyframe Recording')['state'] = (
+                'normal')
             
             
     def recordTask(self, state):
     def recordTask(self, state):
         # Record raw data point
         # Record raw data point
         time = globalClock.getTime() - state.startTime
         time = globalClock.getTime() - state.startTime
+        self.recordPoint(time)
+        return Task.cont
+
+    def endRecordTask(self, state):
+        self.computeCurves()
+
+    def recordPoint(self, time):
         pos = self['nodePath'].getPos()
         pos = self['nodePath'].getPos()
         hpr = self['nodePath'].getHpr()
         hpr = self['nodePath'].getHpr()
         # Add it to the point set
         # Add it to the point set
@@ -429,45 +627,278 @@ class MopathRecorder(AppShell, PandaObject):
         # Add it to the curve fitters
         # Add it to the curve fitters
         self.xyzCurveFitter.addPoint(time, pos )
         self.xyzCurveFitter.addPoint(time, pos )
         self.hprCurveFitter.addPoint(time, hpr)
         self.hprCurveFitter.addPoint(time, hpr)
-        return Task.cont
+        self.fHasPoints = 1
 
 
-    def endRecordTask(self, state):
-        # Create curve
+    def addKeyframe(self):
+        time = Vec3(self['nodePath'].getPos() - self.startPos).length()
+        self.recordPoint(time)
+
+    def computeCurves(self):
+        # MRM: Would be better if curvefitter had getNumPoints
+        if not self.fHasPoints:
+            print 'MopathRecorder: Must define curve first'
+            return
+        # Create curves
+        # XYZ
         self.xyzCurveFitter.computeTangents(1)
         self.xyzCurveFitter.computeTangents(1)
-        self.hprCurveFitter.computeTangents(1)
-        self.nurbsCurve = self.xyzCurveFitter.makeNurbs()
-        self.nurbsCurveDrawer.setCurve(self.nurbsCurve)
-        self.nurbsCurveDrawer.setNumSegs(self.numSegs)
+        self.xyzNurbsCurve = self.xyzCurveFitter.makeNurbs()
+        self.nurbsCurveDrawer.setCurve(self.xyzNurbsCurve)
         self.nurbsCurveDrawer.draw()
         self.nurbsCurveDrawer.draw()
-        self.curveNodePath = render.attachNewNode(
-            self.nurbsCurveDrawer.getGeomNode())
-        
+        # HPR
+        self.hprCurveFitter.wrapHpr()
+        self.hprCurveFitter.computeTangents(1)
+        self.hprNurbsCurve = self.hprCurveFitter.makeNurbs()
+        # Update widget based on new curve
+        self.updateCurveInfo()
+
+    def updateCurveInfo(self):
+        if not self.xyzNurbsCurve:
+            return
+        # Widgets depending on max T
+        maxT = '%.2f' % self.xyzNurbsCurve.getMaxT()
+        self.getWidget('Playback', 'Time').configure(max = maxT)
+        self.getVariable('Resample', 'Path Duration').set(maxT)
+        self.getWidget('Refine Page', 'From').configure(max = maxT)
+        self.getWidget('Refine Page', 'To').configure(max = maxT)
+        self.maxT = float(maxT)
+        # Widgets depending on number of knots
+        numKnots = self.xyzNurbsCurve.getNumKnots()
+        self.getWidget('Resample', 'Points Between Samples')['max'] = numKnots
+        self.getWidget('Resample', 'Num. Samples')['max'] = 2 * numKnots
+
+    def selectRecordNodePathNamed(self, name):
+        nodePath = None
+        if name == 'init':
+            nodePath = self['nodePath']
+            # Add Combo box entry for the initial node path
+            self.addRecordNodePath(nodePath)
+        elif name == 'selected':
+            nodePath = direct.selected.last
+            # Add Combo box entry for this selected object
+            self.addRecordNodePath(nodePath)
+        else:
+            nodePath = self.recNodePathDict.get(name, None)
+            if (nodePath == None):
+                # See if this evaluates into a node path
+                try:
+                    nodePath = eval(name)
+                    if isinstance(nodePath, NodePath):
+                        self.addRecordNodePath(nodePath)
+                    else:
+                        # Good eval but not a node path, give up
+                        nodePath = None
+                except:
+                    # Bogus eval
+                    nodePath = None
+                    # Clear bogus entry from listbox
+                    listbox = self.recNodePathMenu.component('scrolledlist')
+                    listbox.setlist(self.recNodePathNames)
+            else:
+                if name == 'widget':
+                    # Record relationship between selected nodes and widget
+                    direct.selected.getWrtAll()                    
+        # Update active node path
+        self.setRecordNodePath(nodePath)
+
+    def setRecordNodePath(self, nodePath):
+        self['nodePath'] = nodePath
+        if self['nodePath']:
+            self.recNodePathMenuEntry.configure(
+                background = self.recNodePathMenuBG)
+        else:
+            # Flash entry
+            self.recNodePathMenuEntry.configure(background = 'Pink')
+
+    def selectPlaybackNodePathNamed(self, name):
+        nodePath = None
+        if name == 'init':
+            nodePath = self['nodePath']
+            # Add Combo box entry for the initial node path
+            self.addPlaybackNodePath(nodePath)
+        elif name == 'selected':
+            nodePath = direct.selected.last
+            # Add Combo box entry for this selected object
+            self.addPlaybackNodePath(nodePath)
+        else:
+            nodePath = self.pbNodePathDict.get(name, None)
+            if (nodePath == None):
+                # See if this evaluates into a node path
+                try:
+                    nodePath = eval(name)
+                    if isinstance(nodePath, NodePath):
+                        self.addPlaybackNodePath(nodePath)
+                    else:
+                        # Good eval but not a node path, give up
+                        nodePath = None
+                except:
+                    # Bogus eval
+                    nodePath = None
+                    # Clear bogus entry from listbox
+                    listbox = self.pbNodePathMenu.component('scrolledlist')
+                    listbox.setlist(self.pbNodePathNames)
+            else:
+                if name == 'widget':
+                    # Record relationship between selected nodes and widget
+                    direct.selected.getWrtAll()                    
+        # Update active node path
+        self.setPlaybackNodePath(nodePath)
+
+    def setPlaybackNodePath(self, nodePath):
+        self.playbackNodePath = nodePath
+        if self.playbackNodePath:
+            self.pbNodePathMenuEntry.configure(
+                background = self.pbNodePathMenuBG)
+        else:
+            # Flash entry
+            self.pbNodePathMenuEntry.configure(background = 'Pink')
+
+    def addRecordNodePath(self, nodePath):
+        self.addNodePathToDict(nodePath, self.recNodePathNames,
+                               self.recNodePathMenu, self.recNodePathDict)
+
+    def addPlaybackNodePath(self, nodePath):
+        self.addNodePathToDict(nodePath, self.pbNodePathNames,
+                               self.pbNodePathMenu, self.pbNodePathDict)
+
+    def addNodePathToDict(self, nodePath, names, menu, dict):
+        if not nodePath:
+            return
+        # Get node path's name
+        name = nodePath.getName()
+        if name in ['parent', 'render', 'camera', 'marker']:
+            dictName = name
+        else:
+            # Generate a unique name for the dict
+            dictName = name + '-' + `nodePath.id().this`
+        if not dict.has_key(dictName):
+            # Update combo box to include new item
+            names.append(dictName)
+            listbox = menu.component('scrolledlist')
+            listbox.setlist(names)
+            # Add new item to dictionary
+            dict[dictName] = nodePath
+        menu.selectitem(dictName)
+
+    def setLoopPlayback(self):
+        self.loopPlayback = self.getVariable('Playback', 'Loop').get()
+
     def setPlaybackTime(self, time):
     def setPlaybackTime(self, time):
-        print time
+        self.playbackGoTo(time)
 
 
-    def stopPlayback(self):
-        print 'stop'
+    def playbackGoTo(self, time):
+        if (self.xyzNurbsCurve == None) & (self.hprNurbsCurve == None):
+            return
+        self.playbackTime = CLAMP(time, 0.0, self.maxT)
+        if self.xyzNurbsCurve != None:
+            pos = Vec3(0)
+            self.xyzNurbsCurve.getPoint(self.playbackTime, pos)
+            self.playbackNodePath.setPos(pos)
+            self.playbackNodePath.setPos(pos)
+        if self.hprNurbsCurve != None:
+            hpr = Vec3(0)
+            self.hprNurbsCurve.getPoint(self.playbackTime, hpr)
+            self.playbackNodePath.setHpr(hpr)
 
 
     def startPlayback(self):
     def startPlayback(self):
-        print 'start'
+        if (self.xyzNurbsCurve == None) & (self.hprNurbsCurve == None):
+            return
+        # Kill any existing tasks
+        self.stopPlayback()
+        # Make sure checkbutton is set
+        self.getVariable('Playback', 'Play').set(1)
+        # Start new playback task
+        t = taskMgr.spawnMethodNamed(
+            self.playbackTask, self['name'] + '-playbackTask')
+        t.startOffset = self.playbackTime
+        t.startTime = globalClock.getTime()
+        
+    def playbackTask(self, state):
+        dTime = globalClock.getTime() - state.startTime
+        if self.loopPlayback:
+            cTime = (state.startOffset + dTime) % self.maxT
+        else:
+            cTime = CLAMP(state.startOffset + dTime, 0.0, self.maxT)
+        self.getWidget('Playback', 'Time').set(cTime)
+        # Stop task if not looping and at end of curve
+        if ((self.loopPlayback == 0) & ((cTime + 0.01) > self.maxT)):
+            self.stopPlayback()
+            return Task.done
+        return Task.cont
 
 
-    def pausePlayback(self):
-        print 'pause'
+    def stopPlayback(self):
+        self.getVariable('Playback', 'Play').set(0)
+        taskMgr.removeTasksNamed(self['name'] + '-playbackTask')
 
 
-    def setDeltaPos(self, dPos):
-        print dPos
+    def jumpToStartOfPlayback(self):
+        self.stopPlayback()
+        self.getWidget('Playback', 'Time').set(0.0)
 
 
-    def setDeltaHpr(self, dHpr):
-        print dHpr
+    def jumpToEndOfPlayback(self):
+        self.stopPlayback()
+        if self.xyzNurbsCurve != None:
+            self.getWidget('Playback', 'Time').set(self.maxT)
 
 
-    def setDeltaTime(self, dTime):
-        print dTime
+    def startStopPlayback(self):
+        if self.getVariable('Playback', 'Play').get():
+            self.startPlayback()
+        else:
+            self.stopPlayback()
 
 
-    def toggleConstantVelocity(self):
-        print self.getWidget('CV Controls', 'Constant Velocity').get()
+    def setDesampleFrequency(self, frequency):
+        self.desampleFrequency = frequency
+        
+    def desampleCurve(self):
+        if not self.fHasPoints:
+            print 'MopathRecorder: Must define curve first'
+            return
+        # Record current curve length
+        maxT = self.maxT
+        # NOTE: This is destructive, points will be deleted from curve fitter
+        self.xyzCurveFitter.desample(self.desampleFrequency)
+        self.hprCurveFitter.desample(self.desampleFrequency)
+        self.computeCurves()
+        # Resize curve to original duration
+        self.setPathDurationTo(maxT)
+
+    def setNumSamples(self, numSamples):
+        self.numSamples = int(numSamples)
+        
+    def sampleCurve(self):
+        if (self.xyzNurbsCurve == None) & (self.hprNurbsCurve == None):
+            print 'MopathRecorder: Must define curve first'
+            return
+        # Record current curve length
+        maxT = self.maxT
+        # Reset curve fitters
+        self.xyzCurveFitter.reset()
+        self.hprCurveFitter.reset()
+        # Get new data points based on given curve
+        self.xyzCurveFitter.sample(
+            self.xyzNurbsCurve, self.numSamples, self.fEven)
+        self.hprCurveFitter.sample(
+            self.hprNurbsCurve, self.numSamples, self.fEven)
+        self.computeCurves()
+        # Resize curve to original duration
+        self.setPathDurationTo(maxT)
+
+    def setEven(self):
+        self.fEven = self.getVariable('Resample', 'Even').get()
 
 
-    def setTotalTime(self):
-        print 'path duration'
+    def setPathDuration(self, event):
+        newMaxT = float(self.getWidget('Resample', 'Path Duration').get())
+        self.setPathDurationTo(newMaxT)
+        
+    def setPathDurationTo(self, newMaxT):
+        sf = newMaxT/self.maxT
+        # Scale knots
+        for i in range(self.xyzNurbsCurve.getNumKnots()):
+            self.xyzNurbsCurve.setKnot(i, sf * self.xyzNurbsCurve.getKnot(i))
+        self.xyzNurbsCurve.recompute()
+        for i in range(self.hprNurbsCurve.getNumKnots()):
+            self.hprNurbsCurve.setKnot(i, sf * self.hprNurbsCurve.getKnot(i))
+        self.hprNurbsCurve.recompute()
+        # Update info
+        self.updateCurveInfo()
 
 
     def setRefineStart(self,value):
     def setRefineStart(self,value):
         print 'refine start'
         print 'refine start'
@@ -492,21 +923,24 @@ class MopathRecorder(AppShell, PandaObject):
         return self.variableDict[category + '-' + text]
         return self.variableDict[category + '-' + text]
 
 
     def createLabeledEntry(self, parent, category, text, balloonHelp,
     def createLabeledEntry(self, parent, category, text, balloonHelp,
-                           initialValue = '',
-                           command = None, relief = 'sunken',
-                           side = 'left', expand = 1, width = 12):
+                           initialValue = '', command = None,
+                           relief = 'sunken', side = LEFT,
+                           expand = 1, width = 12):
+        frame = Frame(parent)
         variable = StringVar()
         variable = StringVar()
         variable.set(initialValue)
         variable.set(initialValue)
-        frame = Frame(parent)
-        Label(frame, text = text).pack(side = 'left', fill = 'x')
+        label = Label(frame, text = text)
+        label.pack(side = LEFT, fill = X)
+        self.widgetDict[category + '-' + text + '-Label'] = label
         entry = Entry(frame, width = width, relief = relief,
         entry = Entry(frame, width = width, relief = relief,
                       textvariable = variable)
                       textvariable = variable)
-        entry.pack(side = 'left', fill = 'x', expand = expand)
+        entry.pack(side = LEFT, fill = X, expand = expand)
         self.widgetDict[category + '-' + text] = entry
         self.widgetDict[category + '-' + text] = entry
         self.variableDict[category + '-' + text] = variable
         self.variableDict[category + '-' + text] = variable
         if command:
         if command:
             entry.bind('<Return>', command)
             entry.bind('<Return>', command)
-        frame.pack(side = side, fill = 'x', expand = expand)
+        frame.pack(side = side, fill = X, expand = expand)
+        return (frame, label, entry)
 
 
     def createButton(self, parent, category, text, balloonHelp, command,
     def createButton(self, parent, category, text, balloonHelp, command,
                      side = 'top', expand = 0):
                      side = 'top', expand = 0):
@@ -520,14 +954,14 @@ class MopathRecorder(AppShell, PandaObject):
         
         
     def createCheckbutton(self, parent, category, text,
     def createCheckbutton(self, parent, category, text,
                           balloonHelp, command, initialState,
                           balloonHelp, command, initialState,
-                          side = 'top'):
+                          side = 'top', fill = X, expand = 0):
         bool = BooleanVar()
         bool = BooleanVar()
         bool.set(initialState)
         bool.set(initialState)
         widget = Checkbutton(parent, text = text, anchor = W,
         widget = Checkbutton(parent, text = text, anchor = W,
                          variable = bool)
                          variable = bool)
         # Do this after the widget so command isn't called on creation
         # Do this after the widget so command isn't called on creation
         widget['command'] = command
         widget['command'] = command
-        widget.pack(side = side, fill = X)
+        widget.pack(side = side, fill = fill, expand = expand)
         self.bind(widget, balloonHelp)
         self.bind(widget, balloonHelp)
         self.widgetDict[category + '-' + text] = widget
         self.widgetDict[category + '-' + text] = widget
         self.variableDict[category + '-' + text] = bool
         self.variableDict[category + '-' + text] = bool
@@ -535,12 +969,12 @@ class MopathRecorder(AppShell, PandaObject):
         
         
     def createRadiobutton(self, parent, side, category, text,
     def createRadiobutton(self, parent, side, category, text,
                           balloonHelp, variable, value,
                           balloonHelp, variable, value,
-                          command):
+                          command, fill = X, expand = 0):
         widget = Radiobutton(parent, text = text, anchor = W,
         widget = Radiobutton(parent, text = text, anchor = W,
                              variable = variable, value = value)
                              variable = variable, value = value)
         # Do this after the widget so command isn't called on creation
         # Do this after the widget so command isn't called on creation
         widget['command'] = command
         widget['command'] = command
-        widget.pack(side = side, fill = X)
+        widget.pack(side = side, fill = fill, expand = expand)
         self.bind(widget, balloonHelp)
         self.bind(widget, balloonHelp)
         self.widgetDict[category + '-' + text] = widget
         self.widgetDict[category + '-' + text] = widget
         return widget
         return widget
@@ -573,7 +1007,8 @@ class MopathRecorder(AppShell, PandaObject):
 
 
     def createEntryScale(self, parent, category, text, balloonHelp,
     def createEntryScale(self, parent, category, text, balloonHelp,
                          command = None, min = 0.0, max = 1.0,
                          command = None, min = 0.0, max = 1.0,
-                         resolution = None, **kw):
+                         resolution = None,
+                         side = TOP, fill = X, expand = 1, **kw):
         kw['text'] = text
         kw['text'] = text
         kw['min'] = min
         kw['min'] = min
         kw['max'] = max
         kw['max'] = max
@@ -581,7 +1016,7 @@ class MopathRecorder(AppShell, PandaObject):
         widget = apply(EntryScale.EntryScale, (parent,), kw)
         widget = apply(EntryScale.EntryScale, (parent,), kw)
         # Do this after the widget so command isn't called on creation
         # Do this after the widget so command isn't called on creation
         widget['command'] = command
         widget['command'] = command
-        widget.pack(fill = X)
+        widget.pack(side = side, fill = fill, expand = expand)
         self.bind(widget, balloonHelp)
         self.bind(widget, balloonHelp)
         self.widgetDict[category + '-' + text] = widget
         self.widgetDict[category + '-' + text] = widget
         return widget
         return widget
@@ -656,7 +1091,7 @@ class MopathRecorder(AppShell, PandaObject):
             widget.selectitem(items[0])
             widget.selectitem(items[0])
         # Bind selection command
         # Bind selection command
         widget['selectioncommand'] = command
         widget['selectioncommand'] = command
-        widget.pack(side = 'left', expand = 0)
+        widget.pack(side = LEFT, expand = 0)
         # Bind help
         # Bind help
         self.bind(widget, balloonHelp)
         self.bind(widget, balloonHelp)
         # Record widget
         # Record widget

+ 24 - 0
direct/src/tkwidgets/EntryScale.py

@@ -18,6 +18,7 @@ class EntryScale(Pmw.MegaWidget):
             ('initialValue',        0.0,           Pmw.INITOPT),
             ('initialValue',        0.0,           Pmw.INITOPT),
             ('resolution',          0.001,         None),
             ('resolution',          0.001,         None),
             ('command',             None,          None),
             ('command',             None,          None),
+            ('callbackData',        [],       None),
             ('min',                 0.0,           self._updateValidate),
             ('min',                 0.0,           self._updateValidate),
             ('max',                 100.0,         self._updateValidate),
             ('max',                 100.0,         self._updateValidate),
             ('text',                'EntryScale',  self._updateLabelText),
             ('text',                'EntryScale',  self._updateLabelText),
@@ -99,6 +100,12 @@ class EntryScale(Pmw.MegaWidget):
         self.scale.pack(side = 'left', expand = 1, fill = 'x')
         self.scale.pack(side = 'left', expand = 1, fill = 'x')
         # Set scale to the middle of its range
         # Set scale to the middle of its range
         self.scale.set(self['initialValue'])
         self.scale.set(self['initialValue'])
+        self.scale.bind('<Button-1>',
+                        lambda event, s = self:
+                        apply(s.onPress, s['callbackData']))
+        self.scale.bind('<ButtonRelease-1>',
+                        lambda event, s = self:
+                        apply(s.onRelease, s['callbackData']))
 
 
         self.maxLabel = self.createcomponent('maxLabel', (), None,
         self.maxLabel = self.createcomponent('maxLabel', (), None,
                                              Button, self.minMaxFrame,
                                              Button, self.minMaxFrame,
@@ -176,7 +183,9 @@ class EntryScale(Pmw.MegaWidget):
     def _entryCommand(self, event = None):
     def _entryCommand(self, event = None):
         try:
         try:
             val = string.atof( self.entryValue.get() )
             val = string.atof( self.entryValue.get() )
+            apply(self.onReturn,self['callbackData'])
             self.set( val )
             self.set( val )
+            apply(self.onReturnRelease,self['callbackData'])
         except ValueError:
         except ValueError:
             pass
             pass
 
 
@@ -213,6 +222,21 @@ class EntryScale(Pmw.MegaWidget):
         if fCommand & (self['command'] is not None):
         if fCommand & (self['command'] is not None):
             self['command']( newVal )
             self['command']( newVal )
 
 
+    def onReturn(self, *args):
+        """ User redefinable callback executed on <Return> in entry """
+        pass
+
+    def onReturnRelease(self, *args):
+        """ User redefinable callback executed on <Return> release in entry """
+        pass
+
+    def onPress(self, *args):
+        """ User redefinable callback executed on button press """
+        pass
+
+    def onRelease(self, *args):
+        """ User redefinable callback executed on button release """
+        pass
 
 
 class EntryScaleGroup(Pmw.MegaToplevel):
 class EntryScaleGroup(Pmw.MegaToplevel):
     def __init__(self, parent = None, **kw):
     def __init__(self, parent = None, **kw):