Hi,

I'm attaching the canvas object code I've been playing with.

The API is still incomplete, but in the spirit of release early,
release often, I'll put it out there for people to comment on.

This should use a common callback mechanism with mpl (e.g., the
user should bind to a axis limit change event in much the same
way that they bind to mouse events on an artist), but for the
purposes of putting together a demo I created a small one of 
my own.  Maybe someone with Traits knowledge can show me how
much better it could be?

        - Paul

"""
Extension to MPL to support the binding of artists to key/mouse events.
"""

class Selection:
    """
    Store and compare selections.
    """
    # TODO: We need some way to check in prop matches, preferably
    # TODO: without imposing structure on prop.
        
    artist=None
    prop={}
    def __init__(self,artist=None,prop={}):
        self.artist,self.prop = artist,self.prop
    def __eq__(self,other):
        return self.artist is other.artist
    def __ne__(self,other):
        return self.artist is not other.artist
    def __nonzero__(self):
        return self.artist is not None

class BindArtist:

    # Track keyboard modifiers for events.
    # TODO: Move keyboard modifier support into the backend.  We cannot
    # TODO: properly support it from outside the windowing system since there
    # TODO: is no way to recognized whether shift is held down when the mouse
    # TODO: first clicks on the the application window.
    control,shift,alt,meta=False,False,False,False

    # Track doubleclick
    dclick_threshhold = 0.25
    _last_button, _last_time = None, 0
    
    # Mouse/keyboard events we can bind to
    events= ['enter','leave','motion','click','dclick','drag','release',
             'scroll','key','keyup']
    # TODO: Need our own event structure
    
    def __init__(self,figure):
        canvas = figure.canvas

        # Link to keyboard/mouse
        self._connections = [
            canvas.mpl_connect('motion_notify_event',self._onMotion),
            canvas.mpl_connect('button_press_event',self._onClick),
            canvas.mpl_connect('button_release_event',self._onRelease),
            canvas.mpl_connect('key_press_event',self._onKey),
            canvas.mpl_connect('key_release_event',self._onKeyRelease),
            canvas.mpl_connect('scroll_event',self._onScroll)
        ]

        # Turn off picker if it hasn't already been done
        try:
            canvas.mpl_disconnect(canvas.button_pick_id)
            canvas.mpl_disconnect(canvas.scroll_pick_id)
        except: 
            pass
        
        self.canvas = canvas
        self.figure = figure
        self.clearall()
        
    def clear(self, *artists):
        """
        self.clear(h1,h2,...)
            Remove connections for artists h1, h2, ...
            
        Use clearall() to reset all connections.
        """

        for h in artists:
            for a in self.events:
                if h in self._actions[a]: del self._actions[a][h]
            if h in self._artists: self._artists.remove(h)
        if self._current.artist in artists: self._current = Selection()
        if self._hasclick.artist in artists: self._hasclick = Selection()
        if self._haskey.artist in artists: self._haskey = Selection()
        
    def clearall(self):
        """
        Clear connections to all artists.
        
        Use clear(h1,h2,...) to reset specific artists.
        """
        # Don't monitor any actions
        self._actions = {}
        for action in self.events:
            self._actions[action] = {}

        # Need activity state
        self._artists = []
        self._current = Selection()
        self._hasclick = Selection()
        self._haskey = Selection()

    def disconnect(self):
        """
        In case we need to disconnect from the canvas...
        """
        for cid in self._connections: canvas.mpl_disconnect(cid)
        self._connections = []

    def __del__(self):
        self.disconnect()

    def __call__(self,trigger,artist,action):
        """Register a callback for an artist to a particular trigger event.
        
        usage:
            self.connect(eventname,artist,action)
    
        where:
            eventname is a string
            artist is the particular graph object to respond to the event
            action(event,**kw) is called when the event is triggered

        The action callback is associated with particular artists.
        Different artists will have different kwargs.  See documentation
        on the contains() method for each artist.  One common properties
        are ind for the index of the item under the cursor, which is
        returned by Line2D and by collections.

        The following events are supported:
            enter: mouse cursor moves into the artist or to a new index
            leave: mouse cursor leaves the artist
            click: mouse button pressed on the artist
            drag: mouse button pressed on the artist and cursor moves
            release: mouse button released for the artist
            key: key pressed when mouse is on the artist
            keyrelease: key released for the artist
    
        The event received by action has a number of attributes:
            name is the event name which was triggered
            artist is the object which triggered the event
            x,y are the screen coordinates of the mouse
            xdata,ydata are the graph coordinates of the mouse
            button is the mouse button being pressed/released
            key is the key being pressed/released
            shift,control,alt,meta are flags which are true if the
                corresponding key is pressed at the time of the event.
            details is a dictionary of artist specific details, such as the 
                id(s) of the point that were clicked.
                
        When receiving an event, first check the modifier state to be
        sure it applies.  E.g., the callback for 'press' might be:
            if event.button == 1 and event.shift: process Shift-click

        TODO: Only receive events with the correct modifiers (e.g., S-click,
        TODO:   or *-click for any modifiers).
        TODO: Only receive button events for the correct button (e.g., click1
        TODO:   release3, or dclick* for any button)
        TODO: Support virtual artist, so that and artist can be flagged as
        TODO:   having a tag list and receive the correct events
        TODO: Support virtual events for binding to button-3 vs shift button-1
        TODO:   without changing callback code
        TODO: Attach multiple callbacks to the same event?
        TODO: Clean up interaction with toolbar modes
        TODO: push/pushclear/pop context so that binding changes for the 
duration
        TODO:   e.g., to support ? context sensitive help
        """
        # Check that the trigger is valid
        if trigger not in self._actions:
            raise ValueError,"%s invalid --- valid triggers are %s"\
                %(trigger,", ".join(self.events))

        # Register the trigger callback
        self._actions[trigger][artist]=action
        #print "==> 
