mirror of
https://github.com/Sneed-Group/Poodletooth-iLand
synced 2024-12-25 20:52:26 -06:00
546 lines
19 KiB
Python
546 lines
19 KiB
Python
""" Finite State Machine Inspector module """
|
|
|
|
__all__ = ['FSMInspector', 'StateInspector']
|
|
|
|
from direct.tkwidgets.AppShell import *
|
|
from direct.showbase.TkGlobal import *
|
|
from tkSimpleDialog import askstring
|
|
from Tkinter import *
|
|
import Pmw, math, operator
|
|
|
|
DELTA = (5.0 / 360.) * 2.0 * math.pi
|
|
|
|
class FSMInspector(AppShell):
|
|
# Override class variables
|
|
appname = 'ClassicFSM Inspector'
|
|
frameWidth = 400
|
|
frameHeight = 450
|
|
usecommandarea = 0
|
|
usestatusarea = 0
|
|
|
|
def __init__(self, fsm, **kw):
|
|
INITOPT = Pmw.INITOPT
|
|
optiondefs = (
|
|
('title', fsm.getName(), None),
|
|
('gridSize', '0.25i', self._setGridSize),
|
|
)
|
|
self.defineoptions(kw, optiondefs)
|
|
|
|
self.fsm = fsm
|
|
# Tell the fsm we are inspecting it so it will send events
|
|
# when it changes state
|
|
self.fsm.inspecting = 1
|
|
|
|
AppShell.__init__(self)
|
|
|
|
self.initialiseoptions(FSMInspector)
|
|
|
|
def appInit(self):
|
|
# Initialize instance variables
|
|
self.states = []
|
|
self.stateInspectorDict = {}
|
|
self.name = self.fsm.getName()
|
|
|
|
def createInterface(self):
|
|
# Create the components
|
|
interior = self.interior()
|
|
menuBar = self.menuBar
|
|
|
|
# ClassicFSM Menu
|
|
menuBar.addmenu('ClassicFSM', 'ClassicFSM Operations')
|
|
menuBar.addmenuitem('ClassicFSM', 'command',
|
|
'Input grid spacing',
|
|
label = 'Grid spacing...',
|
|
command = self.popupGridDialog)
|
|
# Create the checkbutton variable
|
|
self._fGridSnap = IntVar()
|
|
self._fGridSnap.set(1)
|
|
menuBar.addmenuitem('ClassicFSM', 'checkbutton',
|
|
'Enable/disable grid',
|
|
label = 'Snap to grid',
|
|
variable = self._fGridSnap,
|
|
command = self.toggleGridSnap)
|
|
menuBar.addmenuitem('ClassicFSM', 'command',
|
|
'Print out ClassicFSM layout',
|
|
label = 'Print ClassicFSM layout',
|
|
command = self.printLayout)
|
|
|
|
# States Menu
|
|
menuBar.addmenu('States', 'State Inspector Operations')
|
|
menuBar.addcascademenu('States', 'Font Size',
|
|
'Set state label size', tearoff = 1)
|
|
for size in (8, 10, 12, 14, 18, 24):
|
|
menuBar.addmenuitem('Font Size', 'command',
|
|
'Set font to: ' + repr(size) + ' Pts', label = repr(size) + ' Pts',
|
|
command = lambda s = self, sz = size: s.setFontSize(sz))
|
|
menuBar.addcascademenu('States', 'Marker Size',
|
|
'Set state marker size', tearoff = 1)
|
|
for size in ('Small', 'Medium', 'Large'):
|
|
sizeDict = {'Small': '0.25i', 'Medium': '0.375i', 'Large': '0.5i'}
|
|
menuBar.addmenuitem('Marker Size', 'command',
|
|
size + ' markers', label = size + ' Markers',
|
|
command = lambda s = self, sz = size, d = sizeDict:
|
|
s.setMarkerSize(d[sz]))
|
|
|
|
# The Scrolled Canvas
|
|
self._scrolledCanvas = self.createcomponent('scrolledCanvas',
|
|
(), None,
|
|
Pmw.ScrolledCanvas, (interior,),
|
|
hull_width = 400, hull_height = 400,
|
|
usehullsize = 1)
|
|
self._canvas = self._scrolledCanvas.component('canvas')
|
|
self._canvas['scrollregion'] = ('-2i', '-2i', '2i', '2i')
|
|
self._scrolledCanvas.resizescrollregion()
|
|
self._scrolledCanvas.pack(padx = 5, pady = 5, expand=1, fill = BOTH)
|
|
|
|
# Update lines
|
|
self._canvas.bind('<B1-Motion>', self.drawConnections)
|
|
self._canvas.bind('<ButtonPress-2>', self.mouse2Down)
|
|
self._canvas.bind('<B2-Motion>', self.mouse2Motion)
|
|
self._canvas.bind('<Configure>',
|
|
lambda e, sc = self._scrolledCanvas:
|
|
sc.resizescrollregion())
|
|
|
|
self.createStateInspectors()
|
|
|
|
self.initialiseoptions(FSMInspector)
|
|
|
|
def scrolledCanvas(self):
|
|
return self._scrolledCanvas
|
|
|
|
def canvas(self):
|
|
return self._canvas
|
|
|
|
def setFontSize(self, size):
|
|
self._canvas.itemconfigure('labels', font = ('MS Sans Serif', size))
|
|
|
|
def setMarkerSize(self, size):
|
|
for key in self.stateInspectorDict.keys():
|
|
self.stateInspectorDict[key].setRadius(size)
|
|
self.drawConnections()
|
|
|
|
def drawConnections(self, event = None):
|
|
# Get rid of existing arrows
|
|
self._canvas.delete('arrow')
|
|
for key in self.stateInspectorDict.keys():
|
|
si = self.stateInspectorDict[key]
|
|
state = si.state
|
|
if state.getTransitions():
|
|
for name in state.getTransitions():
|
|
self.connectStates(si, self.getStateInspector(name))
|
|
|
|
def connectStates(self, fromState, toState):
|
|
endpts = self.computeEndpoints(fromState, toState)
|
|
line = self._canvas.create_line(endpts, tags = ('arrow',),
|
|
arrow = 'last')
|
|
|
|
def computeEndpoints(self, fromState, toState):
|
|
# Compute angle between two points
|
|
fromCenter = fromState.center()
|
|
toCenter = toState.center()
|
|
angle = self.findAngle(fromCenter, toCenter)
|
|
|
|
# Compute offset fromState point
|
|
newFromPt = map(operator.__add__,
|
|
fromCenter,
|
|
self.computePoint(fromState.radius,
|
|
angle + DELTA))
|
|
|
|
# Compute offset toState point
|
|
newToPt = map(operator.__sub__,
|
|
toCenter,
|
|
self.computePoint(toState.radius,
|
|
angle - DELTA))
|
|
return newFromPt + newToPt
|
|
|
|
def computePoint(self, radius, angle):
|
|
x = radius * math.cos(angle)
|
|
y = radius * math.sin(angle)
|
|
return (x, y)
|
|
|
|
def findAngle(self, fromPoint, toPoint):
|
|
dx = toPoint[0] - fromPoint[0]
|
|
dy = toPoint[1] - fromPoint[1]
|
|
return math.atan2(dy, dx)
|
|
|
|
def mouse2Down(self, event):
|
|
self._width = 1.0 * self._canvas.winfo_width()
|
|
self._height = 1.0 * self._canvas.winfo_height()
|
|
xview = self._canvas.xview()
|
|
yview = self._canvas.yview()
|
|
self._left = xview[0]
|
|
self._top = yview[0]
|
|
self._dxview = xview[1] - xview[0]
|
|
self._dyview = yview[1] - yview[0]
|
|
self._2lx = event.x
|
|
self._2ly = event.y
|
|
|
|
def mouse2Motion(self, event):
|
|
newx = self._left - ((event.x - self._2lx)/self._width) * self._dxview
|
|
self._canvas.xview_moveto(newx)
|
|
newy = self._top - ((event.y - self._2ly)/self._height) * self._dyview
|
|
self._canvas.yview_moveto(newy)
|
|
self._2lx = event.x
|
|
self._2ly = event.y
|
|
self._left = self._canvas.xview()[0]
|
|
self._top = self._canvas.yview()[0]
|
|
|
|
def createStateInspectors(self):
|
|
fsm = self.fsm
|
|
self.states = fsm.getStates()
|
|
# Number of rows/cols needed to fit inspectors in a grid
|
|
dim = int(math.ceil(math.sqrt(len(self.states))))
|
|
# Separation between nodes
|
|
spacing = 2.5 * self._canvas.canvasx('0.375i')
|
|
count = 0
|
|
for state in self.states:
|
|
si = self.addState(state)
|
|
if state.getInspectorPos():
|
|
si.setPos(state.getInspectorPos()[0],
|
|
state.getInspectorPos()[1])
|
|
else:
|
|
row = int(math.floor(count / dim))
|
|
col = count % dim
|
|
si.setPos(col * spacing, row * spacing +
|
|
0.5 * (0, spacing)[col % 2])
|
|
# Add hooks
|
|
self.accept(self.name + '_' + si.getName() + '_entered',
|
|
si.enteredState)
|
|
self.accept(self.name + '_' + si.getName() + '_exited',
|
|
si.exitedState)
|
|
count = count + 1
|
|
self.drawConnections()
|
|
if fsm.getCurrentState():
|
|
self.enteredState(fsm.getCurrentState().getName())
|
|
|
|
def getStateInspector(self, name):
|
|
return self.stateInspectorDict.get(name, None)
|
|
|
|
def addState(self, state):
|
|
si = self.stateInspectorDict[state.getName()] = (
|
|
StateInspector(self, state))
|
|
return si
|
|
|
|
def enteredState(self, stateName):
|
|
si = self.stateInspectorDict.get(stateName, None)
|
|
if si:
|
|
si.enteredState()
|
|
|
|
def exitedState(self, stateName):
|
|
si = self.stateInspectorDict.get(stateName, None)
|
|
if si:
|
|
si.exitedState()
|
|
|
|
def _setGridSize(self):
|
|
self._gridSize = self['gridSize']
|
|
self.setGridSize(self._gridSize)
|
|
|
|
def setGridSize(self, size):
|
|
for key in self.stateInspectorDict.keys():
|
|
self.stateInspectorDict[key].setGridSize(size)
|
|
|
|
def popupGridDialog(self):
|
|
spacing = askstring('ClassicFSM Grid Spacing', 'Grid Spacing:')
|
|
if spacing:
|
|
self.setGridSize(spacing)
|
|
self._gridSize = spacing
|
|
|
|
def toggleGridSnap(self):
|
|
if self._fGridSnap.get():
|
|
self.setGridSize(self._gridSize)
|
|
else:
|
|
self.setGridSize(0)
|
|
|
|
def printLayout(self):
|
|
dict = self.stateInspectorDict
|
|
keys = dict.keys()
|
|
keys.sort
|
|
print "ClassicFSM.ClassicFSM('%s', [" % self.name
|
|
for key in keys[:-1]:
|
|
si = dict[key]
|
|
center = si.center()
|
|
print " State.State('%s'," % si.state.getName()
|
|
print " %s," % si.state.getEnterFunc().__name__
|
|
print " %s," % si.state.getExitFunc().__name__
|
|
print " %s," % si.state.getTransitions()
|
|
print " inspectorPos = ",
|
|
print "[%.1f, %.1f])," % (center[0], center[1])
|
|
for key in keys[-1:]:
|
|
si = dict[key]
|
|
center = si.center()
|
|
print " State.State('%s'," % si.state.getName()
|
|
print " %s," % si.state.getEnterFunc().__name__
|
|
print " %s," % si.state.getExitFunc().__name__
|
|
print " %s," % si.state.getTransitions()
|
|
print " inspectorPos = ",
|
|
print "[%.1f, %.1f])]," % (center[0], center[1])
|
|
print " '%s'," % self.fsm.getInitialState().getName()
|
|
print " '%s')" % self.fsm.getFinalState().getName()
|
|
|
|
def toggleBalloon(self):
|
|
if self.toggleBalloonVar.get():
|
|
self.balloon.configure(state = 'balloon')
|
|
else:
|
|
self.balloon.configure(state = 'none')
|
|
|
|
def onDestroy(self, event):
|
|
""" Called on ClassicFSM Panel shutdown """
|
|
self.fsm.inspecting = 0
|
|
for si in self.stateInspectorDict.values():
|
|
self.ignore(self.name + '_' + si.getName() + '_entered')
|
|
self.ignore(self.name + '_' + si.getName() + '_exited')
|
|
|
|
class StateInspector(Pmw.MegaArchetype):
|
|
def __init__(self, inspector, state, **kw):
|
|
|
|
# Record inspector and state
|
|
self.inspector = inspector
|
|
self.state = state
|
|
# Create a unique tag which you can use to move a marker and
|
|
# and its corresponding text around together
|
|
self.tag = state.getName()
|
|
self.fsm = inspector.fsm
|
|
|
|
# Pointers to the inspector's components
|
|
self.scrolledCanvas = inspector.component('scrolledCanvas')
|
|
self._canvas = self.scrolledCanvas.component('canvas')
|
|
|
|
#define the megawidget options
|
|
optiondefs = (
|
|
('radius', '0.375i', self._setRadius),
|
|
('gridSize', '0.25i', self._setGridSize),
|
|
)
|
|
self.defineoptions(kw, optiondefs)
|
|
|
|
# Initialize the parent class
|
|
Pmw.MegaArchetype.__init__(self)
|
|
|
|
# Draw the oval
|
|
self.x = 0
|
|
self.y = 0
|
|
half = self._canvas.winfo_fpixels(self['radius'])
|
|
self.marker = self._canvas.create_oval((self.x - half),
|
|
(self.y - half),
|
|
(self.x + half),
|
|
(self.y + half),
|
|
fill = 'CornflowerBlue',
|
|
tags = (self.tag,'markers'))
|
|
self.text = self._canvas.create_text(0, 0, text = state.getName(),
|
|
justify = CENTER,
|
|
tags = (self.tag,'labels'))
|
|
# Is this state contain a sub machine?
|
|
if state.hasChildren():
|
|
# reduce half by sqrt of 2.0
|
|
half = half * 0.707106
|
|
self.rect = self._canvas.create_rectangle((- half), (- half),
|
|
half, half,
|
|
tags = (self.tag,))
|
|
|
|
|
|
# The Popup State Menu
|
|
self._popupMenu = Menu(self._canvas, tearoff = 0)
|
|
self._popupMenu.add_command(label = 'Request transition to ' +
|
|
state.getName(),
|
|
command = self.transitionTo)
|
|
if state.hasChildren():
|
|
self._popupMenu.add_command(label = 'Inspect ' + state.getName() +
|
|
' submachine',
|
|
command = self.inspectSubMachine)
|
|
|
|
self.scrolledCanvas.resizescrollregion()
|
|
|
|
# Add bindings
|
|
self._canvas.tag_bind(self.tag, '<Enter>', self.mouseEnter)
|
|
self._canvas.tag_bind(self.tag, '<Leave>', self.mouseLeave)
|
|
self._canvas.tag_bind(self.tag, '<ButtonPress-1>', self.mouseDown)
|
|
self._canvas.tag_bind(self.tag, '<B1-Motion>', self.mouseMotion)
|
|
self._canvas.tag_bind(self.tag, '<ButtonRelease-1>', self.mouseRelease)
|
|
self._canvas.tag_bind(self.tag, '<ButtonPress-3>', self.popupStateMenu)
|
|
|
|
self.initialiseoptions(StateInspector)
|
|
|
|
# Utility methods
|
|
def _setRadius(self):
|
|
self.setRadius(self['radius'])
|
|
|
|
def setRadius(self, size):
|
|
half = self.radius = self._canvas.winfo_fpixels(size)
|
|
c = self.center()
|
|
self._canvas.coords(self.marker,
|
|
c[0] - half, c[1] - half, c[0] + half, c[1] + half)
|
|
if self.state.hasChildren():
|
|
half = self.radius * 0.707106
|
|
self._canvas.coords(self.rect,
|
|
c[0] - half, c[1] - half, c[0] + half, c[1] + half)
|
|
|
|
def _setGridSize(self):
|
|
self.setGridSize(self['gridSize'])
|
|
|
|
def setGridSize(self, size):
|
|
self.gridSize = self._canvas.winfo_fpixels(size)
|
|
if self.gridSize == 0:
|
|
self.fGridSnap = 0
|
|
else:
|
|
self.fGridSnap = 1
|
|
|
|
def setText(self, text = None):
|
|
self._canvas.itemconfigure(self.text, text = text)
|
|
|
|
def setPos(self, x, y, snapToGrid = 0):
|
|
if self.fGridSnap:
|
|
self.x = round(x / self.gridSize) * self.gridSize
|
|
self.y = round(y / self.gridSize) * self.gridSize
|
|
else:
|
|
self.x = x
|
|
self.y = y
|
|
# How far do we have to move?
|
|
cx, cy = self.center()
|
|
self._canvas.move(self.tag, self.x - cx, self.y - cy)
|
|
|
|
def center(self):
|
|
c = self._canvas.coords(self.marker)
|
|
return (c[0] + c[2])/2.0, (c[1] + c[3])/2.0
|
|
|
|
def getName(self):
|
|
return self.tag
|
|
|
|
# Event Handlers
|
|
def mouseEnter(self, event):
|
|
self._canvas.itemconfig(self.marker, width = 2)
|
|
|
|
def mouseLeave(self, event):
|
|
self._canvas.itemconfig(self.marker, width = 1)
|
|
|
|
def mouseDown(self, event):
|
|
self._canvas.lift(self.tag)
|
|
self.startx, self.starty = self.center()
|
|
self.lastx = self._canvas.canvasx(event.x)
|
|
self.lasty = self._canvas.canvasy(event.y)
|
|
|
|
def mouseMotion(self, event):
|
|
dx = self._canvas.canvasx(event.x) - self.lastx
|
|
dy = self._canvas.canvasy(event.y) - self.lasty
|
|
newx, newy = map(operator.__add__, (self.startx, self.starty), (dx, dy))
|
|
self.setPos(newx, newy)
|
|
|
|
def mouseRelease(self, event):
|
|
self.scrolledCanvas.resizescrollregion()
|
|
|
|
def popupStateMenu(self, event):
|
|
self._popupMenu.post(event.widget.winfo_pointerx(),
|
|
event.widget.winfo_pointery())
|
|
|
|
def transitionTo(self):
|
|
self.fsm.request(self.getName())
|
|
|
|
def inspectSubMachine(self):
|
|
print 'inspect ' + self.tag + ' subMachine'
|
|
for childFSM in self.state.getChildren():
|
|
FSMInspector(childFSM)
|
|
|
|
def enteredState(self):
|
|
self._canvas.itemconfigure(self.marker, fill = 'Red')
|
|
|
|
def exitedState(self):
|
|
self._canvas.itemconfigure(self.marker, fill = 'CornflowerBlue')
|
|
|
|
|
|
"""
|
|
# USING FINITE STATE INSPECTOR
|
|
|
|
1) in your Configrc add:
|
|
|
|
want-tk #t
|
|
|
|
2) start up the show and create a Finite State Machine
|
|
|
|
from direct.showbase.ShowBaseGlobal import *
|
|
|
|
from direct.fsm import ClassicFSM
|
|
from direct.fsm import State
|
|
|
|
def enterState():
|
|
print 'enterState'
|
|
|
|
def exitState():
|
|
print 'exitState'
|
|
|
|
fsm = ClassicFSM.ClassicFSM('stopLight',
|
|
[State.State('red', enterState, exitState, ['green']),
|
|
State.State('yellow', enterState, exitState, ['red']),
|
|
State.State('green', enterState, exitState, ['yellow'])],
|
|
'red',
|
|
'red')
|
|
|
|
import FSMInspector
|
|
|
|
inspector = FSMInspector.FSMInspector(fsm, title = fsm.getName())
|
|
|
|
# Note, the inspectorPos argument is optional, the inspector will
|
|
# automagically position states on startup
|
|
fsm = ClassicFSM.ClassicFSM('stopLight', [
|
|
State.State('yellow',
|
|
enterState,
|
|
exitState,
|
|
['red'],
|
|
inspectorPos = [95.9, 48.0]),
|
|
State.State('red',
|
|
enterState,
|
|
exitState,
|
|
['green'],
|
|
inspectorPos = [0.0, 0.0]),
|
|
State.State('green',
|
|
enterState,
|
|
exitState,
|
|
['yellow'],
|
|
inspectorPos = [0.0, 95.9])],
|
|
'red',
|
|
'red')
|
|
|
|
3) Pop open a viewer
|
|
|
|
import FSMInspector
|
|
insp = FSMInspector.FSMInspector(fsm)
|
|
|
|
or if you wish to be fancy:
|
|
|
|
insp = FSMInspector.FSMInspector(fsm, title = fsm.getName())
|
|
|
|
Features:
|
|
- Right mouse button over a state pops up a menu allowing you to
|
|
request a transition to that state
|
|
- Middle mouse button will grab the canvas and slide things around
|
|
if your state machine is bigger than the viewing area
|
|
- There are some self explanatory menu options up at the top, the most
|
|
useful being: "print ClassicFSM layout" which will print out python code
|
|
which will create an ClassicFSM augmented with layout information for the
|
|
viewer so everything shows up in the same place the next time you
|
|
inspect the state machine
|
|
|
|
CAVEAT:
|
|
|
|
There is some unexplained problems with using TK and emacs right now which
|
|
occasionally results in everything locking up. This procedure seems to
|
|
avoid the problem for me:
|
|
|
|
# Start up the show
|
|
from direct.showbase.ShowBaseGlobal import *
|
|
|
|
# You will see the window and a Tk panel pop open
|
|
|
|
# Type a number at the emacs prompt
|
|
>>> 123
|
|
|
|
# At this point everything will lock up and you won't get your prompt back
|
|
|
|
# Hit a bunch of Control-C's in rapid succession, in most cases
|
|
# this will break you out of whatever badness you were in and
|
|
# from that point on everything will behave normally
|
|
|
|
|
|
# This is how you pop up an inspector
|
|
import FSMInspector
|
|
inspector = FSMInspector.FSMInspector(fsm, title = fsm.getName())
|
|
|
|
"""
|
|
|