Browse Source

Added various fixes for transformations in the test application.

Taddeus Kroes 13 năm trước cách đây
mục cha
commit
c8706a27e6
10 tập tin đã thay đổi với 366 bổ sung225 xóa
  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)