added",artist,[artist],"to",trigger,":",self._actions[trigger].keys()

        # Maintain a list of all artists
        if artist not in self._artists: 
            self._artists.append(artist)

    def trigger(self,actor,action,ev):
        """
        Trigger a particular event for the artist.  Fallback to axes,
        to figure, and to 'all' if the event is not processed.
        """
        if action not in self.events:
            raise ValueError, "Trigger expects "+", ".join(self.events)
        
        # Tag the event with modifiers
        for mod in ('alt','control','shift','meta'):
            setattr(ev,mod,getattr(self,mod))
        setattr(ev,'artist',None)
        setattr(ev,'action',action)
        setattr(ev,'prop',{})
        
        # Fallback scheme.  If the event does not return false, pass to parent.
        processed = False
        artist,prop = actor.artist,actor.prop
        if artist in self._actions[action]:
            ev.artist,ev.prop = artist,prop
            processed = self._actions[action][artist](ev)
        if not processed and ev.inaxes in self._actions[action]:
            ev.artist,ev.prop = ev.inaxes,{}
            processed = self._actions[action][ev.inaxes](ev)
        if not processed and self.figure in self._actions[action]:
            ev.artist,ev.prop = self.figure,{}
            processed = self._actions[action][self.figure](ev)
        if not processed and 'all' in self._actions[action]:
            ev.artist,ev.prop = None,{}
            processed = self._actions[action]['all'](ev)
        return processed

    def _find_current(self, event):
        """
        Find the artist who will receive the event.  Only search
        registered artists.  All others are invisible to the mouse.
        """
        # TODO: sort by zorder of axes then by zorder within axes
        self._artists.sort(cmp=lambda x,y: y.zorder-x.zorder)
        # print "search"," ".join([str(h) for h in self._artists])
        found = Selection()
        #print "searching in",self._artists
        for artist in self._artists:
            # TODO: should contains() return false if invisible?
            if not artist.get_visible(): 
                continue
            # TODO: optimization - exclude artists not inaxes
            inside,prop = artist.contains(event)
            if inside:
                found.artist,found.prop = artist,prop
                break
        #print "found",found.artist
        
        # TODO: how to check if prop is equal?
        if found != self._current:
            self.trigger(self._current,'leave',event)
            self.trigger(found,'enter',event)
        self._current = found

        return found
        
    def _onMotion(self,event):
        """
        Track enter/leave/motion through registered artists; all
        other artists are invisible.
        """
        ## Can't kill double-click on motion since Windows produces
        ## spurious motion events.
        #self._last_button = None
        
        # Dibs on the motion event for the clicked artist
        if self._hasclick:
            #print self._hasclick.artist,"has click"
            self.trigger(self._hasclick,'drag',event)
        else:
            found = self._find_current(event)
            #print "found",found.artist
            self.trigger(found,'motion',event)

    def _onClick(self,event):
        """
        Process button click
        """
        import time
        
        # Check for double-click
        event_time = time.time()
        #print event_time,self._last_time,self.dclick_threshhold
        #print (event_time > self._last_time + self.dclick_threshhold)
        #print event.button,self._last_button
        if (event.button != self._last_button) or \
                (event_time > self._last_time + self.dclick_threshhold):
            action = 'click'
        else:
            action = 'dclick'
        self._last_button = event.button
        self._last_time = event_time
        
        # If an artist is already dragging, feed any additional button
        # presses to that artist.
        # TODO: do we want to force a single button model on the user?
        # TODO: that is, once a button is pressed, no other buttons
        # TODO: can come through?  I think this belongs in canvas, not here.
        if self._hasclick:
            found = self._hasclick
        else:
            found = self._find_current(event)
        #print "button %d pressed"%event.button
        # Note: it seems like if "click" returns False then hasclick should
        # not be set.  The problem is that there are two reasons it can
        # return false: because there is no click action for this artist
        # or because the click action returned false.  A related problem
        # is that click actions will go to the canvas if there is no click
        # action for the artist, even if the artist has a drag. I'll leave
        # it to future maintainers to sort out this problem.  For now the
        # recommendation is that users should define click if they have
        # drag or release on the artist.
        self.trigger(found,action,event)
        self._hasclick = found

    def _onDClick(self,event):
        """
        Process button double click
        """
        # If an artist is already dragging, feed any additional button
        # presses to that artist.
        # TODO: do we want to force a single button model on the user?
        # TODO: that is, once a button is pressed, no other buttons
        # TODO: can come through?  I think this belongs in canvas, not here.
        if self._hasclick:
            found = self._hasclick
        else:
            found = self._find_current(event)
        self.trigger(found,'dclick',event)
        self._hasclick = found

    def _onRelease(self,event):
        """
        Process release release
        """
        self.trigger(self._hasclick,'release',event)
        self._hasclick = Selection()
            
    def _onKey(self,event):
        """
        Process key click
        """
        # TODO: Do we really want keyboard focus separate from mouse focus?
        # TODO: Do we need an explicit focus command for keyboard?
        # TODO: Can we tab between items?
        # TODO: How do unhandled events get propogated to axes, figure and
        # TODO: finally to application?  Do we need to implement a full tags 
        # TODO: architecture a la Tk?
        # TODO: Do modifiers cause a grab?  Does the artist see the modifiers?
        if event.key in ('alt','meta','control','shift'):
            setattr(self,event.key,True)
            return

        if self._haskey:
            found = self._haskey
        else:
            found = self._find_current(event)
        self.trigger(found,'key',event)
        self._haskey = found
    
    def _onKeyRelease(self,event):
        """
        Process key release
        """
        if event.key in ('alt','meta','control','shift'):
            setattr(self,event.key,False)
            return
        
        if self._haskey:
            self.trigger(self._haskey,'keyup',event)
        self._haskey = Selection()

    def _onScroll(self,event):
        """
        Process scroll event
        """
        found = self._find_current(event)
        self.trigger(found,'scroll',event)

