Merge branch 'master' of http://skoegl.net/uni/numpty
[numtypysics.git] / tuioinput.py
blobd4a0b79aac84552d62f82e2d1be238414fe6f3b0
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_START_DELAY = .5
33 CURSOR_STOP_DELAY = .5
35 CURSOR_NEIGHBOR_DISTANCE = .35
37 try:
38 import numptyphysics
39 except ImportError, ioe:
40 print >>sys.stderr, """
41 This module can only be loaded from within NumptyPhysics.
42 """
43 sys.exit(1)
45 try:
46 import tuio
47 except ImportError, ioe:
48 print >>sys.stderr, """
49 You have to install PyTUIO and copy it into your $PYTHONPATH.
50 You can grab a tarball from: http://code.google.com/p/pytuio/
51 """
52 sys.exit(2)
54 CURSOR_NEIGHBOR_DISTANCE *= numptyphysics.HEIGHT
56 class NumpytTuioCursor(object):
57 def __init__(self, id, x, y):
58 self.id = id
59 self.x = x
60 self.y = y
61 self.time = time.time()
63 class NumptyCursor(object):
64 DRAW, DRAG, DELETE = range(3)
66 def __init__(self, tuio_id, x, y, on_activate, on_deactivate, on_move):
67 self.id = None
68 self.x = x
69 self.y = y
70 self.mode = self.DRAW
71 self.tuio_ids = {tuio_id: time.time()}
73 self.activated = False
74 self.deactivated = False
75 self.moved = False
77 self.on_activate = on_activate
78 self.on_deactivate = on_deactivate
79 self.on_move = on_move
81 self.activate_at = time.time()+CURSOR_START_DELAY
82 self.deactivate_at = 0
83 print 'new numpty cursor for', tuio_id
84 self.heartbeat()
86 def heartbeat(self):
87 self.deactivate_at = time.time()+CURSOR_STOP_DELAY
89 def is_near(self, x, y):
90 return ((x-self.x)**2 + (y-self.y)**2)**.5 < CURSOR_NEIGHBOR_DISTANCE
92 def move(self, x, y):
93 if self.activated and not self.deactivated:
94 print 'moved', self.id, 'to', x, '/', y, '(which is a', self.mode, ')'
95 if self.x != x or self.y != y:
96 self.x = x
97 self.y = y
98 self.moved = True
100 def want_new_cursors(self):
101 if self.mode == self.DRAW:
102 return len(self.tuio_ids) < 1
103 elif self.mode == self.DRAG:
104 return len(self.tuio_ids) < 2
105 elif self.mode == self.DELETE:
106 return len(self.tuio_ids) < 3
107 return False
109 def seen_tuio_id(self, id, x, y):
110 if self.deactivated:
111 # If this cursor is gone, it's gone
112 return False
114 if not self.activated and self.is_near(x, y) and id not in self.tuio_ids:
115 # this cursor it not yet activated; the tuio id is near
116 if self.mode == self.DRAW:
117 print 'absorbing and setting to drag', id
118 self.mode = self.DRAG
119 elif self.mode == self.DRAG:
120 print 'absorbing and setting to delete', id
121 self.mode = self.DELETE
122 else:
123 return False
125 self.tuio_ids[id] = time.time()
126 self.heartbeat()
127 return True
129 if self.activated and self.want_new_cursors() and self.is_near(x, y) and id not in self.tuio_ids:
130 # i can take more cursors, so absorb this
131 self.tuio_ids[id] = time.time()
132 if id == min(self.tuio_ids):
133 self.move(x, y)
134 self.heartbeat()
135 return True
136 elif id in self.tuio_ids:
137 self.tuio_ids[id] = time.time()
138 if id == min(self.tuio_ids):
139 self.move(x, y)
140 self.heartbeat()
141 return True
143 return False
145 def activate(self):
146 self.activated = True
147 self.on_activate(self)
148 if self.mode == self.DELETE:
149 self.deactivate()
151 def deactivate(self):
152 self.deactivated = True
153 if self.activated:
154 self.on_deactivate(self)
156 def movement(self):
157 self.on_move(self)
158 self.moved = False
160 def process(self):
161 if time.time() > self.deactivate_at and not self.deactivated:
162 self.deactivate()
163 elif time.time() > self.activate_at and not self.activated and not self.deactivated:
164 self.activate()
165 elif self.activated and self.moved:
166 self.movement()
167 else:
168 self.expire()
170 def expire(self):
171 for id, t in self.tuio_ids.items():
172 if t+.5*CURSOR_STOP_DELAY < time.time():
173 del self.tuio_ids[id]
176 class CursorTracker(object):
177 def __init__(self):
178 self.cursors = []
179 self._freeslots = collections.deque(range(numptyphysics.MAX_CURSORS))
180 self.events = collections.deque()
182 def activate_cursor(self, cursor):
183 cursor.id = self.grabslot()
184 if cursor.mode == cursor.DRAW:
185 self.push_event(cursor, numptyphysics.START_STROKE)
186 elif cursor.mode == cursor.DRAG:
187 self.push_event(cursor, numptyphysics.START_DRAG)
188 elif cursor.mode == cursor.DELETE:
189 self.push_event(cursor, numptyphysics.DELETE)
191 def deactivate_cursor(self, cursor):
192 if cursor.mode == cursor.DRAW:
193 self.push_event(cursor, numptyphysics.FINISH_STROKE)
194 elif cursor.mode == cursor.DRAG:
195 self.push_event(cursor, numptyphysics.END_DRAG)
196 self.freeslot(cursor.id)
198 def cursor_movement(self, cursor):
199 if cursor.mode == cursor.DRAW:
200 self.push_event(cursor, numptyphysics.APPEND_STROKE)
201 elif cursor.mode == cursor.DRAG:
202 self.push_event(cursor, numptyphysics.DRAG)
204 def update(self, cursors):
205 new_cursors = []
206 for cursor in cursors:
207 id, x, y = cursor.sessionid, cursor.xpos, cursor.ypos
208 if use_ipod:
209 x, y = y, 1.-x
210 x, y = self.convert_coords(x, y)
211 if use_mtmini:
212 self.push_preview_event(id, x, y)
214 absorbed = False
215 for c in self.cursors:
216 if c.seen_tuio_id(id, x, y):
217 absorbed = True
218 break
220 if not absorbed:
221 new_cursors.append((id, x, y))
223 for id, x, y in new_cursors:
224 self.cursors.append(NumptyCursor(id, x, y, self.activate_cursor, self.deactivate_cursor, self.cursor_movement))
226 self.idle()
228 def idle(self):
229 for cursor in self.cursors:
230 cursor.process()
232 self.cursors = [cursor for cursor in self.cursors if not cursor.deactivated]
234 def grabslot(self):
235 try:
236 return self._freeslots.pop()
237 except IndexError:
238 return None
240 def freeslot(self, slot):
241 self._freeslots.appendleft(slot)
243 def push_event(self, cursor, type_):
244 self.events.appendleft(InputEvent(cursor.x, cursor.y, type_, cursor.id))
246 def push_preview_event(self, id, x, y):
247 self.events.appendleft(InputEvent(x, y, numptyphysics.PREVIEW_CURSOR, id))
249 def convert_coords(self, x, y):
250 return (int(x*numptyphysics.WIDTH), int(y*numptyphysics.HEIGHT))
252 def has_events(self):
253 return len(self.events) > 0
255 def get_events(self):
256 try:
257 while True:
258 yield self.events.pop()
259 except IndexError, ie:
260 raise StopIteration()
262 class InputEvent(object):
263 def __init__(self, x, y, event_type, cursor_id=0):
264 self.x = x
265 self.y = y
266 self.event_type = event_type
267 self.cursor_id = cursor_id
269 class C(threading.Thread):
270 def run(self):
271 tracking = tuio.Tracking('')
272 tracker = CursorTracker()
273 oldcount = False
274 x, y = None, None
275 while True:
276 while tracking.update():
277 # read the socket empty
278 pass
280 tracker.update(tracking.cursors())
282 for event in tracker.get_events():
283 numptyphysics.post_event(event)
285 while not tracking.update() and not tracker.has_events():
286 # wait for something to happen
287 tracker.idle()
288 time.sleep(1./30.)
290 C().start()