Explorar o código

Code cleanup.

Taddeus Kroes %!s(int64=14) %!d(string=hai) anos
pai
achega
1aff9a2ef3
Modificáronse 1 ficheiros con 243 adicións e 148 borrados
  1. 243 148
      src/touch.py

+ 243 - 148
src/touch.py

@@ -6,9 +6,9 @@ from threading import Thread
 
 from tuio_server import TuioServer2D
 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
 pygame.display.init()
@@ -18,14 +18,17 @@ pygame.display.quit()
 
 # Heuristic constants
 # 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
 FLICK_VELOCITY_TRESHOLD = 20
 TAP_TIMEOUT = .2
 MAX_MULTI_DRAG_DISTANCE = .05
 STATIONARY_TIME = .01
 
+# Detect gestures 60 times per second
+GESTURE_UPDATE_RATE = 60
+
 # Minimum distance for two coordinates to be considered different
 # Theoretically, this should be one pixel because that is the minimal movement
 # of a mouse cursor on the screen
@@ -34,7 +37,8 @@ DIST_THRESHOLD = 1. / max(screen_size)
 
 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
     xb, yb = b
@@ -43,6 +47,9 @@ def distance(a, b):
 
 
 def add(a, b):
+    """
+    Add a an b, used for some functional programming calls.
+    """
     return a + b
 
 
@@ -77,39 +84,30 @@ class TouchPoint(object):
     def distance_to_prev(self):
         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):
         return self.angle - self.old_angle
 
+    def pinch_diff(self):
+        return self.pinch - self.old_pinch
+
     def dx(self):
-        return int(self.x - self.px)
+        return self.x - self.px
 
     def dy(self):
-        return int(self.y - self.py)
+        return self.y - self.py
 
     def down_time(self):
         return time.time() - self.start_time
@@ -118,9 +116,6 @@ class TouchPoint(object):
         return self.distance_to_prev() < TAP_TIME \
                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):
         return self.distance_to_prev() < DIST_THRESHOLD
 
@@ -129,9 +124,11 @@ class MultiTouchListener(Logger):
     def __init__(self, verbose=0, tuio_verbose=0, **kwargs):
         super(MultiTouchListener, self).__init__(**kwargs)
         self.verbose = verbose
-        self.tuio_verbose = tuio_verbose
+        self.last_tap = None
         self.last_tap_time = 0
         self.handlers = {}
+        self.thread = None
+        self.points_changed = False
 
         # Session id's pointing to point coordinates
         self.points = []
@@ -139,22 +136,74 @@ class MultiTouchListener(Logger):
         # Put centroid outside screen to prevent misinterpretation
         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):
         """
@@ -164,32 +213,35 @@ class MultiTouchListener(Logger):
         """
         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
 
-        rotation = pinch = 0
         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:
-            self.trigger(RotateEvent(cx, cy, rotation / l, l))
+            self.trigger(Rotate(cx, cy, rotation / l, l))
 
         if pinch:
-            self.trigger(PinchEvent(cx, cy, pinch / l, l))
+            self.trigger(Pinch(cx, cy, pinch / l * 2, l))
 
     def detect_pan(self):
         """
@@ -197,22 +249,30 @@ class MultiTouchListener(Logger):
         fingers moving close-ish together in the same direction.
         """
         l = len(self.points)
+
+        if not l:
+            return False
+
         m = MAX_MULTI_DRAG_DISTANCE
         clustered = l == 1 or all([p.distance_to(*self.centroid) <= m \
                                    for p in self.points])
         directions = [(cmp(p.dx(), 0), cmp(p.dy(), 0)) \
                       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):
         for i, p in enumerate(self.points):
@@ -222,123 +282,158 @@ class MultiTouchListener(Logger):
         if index:
             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):
-        self.log('Stopping event loop')
+        """
+        Stop main event loop.
+        """
+        self.log('Stopping TUIO server')
+        self.server.stop()
 
         if self.thread:
+            self.log('Stopping main loop thread')
             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:
-            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()
             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:
-            self.log('Starting event loop')
-            self.server = TuioServer2D(self, verbose=self.tuio_verbose)
+            self.log('Starting TUIO server')
             self.server.start()
-        except KeyboardInterrupt:
+        except SystemExit:
             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)
 
         if gesture not in self.handlers:
             self.handlers[gesture] = []
 
-        self.handlers[gesture].append(handler)
+        self.handlers[gesture].append((handler, args, kwargs))
 
     def trigger(self, event):
+        """
+        Call all handlers bound to the name of the triggered event.
+        """
         if event.__class__._name in self.handlers:
             h = self.handlers[event.__class__._name]
             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__':
-    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()