#!/usr/bin/env python

"""
Example usage of BindArtist, the MPL extension to allow canvas objects.

Note: the BindArtist api is not yet settled, so this application is likely
to change.
"""

from pylab import *
from binder import BindArtist
from bspline3 import BSpline3
        
# TODO: should profile editors subclass Artist, or maybe Widget?
class BSplinePlot:
    """Edit a parametric B-Spline profile"""
    def __init__(self, ax, spline):
        self.ax = ax
        self.fig = ax.figure
        self.spline = spline

        # Initialize callbacks
        self.callback = BindArtist(self.fig)
        self.callback.clearall()
        self.ax.cla()
        self.callback('dclick',self.ax,self.onAdd)

        # Add artists to the canvas
        [self.hspline] = self.ax.plot([],[],marker='',
                                      linestyle='-',color='green')        
        self.hpoints = []
        for x,y in self.spline:
            self._appendknot(x,y)

        # Draw the canvas
        self.draw()
    
    def _appendknot(self,x,y):
        """
        Append a knot to the list of knots on the canvas.  This had better
        maintain order within the spline if callbacks are to work properly.
        """
        [h] = self.ax.plot([x],[y],marker='s',markersize=8,
                      linestyle='',color='yellow',pickradius=5)
        self.callback('enter',h,self.onHilite)
        self.callback('leave',h,self.onHilite)
        self.callback('drag',h,self.onDrag)
        self.callback('dclick',h,self.onRemove)
        self.hpoints.append(h)
        return True
    
    def _removeknot(self, artist):
        """
        Remove the knot associated with the artist.
        """
        i = self.hpoints.index(artist)
        del self.spline[i]
        del self.hpoints[i]
        artist.remove()
 
    def draw(self):
        """
        Recompute the spline curve and show the canvas.
        """
        x,y = self.spline.sample()
        self.hspline.set_data(x,y)
        self.fig.canvas.draw_idle()

    def onHilite(self,ev):
        """
        Let the user know which point is being edited.
        """
        if ev.action == 'enter':
            ev.artist.set_color('lightblue')
        else:
            ev.artist.set_color('yellow')
        self.fig.canvas.draw_idle()
        return True

    def onDrag(self,ev):
        """
        Move the selected control point.
        """
        if ev.inaxes == self.ax:
            i = self.hpoints.index(ev.artist)
            self.spline[i] = ev.xdata, ev.ydata
            ev.artist.set_data([ev.xdata],[ev.ydata])
            self.draw()
        return True

    def onRemove(self,ev):
        """
        Remove the selected point.
        """
        # Don't delete the last one
        if len(self.spline) > 1:
            self._removeknot(ev.artist)
            self.draw()
        return True
        
    def onAppend(self,ev):
        """
        Append a new control point to the end of the spline.
        """
        self.spline.append(ev.xdata,ev.ydata)
        self._appendknot(ev.xdata,ev.ydata)
        self.draw()
        return True
        
