Sfoglia il codice sorgente

Added various fixes for transformations in the test application.

Taddeus Kroes 13 anni fa
parent
commit
c8706a27e6
10 ha cambiato i file con 366 aggiunte e 225 eliminazioni
  1. 2 1
      src/__init__.py
  2. 1 15
      src/areas.py
  3. 9 1
      src/geometry.py
  4. 43 7
      src/trackers/transform.py
  5. 3 3
      test.py
  6. 0 195
      tests/cairotest.py
  7. 2 2
      tests/draw.py
  8. 2 1
      tests/parse_arguments.py
  9. 269 0
      tests/testapp.py
  10. 35 0
      tests/utils.py

+ 2 - 1
src/__init__.py

@@ -1,4 +1,5 @@
 from logger import Logger
-from tracker import GestureTracker, Gesture
 from drivers import create_driver
+from tracker import GestureTracker, Gesture
+from geometry import Positionable
 from areas import *

+ 1 - 15
src/areas.py

@@ -31,21 +31,7 @@ class RectangularArea(Area):
                and self.y <= y <= self.y + self.height
 
     def contains_event(self, event):
-        if self.contains(*event.get_position()):
-            return True
-
-        if isinstance(event, PointMoveEvent):
-            px, py = event.point.get_previous_position()
-
-            if self.parent:
-                x, y = self.parent.get_screen_offset()
-            else:
-                x = y = 0
-
-            if self.contains(px - x, py - y):
-                self.handle_event(PointUpEvent(event.point))
-
-        return False
+        return self.contains(*event.get_position())
 
 
 class CircularArea(Area):

+ 9 - 1
src/geometry.py

@@ -1,5 +1,5 @@
 from __future__ import division
-from math import atan2, pi
+from math import atan2, pi, sin, cos
 import time
 
 
@@ -60,6 +60,14 @@ class Positionable(object):
     def translate(self, x, y):
         self.set_position(self.x + x, self.y + y)
 
