Sound support + better cursor input handling
[numtypysics.git] / tuioinput.py
blob33cf8bc04f9986f735d2e5704f65cd5ba888dab1
3 # Python Input Module for NumptyPhysics
4 # Copyright (c) 2009 Thomas Perl <thpinfo.com>
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License as
8 # published by the Free Software Foundation; either version 3 of the
9 # License, or (at your option) any later version.
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
19 import sys
20 import threading
21 import time
22 import collections
23 import os.path
25 # Create the file "use-ipod" if you want to use an iPod Touch
26 # and its OSCemote application for TUIO input (rm it if you don't)
27 use_ipod = os.path.exists('use-ipod')
29 # Create the file "use-mtmini" if you want to show cursor previews
30 use_mtmini = os.path.exists('use-mtmini')
32 CURSOR_ACTIVATE_DELAY = .01
33 CURSOR_UPGRADE_DELAY = .5
34 CURSOR_STOP_DELAY = .5
36 CURSOR_NEIGHBOR_DISTANCE = .2
38 try:
39 import numptyphysics
40 except ImportError, ioe:
41 print >>sys.stderr, """
42 This module can only be loaded from within NumptyPhysics.
43 """
44 sys.exit(1)
46 try:
47 import tuio
48 except ImportError, ioe:
49 print >>sys.stderr, """
50 You have to install PyTUIO and copy it into your $PYTHONPATH.
51 You can grab a tarball from: http://code.google.com/p/pytuio/
52 """
53 sys.exit(2)
55 CURSOR_NEIGHBOR_DISTANCE *= numptyphysics.HEIGHT
57 class NumpytTuioCursor(object):
58 def __init__(self, id, x, y):
59 self.id = id
60 self.x = x
61 self.y = y
62 self.time = time.time()
64 class NumptyCursor(object):
65 DRAW, DRAG, DELETE = range(3)
67 def __init__(self, tuio_id, x, y, on_activate, on_deactivate, on_move, on_upgrade):
68 self.id = None
69 self.x = x
70 self.y = y
71 self.mode = self.DRAW
72 self.tuio_ids = {tuio_id: time.time()}
74 self.activated = False
75 self.deactivated = False
76 self.moved = False
77 self.upgraded = False
79 self.on_activate = on_activate
80 self.on_deactivate = on_deactivate
81 self.on_move = on_move
82 self.on_upgrade = on_upgrade
84 self.activate_at = time.time()+CURSOR_ACTIVATE_DELAY
85 self.upgrade_at = time.time()+CURSOR_UPGRADE_DELAY
86 self.deactivate_at = 0
87 print 'new numpty cursor for', tuio_id
88 self.heartbeat()
90 def heartbeat(self):
91 self.deactivate_at = time.time()+CURSOR_STOP_DELAY
93 def is_near(self, x, y):
94 return ((x-self.x)**2 + (y-self.y)**2)**.5 < CURSOR_NEIGHBOR_DISTANCE
96 def move(self, x, y):
97 if self.activated and not self.deactivated:
98 print 'moved', self.id, 'to', x, '/', y, '(which is a', self.mode, ')'
99 if self.x != x or self.y != y:
100 self.x = x
101 self.y = y
102 self.moved = True
104 def want_new_cursors(self):
105 if self.mode == self.DRAW:
106 return len(self.tuio_ids) < 1
107 elif self.mode == self.DRAG:
108 return len(self.tuio_ids) < 2
109 elif self.mode == self.DELETE:
110 return len(self.tuio_ids) < 3
111 return False
113 def seen_tuio_id(self, id, x, y):
114 if self.deactivated:
115 # If this cursor is gone, it's gone
116 return False
118 if not self.upgraded and self.is_near(x, y) and id not in self.tuio_ids:
119 # this cursor it not yet upgraded; the tuio id is near
120 if self.mode == self.DRAW:
121 print 'absorbing and setting to drag', id
122 self.mode = self.DRAG
123 elif self.mode == self.DRAG:
124 print 'absorbing and setting to delete', id
125 self.mode = self.DELETE
126 else:
127 return False
129 self.tuio_ids[id] = time.time()
130 self.heartbeat()
131 return True
133 if self.upgraded and self.want_new_cursors() and self.is_near(x, y) and id not in self.tuio_ids:
134 # i can take more cursors, so absorb this
135 self.tuio_ids[id] = time.time()
136 if id == min(self.tuio_ids) and self.id:
137 self.move(x, y)
138 self.heartbeat()
139 return True
140 elif id in self.tuio_ids:
141 self.tuio_ids[id] = time.time()
142 if id == min(self.tuio_ids) and self.id:
143 self.move(x, y)
144 self.heartbeat()
145 return True
147 return False
149 def activate(self):
150 self.activated = True
151 self.on_activate(self)
153 def upgrade(self):
154 self.upgraded = True
155 self.on_upgrade(self)
156 if self.mode == self.DELETE:
157 self.deactivate()
159 def deactivate(self):
160 self.deactivated = True
161 if self.activated:
162 self.on_deactivate(self)
164 def movement(self):
165 self.on_move(self)
166 self.moved = False
168 def process(self):
169 if time.time() > self.deactivate_at and not self.deactivated:
170 self.deactivate()
171 elif time.time() > self.upgrade_at and not self.upgraded and not self.deactivated:
172 self.upgrade()
173 elif time.time() > self.activate_at and not self.activated and not self.upgraded and not self.deactivated:
174 self.activate()
175 elif self.activated and self.moved:
176 self.movement()
177 else:
178 self.expire()
180 def expire(self):
181 for id, t in self.tuio_ids.items():
182 if t+.5*CURSOR_STOP_DELAY < time.time():
183 del self.tuio_ids[id]
186 class CursorTracker(object):
187 def __init__(self):
188 self.cursors = []
189 self._freeslots = collections.deque(range(numptyphysics.MAX_CURSORS))
190 self._highest_id = -1
191 self.events = collections.deque()
193 def activate_cursor(self, cursor):
194 cursor.id = self.grabslot()
195 if cursor.mode == cursor.DRAW:
196 self.push_event(cursor, numptyphysics.START_STROKE)
197 elif cursor.mode == cursor.DRAG:
198 self.push_event(cursor, numptyphysics.START_DRAG)
199 elif cursor.mode == cursor.DELETE:
200 self.push_event(cursor, numptyphysics.DELETE)
202 def deactivate_cursor(self, cursor):
203 if cursor.mode == cursor.DRAW:
204 self.push_event(cursor, numptyphysics.FINISH_STROKE)
205 elif cursor.mode == cursor.DRAG:
206 self.push_event(cursor, numptyphysics.END_DRAG)
207 self.freeslot(cursor.id)
209 def cursor_movement(self, cursor):
210 if cursor.mode == cursor.DRAW:
211 self.push_event(cursor, numptyphysics.APPEND_STROKE)
212 elif cursor.mode == cursor.DRAG:
213 self.push_event(cursor, numptyphysics.DRAG)
215 def upgrade_cursor(self, cursor):
216 if cursor.mode == cursor.DRAW:
217 # "draw" cursors do not need to be upgraded
218 return
220 # cancel the in-progress draw event + start "real" event
221 self.push_event(cursor, numptyphysics.CANCEL_DRAW)
222 if cursor.mode == cursor.DRAG:
223 self.push_event(cursor, numptyphysics.START_DRAG)
224 elif cursor.mode == cursor.DELETE:
225 self.push_event(cursor, numptyphysics.DELETE)
227 def update(self, cursors):
228 new_cursors = []
229 for cursor in cursors:
230 id, x, y = cursor.sessionid, cursor.xpos, cursor.ypos
231 if x == 0 and y == 0:
232 continue
233 if use_ipod:
234 x, y = y, 1.-x
235 x, y = self.convert_coords(x, y)
236 if use_mtmini:
237 self.push_preview_event(id, x, y)
239 absorbed = False
240 for c in self.cursors:
241 if c.seen_tuio_id(id, x, y):
242 absorbed = True
243 break
245 if not absorbed and id > self._highest_id:
246 new_cursors.append((id, x, y))
248 if id > self._highest_id:
249 self._highest_id = id
251 for id, x, y in new_cursors:
252 self.cursors.append(NumptyCursor(id, x, y, self.activate_cursor, self.deactivate_cursor, self.cursor_movement, self.upgrade_cursor))
254 self.idle()
256 def idle(self):
257 for cursor in self.cursors:
258 cursor.process()
260 self.cursors = [cursor for cursor in self.cursors if not cursor.deactivated]
262 def grabslot(self):
263 try:
264 return self._freeslots.pop()
265 except IndexError:
266 return None
268 def freeslot(self, slot):
269 self._freeslots.appendleft(slot)
271 def push_event(self, cursor, type_):
272 if cursor.id:
273 print 'pushing event type %d for cursor %s' % (type_, repr(cursor.id))
274 self.events.appendleft(InputEvent(cursor.x, cursor.y, type_, cursor.id))
276 def push_preview_event(self, id, x, y):
277 self.events.appendleft(InputEvent(x, y, numptyphysics.PREVIEW_CURSOR, id))
279 def convert_coords(self, x, y):
280 return (int(x*numptyphysics.WIDTH), int(y*numptyphysics.HEIGHT))
282 def has_events(self):
283 return len(self.events) > 0
285 def get_events(self):
286 try:
287 while True:
288 yield self.events.pop()
289 except IndexError, ie:
290 raise StopIteration()
292 class InputEvent(object):
293 def __init__(self, x, y, event_type, cursor_id=0):
294 self.x = x
295 self.y = y
296 self.event_type = event_type
297 self.cursor_id = cursor_id
299 class C(threading.Thread):
300 def run(self):
301 tracking = tuio.Tracking('')
302 tracker = CursorTracker()
303 oldcount = False
304 x, y = None, None
305 while True:
306 while tracking.update():
307 # read the socket empty
308 pass
310 tracker.update(tracking.cursors())
312 for event in tracker.get_events():
313 numptyphysics.post_event(event)
315 while not tracking.update() and not tracker.has_events():
316 # wait for something to happen
317 tracker.idle()
318 time.sleep(1./30.)
320 C().start()