def demo():
    Ix = nx.array([1,2,3,4,5,6],'f')
    Iy = nx.array([1,2,3,2,1,2],'f')
    model = BSplinePlot(subplot(111),BSpline3(Ix,Iy))
    show()

if __name__ == "__main__": demo()
import numpy as N

def max(a,b):
    return (a<b).choose(a,b)

def min(a,b):
    return (a>b).choose(a,b)

def lookup(a,b):
    return a.searchsorted(b)

def cat(*args):
    return N.concatenate(args)

# f, f', f'', f''' = bspline3(knot, control, t, nderiv=0)
#   Evaluate the B-spline specified by the given knot sequence and
#   control values at the parametric points t.   The knot sequence
#   should be four elements longer than the control sequence.
#   Returns up to p(t), p'(t), p''(t), p'''(t) depending on nderiv.
# bspline3(knot, control, t, clamp=True)
#   Clamps the spline to the value of the final control point beyond
#   the ends of the knot sequence.   Default is 'zero' for clamping 
#   the spline to zero.
def bspline3(knot, control, t, clamp=False, nderiv=0):
  degree = len(knot) - len(control);
  if degree != 4: raise ValueError, "must have two extra knots at each end"

  if clamp:
    # Alternative approach spline is clamped to initial/final control values
    control = cat([control[0]]*(degree-1), control, [control[-1]])
  else:
    # Traditional approach: spline goes to zero at +/- infinity.
    control = cat([0]*(degree-1), control, [0])


  # Deal with values outside the range
  valid = (t > knot[0]) & (t <= knot[-1])
  tv = t[valid]
  f = N.zeros(t.shape)
  df = N.zeros(t.shape)
  d2f = N.zeros(t.shape)
  d3f = N.zeros(t.shape)
  f[t<=knot[0]] = control[0]
  f[t>=knot[-1]] = control[-1]

  # Find B-Spline parameters for the individual segments 
  end = len(knot)-1
  segment = lookup(knot,tv)-1
  tm2 = knot[max(segment-2,0)]
  tm1 = knot[max(segment-1,0)]
  tm0 = knot[max(segment-0,0)]
  tp1 = knot[min(segment+1,end)]
  tp2 = knot[min(segment+2,end)]
  tp3 = knot[min(segment+3,end)]

  P4 = control[min(segment+3,end)]
  P3 = control[min(segment+2,end)]
  P2 = control[min(segment+1,end)]
  P1 = control[min(segment+0,end)]

  # Compute second and third derivatives
  if nderiv > 1: 
    # First derivative is available almost for free
    # Second or more derivative requires extra computation
    Q4 = (P4 - P3) * 3 / (tp3-tm0)
    Q3 = (P3 - P2) * 3 / (tp2-tm1)
    Q2 = (P2 - P1) * 3 / (tp1-tm2)
    R4 = (Q4 - Q3) * 2 / (tp2-tm0)
    R3 = (Q3 - Q2) * 2 / (tp1-tm1)
    S4 = (R4 - R3) * 1 / (tp1-tm0)

    R4 = ( (tv-tm0)*R4 + (tp1-tv)*R3 ) / (tp1 - tm0)

    d2f[valid] = R4
    d3f[valid] = S4

  # Compute function value and first derivative
  P4 = ( (tv-tm0)*P4 + (tp3-tv)*P3 ) / (tp3 - tm0)
  P3 = ( (tv-tm1)*P3 + (tp2-tv)*P2 ) / (tp2 - tm1)
  P2 = ( (tv-tm2)*P2 + (tp1-tv)*P1 ) / (tp1 - tm2)
  P4 = ( (tv-tm0)*P4 + (tp2-tv)*P3 ) / (tp2 - tm0)
  P3 = ( (tv-tm1)*P3 + (tp1-tv)*P2 ) / (tp1 - tm1)
  fastdf = (P4-P3) * 3 / (tp1-tm0)
  P4 = ( (tv-tm0)*P4 + (tp1-tv)*P3 ) / (tp1 - tm0)

  # Check that fast df calculation matches the direct Q4 calculation.
  # if nderiv > 1: print "|fast df - df| = ",norm(df-Q4)

  df[valid] = fastdf
  f[valid] = P4

  if nderiv == 0: return f
  elif nderiv == 1: return f,df
  elif nderiv == 2: return f,df,d2f
  else: return f,df,d2f,d3f

