""" DIRECT Nine DoF Manipulation Panel """
__all__ = ['Placer', 'place']
# Import Tkinter, Pmw, and the dial code from this directory tree.
from panda3d.core import NodePath, Vec3
from direct.tkwidgets.AppShell import AppShell
from direct.tkwidgets import Dial
from direct.tkwidgets import Floater
from direct.directtools.DirectGlobals import ZERO_VEC, UNIT_VEC
from direct.showbase.MessengerGlobal import messenger
from direct.showbase import ShowBaseGlobal
from direct.task.TaskManagerGlobal import taskMgr
import Pmw
import tkinter as tk
#TODO: Task to monitor pose
[docs]class Placer(AppShell):
# Override class variables here
appname = 'Placer Panel'
frameWidth = 625
frameHeight = 215
usecommandarea = 0
usestatusarea = 0
[docs] def __init__(self, parent = None, **kw):
INITOPT = Pmw.INITOPT
optiondefs = (
('title', self.appname, None),
('nodePath', ShowBaseGlobal.direct.camera, None),
)
self.defineoptions(kw, optiondefs)
# Call superclass initialization function
AppShell.__init__(self)
self.initialiseoptions(Placer)
[docs] def appInit(self):
# Initialize state
self.tempCS = ShowBaseGlobal.direct.group.attachNewNode('placerTempCS')
self.orbitFromCS = ShowBaseGlobal.direct.group.attachNewNode(
'placerOrbitFromCS')
self.orbitToCS = ShowBaseGlobal.direct.group.attachNewNode('placerOrbitToCS')
self.refCS = self.tempCS
# Dictionary keeping track of all node paths manipulated so far
self.nodePathDict = {}
self.nodePathDict['camera'] = ShowBaseGlobal.direct.camera
self.nodePathDict['widget'] = ShowBaseGlobal.direct.widget
self.nodePathNames = ['camera', 'widget', 'selected']
self.refNodePathDict = {}
self.refNodePathDict['parent'] = self['nodePath'].getParent()
self.refNodePathDict['render'] = render
self.refNodePathDict['camera'] = ShowBaseGlobal.direct.camera
self.refNodePathDict['widget'] = ShowBaseGlobal.direct.widget
self.refNodePathNames = ['parent', 'self', 'render',
'camera', 'widget', 'selected']
# Initial state
self.initPos = Vec3(0)
self.initHpr = Vec3(0)
self.initScale = Vec3(1)
self.deltaHpr = Vec3(0)
# Offset for orbital mode
self.posOffset = Vec3(0)
# Set up event hooks
self.undoEvents = [('DIRECT_undo', self.undoHook),
('DIRECT_pushUndo', self.pushUndoHook),
('DIRECT_undoListEmpty', self.undoListEmptyHook),
('DIRECT_redo', self.redoHook),
('DIRECT_pushRedo', self.pushRedoHook),
('DIRECT_redoListEmpty', self.redoListEmptyHook)]
for event, method in self.undoEvents:
self.accept(event, method)
# Init movement mode
self.movementMode = 'Relative To:'
[docs] def createInterface(self):
# The interior of the toplevel panel
interior = self.interior()
interior['relief'] = tk.FLAT
# Add placer commands to menubar
self.menuBar.addmenu('Placer', 'Placer Panel Operations')
self.menuBar.addmenuitem('Placer', 'command',
'Zero Node Path',
label = 'Zero All',
command = self.zeroAll)
self.menuBar.addmenuitem('Placer', 'command',
'Reset Node Path',
label = 'Reset All',
command = self.resetAll)
self.menuBar.addmenuitem('Placer', 'command',
'Print Node Path Info',
label = 'Print Info',
command = self.printNodePathInfo)
self.menuBar.addmenuitem(
'Placer', 'command',
'Toggle widget visability',
label = 'Toggle Widget Vis',
command = ShowBaseGlobal.direct.toggleWidgetVis)
self.menuBar.addmenuitem(
'Placer', 'command',
'Toggle widget manipulation mode',
label = 'Toggle Widget Mode',
command = ShowBaseGlobal.direct.manipulationControl.toggleObjectHandlesMode)
# Get a handle to the menu frame
menuFrame = self.menuFrame
self.nodePathMenu = Pmw.ComboBox(
menuFrame, labelpos = tk.W, label_text = 'Node Path:',
entry_width = 20,
selectioncommand = self.selectNodePathNamed,
scrolledlist_items = self.nodePathNames)
self.nodePathMenu.selectitem('selected')
self.nodePathMenuEntry = (
self.nodePathMenu.component('entryfield_entry'))
self.nodePathMenuBG = (
self.nodePathMenuEntry.configure('background')[3])
self.nodePathMenu.pack(side = 'left', fill = 'x', expand = 1)
self.bind(self.nodePathMenu, 'Select node path to manipulate')
modeMenu = Pmw.OptionMenu(menuFrame,
items = ('Relative To:',
'Orbit:'),
initialitem = 'Relative To:',
command = self.setMovementMode,
menubutton_width = 8)
modeMenu.pack(side = 'left', expand = 0)
self.bind(modeMenu, 'Select manipulation mode')
self.refNodePathMenu = Pmw.ComboBox(
menuFrame, entry_width = 16,
selectioncommand = self.selectRefNodePathNamed,
scrolledlist_items = self.refNodePathNames)
self.refNodePathMenu.selectitem('parent')
self.refNodePathMenuEntry = (
self.refNodePathMenu.component('entryfield_entry'))
self.refNodePathMenu.pack(side = 'left', fill = 'x', expand = 1)
self.bind(self.refNodePathMenu, 'Select relative node path')
self.undoButton = tk.Button(menuFrame, text = 'Undo',
command = ShowBaseGlobal.direct.undo)
if ShowBaseGlobal.direct.undoList:
self.undoButton['state'] = 'normal'
else:
self.undoButton['state'] = 'disabled'
self.undoButton.pack(side = 'left', expand = 0)
self.bind(self.undoButton, 'Undo last operation')
self.redoButton = tk.Button(menuFrame, text = 'Redo',
command = ShowBaseGlobal.direct.redo)
if ShowBaseGlobal.direct.redoList:
self.redoButton['state'] = 'normal'
else:
self.redoButton['state'] = 'disabled'
self.redoButton.pack(side = 'left', expand = 0)
self.bind(self.redoButton, 'Redo last operation')
# Create and pack the Pos Controls
posGroup = Pmw.Group(interior,
tag_pyclass = tk.Menubutton,
tag_text = 'Position',
tag_font=('MSSansSerif', 14),
tag_activebackground = '#909090',
ring_relief = tk.RIDGE)
posMenubutton = posGroup.component('tag')
self.bind(posMenubutton, 'Position menu operations')
posMenu = tk.Menu(posMenubutton, tearoff = 0)
posMenu.add_command(label = 'Set to zero', command = self.zeroPos)
posMenu.add_command(label = 'Reset initial',
command = self.resetPos)
posMenubutton['menu'] = posMenu
posGroup.pack(side='left', fill = 'both', expand = 1)
posInterior = posGroup.interior()
# Create the dials
self.posX = self.createcomponent('posX', (), None,
Floater.Floater, (posInterior,),
text = 'X', relief = tk.FLAT,
value = 0.0,
label_foreground = 'Red')
self.posX['commandData'] = ['x']
self.posX['preCallback'] = self.xformStart
self.posX['postCallback'] = self.xformStop
self.posX['callbackData'] = ['x']
self.posX.pack(expand=1, fill='both')
self.posY = self.createcomponent('posY', (), None,
Floater.Floater, (posInterior,),
text = 'Y', relief = tk.FLAT,
value = 0.0,
label_foreground = '#00A000')
self.posY['commandData'] = ['y']
self.posY['preCallback'] = self.xformStart
self.posY['postCallback'] = self.xformStop
self.posY['callbackData'] = ['y']
self.posY.pack(expand=1, fill='both')
self.posZ = self.createcomponent('posZ', (), None,
Floater.Floater, (posInterior,),
text = 'Z', relief = tk.FLAT,
value = 0.0,
label_foreground = 'Blue')
self.posZ['commandData'] = ['z']
self.posZ['preCallback'] = self.xformStart
self.posZ['postCallback'] = self.xformStop
self.posZ['callbackData'] = ['z']
self.posZ.pack(expand=1, fill='both')
# Create and pack the Hpr Controls
hprGroup = Pmw.Group(interior,
tag_pyclass = tk.Menubutton,
tag_text = 'Orientation',
tag_font=('MSSansSerif', 14),
tag_activebackground = '#909090',
ring_relief = tk.RIDGE)
hprMenubutton = hprGroup.component('tag')
self.bind(hprMenubutton, 'Orientation menu operations')
hprMenu = tk.Menu(hprMenubutton, tearoff = 0)
hprMenu.add_command(label = 'Set to zero', command = self.zeroHpr)
hprMenu.add_command(label = 'Reset initial', command = self.resetHpr)
hprMenubutton['menu'] = hprMenu
hprGroup.pack(side='left', fill = 'both', expand = 1)
hprInterior = hprGroup.interior()
# Create the dials
self.hprH = self.createcomponent('hprH', (), None,
Dial.AngleDial, (hprInterior,),
style = 'mini',
text = 'H', value = 0.0,
relief = tk.FLAT,
label_foreground = 'blue')
self.hprH['commandData'] = ['h']
self.hprH['preCallback'] = self.xformStart
self.hprH['postCallback'] = self.xformStop
self.hprH['callbackData'] = ['h']
self.hprH.pack(expand=1, fill='both')
self.hprP = self.createcomponent('hprP', (), None,
Dial.AngleDial, (hprInterior,),
style = 'mini',
text = 'P', value = 0.0,
relief = tk.FLAT,
label_foreground = 'red')
self.hprP['commandData'] = ['p']
self.hprP['preCallback'] = self.xformStart
self.hprP['postCallback'] = self.xformStop
self.hprP['callbackData'] = ['p']
self.hprP.pack(expand=1, fill='both')
self.hprR = self.createcomponent('hprR', (), None,
Dial.AngleDial, (hprInterior,),
style = 'mini',
text = 'R', value = 0.0,
relief = tk.FLAT,
label_foreground = '#00A000')
self.hprR['commandData'] = ['r']
self.hprR['preCallback'] = self.xformStart
self.hprR['postCallback'] = self.xformStop
self.hprR['callbackData'] = ['r']
self.hprR.pack(expand=1, fill='both')
# Create and pack the Scale Controls
# The available scaling modes
self.scalingMode = tk.StringVar()
self.scalingMode.set('Scale Uniform')
# The scaling widgets
scaleGroup = Pmw.Group(interior,
tag_text = 'Scale Uniform',
tag_pyclass = tk.Menubutton,
tag_font=('MSSansSerif', 14),
tag_activebackground = '#909090',
ring_relief = tk.RIDGE)
self.scaleMenubutton = scaleGroup.component('tag')
self.bind(self.scaleMenubutton, 'Scale menu operations')
self.scaleMenubutton['textvariable'] = self.scalingMode
# Scaling menu
scaleMenu = tk.Menu(self.scaleMenubutton, tearoff = 0)
scaleMenu.add_command(label = 'Set to unity',
command = self.unitScale)
scaleMenu.add_command(label = 'Reset initial',
command = self.resetScale)
scaleMenu.add_radiobutton(label = 'Scale Free',
variable = self.scalingMode)
scaleMenu.add_radiobutton(label = 'Scale Uniform',
variable = self.scalingMode)
scaleMenu.add_radiobutton(label = 'Scale Proportional',
variable = self.scalingMode)
self.scaleMenubutton['menu'] = scaleMenu
# Pack group widgets
scaleGroup.pack(side='left', fill = 'both', expand = 1)
scaleInterior = scaleGroup.interior()
# Create the dials
self.scaleX = self.createcomponent('scaleX', (), None,
Floater.Floater, (scaleInterior,),
text = 'X Scale',
relief = tk.FLAT,
min = 0.0001, value = 1.0,
resetValue = 1.0,
label_foreground = 'Red')
self.scaleX['commandData'] = ['sx']
self.scaleX['callbackData'] = ['sx']
self.scaleX['preCallback'] = self.xformStart
self.scaleX['postCallback'] = self.xformStop
self.scaleX.pack(expand=1, fill='both')
self.scaleY = self.createcomponent('scaleY', (), None,
Floater.Floater, (scaleInterior,),
text = 'Y Scale',
relief = tk.FLAT,
min = 0.0001, value = 1.0,
resetValue = 1.0,
label_foreground = '#00A000')
self.scaleY['commandData'] = ['sy']
self.scaleY['callbackData'] = ['sy']
self.scaleY['preCallback'] = self.xformStart
self.scaleY['postCallback'] = self.xformStop
self.scaleY.pack(expand=1, fill='both')
self.scaleZ = self.createcomponent('scaleZ', (), None,
Floater.Floater, (scaleInterior,),
text = 'Z Scale',
relief = tk.FLAT,
min = 0.0001, value = 1.0,
resetValue = 1.0,
label_foreground = 'Blue')
self.scaleZ['commandData'] = ['sz']
self.scaleZ['callbackData'] = ['sz']
self.scaleZ['preCallback'] = self.xformStart
self.scaleZ['postCallback'] = self.xformStop
self.scaleZ.pack(expand=1, fill='both')
# Make sure appropriate labels are showing
self.setMovementMode('Relative To:')
# Set up placer for inital node path
self.selectNodePathNamed('init')
self.selectRefNodePathNamed('parent')
# Update place to reflect initial state
self.updatePlacer()
# Now that you're done setting up, attach commands
self.posX['command'] = self.xform
self.posY['command'] = self.xform
self.posZ['command'] = self.xform
self.hprH['command'] = self.xform
self.hprP['command'] = self.xform
self.hprR['command'] = self.xform
self.scaleX['command'] = self.xform
self.scaleY['command'] = self.xform
self.scaleZ['command'] = self.xform
### WIDGET OPERATIONS ###
[docs] def setMovementMode(self, movementMode):
# Set prefix
namePrefix = ''
self.movementMode = movementMode
if movementMode == 'Relative To:':
namePrefix = 'Relative '
elif movementMode == 'Orbit:':
namePrefix = 'Orbit '
# Update pos widgets
self.posX['text'] = namePrefix + 'X'
self.posY['text'] = namePrefix + 'Y'
self.posZ['text'] = namePrefix + 'Z'
# Update hpr widgets
if movementMode == 'Orbit:':
namePrefix = 'Orbit delta '
self.hprH['text'] = namePrefix + 'H'
self.hprP['text'] = namePrefix + 'P'
self.hprR['text'] = namePrefix + 'R'
# Update temp cs and initialize widgets
self.updatePlacer()
[docs] def setScalingMode(self):
if self['nodePath']:
scale = self['nodePath'].getScale()
if scale[0] != scale[1] or \
scale[0] != scale[2] or \
scale[1] != scale[2]:
self.scalingMode.set('Scale Free')
[docs] def selectNodePathNamed(self, name):
nodePath = None
if name == 'init':
nodePath = self['nodePath']
# Add Combo box entry for the initial node path
self.addNodePath(nodePath)
elif name == 'selected':
nodePath = ShowBaseGlobal.direct.selected.last
# Add Combo box entry for this selected object
self.addNodePath(nodePath)
else:
nodePath = self.nodePathDict.get(name, None)
if nodePath is None:
# See if this evaluates into a node path
try:
nodePath = eval(name)
if isinstance(nodePath, NodePath):
self.addNodePath(nodePath)
else:
# Good eval but not a node path, give up
nodePath = None
except Exception:
# Bogus eval
nodePath = None
# Clear bogus entry from listbox
listbox = self.nodePathMenu.component('scrolledlist')
listbox.setlist(self.nodePathNames)
else:
if name == 'widget':
# Record relationship between selected nodes and widget
ShowBaseGlobal.direct.selected.getWrtAll()
# Update active node path
self.setActiveNodePath(nodePath)
[docs] def setActiveNodePath(self, nodePath):
self['nodePath'] = nodePath
if self['nodePath']:
self.nodePathMenuEntry.configure(
background = self.nodePathMenuBG)
# Check to see if node path and ref node path are the same
if self.refCS is not None and self.refCS == self['nodePath']:
# Yes they are, use temp CS as ref
# This calls updatePlacer
self.setReferenceNodePath(self.tempCS)
# update listbox accordingly
self.refNodePathMenu.selectitem('parent')
else:
# Record initial value and initialize the widgets
self.updatePlacer()
# Record initial position
self.updateResetValues(self['nodePath'])
# Set scaling mode based on node path's current scale
self.setScalingMode()
else:
# Flash entry
self.nodePathMenuEntry.configure(background = 'Pink')
[docs] def selectRefNodePathNamed(self, name):
nodePath = None
if name == 'self':
nodePath = self.tempCS
elif name == 'selected':
nodePath = ShowBaseGlobal.direct.selected.last
# Add Combo box entry for this selected object
self.addRefNodePath(nodePath)
elif name == 'parent':
nodePath = self['nodePath'].getParent()
else:
nodePath = self.refNodePathDict.get(name, None)
if nodePath is None:
# See if this evaluates into a node path
try:
nodePath = eval(name)
if isinstance(nodePath, NodePath):
self.addRefNodePath(nodePath)
else:
# Good eval but not a node path, give up
nodePath = None
except Exception:
# Bogus eval
nodePath = None
# Clear bogus entry from listbox
listbox = self.refNodePathMenu.component('scrolledlist')
listbox.setlist(self.refNodePathNames)
# Check to see if node path and ref node path are the same
if nodePath is not None and nodePath == self['nodePath']:
# Yes they are, use temp CS and update listbox accordingly
nodePath = self.tempCS
self.refNodePathMenu.selectitem('parent')
# Update ref node path
self.setReferenceNodePath(nodePath)
[docs] def setReferenceNodePath(self, nodePath):
self.refCS = nodePath
if self.refCS:
self.refNodePathMenuEntry.configure(
background = self.nodePathMenuBG)
# Update placer to reflect new state
self.updatePlacer()
else:
# Flash entry
self.refNodePathMenuEntry.configure(background = 'Pink')
[docs] def addNodePath(self, nodePath):
self.addNodePathToDict(nodePath, self.nodePathNames,
self.nodePathMenu, self.nodePathDict)
[docs] def addRefNodePath(self, nodePath):
self.addNodePathToDict(nodePath, self.refNodePathNames,
self.refNodePathMenu, self.refNodePathDict)
[docs] 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']:
dictName = name
else:
# Generate a unique name for the dict
dictName = name + '-' + repr(hash(nodePath))
if dictName not in dict:
# 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)
[docs] def updatePlacer(self):
pos = Vec3(0)
hpr = Vec3(0)
scale = Vec3(1)
np = self['nodePath']
if np is not None and isinstance(np, NodePath):
# Update temp CS
self.updateAuxiliaryCoordinateSystems()
# Update widgets
if self.movementMode == 'Orbit:':
pos.assign(self.posOffset)
hpr.assign(ZERO_VEC)
scale.assign(np.getScale())
elif self.refCS:
pos.assign(np.getPos(self.refCS))
hpr.assign(np.getHpr(self.refCS))
scale.assign(np.getScale())
self.updatePosWidgets(pos)
self.updateHprWidgets(hpr)
self.updateScaleWidgets(scale)
[docs] def updateAuxiliaryCoordinateSystems(self):
# Temp CS
self.tempCS.setPosHpr(self['nodePath'], 0, 0, 0, 0, 0, 0)
# Orbit CS
# At reference
self.orbitFromCS.setPos(self.refCS, 0, 0, 0)
# But aligned with target
self.orbitFromCS.setHpr(self['nodePath'], 0, 0, 0)
# Also update to CS
self.orbitToCS.setPosHpr(self.orbitFromCS, 0, 0, 0, 0, 0, 0)
# Get offset from origin
self.posOffset.assign(self['nodePath'].getPos(self.orbitFromCS))
### NODE PATH TRANSFORMATION OPERATIONS ###
[docs] def zeroAll(self):
self.xformStart(None)
self.updatePosWidgets(ZERO_VEC)
self.updateHprWidgets(ZERO_VEC)
self.updateScaleWidgets(UNIT_VEC)
self.xformStop(None)
[docs] def zeroPos(self):
self.xformStart(None)
self.updatePosWidgets(ZERO_VEC)
self.xformStop(None)
[docs] def zeroHpr(self):
self.xformStart(None)
self.updateHprWidgets(ZERO_VEC)
self.xformStop(None)
[docs] def unitScale(self):
self.xformStart(None)
self.updateScaleWidgets(UNIT_VEC)
self.xformStop(None)
[docs] def updateResetValues(self, nodePath):
self.initPos.assign(nodePath.getPos())
self.posX['resetValue'] = self.initPos[0]
self.posY['resetValue'] = self.initPos[1]
self.posZ['resetValue'] = self.initPos[2]
self.initHpr.assign(nodePath.getHpr())
self.hprH['resetValue'] = self.initHpr[0]
self.hprP['resetValue'] = self.initHpr[1]
self.hprR['resetValue'] = self.initHpr[2]
self.initScale.assign(nodePath.getScale())
self.scaleX['resetValue'] = self.initScale[0]
self.scaleY['resetValue'] = self.initScale[1]
self.scaleZ['resetValue'] = self.initScale[2]
[docs] def resetAll(self):
if self['nodePath']:
self.xformStart(None)
self['nodePath'].setPosHprScale(
self.initPos, self.initHpr, self.initScale)
self.xformStop(None)
[docs] def resetPos(self):
if self['nodePath']:
self.xformStart(None)
self['nodePath'].setPos(self.initPos)
self.xformStop(None)
[docs] def resetHpr(self):
if self['nodePath']:
self.xformStart(None)
self['nodePath'].setHpr(self.initHpr)
self.xformStop(None)
[docs] def resetScale(self):
if self['nodePath']:
self.xformStart(None)
self['nodePath'].setScale(self.initScale)
self.xformStop(None)
[docs] def pushUndo(self, fResetRedo = 1):
ShowBaseGlobal.direct.pushUndo([self['nodePath']])
[docs] def undoHook(self, nodePathList = []):
# Reflect new changes
self.updatePlacer()
[docs] def pushUndoHook(self):
# Make sure button is reactivated
self.undoButton.configure(state = 'normal')
[docs] def undoListEmptyHook(self):
# Make sure button is deactivated
self.undoButton.configure(state = 'disabled')
[docs] def pushRedo(self):
ShowBaseGlobal.direct.pushRedo([self['nodePath']])
[docs] def redoHook(self, nodePathList = []):
# Reflect new changes
self.updatePlacer()
[docs] def pushRedoHook(self):
# Make sure button is reactivated
self.redoButton.configure(state = 'normal')
[docs] def redoListEmptyHook(self):
# Make sure button is deactivated
self.redoButton.configure(state = 'disabled')
[docs] def printNodePathInfo(self):
np = self['nodePath']
if np:
name = np.getName()
pos = np.getPos()
hpr = np.getHpr()
scale = np.getScale()
posString = '%.2f, %.2f, %.2f' % (pos[0], pos[1], pos[2])
hprString = '%.2f, %.2f, %.2f' % (hpr[0], hpr[1], hpr[2])
scaleString = '%.2f, %.2f, %.2f' % (scale[0], scale[1], scale[2])
print('NodePath: %s' % name)
print('Pos: %s' % posString)
print('Hpr: %s' % hprString)
print('Scale: %s' % scaleString)
print(('%s.setPosHprScale(%s, %s, %s)' %
(name, posString, hprString, scaleString)))
[docs] def onDestroy(self, event):
# Remove hooks
for event, method in self.undoEvents:
self.ignore(event)
self.tempCS.removeNode()
self.orbitFromCS.removeNode()
self.orbitToCS.removeNode()
[docs]def place(nodePath):
return Placer(nodePath = nodePath)