|
@@ -6,9 +6,9 @@ from threading import Thread
|
|
|
|
|
|
|
|
from tuio_server import TuioServer2D
|
|
from tuio_server import TuioServer2D
|
|
|
from logger import Logger
|
|
from logger import Logger
|
|
|
-from events import BasicEvent, DownEvent, UpEvent, MoveEvent, TapEvent, \
|
|
|
|
|
- SingleTapEvent, DoubleTapEvent, FlickEvent, RotateEvent, PinchEvent, \
|
|
|
|
|
- PanEvent
|
|
|
|
|
|
|
+from events import Event, DownEvent, UpEvent, MoveEvent, Tap, \
|
|
|
|
|
+ SingleTap, DoubleTap, Flick, Rotate, Pinch, \
|
|
|
|
|
+ Pan
|
|
|
|
|
|
|
|
# get screen resolution
|
|
# get screen resolution
|
|
|
pygame.display.init()
|
|
pygame.display.init()
|
|
@@ -18,14 +18,17 @@ pygame.display.quit()
|
|
|
|
|
|
|
|
# Heuristic constants
|
|
# Heuristic constants
|
|
|
# TODO: Encapsulate screen resolution in distance heuristics
|
|
# TODO: Encapsulate screen resolution in distance heuristics
|
|
|
-SUPPORTED_GESTURES = ('down', 'up', 'move', 'tap', 'single_tap', 'double_tap',
|
|
|
|
|
- 'pan', 'flick', 'rotate', 'pinch')
|
|
|
|
|
|
|
+SUPPORTED_EVENTS = ('down', 'up', 'move', 'tap', 'single_tap', 'double_tap',
|
|
|
|
|
+ 'pan', 'flick', 'rotate', 'pinch')
|
|
|
DOUBLE_TAP_DISTANCE = .05
|
|
DOUBLE_TAP_DISTANCE = .05
|
|
|
FLICK_VELOCITY_TRESHOLD = 20
|
|
FLICK_VELOCITY_TRESHOLD = 20
|
|
|
TAP_TIMEOUT = .2
|
|
TAP_TIMEOUT = .2
|
|
|
MAX_MULTI_DRAG_DISTANCE = .05
|
|
MAX_MULTI_DRAG_DISTANCE = .05
|
|
|
STATIONARY_TIME = .01
|
|
STATIONARY_TIME = .01
|
|
|
|
|
|
|
|
|
|
+# Detect gestures 60 times per second
|
|
|
|
|
+GESTURE_UPDATE_RATE = 60
|
|
|
|
|
+
|
|
|
# Minimum distance for two coordinates to be considered different
|
|
# Minimum distance for two coordinates to be considered different
|
|
|
# Theoretically, this should be one pixel because that is the minimal movement
|
|
# Theoretically, this should be one pixel because that is the minimal movement
|
|
|
# of a mouse cursor on the screen
|
|
# of a mouse cursor on the screen
|
|
@@ -34,7 +37,8 @@ DIST_THRESHOLD = 1. / max(screen_size)
|
|
|
|
|
|
|
|
def distance(a, b):
|
|
def distance(a, b):
|
|
|
"""
|
|
"""
|
|
|
- Calculate the distance between points a and b.
|
|
|
|
|
|
|
+ Calculate the Pythagorian distance between points a and b (which are (x, y)
|
|
|
|
|
+ tuples).
|
|
|
"""
|
|
"""
|
|
|
xa, ya = a
|
|
xa, ya = a
|
|
|
xb, yb = b
|
|
xb, yb = b
|
|
@@ -43,6 +47,9 @@ def distance(a, b):
|
|
|
|
|
|
|
|
|
|
|
|
|
def add(a, b):
|
|
def add(a, b):
|
|
|
|
|
+ """
|
|
|
|
|
+ Add a an b, used for some functional programming calls.
|
|
|
|
|
+ """
|
|
|
return a + b
|
|
return a + b
|
|
|
|
|
|
|
|
|
|
|
|
@@ -77,39 +84,30 @@ class TouchPoint(object):
|
|
|
def distance_to_prev(self):
|
|
def distance_to_prev(self):
|
|
|
return self.distance_to(self.px, self.py)
|
|
return self.distance_to(self.px, self.py)
|
|
|
|
|
|
|
|
- def init_gesture_data(self, cx, cy):
|
|
|
|
|
- self.pinch = self.old_pinch = self.distance_to(cx, cy)
|
|
|
|
|
- self.angle = self.old_angle = atan2(self.y - cy, self.x - cx)
|
|
|
|
|
-
|
|
|
|
|
- def rotation_around(cx, cy):
|
|
|
|
|
- angle = atan2(cy - self.y, self.x - cx)
|
|
|
|
|
- prev_angle = atan2(cy - self.py, self.px - cx)
|
|
|
|
|
- da = angle - prev_angle
|
|
|
|
|
-
|
|
|
|
|
- # Assert that angle is in [-pi, pi]
|
|
|
|
|
- if da > pi:
|
|
|
|
|
- da -= 2 * pi
|
|
|
|
|
- elif da < pi:
|
|
|
|
|
- da += 2 * pi
|
|
|
|
|
|
|
+ def set_centroid(self, cx, cy):
|
|
|
|
|
+ self.pinch = self.distance_to(cx, cy)
|
|
|
|
|
+ self.angle = atan2(cy - self.y, self.x - cx)
|
|
|
|
|
|
|
|
- return da
|
|
|
|
|
-
|
|
|
|
|
- def set_angle(self, angle):
|
|
|
|
|
- self.old_angle = self.angle
|
|
|
|
|
- self.angle = angle
|
|
|
|
|
-
|
|
|
|
|
- def set_pinch(self, pinch):
|
|
|
|
|
- self.old_pinch = self.pinch
|
|
|
|
|
- self.pinch = pinch
|
|
|
|
|
|
|
+ if self.pinch != None:
|
|
|
|
|
+ self.old_pinch = self.pinch
|
|
|
|
|
+ self.old_angle = self.angle
|
|
|
|
|
+ self.pinch = pinch
|
|
|
|
|
+ self.angle = angle
|
|
|
|
|
+ else:
|
|
|
|
|
+ self.old_pinch = self.pinch = pinch
|
|
|
|
|
+ self.old_angle = self.angle = angle
|
|
|
|
|
|
|
|
def angle_diff(self):
|
|
def angle_diff(self):
|
|
|
return self.angle - self.old_angle
|
|
return self.angle - self.old_angle
|
|
|
|
|
|
|
|
|
|
+ def pinch_diff(self):
|
|
|
|
|
+ return self.pinch - self.old_pinch
|
|
|
|
|
+
|
|
|
def dx(self):
|
|
def dx(self):
|
|
|
- return int(self.x - self.px)
|
|
|
|
|
|
|
+ return self.x - self.px
|
|
|
|
|
|
|
|
def dy(self):
|
|
def dy(self):
|
|
|
- return int(self.y - self.py)
|
|
|
|
|
|
|
+ return self.y - self.py
|
|
|
|
|
|
|
|
def down_time(self):
|
|
def down_time(self):
|
|
|
return time.time() - self.start_time
|
|
return time.time() - self.start_time
|
|
@@ -118,9 +116,6 @@ class TouchPoint(object):
|
|
|
return self.distance_to_prev() < TAP_TIME \
|
|
return self.distance_to_prev() < TAP_TIME \
|
|
|
and self.distance_to(self.start_x, self.start_y) < TAP_DISTANCE
|
|
and self.distance_to(self.start_x, self.start_y) < TAP_DISTANCE
|
|
|
|
|
|
|
|
- def movement(self):
|
|
|
|
|
- return self.x - self.px, self.y - self.py
|
|
|
|
|
-
|
|
|
|
|
def is_stationary(self):
|
|
def is_stationary(self):
|
|
|
return self.distance_to_prev() < DIST_THRESHOLD
|
|
return self.distance_to_prev() < DIST_THRESHOLD
|
|
|
|
|
|
|
@@ -129,9 +124,11 @@ class MultiTouchListener(Logger):
|
|
|
def __init__(self, verbose=0, tuio_verbose=0, **kwargs):
|
|
def __init__(self, verbose=0, tuio_verbose=0, **kwargs):
|
|
|
super(MultiTouchListener, self).__init__(**kwargs)
|
|
super(MultiTouchListener, self).__init__(**kwargs)
|
|
|
self.verbose = verbose
|
|
self.verbose = verbose
|
|
|
- self.tuio_verbose = tuio_verbose
|
|
|
|
|
|
|
+ self.last_tap = None
|
|
|
self.last_tap_time = 0
|
|
self.last_tap_time = 0
|
|
|
self.handlers = {}
|
|
self.handlers = {}
|
|
|
|
|
+ self.thread = None
|
|
|
|
|
+ self.points_changed = False
|
|
|
|
|
|
|
|
# Session id's pointing to point coordinates
|
|
# Session id's pointing to point coordinates
|
|
|
self.points = []
|
|
self.points = []
|
|
@@ -139,22 +136,74 @@ class MultiTouchListener(Logger):
|
|
|
# Put centroid outside screen to prevent misinterpretation
|
|
# Put centroid outside screen to prevent misinterpretation
|
|
|
self.centroid = (-1., -1.)
|
|
self.centroid = (-1., -1.)
|
|
|
|
|
|
|
|
- def update_centroid(self, moving=None):
|
|
|
|
|
- self.old_centroid = self.centroid
|
|
|
|
|
|
|
+ self.server = TuioServer2D(self, verbose=tuio_verbose)
|
|
|
|
|
|
|
|
- if not len(self.points):
|
|
|
|
|
- self.centroid = (-1., -1.)
|
|
|
|
|
- return
|
|
|
|
|
|
|
+ def point_down(self, sid, x, y):
|
|
|
|
|
+ """
|
|
|
|
|
+ Called by TUIO listener when a new touch point is created, triggers a
|
|
|
|
|
+ DownEvent.
|
|
|
|
|
+ """
|
|
|
|
|
+ if self.find_point(sid):
|
|
|
|
|
+ raise ValueError('Point with session id "%d" already exists.'
|
|
|
|
|
+ % sid)
|
|
|
|
|
|
|
|
- #use = filter(TouchPoint.is_stationary, self.points)
|
|
|
|
|
- use = filter(lambda p: p != moving, self.points)
|
|
|
|
|
|
|
+ p = TouchPoint(sid, x, y)
|
|
|
|
|
+ self.points.append(p)
|
|
|
|
|
+ self.trigger(DownEvent(p))
|
|
|
|
|
+ self.points_changed = True
|
|
|
|
|
|
|
|
- if not use:
|
|
|
|
|
- use = self.points
|
|
|
|
|
|
|
+ def point_up(self, sid):
|
|
|
|
|
+ """
|
|
|
|
|
+ Called by TUIO listener when a touch point is removed, triggers an
|
|
|
|
|
+ UpEvent. Also, simple/double tap detection is located here instead of
|
|
|
|
|
+ in the gesture thread (for responsiveness reasons).
|
|
|
|
|
+ """
|
|
|
|
|
+ i, p = self.find_point(sid, index=True)
|
|
|
|
|
|
|
|
- l = len(use)
|
|
|
|
|
- cx, cy = zip(*[(p.x, p.y) for p in use])
|
|
|
|
|
- self.centroid = (reduce(add, cx, 0) / l, reduce(add, cy, 0) / l)
|
|
|
|
|
|
|
+ if not p:
|
|
|
|
|
+ raise KeyError('No point with session id "%d".' % sid)
|
|
|
|
|
+
|
|
|
|
|
+ del self.points[i]
|
|
|
|
|
+ self.trigger(UpEvent(p))
|
|
|
|
|
+
|
|
|
|
|
+ if p.is_tap():
|
|
|
|
|
+ # Always trigger a regular tap event, also in case of double tap
|
|
|
|
|
+ # (use the 'single_tap' event to keep single/double apart from
|
|
|
|
|
+ # eachother)
|
|
|
|
|
+ self.trigger(Tap(p.x, p.y))
|
|
|
|
|
+
|
|
|
|
|
+ # Detect double tap by comparing time and distance to last tap
|
|
|
|
|
+ # event
|
|
|
|
|
+ t = time.time()
|
|
|
|
|
+
|
|
|
|
|
+ if t - self.last_tap_time < TAP_TIMEOUT \
|
|
|
|
|
+ and p.distance_to(*self.last_tap.xy) < DOUBLE_TAP_DISTANCE:
|
|
|
|
|
+ self.trigger(DoubleTap(p.x, p.y))
|
|
|
|
|
+ self.last_tap = None
|
|
|
|
|
+ self.last_tap_time = 0
|
|
|
|
|
+ else:
|
|
|
|
|
+ self.last_tap = p
|
|
|
|
|
+ self.last_tap_time = t
|
|
|
|
|
+
|
|
|
|
|
+ # TODO: Detect flick
|
|
|
|
|
+ #elif p.is_flick():
|
|
|
|
|
+ # self.trigger(Flick(p.x, p.y))
|
|
|
|
|
+
|
|
|
|
|
+ self.points_changed = True
|
|
|
|
|
+
|
|
|
|
|
+ def point_move(self, sid, x, y):
|
|
|
|
|
+ """
|
|
|
|
|
+ Called by TUIO listener when a touch point moves, triggers a MoveEvent.
|
|
|
|
|
+ The move event is only used if the movement distance is greater that a
|
|
|
|
|
+ preset constant, so that negligible movement is ignored. This prevents
|
|
|
|
|
+ unnecessary gesture detection.
|
|
|
|
|
+ """
|
|
|
|
|
+ p = self.find_point(sid)
|
|
|
|
|
+
|
|
|
|
|
+ if p.distance_to(x, y) > DIST_THRESHOLD:
|
|
|
|
|
+ p.update(x, y)
|
|
|
|
|
+ self.trigger(MoveEvent(p))
|
|
|
|
|
+ self.points_changed = True
|
|
|
|
|
|
|
|
def detect_rotation_and_pinch(self):
|
|
def detect_rotation_and_pinch(self):
|
|
|
"""
|
|
"""
|
|
@@ -164,32 +213,35 @@ class MultiTouchListener(Logger):
|
|
|
"""
|
|
"""
|
|
|
l = len(self.points)
|
|
l = len(self.points)
|
|
|
|
|
|
|
|
- if 'pinch' not in self.handlers or l < 2:
|
|
|
|
|
|
|
+ if l < 2 or ('pinch' not in self.handlers \
|
|
|
|
|
+ and 'rotate' not in self.handlers):
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
- rotation = pinch = 0
|
|
|
|
|
cx, cy = self.centroid
|
|
cx, cy = self.centroid
|
|
|
|
|
+ #rotation = pinch = 0
|
|
|
|
|
|
|
|
- for p in self.points:
|
|
|
|
|
- p.set_angle(atan2(p.y - cy, p.x - cx))
|
|
|
|
|
- da = p.angle_diff()
|
|
|
|
|
|
|
+ #for p in self.points:
|
|
|
|
|
+ # da = p.angle_diff()
|
|
|
|
|
|
|
|
- # Assert that angle is in [-pi, pi]
|
|
|
|
|
- if da > pi:
|
|
|
|
|
- da -= 2 * pi
|
|
|
|
|
- elif da < pi:
|
|
|
|
|
- da += 2 * pi
|
|
|
|
|
|
|
+ # # Assert that angle is in [-pi, pi]
|
|
|
|
|
+ # if da > pi:
|
|
|
|
|
+ # da -= 2 * pi
|
|
|
|
|
+ # elif da < pi:
|
|
|
|
|
+ # da += 2 * pi
|
|
|
|
|
|
|
|
- rotation += da
|
|
|
|
|
|
|
+ # rotation += da
|
|
|
|
|
+ # pinch += p.pinch_diff()
|
|
|
|
|
|
|
|
- p.set_pinch(p.distance_to(cx, cy))
|
|
|
|
|
- pinch += p.pinch_diff()
|
|
|
|
|
|
|
+ angles, pinches = zip(*[(p.angle_diff(), p.pinch_diff())
|
|
|
|
|
+ for p in self.points])
|
|
|
|
|
+ rotation = reduce(add, angles)
|
|
|
|
|
+ pinch = reduce(add, pinches)
|
|
|
|
|
|
|
|
if rotation:
|
|
if rotation:
|
|
|
- self.trigger(RotateEvent(cx, cy, rotation / l, l))
|
|
|
|
|
|
|
+ self.trigger(Rotate(cx, cy, rotation / l, l))
|
|
|
|
|
|
|
|
if pinch:
|
|
if pinch:
|
|
|
- self.trigger(PinchEvent(cx, cy, pinch / l, l))
|
|
|
|
|
|
|
+ self.trigger(Pinch(cx, cy, pinch / l * 2, l))
|
|
|
|
|
|
|
|
def detect_pan(self):
|
|
def detect_pan(self):
|
|
|
"""
|
|
"""
|
|
@@ -197,22 +249,30 @@ class MultiTouchListener(Logger):
|
|
|
fingers moving close-ish together in the same direction.
|
|
fingers moving close-ish together in the same direction.
|
|
|
"""
|
|
"""
|
|
|
l = len(self.points)
|
|
l = len(self.points)
|
|
|
|
|
+
|
|
|
|
|
+ if not l:
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
m = MAX_MULTI_DRAG_DISTANCE
|
|
m = MAX_MULTI_DRAG_DISTANCE
|
|
|
clustered = l == 1 or all([p.distance_to(*self.centroid) <= m \
|
|
clustered = l == 1 or all([p.distance_to(*self.centroid) <= m \
|
|
|
for p in self.points])
|
|
for p in self.points])
|
|
|
directions = [(cmp(p.dx(), 0), cmp(p.dy(), 0)) \
|
|
directions = [(cmp(p.dx(), 0), cmp(p.dy(), 0)) \
|
|
|
for p in self.points]
|
|
for p in self.points]
|
|
|
|
|
|
|
|
- if any(map(all, zip(*directions))) and clustered:
|
|
|
|
|
- if l == 1:
|
|
|
|
|
- p = self.points[0]
|
|
|
|
|
- cx, cy, dx, dy = p.x, p.y, p.dx(), p.dy()
|
|
|
|
|
- else:
|
|
|
|
|
- cx, cy = self.centroid
|
|
|
|
|
- old_cx, old_cy = self.old_centroid
|
|
|
|
|
- dx, dy = cx - old_cx, cy - old_cy
|
|
|
|
|
|
|
+ if not clustered or not any(map(all, zip(*directions))):
|
|
|
|
|
+ return False
|
|
|
|
|
|
|
|
- self.trigger(PanEvent(cx, cy, dx, dy, l))
|
|
|
|
|
|
|
+ if l == 1:
|
|
|
|
|
+ p = self.points[0]
|
|
|
|
|
+ cx, cy, dx, dy = p.x, p.y, p.dx(), p.dy()
|
|
|
|
|
+ else:
|
|
|
|
|
+ cx, cy = self.centroid
|
|
|
|
|
+ old_cx, old_cy = self.old_centroid
|
|
|
|
|
+ dx, dy = cx - old_cx, cy - old_cy
|
|
|
|
|
+
|
|
|
|
|
+ self.trigger(Pan(cx, cy, dx, dy, l))
|
|
|
|
|
+
|
|
|
|
|
+ return True
|
|
|
|
|
|
|
|
def find_point(self, sid, index=False):
|
|
def find_point(self, sid, index=False):
|
|
|
for i, p in enumerate(self.points):
|
|
for i, p in enumerate(self.points):
|
|
@@ -222,123 +282,158 @@ class MultiTouchListener(Logger):
|
|
|
if index:
|
|
if index:
|
|
|
return -1, None
|
|
return -1, None
|
|
|
|
|
|
|
|
- def point_down(self, sid, x, y):
|
|
|
|
|
- if self.find_point(sid):
|
|
|
|
|
- raise ValueError('Point with session id "%d" already exists.' % sid)
|
|
|
|
|
|
|
+ def detect_pinch(self, moved):
|
|
|
|
|
+ cx, cy = self.centroid
|
|
|
|
|
+ dist = moved.distance_to(cx, cy)
|
|
|
|
|
+ old_dist = distance((moved.px, moved.py), self.centroid)
|
|
|
|
|
|
|
|
- p = TouchPoint(sid, x, y)
|
|
|
|
|
- self.points.append(p)
|
|
|
|
|
- self.update_centroid()
|
|
|
|
|
- self.trigger(DownEvent(p))
|
|
|
|
|
|
|
+ if abs(dist - old_dist) > DIST_THRESHOLD:
|
|
|
|
|
+ self.trigger(Pinch(cx, cy, dist / old_dist,
|
|
|
|
|
+ len(self.points)))
|
|
|
|
|
|
|
|
- def point_up(self, sid):
|
|
|
|
|
- i, p = self.find_point(sid, index=True)
|
|
|
|
|
|
|
+ def detect_single_tap(self):
|
|
|
|
|
+ """
|
|
|
|
|
+ Check if a single tap event should be triggered by checking is the last
|
|
|
|
|
+ tap.
|
|
|
|
|
+ """
|
|
|
|
|
+ if self.last_tap and time.time() - self.last_tap_time >= TAP_TIMEOUT:
|
|
|
|
|
+ self.trigger(SingleTap(*self.last_tap.xy))
|
|
|
|
|
+ self.last_tap = None
|
|
|
|
|
+ self.last_tap_time = 0
|
|
|
|
|
|
|
|
- if not p:
|
|
|
|
|
- raise KeyError('No point with session id "%d".' % sid)
|
|
|
|
|
|
|
+ def update_centroid(self):
|
|
|
|
|
+ """
|
|
|
|
|
+ Calculate the centroid of all current touch points.
|
|
|
|
|
+ """
|
|
|
|
|
+ self.old_centroid = self.centroid
|
|
|
|
|
+ l = len(self.points)
|
|
|
|
|
|
|
|
- del self.points[i]
|
|
|
|
|
- self.update_centroid()
|
|
|
|
|
- self.trigger(UpEvent(p))
|
|
|
|
|
|
|
+ # If there are no touch points, move the entroid to outside the screen
|
|
|
|
|
+ if not l:
|
|
|
|
|
+ self.centroid = (-1., -1.)
|
|
|
|
|
+ return
|
|
|
|
|
|
|
|
- if p.is_tap():
|
|
|
|
|
- # Always trigger a regular tap event, also in case of double tap
|
|
|
|
|
- # (use the 'single_tap' event to keep single/double apart from
|
|
|
|
|
- # eachother)
|
|
|
|
|
- self.trigger(TapEvent(p.x, p.y))
|
|
|
|
|
|
|
+ cx, cy = zip(*[(p.x, p.y) for p in self.points])
|
|
|
|
|
+ self.centroid = (reduce(add, cx, 0) / l, reduce(add, cy, 0) / l)
|
|
|
|
|
|
|
|
- # Detect double tap by comparing time and distance from last tap
|
|
|
|
|
- # event
|
|
|
|
|
- t = time.time()
|
|
|
|
|
|
|
+ # Update angle and pinch of all touch points
|
|
|
|
|
+ for p in self.points:
|
|
|
|
|
+ p.set_centroid(*self.centroid)
|
|
|
|
|
|
|
|
- if t - self.last_tap_time < TAP_TIMEOUT \
|
|
|
|
|
- and p.distance_to(*self.last_tap.xy) < DOUBLE_TAP_DISTANCE:
|
|
|
|
|
- self.trigger(DoubleTapEvent(p.x, p.y))
|
|
|
|
|
- else:
|
|
|
|
|
- self.last_tap = p
|
|
|
|
|
- self.last_tap_time = t
|
|
|
|
|
|
|
+ def centroid_movement(self):
|
|
|
|
|
+ cx, cy = self.centroid
|
|
|
|
|
+ ocx, ocy = self.old_centroid
|
|
|
|
|
|
|
|
- # TODO: Detect flick
|
|
|
|
|
- #elif p.is_flick():
|
|
|
|
|
- # self.trigger(FlickEvent(p.x, p.y))
|
|
|
|
|
|
|
+ return cx - ocx, cy - ocy
|
|
|
|
|
|
|
|
- def point_move(self, sid, x, y):
|
|
|
|
|
- p = self.find_point(sid)
|
|
|
|
|
|
|
+ def detect_gestures(self):
|
|
|
|
|
+ """
|
|
|
|
|
+ Detect if any gestures have occured in the past gesture frame. This
|
|
|
|
|
+ method is called in each time interval of the gesture thread, for
|
|
|
|
|
+ gestures that can only be detected using accumulated point down/up/move
|
|
|
|
|
+ events.
|
|
|
|
|
+ """
|
|
|
|
|
+ # Simple and double taps are detected in the main thread, specific
|
|
|
|
|
+ # single-tap in the gesture thread
|
|
|
|
|
+ self.detect_single_tap()
|
|
|
|
|
|
|
|
- # Optimization: only update if the point has moved far enough
|
|
|
|
|
- if p.distance_to(x, y) > DIST_THRESHOLD:
|
|
|
|
|
- p.update(x, y)
|
|
|
|
|
- self.update_centroid(moving=p)
|
|
|
|
|
- self.trigger(MoveEvent(p))
|
|
|
|
|
- self.detect_pinch(p)
|
|
|
|
|
|
|
+ # If the touch points have not been updated, neither have the gestures
|
|
|
|
|
+ if self.points_changed:
|
|
|
|
|
+ # The new centroid is used for pan/rotate/pinch detection
|
|
|
|
|
+ self.update_centroid()
|
|
|
|
|
|
|
|
- # TODO: Detect pan
|
|
|
|
|
|
|
+ # If a pan event is detected, ignore any rotate or pinch movement
|
|
|
|
|
+ # (they are considered noise)
|
|
|
|
|
+ if not self.detect_pan():
|
|
|
|
|
+ self.detect_rotation_and_pinch()
|
|
|
|
|
|
|
|
- def detect_pinch(self, moved):
|
|
|
|
|
- cx, cy = self.centroid
|
|
|
|
|
- dist = moved.distance_to(cx, cy)
|
|
|
|
|
- old_dist = distance((moved.px, moved.py), self.centroid)
|
|
|
|
|
|
|
+ self.points_changed = False
|
|
|
|
|
|
|
|
- if abs(dist - old_dist) > DIST_THRESHOLD:
|
|
|
|
|
- self.trigger(PinchEvent(cx, cy, dist / old_dist,
|
|
|
|
|
- len(self.points)))
|
|
|
|
|
|
|
+ def start_gesture_thread(self):
|
|
|
|
|
+ """
|
|
|
|
|
+ Loop of the gesture thread.
|
|
|
|
|
+ """
|
|
|
|
|
+ interval = 1. / GESTURE_UPDATE_RATE
|
|
|
|
|
+
|
|
|
|
|
+ while True:
|
|
|
|
|
+ self.detect_gestures()
|
|
|
|
|
+ time.sleep(interval)
|
|
|
|
|
|
|
|
def stop(self):
|
|
def stop(self):
|
|
|
- self.log('Stopping event loop')
|
|
|
|
|
|
|
+ """
|
|
|
|
|
+ Stop main event loop.
|
|
|
|
|
+ """
|
|
|
|
|
+ self.log('Stopping TUIO server')
|
|
|
|
|
+ self.server.stop()
|
|
|
|
|
|
|
|
if self.thread:
|
|
if self.thread:
|
|
|
|
|
+ self.log('Stopping main loop thread')
|
|
|
self.thread.join()
|
|
self.thread.join()
|
|
|
- self.thread = False
|
|
|
|
|
- else:
|
|
|
|
|
- self.server.stop()
|
|
|
|
|
|
|
+ self.thread = None
|
|
|
|
|
|
|
|
- def start(self, threaded=False):
|
|
|
|
|
|
|
+ def start(self, threaded=False, daemon=False):
|
|
|
"""
|
|
"""
|
|
|
- Start event loop.
|
|
|
|
|
|
|
+ Start main event loop. If threaded is set to True, the main loop is
|
|
|
|
|
+ started in a new thread. If daemon is also set to True, that thread
|
|
|
|
|
+ will be daemonic. The daemon option makes a call to stop() unnecessary.
|
|
|
"""
|
|
"""
|
|
|
if threaded:
|
|
if threaded:
|
|
|
- self.thread = Thread(target=self.start, kwargs={'threaded': False})
|
|
|
|
|
- self.thread.daemon = True
|
|
|
|
|
|
|
+ self.log('Creating %sthread for main loop'
|
|
|
|
|
+ % ('daemon ' if daemon else ''))
|
|
|
|
|
+ self.thread = Thread(target=self.start)
|
|
|
|
|
+ self.thread.daemon = daemon
|
|
|
self.thread.start()
|
|
self.thread.start()
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
|
|
+ # Start gesture thread
|
|
|
|
|
+ self.log('Starting gesture thread')
|
|
|
|
|
+ gesture_thread = Thread(target=self.start_gesture_thread)
|
|
|
|
|
+ gesture_thread.daemon = True
|
|
|
|
|
+ gesture_thread.start()
|
|
|
|
|
+
|
|
|
|
|
+ # Start TUIO listener
|
|
|
try:
|
|
try:
|
|
|
- self.log('Starting event loop')
|
|
|
|
|
- self.server = TuioServer2D(self, verbose=self.tuio_verbose)
|
|
|
|
|
|
|
+ self.log('Starting TUIO server')
|
|
|
self.server.start()
|
|
self.server.start()
|
|
|
- except KeyboardInterrupt:
|
|
|
|
|
|
|
+ except SystemExit:
|
|
|
self.stop()
|
|
self.stop()
|
|
|
|
|
|
|
|
- def bind(self, gesture, handler):
|
|
|
|
|
- if gesture not in SUPPORTED_GESTURES:
|
|
|
|
|
|
|
+ def bind(self, gesture, handler, *args, **kwargs):
|
|
|
|
|
+ """
|
|
|
|
|
+ Bind a handler to an event or gesture.
|
|
|
|
|
+ """
|
|
|
|
|
+ if gesture not in SUPPORTED_EVENTS:
|
|
|
raise ValueError('Unsupported gesture "%s".' % gesture)
|
|
raise ValueError('Unsupported gesture "%s".' % gesture)
|
|
|
|
|
|
|
|
if gesture not in self.handlers:
|
|
if gesture not in self.handlers:
|
|
|
self.handlers[gesture] = []
|
|
self.handlers[gesture] = []
|
|
|
|
|
|
|
|
- self.handlers[gesture].append(handler)
|
|
|
|
|
|
|
+ self.handlers[gesture].append((handler, args, kwargs))
|
|
|
|
|
|
|
|
def trigger(self, event):
|
|
def trigger(self, event):
|
|
|
|
|
+ """
|
|
|
|
|
+ Call all handlers bound to the name of the triggered event.
|
|
|
|
|
+ """
|
|
|
if event.__class__._name in self.handlers:
|
|
if event.__class__._name in self.handlers:
|
|
|
h = self.handlers[event.__class__._name]
|
|
h = self.handlers[event.__class__._name]
|
|
|
self.log('Event triggered: "%s" (%d handlers)' % (event, len(h)),
|
|
self.log('Event triggered: "%s" (%d handlers)' % (event, len(h)),
|
|
|
- 1 + int(isinstance(event, BasicEvent)))
|
|
|
|
|
-
|
|
|
|
|
- for handler in h:
|
|
|
|
|
- handler(event)
|
|
|
|
|
-
|
|
|
|
|
- def centroid_movement(self):
|
|
|
|
|
- cx, cy = self.centroid
|
|
|
|
|
- ocx, ocy = self.old_centroid
|
|
|
|
|
|
|
+ 1 + int(isinstance(event, Event)))
|
|
|
|
|
|
|
|
- return cx - ocx, cy - ocy
|
|
|
|
|
|
|
+ for handler, args, kwargs in h:
|
|
|
|
|
+ handler(event, *args, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
if __name__ == '__main__':
|
|
|
- def tap(event):
|
|
|
|
|
- print 'tap:', event
|
|
|
|
|
-
|
|
|
|
|
- loop = MultiTouchListener(verbose=1, tuio_verbose=0)
|
|
|
|
|
- loop.bind('tap', tap)
|
|
|
|
|
- loop.bind('double_tap', tap)
|
|
|
|
|
- loop.start()
|
|
|
|
|
|
|
+ def tap(event, tap_type):
|
|
|
|
|
+ print 'tap:', tap_type
|
|
|
|
|
+
|
|
|
|
|
+ listener = MultiTouchListener(verbose=1, tuio_verbose=0)
|
|
|
|
|
+ listener.bind('tap', tap, 0)
|
|
|
|
|
+ listener.bind('single_tap', tap, 1)
|
|
|
|
|
+ listener.bind('double_tap', tap, 2)
|
|
|
|
|
+ listener.bind('rotate', lambda e: 0)
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ listener.start()
|
|
|
|
|
+ except KeyboardInterrupt:
|
|
|
|
|
+ listener.stop()
|