# Assertions left over from original octave code --- I'm not ready
# to write a generic assert yet in Python
#!assert(bspline3([0 0 0 1 1 3 4 6 6 6],[0 0 0 0 0 0],2.2),0,10*eps);
#!assert(bspline3([0 0 0 1 1 3 4 6 6 6],[1 1 1 1 1 1],2.2),1,10*eps);
#!assert(bspline3([0 0 0 0 1 4 5 5 5 5],[1:6],2),761/240,10*eps);
#!assert(bspline3([0 0 0 0 1 4 5 5 5 5],[1:6],[2,2]),[761/240,761/240],10*eps);
#!assert(bspline3([0 0 0 1 1 3 4 6 6 6],[1:6],3.2),4.2976,10*eps);

import numpy as nx

class BSpline3:
    """Manage control points for parametric B-spline."""
    # TODO: this class doesn't give much control over knots.
    def __init__(self, x, y, clamp=True):
        n = len(x)
        self.knot = nx.concatenate([[0.]*2, range(n), [n-1]*2])
        self.x = x
        self.y = y
        self.clamp = clamp
        
    def __len__(self): 
        """Count the knots"""
        return len(self.x)

    def __getitem__(self, i):
        """Set control point for a knot"""
        return self.x[i], self.y[i]
    
    def __setitem__(self, i, pair):
        """Get control point for a knot"""
        self.x[i],self.y[i] = pair
    
    def __delitem__(self, i):
        """Delete a knot"""
        if i < 0 or i >= len(self.x): raise IndexError
        self.x = nx.delete(self.x,i)
        self.y = nx.delete(self.y,i)
        self.knot = nx.delete(self.knot,i+2)
        if i == 0:
            self.knot[0:2] = self.knot[2]
        elif i == len(self.x)-2:
            self.knot[-2:-1] = self.knot[-3]
            
    def __call__(self, t):
        """Evalaute a B-spline at points t"""
        fx = bspline3(self.knot,self.x,t,clamp=self.clamp)
        fy = bspline3(self.knot,self.y,t,clamp=self.clamp)
        return fx,fy

    def append(self,x,y):
        """Add a knot to the end"""
        self.x = nx.concatenate([self.x,[x]])
        self.y = nx.concatenate([self.y,[y]])
        k = self.knot[-1]+1
        self.knot = nx.concatenate([self.knot,[k]])
        self.knot[-3:-1] = k
        
    def sample(self,n=400):
        """Sample the B-spline at n equidistance points in t"""
        t = nx.linspace(self.knot[2],self.knot[-3],n)
        return self.__call__(t)
        

def demo():
  import pylab
  t = N.linspace(-1,7,40 );
  knot = N.array([0, 1, 1, 3, 4, 6],'f')
  #knot = N.array([0, 0, 1, 4, 5, 5],'f')
  control = N.array([1, 2, 3, 2, 1, 2],'f') 
  knotseq = cat([knot[0]-1,knot[0]], knot, [knot[-1],knot[-1]+1])
  f = bspline3(knotseq,control,t,clamp=True);
  #print zip(t,f)
  pylab.plot(t,f,'-',knot,control,'x');
  pylab.show()

if __name__ == "__main__": demo()
-------------------------------------------------------------------------
This SF.net email is sponsored by: Microsoft
Defy all challenges. Microsoft(R) Visual Studio 2005.
http://clk.atdmt.com/MRT/go/vse0120000070mrt/direct/01/
_______________________________________________
Matplotlib-devel mailing list
Matplotlib-devel@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/matplotlib-devel

Reply via email to