Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
M
multitouch
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Taddeüs Kroes
multitouch
Commits
1aff9a2e
Commit
1aff9a2e
authored
May 04, 2012
by
Taddeus Kroes
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Code cleanup.
parent
9807d347
Changes
1
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
243 additions
and
148 deletions
+243
-148
src/touch.py
src/touch.py
+243
-148
No files found.
src/touch.py
View file @
1aff9a2e
...
...
@@ -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
,
\
SingleTap
Event
,
DoubleTapEvent
,
FlickEvent
,
RotateEvent
,
PinchEvent
,
\
Pan
Event
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_
GESTURE
S
=
(
'down'
,
'up'
,
'move'
,
'tap'
,
'single_tap'
,
'double_tap'
,
'pan'
,
'flick'
,
'rotate'
,
'pinch'
)
SUPPORTED_
EVENT
S
=
(
'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_verbos
e
self
.
last_tap
=
Non
e
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
(
Rotate
Event
(
cx
,
cy
,
rotation
/
l
,
l
))
self
.
trigger
(
Rotate
(
cx
,
cy
,
rotation
/
l
,
l
))
if
pinch
:
self
.
trigger
(
Pinch
Event
(
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
KeyboardInterrup
t
:
except
SystemExi
t
:
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
()
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment