Commit 12ada91d authored by Taddeüs Kroes's avatar Taddeüs Kroes

Implemented widget tree, gesture propagation and some more stuff:

- New naming rule: an "event" is triggered by a driver, a "gesture" is
  triggered by a tracker and delegated to the application by a widget.
- point down, move and up have been generalized to "events".
- Windows have been replaced by widgets, chich also support a tree structure.
- Gestures are propagated to parent widgets, events are delegated to child
  widgets. both propagation and delegation can be stopped by a handler.
- Gestures now save their originating event, events save their originating
  touch points.
- Test programs have been adapted to the new gesture binding style.
parent 6106f7e6
from tuio import TuioDriver
def select_driver(event_server):
"""
Create an object of a driver implementation, based on hardware support.
Currently, only a TUIO driver implementation exists. In the future, this
method should select one driver from a set of implementations.
"""
return TuioDriver(event_server)
from OSC import OSCServer
# FIXME: don't print tracebacks in final implementation?
OSCServer.print_tracebacks = True
from event_server import EventServer
from screen import pixel_coords
from ..event_driver import EventDriver
from ..events import PointDownEvent, PointMoveEvent, PointUpEvent
from ..touch_objects import TouchPoint
from ..screen import pixel_coords
class TuioServer2D(EventServer):
class TuioDriver(EventDriver):
tuio_address = 'localhost', 3333
def __init__(self, handler_obj):
super(TuioServer2D, self).__init__(handler_obj)
def __init__(self, event_server):
super(TuioDriver, self).__init__(event_server)
# OSC server that listens to incoming TUIO events
self.server = OSCServer(self.tuio_address)
self.server.addDefaultHandlers()
self.server.addMsgHandler('/tuio/2Dobj', self._receive)
self.server.addMsgHandler('/tuio/2Dcur', self._receive)
self.server.addMsgHandler('/tuio/2Dblb', self._receive)
self.server.addMsgHandler('/tuio/2Dobj', self.receive)
self.server.addMsgHandler('/tuio/2Dcur', self.receive)
self.server.addMsgHandler('/tuio/2Dblb', self.receive)
# List of alive seddion id's
# List of alive session id's
self.alive = set()
# List of session id's of points that have generated a 'point_down'
# event
self.down = set()
def _receive(self, addr, tags, data, source):
# Map of session id to touch point
self.points = {}
def receive(self, addr, tags, data, source):
surface = addr[8:]
#self.debug('Received message <surface=%s tags="%s" '
# 'data=%s source=%s>' % (surface, tags, data, source))
msg_type = data[0]
# FIXME: Ignore obj/blb events?
# FIXME: obj/blb events are ignored (for now)
if surface != 'cur':
return
......@@ -45,12 +51,15 @@ class TuioServer2D(EventServer):
self.down -= released
for sid in released:
self.handler_obj.on_point_up(sid)
point = self.points[sid]
del self.points[sid]
self.event_server.delegate_event(PointUpEvent(point))
#self.event_server.on_point_up(sid)
elif msg_type == 'set':
sid, x, y = data[1:4]
if sid not in self.alive:
raise ValueError('Point with sid "%d" is not alive.' % sid)
raise ValueError('Point with sid %d is not alive.' % sid)
# Translate to pixel coordinates
px, py = pixel_coords(x, y)
......@@ -58,14 +67,19 @@ class TuioServer2D(EventServer):
# Check if 'point_down' has already been triggered. If so, trigger
# a 'point_move' event instead
if sid in self.down:
self.debug('Moved %d to (%.4f, %.4f) or (%d, %d).'
self.debug('Moved %d to (%.4f, %.4f), in pixels: (%d, %d).'
% (sid, x, y, px, py))
self.handler_obj.on_point_move(sid, px, py)
point = self.points[sid]
point.set_position(px, py)
self.event_server.delegate_event(PointMoveEvent(point))
#self.event_server.on_point_move(sid, px, py)
else:
self.debug('Down %d at (%.4f, %.4f) or (%d, %d).'
self.debug('Down %d at (%.4f, %.4f), in pixels: (%d, %d).'
% (sid, x, y, px, py))
self.down.add(sid)
self.handler_obj.on_point_down(sid, px, py)
self.points[sid] = point = TouchPoint(px, py)
self.event_server.delegate_event(PointDownEvent(point))
#self.event_server.on_point_down(sid, px, py)
def run(self):
self.server.handle_request()
......@@ -77,44 +91,3 @@ class TuioServer2D(EventServer):
def stop(self):
self.info('Stopping OSC server')
self.server.close()
if __name__ == '__main__':
import argparse
import logging
from logger import Logger
from event_server import EventServerHandler
parser = argparse.ArgumentParser(description='TUIO server test.')
parser.add_argument('--log', metavar='LOG_LEVEL', default='INFO',
choices=['DEBUG', 'INFO', 'WARNING'], help='Global log level.')
parser.add_argument('--logfile', metavar='FILENAME', help='Filename for '
'the log file (the log is printed to stdout by default).')
args = parser.parse_args()
# Configure logger
log_config = dict(level=getattr(logging, args.log))
if args.logfile:
log_config['filename'] = args.logfile
Logger.configure(**log_config)
# Define handlers
class Handler(EventServerHandler, Logger):
def on_point_down(self, sid, x, y):
self.info('Point down: sid=%d (%.4f, %.4f)' % (sid, x, y))
def on_point_up(self, sid):
self.info('Point up: sid=%d' % sid)
def on_point_move(self, sid, x, y):
self.info('Point move: sid=%d (%.4f, %.4f)' % (sid, x, y))
server = TuioServer2D(Handler())
try:
server.start()
except KeyboardInterrupt:
server.stop()
from touch_objects import OBJECT_NAMES
class Event(object):
"""
Abstract base class for events triggered by an event driver. These events
are delegated to gesture trackers, to be translated to gestures. To be able
to check whether an event is located within a widget, a position is
required. Therefore, the touch object that triggers the event is is linked
to the event object.
"""
_type = NotImplemented
def __init__(self, touch_object):
self.touch_object = touch_object
self.stop = False
def __getattr__(self, name):
if name in OBJECT_NAMES \
and type(self.touch_object) == OBJECT_NAMES[name]:
return self.touch_object
raise AttributeError("'%s' object has no attribute '%s'"
% (self.__class__.__name__, name))
def get_type(self):
return self._type
def get_touch_object(self):
return self.touch_object
def get_position(self):
return self.touch_object.get_position()
def stop_delegation(self):
self.stop = True
def is_delegation_stopped(self):
return self.stop
from logger import Logger
class EventDriver(Logger):
"""
Abstract factory class for drivers. A driver translates driver-specific
messages to a common set of events. The minimal set is {point_down,
point_move, point_up}. A driver implementation should define the methods
'start' and 'stop', which starts/stops some event loop that triggers the
'delegate_event' method of a widget.
"""
def __init__(self, event_server):
self.event_server = event_server
def start(self):
"""
Start the event loop.
"""
raise NotImplementedError
def stop(self):
"""
Stop the event loop.
"""
raise NotImplementedError
def run(self):
"""
Execute a single loop iteration. If implemented, this method provides a
way to run the driver within another event loop.
"""
raise NotImplementedError
from logger import Logger
from drivers import select_driver
class EventServer(Logger):
"""
Abstract class for event servers. An event server translates driver
events to point 'down', 'move' and 'up' events. An event server
implementation should define the methods 'start' and 'stop', which
starts/stops some event loop that triggers on_point_up, on_point_move and
on_point_down methods on the 'handler_obj' object.
The event server uses an event driver to receive events, which are
delegated to a widget tree (and eventually to gesture trackers).
"""
def __init__(self, handler_obj):
self.handler_obj = handler_obj
def __init__(self, root_widget=None):
# Root widget to which events are delegated
self.root_widget = root_widget
def start(self):
raise NotImplementedError
# Driver implementation that will be serving events
self.event_driver = select_driver(self)
def stop(self):
raise NotImplementedError
def get_root_widget(self):
return self.root_widget
def set_root_widget(self, widget):
self.root_widget = widget
class EventServerHandler(Logger):
"""
Interface for gesture server. Defines empty on_point_up, on_point_move and
on_point_down handlers.
"""
def on_point_down(self, sid, x, y):
return NotImplemented
def delegate_event(self, event):
"""
Delegate an event that has been triggered by the event driver to the
widget tree.
"""
self.root_widget.handle_event(event)
def on_point_move(self, sid, x, y):
return NotImplemented
def start(self):
"""
Start the event loop. A root widget is needed to be able to delegate
events, so check if it exists first.
"""
if not self.root_widget:
raise ValueError('Cannot start event server without root widget.')
self.event_driver.start()
def on_point_up(self, sid):
return NotImplemented
def stop(self):
"""
Stop the event loop.
"""
self.event_driver.stop()
def run(self):
"""
Execute a single loop iteration of the event driver.
"""
self.event_driver.run()
from event import Event
class PointDownEvent(Event):
"""
Addition of a simple touch point to the screen.
"""
_type = 'point_down'
class PointMoveEvent(Event):
"""
Movement of a simple touch point from the screen.
"""
_type = 'point_move'
class PointUpEvent(Event):
"""
Removal of a simple touch point from the screen.
"""
_type = 'point_up'
......@@ -120,62 +120,3 @@ class AcceleratedPositionable(MovingPositionable):
Calculate the acceleration in pixels/second.
"""
return self.movement_distance() / self.movement_time()
class Surface(Positionable):
"""
Interface class for surfaces with a position. Defines a function
'contains', which calculates whether a position is located in the surface.
"""
def contains(self, point):
raise NotImplementedError
class RectangularSurface(Surface):
"""
Rectangle, represented by a left-top position (x, y) and a size (width,
height).
"""
def __init__(self, x, y, width, height):
super(RectangularSurface, self).__init__(x, y)
self.set_size(width, height)
def __str__(self):
return '<%s at (%s, %s) size=(%s, %s)>' \
% (self.__class__.__name__, self.x, self.y, self.width,
self.height)
def set_size(self, width, height):
self.width = width
self.height = height
def get_size(self):
return self.width, self.height
def contains(self, point):
x, y = point.get_position()
return self.x <= x <= self.x + self.width \
and self.y <= y <= self.y + self.height
class CircularSurface(Surface):
"""
Circle, represented by a center position (x, y) and a radius.
"""
def __init__(self, x, y, radius):
super(CircularSurface, self).__init__(x, y)
self.set_radius(radius)
def __str__(self):
return '<%s at (%s, %s) size=(%s, %s)>' \
% (self.__class__.__name__, self.x, self.y, self.width,
self.height)
def set_radius(self, radius):
self.radius = radius
def get_radius(self):
return self.radius
def contains(self, point):
return self.distance_to(point) <= self.radius
from event_server import EventServerHandler
from tuio_server import TuioServer2D
from window import FullscreenWindow
from point import TouchPoint
class GestureServer(EventServerHandler):
"""
Multi-touch gesture server. This uses a TUIO server to receive basic touch
events, which are translated to gestures using gesture trackers. Trackers
are assigned to a Window object, and gesture handlers are bound to a
tracker.
"""
def __init__(self):
# List of all connected windows
self.windows = []
# Map of point sid to TouchPoint object
self.points = {}
# Create TUIO server, to be started later
self.tuio_server = TuioServer2D(self)
def add_window(self, window):
self.windows.append(window)
def remove_window(self, window):
self.windows.remove(window)
# TODO: Remove window from touch points
def on_point_down(self, sid, x, y):
if sid in self.points:
raise ValueError('Point with sid %d already exists.' % sid)
# Create a new touch point
self.points[sid] = point = TouchPoint(x, y, sid)
# Save the windows containing the point in a dictionary, and update
# their trackers
for window in self.windows:
if window.contains(point):
point.add_window(window)
window.update_trackers('down', point)
def on_point_move(self, sid, x, y):
if sid not in self.points:
raise KeyError('No point with sid %d exists.' % sid)
# Update point position and corresponding window trackers
point = self.points[sid]
point.set_position(x, y)
point.update_window_trackers('move')
def on_point_up(self, sid):
if sid not in self.points:
raise KeyError('No point with sid %d exists.' % sid)
# Clear the list of windows containing the point, and update their
# trackers
self.points[sid].update_window_trackers('up')
del self.points[sid]
def start(self):
# Assert that at least one window exists, by adding a fullscreen window
# if none have been added yet
if not self.windows:
self.add_window(FullscreenWindow())
self.tuio_server.start()
def stop(self):
self.tuio_server.stop()
def run(self):
self.tuio_server.run()
......@@ -4,30 +4,29 @@ from geometry import AcceleratedPositionable
class TouchPoint(AcceleratedPositionable):
"""
Representation of an object touching the screen. The simplest form of a
touch object is a 'point', represented by an (x, y) position and a TUIO
session id (sid). All dimensions are in pixels.
touch object is a 'point', represented by an (x, y) position and a unique
object id. All dimensions are in pixels.
"""
def __init__(self, x, y, sid):
def __init__(self, x, y):
super(TouchPoint, self).__init__(x, y)
self.sid = sid
self.object_id = self.__class__.create_object_id()
# List of all windows this point is located in
self.windows = []
def get_id(self):
return self.object_id
def __str__(self):
return '<%s at (%s, %s) sid=%d>' \
% (self.__class__.__name__, self.x, self.y, self.sid)
object_id_count = 0
def add_window(self, window):
self.windows.append(window)
def remove_window(self, window):
self.windows.remove(window)
def update_window_trackers(self, event_type):
for window in self.windows:
window.update_trackers(event_type, self)
@classmethod
def create_object_id(cls):
cls.object_id_count += 1
return cls.object_id_count
# TODO: Extend with more complex touch object, e.g.:
#class TouchFiducial(TouchPoint): ...
OBJECT_NAMES = {
'point': TouchPoint,
#'fiducial': TouchFiducial,
}
......@@ -6,57 +6,33 @@ class GestureTracker(Logger):
Abstract class for gesture tracker definitions. Contains methods for
changing the state of touch points.
"""
# Supported gesture types
# Supported gesture classes
supported_gestures = NotImplemented
# Supported gesture types (filled on factory registration)
gesture_types = []
# Configurable properties (see configure() method)
configurable = []
def __init__(self, window=None):
# Hashmap of gesture types
self.handlers = {}
if window:
window.add_tracker(self)
def __init__(self, widget):
self.widget = widget
def bind(self, gesture_type, handler, *args, **kwargs):
def handle_event(self, event):
"""
Bind a handler to a gesture type. Multiple handlers can be bound to a
single gesture type. Optionally, (keyword) arguments that will be
passed to the handler along with a Gesture object can be specified.
Handle an event that was delegated by a widget. The tracker
implementation should define a handler function for the event.
Otherwise, the event will be ignored.
"""
if gesture_type not in self.gesture_types:
raise ValueError('Unsupported gesture type "%s".' % gesture_type)
h = handler, args, kwargs
handler_name = 'on_' + event.get_type()
if gesture_type not in self.handlers:
self.handlers[gesture_type] = [h]
else:
self.handlers[gesture_type].append(h)
if hasattr(self, handler_name):
getattr(self, handler_name)(event)
def trigger(self, gesture):
if gesture._type not in self.handlers:
self.debug('Triggered "%s", but no handlers are bound.'
% gesture._type)
return
gesture.set_tracker(self)
self.info('Triggered %s.' % gesture)
for handler, args, kwargs in self.handlers[gesture._type]:
handler(gesture, *args, **kwargs)
def is_type_bound(self, gesture_type):
return gesture_type in self.handlers
def on_point_down(self, point):
pass
def on_point_move(self, point):
pass
def on_point_up(self, point):
pass
self.widget.handle_gesture(gesture)
def configure(self, **kwargs):
for name, value in kwargs.iteritems():
......@@ -66,22 +42,35 @@ class GestureTracker(Logger):
setattr(self, name, value)
def __getattr__(self, name):
"""
Allow calls like:
tracker.gesture(...)
instead of:
tracker.bind('gesture', ...)
"""
if name not in self.gesture_types:
raise AttributeError("'%s' has no attribute '%s'"
% (self.__class__.__name__, name))
return lambda handler: self.bind(name, handler)
class Gesture(object):
"""
Abstract class that represents a triggered gesture.
"""
pass
_type = NotImplemented
def __init__(self, originating_event=None):
self.stop = False
self.tracker = None
self.originating_event = originating_event
def get_type(self):
return self._type
def get_tracker(self):
return self.tracker
def get_event(self):
return self.originating_event
def has_event(self):
return bool(self.originating_event)
def set_tracker(self, tracker):
self.tracker = tracker
def stop_propagation(self):
self.stop = True
def is_propagation_stopped(self):
return self.stop
from basic import BasicEventTracker
from tap import TapTracker
from transform import TransformationTracker
# Map of gesture type to tracker type
_tracker_types = {}
def _register_tracker(tracker_type):
tracker_type.gesture_types = \
[gesture._type for gesture in tracker_type.supported_gestures]
for gesture_type in tracker_type.gesture_types:
if gesture_type in _tracker_types:
raise ValueError('Gesture type "%s" is already registered to '
'tracker type "%s".' % (gesture_type,
_tracker_types[gesture_type].__name__))
_tracker_types[gesture_type] = tracker_type
def create_tracker(gesture_type, widget):
if gesture_type not in _tracker_types:
raise KeyError('No tracker type registered for gesture type "%s".'
% gesture_type)
return _tracker_types[gesture_type](widget)
_register_tracker(BasicEventTracker)
_register_tracker(TapTracker)
_register_tracker(TransformationTracker)
......@@ -2,30 +2,30 @@ from ..tracker import GestureTracker
from utils import PointGesture
class BasicTracker(GestureTracker):
"""
The main goal of this class is to provide a triggering mechanism for the
low-level point-down, point-move and point-up events.
"""
gesture_types = ['down', 'move', 'up']
class DownGesture(PointGesture):
_type = 'point_down'
def on_point_down(self, point):
self.trigger(DownGesture(point))
def on_point_move(self, point):
self.trigger(MoveGesture(point))
class MoveGesture(PointGesture):
_type = 'point_move'
def on_point_up(self, point):
self.trigger(UpGesture(point))
class UpGesture(PointGesture):
_type = 'point_up'
class DownGesture(PointGesture):
_type = 'down'
class BasicEventTracker(GestureTracker):
"""
The main goal of this class is to provide a triggering mechanism for the
low-level point-down, point-move and point-up events.
"""
supported_gestures = [DownGesture, MoveGesture, UpGesture]
class MoveGesture(PointGesture):
_type = 'move'
def on_point_down(self, event):
self.trigger(DownGesture(event))
def on_point_move(self, event):
self.trigger(MoveGesture(event))
class UpGesture(PointGesture):
_type = 'up'
def on_point_up(self, event):
self.trigger(UpGesture(event))
......@@ -6,8 +6,32 @@ from ..geometry import Positionable
from utils import PointGesture
class TapGesture(PointGesture):
"""
A tap gesture is triggered when a touch point releases from the screen
within a certain time and distance from its 'point_down' event.
"""
_type = 'tap'
class SingleTapGesture(TapGesture):
"""
A single tap gesture is triggered after a regular tap gesture, if no double
tap is triggered for that gesture.
"""
_type = 'single_tap'
class DoubleTapGesture(TapGesture):
"""
A double tap gesture is triggered if two sequential taps are triggered
within a certain time and distance of eachother.
"""
_type = 'double_tap'
class TapTracker(GestureTracker):
gesture_types = ['tap', 'single_tap', 'double_tap']
supported_gestures = [TapGesture, SingleTapGesture, DoubleTapGesture]
configurable = ['tap_distance', 'tap_time', 'double_tap_time',
'double_tap_distance', 'update_rate']
......@@ -15,7 +39,7 @@ class TapTracker(GestureTracker):
def __init__(self, window=None):
super(TapTracker, self).__init__(window)
# Map of TUIO session id to tuple (timestamp, position) of point down
# Map of touch object id to tuple (timestamp, position) of point down
self.reg = {}
# Maximum radius in which a touch point can move in order to be a tap
......@@ -48,7 +72,7 @@ class TapTracker(GestureTracker):
if self.last_tap and time_diff > self.double_tap_time:
# Last tap is too long ago to be a double tap, so trigger a
# single tap
self.trigger(SingleTapGesture(self.last_tap))
self.trigger(SingleTapGesture(None, self.last_tap))
self.reset_last_tap()
time.sleep(1. / self.update_rate)
......@@ -57,28 +81,32 @@ class TapTracker(GestureTracker):
self.last_tap_time = 0
self.last_tap = None
def on_point_down(self, point):
x, y = point.get_position()
self.reg[point.sid] = time.time(), Positionable(x, y)
def on_point_down(self, event):
x, y = event.get_position()
self.reg[event.point.get_id()] = time.time(), Positionable(x, y)
def on_point_move(self, event):
oid = event.point.get_id()
def on_point_move(self, point):
if point.sid not in self.reg:
if oid not in self.reg:
return
# If a stationary point moves beyond a threshold, delete it so that the
# 'up' event will not trigger a 'tap'
t, initial_position = self.reg[point.sid]
t, initial_position = self.reg[oid]
if point.distance_to(initial_position) > self.tap_distance:
del self.reg[point.sid]
if event.point.distance_to(initial_position) > self.tap_distance:
del self.reg[oid]
def on_point_up(self, event):
oid = event.point.get_id()
def on_point_up(self, point):
# Assert that the point has not been deleted by a 'move' event yet
if point.sid not in self.reg:
if oid not in self.reg:
return
down_time = self.reg[point.sid][0]
del self.reg[point.sid]
down_time = self.reg[oid][0]
del self.reg[oid]
# Only trigger a tap event if the 'up' is triggered within a certain
# time afer the 'down'
......@@ -87,43 +115,20 @@ class TapTracker(GestureTracker):
if current_time - down_time > self.tap_time:
return
tap = TapGesture(point)
tap = TapGesture(event)
self.trigger(tap)
# Trigger double tap if the threshold has not not expired yet
if self.last_tap:
if self.last_tap.distance_to(tap) <= self.double_tap_distance:
# Close enough to be a double tap
self.trigger(DoubleTapGesture(self.last_tap))
self.trigger(DoubleTapGesture(event, self.last_tap))
self.reset_last_tap()
return
# Generate a seperate single tap gesture for the last tap,
# because the lat tap variable is overwritten now
self.trigger(SingleTapGesture(self.last_tap))
self.trigger(SingleTapGesture(event, self.last_tap))
self.last_tap_time = current_time
self.last_tap = tap
class TapGesture(PointGesture):
"""
A tap gesture is triggered
"""
_type = 'tap'
class SingleTapGesture(TapGesture):
"""
A single tap gesture is triggered after a regular tap gesture, if no double
tap is triggered for that gesture.
"""
_type = 'single_tap'
class DoubleTapGesture(TapGesture):
"""
A double tap gesture is triggered if two sequential taps are triggered
within a certain time and distance of eachother.
"""
_type = 'double_tap'
......@@ -4,15 +4,71 @@ from ..tracker import GestureTracker, Gesture
from ..geometry import Positionable, MovingPositionable
class RotationGesture(Gesture, Positionable):
"""
A rotation gesture has a angle in radians and a rotational centroid.
"""
_type = 'rotate'
def __init__(self, event, centroid, angle):
Gesture.__init__(self, event)
Positionable.__init__(self, *centroid.get_position())
self.angle = angle
def __str__(self):
return '<RotationGesture at (%s, %s) angle=%s>' \
% (self.x, self.y, self.angle)
def get_angle(self):
return self.angle
class PinchGesture(Gesture, Positionable):
"""
A pinch gesture has a scale (1.0 means no scaling) and a centroid from
which the scaling originates.
"""
_type = 'pinch'
def __init__(self, event, centroid, scale):
Gesture.__init__(self, event)
Positionable.__init__(self, *centroid.get_position())
self.scale = scale
def __str__(self):
return '<PinchGesture at (%s, %s) scale=%s>' \
% (self.x, self.y, self.scale)
def get_scale(self):
return self.scale
class DragGesture(Gesture, Positionable):
"""
A momevent gesture has an initial position, and a translation from that
position.
"""
_type = 'drag'
def __init__(self, event, initial_position, translation):
Gesture.__init__(self, event)
Positionable.__init__(self, *initial_position.get_position())
self.translation = translation
def __str__(self):
return '<DragGesture at (%s, %s) translation=(%s, %s)>' \
% (self.get_position() + self.translation.get_position())
class TransformationTracker(GestureTracker):
"""
Tracker for linear transformations. This implementation detects rotation,
scaling and translation using the centroid of all touch points.
"""
gesture_types = ['rotate', 'pinch', 'move']
supported_gestures = [RotationGesture, PinchGesture, DragGesture]
def __init__(self, window=None):
super(TransformationTracker, self).__init__(window)
def __init__(self, widget=None):
super(TransformationTracker, self).__init__(widget)
# All touch points performing the transformation
self.points = []
......@@ -40,87 +96,33 @@ class TransformationTracker(GestureTracker):
else:
self.centroid = MovingPositionable(x, y)
def on_point_down(self, point):
self.points.append(point)
def on_point_down(self, event):
self.points.append(event.point)
self.update_centroid()
def on_point_move(self, point):
def on_point_move(self, event):
point = event.point
l = len(self.points)
if l > 1:
# Rotation (around the previous centroid)
if self.is_type_bound('rotate'):
rotation = point.rotation_around(self.centroid) / l
self.trigger(RotationGesture(self.centroid, rotation))
rotation = point.rotation_around(self.centroid) / l
self.trigger(RotationGesture(event, self.centroid, rotation))
# Scale
if self.is_type_bound('pinch'):
prev = point.get_previous_position().distance_to(self.centroid)
dist = point.distance_to(self.centroid)
dist = prev + (dist - prev) / l
scale = dist / prev
self.trigger(PinchGesture(self.centroid, scale))
prev = point.get_previous_position().distance_to(self.centroid)
dist = point.distance_to(self.centroid)
dist = prev + (dist - prev) / l
scale = dist / prev
self.trigger(PinchGesture(event, self.centroid, scale))
# Update centroid before movement can be detected
self.update_centroid()
# Movement
self.trigger(MovementGesture(self.centroid,
self.centroid.translation()))
self.trigger(DragGesture(event, self.centroid,
self.centroid.translation()))
def on_point_up(self, point):
self.points.remove(point)
def on_point_up(self, event):
self.points.remove(event.point)
self.update_centroid()
class RotationGesture(Positionable, Gesture):
"""
A rotation gesture has a angle in radians and a rotational centroid.
"""
_type = 'rotate'
def __init__(self, centroid, angle):
Positionable.__init__(self, *centroid.get_position())
self.angle = angle
def __str__(self):
return '<RotationGesture at (%s, %s) angle=%s>' \
% (self.x, self.y, self.angle)
def get_angle(self):
return self.angle
class PinchGesture(Positionable, Gesture):
"""
A pinch gesture has a scale (1.0 means no scaling) and a centroid from
which the scaling originates.
"""
_type = 'pinch'
def __init__(self, centroid, scale):
Positionable.__init__(self, *centroid.get_position())
self.scale = scale
def __str__(self):
return '<PinchGesture at (%s, %s) scale=%s>' \
% (self.x, self.y, self.scale)
def get_scale(self):
return self.scale
class MovementGesture(Positionable, Gesture):
"""
A momevent gesture has an initial position, and a translation from that
position.
"""
_type = 'move'
def __init__(self, initial_position, translation):
Positionable.__init__(self, *initial_position.get_position())
self.translation = translation
def __str__(self):
return '<MovementGesture at (%s, %s) translation=(%s, %s)>' \
% (self.get_position() + self.translation.get_position())
......@@ -2,11 +2,15 @@ from ..tracker import Gesture
from ..geometry import Positionable
class PointGesture(Positionable, Gesture):
class PointGesture(Gesture, Positionable):
"""
Abstract base class for positionable gestures that have the same
coordinated as a touch point.
Abstract base class for positionable gestures that have an assigned touch
point.
"""
def __init__(self, point):
# Use the coordinates of the touch point
def __init__(self, event, point=None):
Gesture.__init__(self, event)
if not point:
point = event.get_touch_object()
Positionable.__init__(self, *point.get_position())
from geometry import Positionable
from logger import Logger
from trackers import create_tracker
class Widget(Positionable, Logger):
"""
A widget represents a 2D object on the screen in which gestures can occur.
Handlers for a specific gesture type can be bound to a widget. The widget
will
"""
def __init__(self, x=None, y=None):
Positionable.__init__(self, x, y)
# Map of gesture types to gesture trackers
self.trackers = {}
# Map of gesture types to a list of handlers for that type
self.handlers = {}
# Widget tree references
self.parent = None
self.children = []
def add_widget(self, widget):
"""
Add a new child widget.
"""
self.children.append(widget)
widget.set_parent(self)
def remove_widget(self, widget):
"""
Remove a child widget.
"""
self.children.remove(widget)
widget.set_parent(None)
def set_parent(self, widget):
"""
Set a new parent widget. If a parent widget has already been assigned,
remove the widget from that parent first.
"""
if self.parent:
self.parent.remove_widget(self)
self.parent = widget
def unbind(self, gesture_type, handler=None):
"""
Unbind a single handler, or all handlers bound to a gesture type.
Remove the corresponding tracker if it is no longer needed.
"""
if gesture_type not in self.handlers:
raise KeyError('Gesture type "%s" is not bound.')
if handler:
# Remove a specific handler
self.handlers[gesture_type].remove(handler)
# Only remove the handler list and optionally the tracker if no
# other handlers exist for the gesture type
if self.handlers[gesture_type]:
return
else:
# Remove handler list
del self.handlers[gesture_type][:]
# Check if any other handlers need the tracker
for gtype in self.trackers[gesture_type].gesture_types:
if gtype in self.handlers:
return
# No more handlers are bound, remove unused tracker and handlers
del self.trackers[gesture_type]
for gtype in self.trackers[gesture_type].gesture_types:
del self.handlers[gtype]
def bind(self, gesture_type, handler, remove_existing=False):
"""
Bind a handler to the specified type of gesture. Create a tracker for
the gesture type if it does not exists yet.
"""
if gesture_type not in self.handlers:
tracker = create_tracker(gesture_type, self)
self.trackers[gesture_type] = tracker
self.handlers[gesture_type] = []
# Create empty tracker lists for all supported gestures
for gtype in tracker.gesture_types:
self.handlers[gtype] = []
elif remove_existing:
del self.handlers[gesture_type][:]
self.handlers[gesture_type].append(handler)
def __getattr__(self, name):
"""
Allow calls like:
widget.on_gesture(...)
instead of:
widget.bind('gesture', ...)
"""
if len(name) < 4 or name[:3] != 'on_':
raise AttributeError("'%s' has no attribute '%s'"
% (self.__class__.__name__, name))
return lambda handler: self.bind(name[3:], handler)
def contains_event(self, event):
"""
Check if the coordinates of an event are contained within this widget.
"""
raise NotImplementedError
def handle_event(self, event):
"""
Delegate a triggered event to gesture trackers and child widgets. A
handler can stop the delegation of the event, preventing it from being
delegated to child widgets.
"""
for tracker in set(self.trackers.itervalues()):
tracker.handle_event(event)
if not event.is_delegation_stopped():
for child in self.children:
if child.contains_event(event):
child.handle_event(event)
def handle_gesture(self, gesture):
"""
Handle a gesture that is triggered by a gesture tracker. First, all
handlers bound to the gesture type are called. Second, if the gesture's
propagation has not been stopped by a handler, the gesture is
propagated to the parent widget.
"""
for handler in self.handlers.get(gesture.get_type(), ()):
handler(gesture)
if self.parent and not gesture.is_propagation_stopped():
self.parent.handle_gesture(gesture)
from widget import Widget
from screen import screen_size
class RectangularWidget(Widget):
"""
Rectangular widget, has a position and a size.
"""
def __init__(self, x, y, width, height):
super(RectangularWidget, self).__init__(x, y)
self.set_size(width, height)
def __str__(self):
return '<%s at (%s, %s) size=(%s, %s)>' \
% (self.__class__.__name__, self.x, self.y, self.width,
self.height)
def set_size(self, width, height):
self.width = width
self.height = height
def get_size(self):
return self.width, self.height
def contains_event(self, event):
x, y = event.get_position()
return self.x <= x <= self.x + self.width \
and self.y <= y <= self.y + self.height
class CircularWidget(Widget):
"""
Circular widget, has a position and a radius.
"""
def __init__(self, x, y, radius):
super(CircularWidget, self).__init__(x, y)
self.set_radius(radius)
def __str__(self):
return '<%s at (%s, %s) size=(%s, %s)>' \
% (self.__class__.__name__, self.x, self.y, self.width,
self.height)
def set_radius(self, radius):
self.radius = radius
def get_radius(self):
return self.radius
def contains_event(self, event):
return self.distance_to(event.get_touch_object()) <= self.radius
class FullscreenWidget(RectangularWidget):
"""
Widget representation for the entire screen. This class provides an easy
way to create a single rectangular widget that catches all gestures.
"""
def __init__(self):
super(FullscreenWidget, self).__init__(0, 0, *screen_size)
def __str__(self):
return '<FullscreenWidget size=(%d, %d)>' % self.get_size()
def contains_event(self, event):
return True
from geometry import Surface, RectangularSurface, CircularSurface
from screen import screen_size
class Window(Surface):
"""
Abstract class that represents a 2D object on the screen to which gesture
trackers can be added. Implementations of this class should define a method
that can detect wether a touch point is located within the window.
Because any type of window on the screen has (at least) an (x, y) position,
it extends the Positionable object.
Note: Dimensions used in implementations of this class should be in pixels.
"""
def __init__(self, **kwargs):
# All trackers that are currently bound to this window
self.trackers = []
# List of points that originated in this window
self.points = []
if 'server' in kwargs:
kwargs['server'].add_window(self)
def add_tracker(self, tracker):
self.trackers.append(tracker)
def remove_tracker(self, tracker):
self.trackers.remove(tracker)
def on_point_down(self, point):
self.points.append(point)
self.update_trackers('down', point)
def on_point_move(self, point):
self.update_trackers('move', point)
def on_point_up(self, point):
self.points.remove(point)
self.update_trackers('up', point)
def update_trackers(self, event_type, point):
"""
Update all gesture trackers that are bound to this window with an
added/moved/removed touch point.
"""
handler_name = 'on_point_' + event_type
for tracker in self.trackers:
if hasattr(tracker, handler_name):
getattr(tracker, handler_name)(point)
class RectangularWindow(Window, RectangularSurface):
"""
Rectangular window.
"""
def __init__(self, x, y, width, height, **kwargs):
Window.__init__(self, **kwargs)
RectangularSurface.__init__(self, x, y, width, height)
class CircularWindow(Window, CircularSurface):
"""
Circular window.
"""
def __init__(self, x, y, radius, **kwargs):
Window.__init__(self, **kwargs)
CircularSurface.__init__(self, x, y, radius)
class FullscreenWindow(RectangularWindow):
"""
Window representation for the entire screen. This class provides an easy
way to create a single rectangular window that catches all gestures.
"""
def __init__(self, **kwargs):
super(FullscreenWindow, self).__init__(0, 0, *screen_size, **kwargs)
def __str__(self):
return '<FullscreenWindow size=(%d, %d)>' % (self.width, self.height)
def contains(self, point):
return True
from src.gesture_server import GestureServer
from src.window import FullscreenWindow
from src.trackers.basic import BasicTracker
from src.event_server import EventServer
from src.widgets import FullscreenWidget
from tests.parse_arguments import create_parser, parse_args
parse_args(create_parser())
# Create server
server = GestureServer()
# Create server and fullscreen widget
screen = FullscreenWidget()
server = EventServer(screen)
# Create a window to add trackers to
win = FullscreenWindow(server=server)
# Bind handlers
# Add tracker and handlers
tracker = BasicTracker(win)
tracker.down(lambda g: 0)
tracker.move(lambda g: 0)
tracker.up(lambda g: 0)
screen.on_point_down(lambda g: 0)
screen.on_point_move(lambda g: 0)
screen.on_point_up(lambda g: 0)
# Start listening to TUIO events
try:
......
......@@ -4,19 +4,17 @@ import pygame
from threading import Thread
from math import degrees
from src.gesture_server import GestureServer
from src.window import FullscreenWindow
from src.trackers.transform import TransformationTracker
from src.trackers.tap import TapTracker
from src.event_server import EventServer
from src.widgets import FullscreenWidget
from tests.parse_arguments import create_parser, parse_args
from src.screen import screen_size
from tests.parse_arguments import create_parser, parse_args
# Parse arguments
parser = create_parser()
parser.add_argument('-f', '--fullscreen', action='store_true', default=False,
help='run in fullscreen')
args = parse_args(parser)
pygame.init()
# Config
......@@ -56,6 +54,7 @@ angle = 0
scale = 1
taps = []
dtaps = []
points = []
def update():
......@@ -73,14 +72,14 @@ def update():
screen.blit(transformed, rect)
# Draw touch points
if transform.centroid:
c = coord(*transform.centroid.xy)
if transform and transform.centroid:
c = coord(*transform.centroid.xy)
for p in server.points.itervalues():
for p in points:
xy = coord(p.x, p.y)
# Draw line to centroid
if transform.centroid:
if transform and transform.centroid:
pygame.draw.line(screen, LINE_COLOR, xy, c, 1)
# Draw outlined circle around touch point
......@@ -90,7 +89,7 @@ def update():
pygame.draw.circle(screen, BG_COLOR, xy, FINGER_RADIUS - 1, 0)
# Draw filled circle around centroid
if transform.centroid:
if transform and transform.centroid:
pygame.draw.circle(screen, CIRCLE_COLOR, c, CENTROID_RADIUS)
# Draw an expanding circle around each tap event
......@@ -125,30 +124,39 @@ def update():
pygame.display.flip()
# Create server and fullscreen window
server = GestureServer()
win = FullscreenWindow(server=server)
transform = None
def save_tracker(gesture):
global transform
if not transform:
transform = gesture.get_tracker()
# Bind trackers
def rotate(gesture):
global angle
angle += gesture.get_angle()
save_tracker(gesture)
def pinch(gesture):
global scale
scale = min(scale * gesture.get_scale(), MAX_SCALE)
save_tracker(gesture)
widget = FullscreenWidget()
server = EventServer(widget)
widget.on_rotate(rotate)
widget.on_pinch(pinch)
transform = TransformationTracker(win)
transform.rotate(rotate)
transform.pinch(pinch)
widget.on_tap(lambda g: taps.append([coord(*g.xy), FINGER_RADIUS]))
widget.on_single_tap(lambda g: dtaps.append(list(coord(*g.xy)) + [1]))
widget.on_double_tap(lambda g: dtaps.append(list(coord(*g.xy)) + [0]))
tap = TapTracker(win)
tap.tap(lambda g: taps.append([coord(*g.xy), FINGER_RADIUS]))
tap.single_tap(lambda g: dtaps.append(list(coord(*g.xy)) + [1]))
tap.double_tap(lambda g: dtaps.append(list(coord(*g.xy)) + [0]))
widget.on_point_down(lambda g: points.append(g.get_event().point))
widget.on_point_up(lambda g: points.remove(g.get_event().point))
try:
......
from src.gesture_server import GestureServer
from src.window import FullscreenWindow
from src.trackers.tap import TapTracker
from src.event_server import EventServer
from src.widgets import FullscreenWidget
from tests.parse_arguments import create_parser, parse_args
parse_args(create_parser())
# Create server
server = GestureServer()
# Create a window to add trackers to
win = FullscreenWindow(server=server)
# Above is short for:
#win = FullscreenWindow()
#server.add_window(win)
# Add tracker and handlers
tracker = TapTracker(win)
def handler(gesture):
pass
parse_args(create_parser())
# Create server and fullscreen widget
screen = FullscreenWidget()
server = EventServer(screen)
tracker.tap(handler)
tracker.single_tap(handler)
tracker.double_tap(handler)
# Bind handlers
screen.on_tap(lambda g: 0)
screen.on_single_tap(lambda g: 0)
screen.on_double_tap(lambda g: 0)
# Start listening to TUIO events
try:
......
from src.gesture_server import GestureServer
from src.window import FullscreenWindow
from src.trackers.transform import TransformationTracker
from src.event_server import EventServer
from src.widgets import FullscreenWidget
from tests.parse_arguments import create_parser, parse_args
parse_args(create_parser())
# Create server
server = GestureServer()
parse_args(create_parser())
# Create a window to add trackers to
win = FullscreenWindow(server=server)
# Create server and fullscreen widget
screen = FullscreenWidget()
server = EventServer(screen)
# Add tracker and handlers
tracker = TransformationTracker(win)
tracker.rotate(lambda g: 0)
tracker.pinch(lambda g: 0)
tracker.move(lambda g: 0)
# Bind handlers
screen.on_rotate(lambda g: 0)
screen.on_pinch(lambda g: 0)
screen.on_drag(lambda g: 0)
# Start listening to TUIO events
try:
......
......@@ -2,25 +2,20 @@ import vtk
from threading import Thread
from math import degrees
from src.gesture_server import GestureServer
from src.trackers.transform import TransformationTracker
from src.trackers.tap import TapTracker
from src.window import FullscreenWindow
from src.event_server import EventServer
from src.widgets import FullscreenWidget
class vtkMultitouchInteractor():
def __init__(self):
self.iren = vtk.vtkRenderWindowInteractor()
self.server = GestureServer()
self.window = FullscreenWindow(server=self.server)
self.widget = FullscreenWidget()
self.server = EventServer(screen)
transform = TransformationTracker(window=self.window)
transform.rotate(self.on_rotate)
transform.pinch(self.on_pinch)
tap = TapTracker(window=self.window)
tap.tap(self.on_tap)
widget.on_rotate(self.on_rotate)
widget.on_pinch(self.on_pinch)
widget.on_tap(self.on_tap)
def SetRenderWindow(self, window):
self.iren.SetRenderWindow(window)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment