Add pointlog2svg utility for the thesis
[numtypysics.git] / tuioinput.py
blob47ac8ad68e747fa8e6ade1ac1e862ec4c18b4142
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 # Create the file "use-justdraw" if you don't want dragging+deleting
33 use_justdraw = os.path.exists('use-justdraw')
35 CURSOR_ACTIVATE_DELAY = .01
36 CURSOR_UPGRADE_DELAY = .5
37 CURSOR_STOP_DELAY = .5
39 CURSOR_NEIGHBOR_DISTANCE = .1
41 try:
42 import numptyphysics
43 except ImportError, ioe:
44 print >>sys.stderr, """
45 This module can only be loaded from within NumptyPhysics.
46 """
47 sys.exit(1)
49 try:
50 import tuio
51 except ImportError, ioe:
52 print >>sys.stderr, """
53 You have to install PyTUIO and copy it into your $PYTHONPATH.
54 You can grab a tarball from: http://code.google.com/p/pytuio/
55 """
56 sys.exit(2)
58 CURSOR_NEIGHBOR_DISTANCE *= numptyphysics.HEIGHT
60 class NumpytTuioCursor(object):
61 def __init__(self, id, x, y):
62 self.id = id
63 self.x = x
64 self.y = y
65 self.time = time.time()
67 class NumptyCursor(object):
68 DRAW, DRAG, DELETE = range(3)
70 def __init__(self, tuio_id, x, y, on_activate, on_deactivate, on_move, on_upgrade):
71 self.id = None
72 self.x = x
73 self.y = y
74 self.mode = self.DRAW
75 self.tuio_ids = {tuio_id: time.time()}
77 self.activated = False
78 self.deactivated = False
79 self.moved = False
80 self.upgraded = False
82 self.on_activate = on_activate
83 self.on_deactivate = on_deactivate
84 self.on_move = on_move
85 self.on_upgrade = on_upgrade
87 self.activate_at = time.time()+CURSOR_ACTIVATE_DELAY
88 self.upgrade_at = time.time()+CURSOR_UPGRADE_DELAY
89 self.deactivate_at = 0
90 print 'new numpty cursor for', tuio_id
91 self.heartbeat()
93 def heartbeat(self):
94 self.deactivate_at = time.time()+CURSOR_STOP_DELAY
96 def is_near(self, x, y):
97 return ((x-self.x)**2 + (y-self.y)**2) < CURSOR_NEIGHBOR_DISTANCE**2
99 def move(self, x, y):
100 if self.activated and not self.deactivated:
101 print 'moved', self.id, 'to', x, '/', y, '(which is a', self.mode, ')'
102 if self.x != x or self.y != y:
103 self.x = x
104 self.y = y
105 self.moved = True
107 def want_new_cursors(self):
108 if self.mode == self.DRAW:
109 return len(self.tuio_ids) < 1
110 elif self.mode == self.DRAG:
111 return len(self.tuio_ids) < 2
112 elif self.mode == self.DELETE:
113 return len(self.tuio_ids) < 3
114 return False
116 def seen_tuio_id(self, id, x, y):
117 if self.deactivated:
118 # If this cursor is gone, it's gone
119 return False
121 if not self.upgraded and self.is_near(x, y) and id not in self.tuio_ids and not use_justdraw:
122 # this cursor it not yet upgraded; the tuio id is near
123 if self.mode == self.DRAW:
124 print 'absorbing and setting to drag', id
125 self.mode = self.DRAG
126 elif self.mode == self.DRAG:
127 print 'absorbing and setting to delete', id
128 self.mode = self.DELETE
129 else:
130 return False
132 self.tuio_ids[id] = time.time()
133 self.heartbeat()
134 return True
136 if self.upgraded and self.want_new_cursors() and self.is_near(x, y) and id not in self.tuio_ids:
137 # i can take more cursors, so absorb this
138 self.tuio_ids[id] = time.time()
139 if id == min(self.tuio_ids) and self.id:
140 self.move(x, y)
141 self.heartbeat()
142 return True
143 elif id in self.tuio_ids:
144 self.tuio_ids[id] = time.time()
145 if id == min(self.tuio_ids) and self.id:
146 self.move(x, y)
147 self.heartbeat()
148 return True
150 return False
152 def activate(self):
153 self.activated = True
154 self.on_activate(self)
156 def upgrade(self):
157 self.upgraded = True
158 self.on_upgrade(self)
159 if self.mode == self.DELETE:
160 self.deactivate()
162 def deactivate(self):
163 self.deactivated = True
164 if self.activated:
165 self.on_deactivate(self)
167 def movement(self):
168 self.on_move(self)
169 self.moved = False
171 def process(self):
172 if time.time() > self.deactivate_at and not self.deactivated:
173 self.deactivate()
174 elif time.time() > self.upgrade_at and not self.upgraded and not self.deactivated:
175 self.upgrade()
176 elif time.time() > self.activate_at and not self.activated and not self.upgraded and not self.deactivated:
177 self.activate()
178 elif self.activated and self.moved:
179 self.movement()
180 else:
181 self.expire()
183 def expire(self):
184 for id, t in self.tuio_ids.items():
185 if t+.5*CURSOR_STOP_DELAY < time.time():
186 del self.tuio_ids[id]
189 class CursorTracker(object):
190 def __init__(self):
191 self.cursors = []
192 self._freeslots = collections.deque(range(numptyphysics.MAX_CURSORS))
193 self._highest_id = -1
194 self.events = collections.deque()
196 def activate_cursor(self, cursor):
197 cursor.id = self.grabslot()
198 if cursor.mode == cursor.DRAW:
199 self.push_event(cursor, numptyphysics.START_STROKE)
200 elif cursor.mode == cursor.DRAG:
201 self.push_event(cursor, numptyphysics.START_DRAG)
202 elif cursor.mode == cursor.DELETE:
203 self.push_event(cursor, numptyphysics.DELETE)
205 def deactivate_cursor(self, cursor):
206 if cursor.mode == cursor.DRAW:
207 self.push_event(cursor, numptyphysics.FINISH_STROKE)
208 elif cursor.mode == cursor.DRAG:
209 self.push_event(cursor, numptyphysics.END_DRAG)
210 self.freeslot(cursor.id)
212 def cursor_movement(self, cursor):
213 if cursor.mode == cursor.DRAW:
214 self.push_event(cursor, numptyphysics.APPEND_STROKE)
215 elif cursor.mode == cursor.DRAG:
216 self.push_event(cursor, numptyphysics.DRAG)
218 def upgrade_cursor(self, cursor):
219 if cursor.mode == cursor.DRAW:
220 # "draw" cursors do not need to be upgraded
221 return
223 # cancel the in-progress draw event + start "real" event
224 self.push_event(cursor, numptyphysics.CANCEL_DRAW)
225 if cursor.mode == cursor.DRAG:
226 self.push_event(cursor, numptyphysics.START_DRAG)
227 elif cursor.mode == cursor.DELETE:
228 self.push_event(cursor, numptyphysics.DELETE)
230 def update(self, cursors):
231 new_cursors = []
232 for cursor in cursors:
233 id, x, y = cursor.sessionid, cursor.xpos, cursor.ypos
234 if x == 0 and y == 0:
235 continue
236 if use_ipod:
237 x, y = y, 1.-x
238 x, y = self.convert_coords(x, y)
239 if use_mtmini:
240 self.push_preview_event(id, x, y)
242 absorbed = False
243 for c in self.cursors:
244 if c.seen_tuio_id(id, x, y):
245 absorbed = True
246 break
248 if not absorbed and id > self._highest_id:
249 new_cursors.append((id, x, y))
251 if id > self._highest_id:
252 self._highest_id = id
254 for id, x, y in new_cursors:
255 self.cursors.append(NumptyCursor(id, x, y, self.activate_cursor, self.deactivate_cursor, self.cursor_movement, self.upgrade_cursor))
257 self.idle()
259 def idle(self):
260 for cursor in self.cursors:
261 cursor.process()
263 self.cursors = [cursor for cursor in self.cursors if not cursor.deactivated]
265 def grabslot(self):
266 try:
267 return self._freeslots.pop()
268 except IndexError:
269 return None
271 def freeslot(self, slot):
272 self._freeslots.appendleft(slot)
274 def push_event(self, cursor, type_):
275 if cursor.id:
276 print 'pushing event type %d for cursor %s' % (type_, repr(cursor.id))
277 self.events.appendleft(InputEvent(cursor.x, cursor.y, type_, cursor.id))
279 def push_preview_event(self, id, x, y):
280 self.events.appendleft(InputEvent(x, y, numptyphysics.PREVIEW_CURSOR, id))
282 def convert_coords(self, x, y):
283 return (int(x*numptyphysics.WIDTH), int(y*numptyphysics.HEIGHT))
285 def has_events(self):
286 return len(self.events) > 0
288 def get_events(self):
289 try:
290 while True:
291 yield self.events.pop()
292 except IndexError, ie:
293 raise StopIteration()
295 class InputEvent(object):
296 def __init__(self, x, y, event_type, cursor_id=0):
297 self.x = x
298 self.y = y
299 self.event_type = event_type
300 self.cursor_id = cursor_id
302 class C(threading.Thread):
303 def run(self):
304 pointlog = open('pointlog-'+str(int(time.time()))+'.np', 'w')
305 tracking = tuio.Tracking('')
306 tracker = CursorTracker()
307 oldcount = False
308 x, y = None, None
309 while True:
310 while tracking.update():
311 # read the socket empty
312 pass
314 tracker.update(tracking.cursors())
316 for event in tracker.get_events():
317 pointlog.write('/'.join((str(event.x), str(event.y), str(int(time.time())))))
318 pointlog.write('\n')
319 numptyphysics.post_event(event)
321 while not tracking.update() and not tracker.has_events():
322 # wait for something to happen
323 tracker.idle()
324 time.sleep(1./30.)
326 C().start()