+    def scale(self, scale):
+        self.set_position(self.x * scale, self.y * scale)
+
+    def rotate(self, angle):
+        c = cos(angle)
+        s = sin(angle)
+        self.set_position(c * self.x - s * self.y, s * self.x + c * self.y)
+
 
 class MovingPositionable(Positionable):
     """

+ 43 - 7
src/trackers/transform.py

@@ -21,7 +21,7 @@ class RotationGesture(Gesture, Positionable):
                % (self.x, self.y, self.angle)
 
     def get_angle(self):
-        return self.angle
+        return -self.angle
 
 
 class PinchGesture(Gesture, Positionable):
@@ -84,6 +84,8 @@ class TransformationTracker(GestureTracker):
         # Current and previous centroid of all touch points
         self.centroid = None
 
+        self.deleted = []
+
     def update_centroid(self):
         if not self.points:
             self.centroid = None
@@ -105,18 +107,28 @@ class TransformationTracker(GestureTracker):
     def on_point_down(self, event):
         self.points.append(event.point)
         self.update_centroid()
+        event.stop_propagation()
 
     def on_point_move(self, event):
         point = event.point
 
         if point not in self.points:
-            return
+            pid = point.get_id()
+
+            if pid not in self.deleted:
+                return
 
+            self.debug('recovered %s' % point)
+            self.deleted.remove(pid)
+            self.points.append(point)
+            self.update_centroid()
+
+        event.stop_propagation()
+        self.invalidate_points()
         l = len(self.points)
 
         if l > 1:
             offset_centroid = self.centroid - self.area.get_screen_offset()
-            print self.centroid, self.area, offset_centroid
 
             # Rotation (around the previous centroid)
             rotation = point.rotation_around(self.centroid) / l
@@ -137,8 +149,32 @@ class TransformationTracker(GestureTracker):
                                  self.centroid.translation(), l))
 
     def on_point_up(self, event):
-        if event.point not in self.points:
-            return
+        if event.point in self.points:
+            self.points.remove(event.point)
+            self.update_centroid()
+            event.stop_propagation()
+
+    def invalidate_points(self):
+        """
+        Check if all points are still in the corresponding area, and remove
+        those which are not.
+        """
+        delete = []
+
+        if self.area.parent:
+            ox, oy = self.area.parent.get_screen_offset()
+        else:
+            ox = oy = 0
 
-        self.points.remove(event.point)
-        self.update_centroid()
+        for i, p in enumerate(self.points):
+            x, y = p.get_position()
+
+            if not self.area.contains(x - ox, y - oy):
+                self.debug('deleted %s' % p)
+                delete.append(i)
+                self.deleted.append(p.get_id())
+
+        if delete:
+            self.points = [p for i, p in enumerate(self.points)
+                           if i not in delete]
+            self.update_centroid()

+ 3 - 3
test.py

@@ -1,9 +1,9 @@
 #!/usr/bin/env python
 import os
-src_path = os.path.realpath('src')
-
 import sys
-sys.path.insert(0, src_path)
+
+sys.path.insert(0, os.path.realpath('src'))
+sys.path.insert(0, os.path.realpath('tests'))
 
 del sys.argv[0]
 execfile(sys.argv[0])

+ 0 - 195
tests/cairotest.py

@@ -1,195 +0,0 @@
-#!/usr/bin/env python
-from __future__ import division
-import gtk
-import cairo
-from math import radians
-from threading import Thread
-
-import src as mt
-
-
-class Rectangle(mt.RectangularArea):
-    def __init__(self, x, y, width, height, color=(1, 0, 0)):
-        super(Rectangle, self).__init__(x, y, width, height)
-        self.w = width
-        self.h = height
-        self.scale = 1
-        self.angle = 0
-        self.color = color
-        self.t = cairo.Matrix()
-        self.t.translate(x, y)
-
-        self.on_drag(self.move)
-        self.on_pinch(self.resize)
-        #self.on_rotate(self.rotate)
-
-    def move(self, g):
-        print 'move:', g
-        self.translate(*g.get_translation())
-        self.ttrans(*g.get_translation())
-        refresh()
-
-    def resize(self, g):
-        print 'resize:', g
-
-        x, y = g.get_position()
-        scale = g.get_scale()
-        self.ttrans(x, y)
-        self.tscale(scale)
-        self.ttrans(-x, -y)
-
-        self.translate(x - x * scale, y - y * scale)
-
-        self.width *= scale
-        self.height *= scale
-
-        refresh()
-
-    def rotate(self, g):
-        print 'rotate:', g
-
-        x, y = g.get_position()
-        self.ttrans(x, y)
-        self.trot(-g.get_angle())
-        self.ttrans(-x, -y)
-
-        refresh()
-
-    def ttrans(self, tx, ty):
-        t = cairo.Matrix()
-        t.translate(tx, ty)
-        self.t = t * self.t
-
-    def tscale(self, s):
-        t = cairo.Matrix()
-        t.scale(s, s)
-        self.t = t * self.t
-
-    def trot(self, a):
-        t = cairo.Matrix()
-        t.rotate(a)
-        self.t = t * self.t
-
-    def draw(self, cr):
-        cr.transform(self.t)
-        cr.rectangle(0, 0, self.w, self.h)
-        cr.set_source_rgb(*self.color)
-        cr.fill()
-
-
-fullscreen = False
-W, H = mt.screen.screen_size
-
-
-def create_context_window(w, h, callback):
-    def create_context(area, event):
-        """Add Cairo context to GTK window and draw state."""
-        global cr
-        cr = area.window.cairo_create()
-        draw()
-
-    def move_window(win, event):
-        """Synchronize root multi-touch area with GTK window."""
-        root.set_position(*event.get_coords())
-        root.set_size(event.width, event.height)
-        draw()
-
-    def handle_key(win, event):
-        """Handle key event. 'f' toggles fullscreen, 'q' exits the program."""
-        if event.keyval >= 256:
-            return
-
-        key = chr(event.keyval)
-
-        if key == 'f':
-            global fullscreen
-            (win.unfullscreen if fullscreen else win.fullscreen)()
-            fullscreen = not fullscreen
-        elif key == 'q':
-            quit()
-
-    # Root area (will be synchronized with GTK window)
-    global root
-    root = mt.RectangularArea(0, 0, w, h)
-
-    # GTK window
-    global window
-    window = gtk.Window()
-    window.set_title('Cairo test')
-    window.connect('destroy', quit)
-    window.connect('key-press-event', handle_key)
-    window.connect('configure-event', move_window)
-    window.connect('show', callback)
-
-    # Drawing area, needed by cairo context for drawing
-    area = gtk.DrawingArea()
-    area.set_size_request(w, h)
-    area.connect('expose-event', create_context)
-
-    window.add(area)
-    area.show()
-    window.show()
-
-
-def draw(*args):
-    if not cr:
-        return
-
-    # Background
-    cr.rectangle(0, 0, *root.get_size())
-    cr.set_source_rgb(0, 1, 0)
-    cr.fill()
-
-    # Drawable objects (use save and restore to allow transformations)
-    for obj in draw_objects:
-        cr.save()
-        obj.draw(cr)
-        cr.restore()
-
-
-def refresh():
-    window.queue_draw()
-
-
-def quit(*args):
-    gtk.main_quit()
-
-
-# Initialization
-window = cr = root = None
-draw_objects = []
-
-
-def on_show(window):
-    def root_dtap(g): print 'double tapped on root'
-    root.on_double_tap(root_dtap)
-
-    # Create blue rectangle
-    rect = Rectangle(300, 200, 250, 150, color=(0, 0, 1))
-    draw_objects.append(rect)
-    root.add_area(rect)
-
-    def rect_tap(g): print 'tapped on rectangle'
-    rect.on_tap(rect_tap, propagate_up_event=False)
-
-
-if __name__ == '__main__':
-    # Parse arguments
-    from tests.parse_arguments import create_parser, parse_args
-    parse_args(create_parser())
-
-    # Create a window with a Cairo context in it and a multi-touch area
-    # syncronized with it
-    create_context_window(640, 460, on_show)
-
-    # Run multi-touch gesture server in separate thread
-    driver = mt.create_driver(root)
-    mt_thread = Thread(target=driver.start)
-    mt_thread.daemon = True
-    mt_thread.start()
-
-    # Initialize threads in GTK so that the thread started above will work
-    gtk.gdk.threads_init()
-
-    # Start main loop in current thread
-    gtk.main()

+ 2 - 2
tests/draw.py

@@ -1,12 +1,12 @@
-#!/usr/bin/env python
+lambda g: #!/usr/bin/env python
 from __future__ import division
 import pygame
 from threading import Thread
 from math import degrees
 
-from tests.parse_arguments import create_parser, parse_args
 from src import FullscreenArea, create_driver
 from src.screen import screen_size
+from parse_arguments import create_parser, parse_args
 
 # Parse arguments
 parser = create_parser()

+ 2 - 1
tests/parse_arguments.py

@@ -1,7 +1,7 @@
 import argparse
 import logging
 
-from src.logger import Logger
+from src import Logger
 
 
 # Parse arguments
@@ -18,6 +18,7 @@ def create_parser():
 
 
 def parse_args(parser):
+    print 'here:', parser.format_usage()
     args = parser.parse_args()
 
     # Configure logger

+ 269 - 0
tests/testapp.py

@@ -0,0 +1,269 @@
+#!/usr/bin/env python
+from __future__ import division
+import gtk
+from threading import Thread
+from math import pi, tan
+
+import src as mt
+from utils import BoundingBoxArea
+
+
+RED = 1, 0, 0
+GREEN = 0, 1, 0
+BLUE = 0, 0, 1
+WHITE = 1, 1, 1
+BLACK = 0, 0, 0
+
+
+class Rectangle(mt.RectangularArea):
+    def __init__(self, x, y, width, height, color=(1, 0, 0)):
+        super(Rectangle, self).__init__(x, y, width, height)
+        self.color = color
+        self.on_drag(self.handle_drag)
+
+    def handle_drag(self, g):
+        tx, ty = g.get_translation()
+        self.translate(tx, ty)
+        refresh()
+
+    def draw(self, cr):
+        cr.rectangle(self.x, self.y, self.width, self.height)
+        cr.set_source_rgb(*self.color)
+        cr.fill()
+
+
+class Polygon(BoundingBoxArea):
+    def __init__(self, x, y, points, color=BLUE, border_color=RED):
+        super(Polygon, self).__init__(x, y, points)
+        self.fill_color = color
+        self.border_color = border_color
+
+        self.on_drag(self.handle_drag)
+        self.on_pinch(self.handle_pinch)
+        self.on_rotate(self.handle_rotate)
+
+    def handle_drag(self, g):
+        tx, ty = g.get_translation()
+        self.translate(tx, ty)
+        refresh()
+
+    def handle_pinch(self, g):
+        cx, cy = g.get_position()
+        self.scale_points(g.get_scale(), cx, cy)
+        self.update_bounds()
+        refresh()
+
+    def handle_rotate(self, g):
+        cx, cy = g.get_position()
+        self.rotate_points(g.get_angle(), cx, cy)
+        self.update_bounds()
+        refresh()
+
+    def draw(self, cr):
+        # Draw bounding box
+        if draw_bounding_boxes:
+            cr.rectangle(self.x, self.y, self.width, self.height)
+            cr.set_source_rgb(*self.border_color)
+            cr.set_line_width(3)
+            cr.stroke()
+
+        # Fill polygon
+        cr.translate(self.x, self.y)
+        cr.new_path()
+
+        for x, y in zip(*self.points):
+            cr.line_to(x, y)
+
+        cr.set_source_rgb(*self.fill_color)
+        cr.fill()
+
+
+fullscreen = False
+draw_bounding_boxes = draw_touch_points = True
+W, H = mt.screen.screen_size
+
+
+def create_context_window(w, h, callback):
+    def create_context(area, event):
+        """Add Cairo context to GTK window and draw state."""
+        global cr
+        cr = area.window.cairo_create()
+        draw()
+
+    def move_window(win, event):
+        """Synchronize root multi-touch area with GTK window."""
+        root.set_position(*event.get_coords())
+        root.set_size(event.width, event.height)
+        overlay.set_size(event.width, event.height)
+        draw()
+
+    def handle_key(win, event):
+        """Handle key event. 'f' toggles fullscreen, 'q' exits the program, 'b'
+        toggles bounding boxes, 'p' toggles touch points."""
+        if event.keyval >= 256:
+            return
+
+        key = chr(event.keyval)
+
+        if key == 'f':
+            global fullscreen
+            (win.unfullscreen if fullscreen else win.fullscreen)()
+            fullscreen = not fullscreen
+        elif key == 'q':
+            quit()
+        elif key == 'b':
+            global draw_bounding_boxes
+            draw_bounding_boxes = not draw_bounding_boxes
+            refresh()
+        elif key == 'p':
+            global draw_touch_points
+            draw_touch_points = not draw_touch_points
+            refresh()
+
+    # Root area (will be synchronized with GTK window)
+    global root, overlay
+    root = mt.RectangularArea(0, 0, w, h)
+    overlay = mt.RectangularArea(0, 0, w, h)
+
+    # GTK window
+    global window
+    window = gtk.Window()
+    window.set_title('Cairo test')
+    window.connect('destroy', quit)
+    window.connect('key-press-event', handle_key)
+    window.connect('configure-event', move_window)
+    window.connect('show', callback)
+
+    if fullscreen:
+        window.fullscreen()
+
+    # Drawing area, needed by cairo context for drawing
+    area = gtk.DrawingArea()
+    area.set_size_request(w, h)
+    area.connect('expose-event', create_context)
+
+    window.add(area)
+    area.show()
+    window.show()
+
+
+def draw(*args):
+    if not cr:
+        return
+
+    # Background
+    cr.rectangle(0, 0, *root.get_size())
+    cr.set_source_rgb(*BLACK)
+    cr.fill()
+
+    # Drawable objects (use save and restore to allow transformations)
+    for obj in draw_objects:
+        cr.save()
+        obj.draw(cr)
+        cr.restore()
+
+    if draw_touch_points:
+        ox, oy = root.get_position()
+        cr.set_source_rgb(*WHITE)
+
+        for x, y in touch_points.itervalues():
+            x -= ox
+            y -= oy
+
+            cr.set_line_width(3)
+            cr.arc(x, y, 20, 0, 2 * pi)
+            cr.stroke()
+
+            cr.set_line_width(1)
+            cr.move_to(x - 8, y)
+            cr.line_to(x + 8, y)
+            cr.move_to(x, y - 8)
+            cr.line_to(x, y + 8)
+            cr.stroke()
+
+def refresh():
+    window.queue_draw()
+
+
+def quit(*args):
+    gtk.main_quit()
+
+
+# Initialization
+window = cr = root = overlay = None
+draw_objects = []
+touch_points = {}
+
+
+def triangle_height(width):
+    return abs(.5 * width * tan(2 / 3 * pi))
+
+
+def on_show(window):
+    def root_dtap(g): print 'double tapped on root'
+    root.on_double_tap(root_dtap)
+
+    # Create blue rectangle
+    x, y, w, h = 0, 0, 250, 150
+    rect = Polygon(x, y, [(0, 0), (0, h), (w, h), (w, 0)])
+    draw_objects.append(rect)
+    root.add_area(rect)
+
+    def rect_tap(g): print 'tapped on rectangle'
+    rect.on_tap(rect_tap, propagate_up_event=False)
+
+    # Create green triangle
+    x, y, w = 400, 400, 200
+    h = triangle_height(w)
+    triangle = Polygon(x, y, [(0, h), (w, h), (w / 2, 0)], color=GREEN)
+    draw_objects.append(triangle)
+    root.add_area(triangle)
+
+    # Overlay catches basic events
+    def handle_down(gesture):
+        point = gesture.get_event().get_touch_object()
+        touch_points[point.get_id()] = point.get_position()
+
+        if draw_touch_points:
+            refresh()
+
+    def handle_up(gesture):
+        point = gesture.get_event().get_touch_object()
+        del touch_points[point.get_id()]
+
+        if draw_touch_points:
+            refresh()
+
+    overlay.on_point_down(handle_down)
+    overlay.on_point_move(handle_down)
+    overlay.on_point_up(handle_up)
+    root.add_area(overlay)
+
+
+if __name__ == '__main__':
+    from 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 initially')
+    args = parse_args(parser)
+
+    fullscreen = args.fullscreen
+
+
+    # Create a window with a Cairo context in it and a multi-touch area
+    # syncronized with it
+    create_context_window(640, 460, on_show)
+
+    # Run multi-touch gesture server in separate thread
+    driver = mt.create_driver(root)
+    mt_thread = Thread(target=driver.start)
+    mt_thread.daemon = True
+    mt_thread.start()
+
+    # Initialize threads in GTK so that the thread started above will work
+    gtk.gdk.threads_init()
+
+    # Start main loop in current thread
+    gtk.main()

+ 35 - 0
tests/utils.py

@@ -0,0 +1,35 @@
+from numpy import array, diag, dot, cos, sin
+from src import RectangularArea
+
+
+class BoundingBoxArea(RectangularArea):
+    def __init__(self, x, y, points):
+        super(BoundingBoxArea, self).__init__(x, y, 0, 0)
+        self.points = array(points).T
+        self.update_bounds()
+
+    def translate_points(self, tx, ty):
+        self.points += [[tx], [ty]]
+
+    def scale_points(self, scale, cx, cy):
+        self.translate_points(-cx, -cy)
+        self.points = dot(diag([scale, scale]), self.points)
+        self.translate_points(cx, cy)
+
+    def rotate_points(self, angle, cx, cy):
+        cosa = cos(angle)
+        sina = sin(angle)
+        mat = array([[cosa, -sina], [sina, cosa]])
+
+        self.translate_points(-cx, -cy)
+        self.points = dot(mat, self.points)
+        self.translate_points(cx, cy)
+
+    def update_bounds(self):
+        min_x, min_y = self.points.min(1)
+        max_x, max_y = self.points.max(1)
+        self.set_size(max_x - min_x, max_y - min_y)
+
+        if min_x or min_y:
+            self.translate(min_x, min_y)
+            self.translate_points(-min_x, -min_y)