1
2
3
4
5
6
7
8
9
10
11 try:
12 import sys, ode, random, re, thread, traceback, psyco, pygame
13 from pygame.locals import *
14 from socket import *
15 from utils import *
16 except ImportError, errormsg:
17 error('Unable to load one or more modules. %s' % errormsg)
18 raise SystemExit, 1
19
20
21 -class canvas(ode.World, pygame.sprite.RenderUpdates):
22 """This is a container class for holding the objects printed to the 'canvas' area of the application.
23 The position, physical properties (shapes), and orientation of each objects are held in this data
24 structure for easy management and manipulation around the canvas through user-driven actions and
25 the simulated actions of the virtual canvas environment.
26 Parameter:
27 - ode.World, supertype for extending containment of bodies and joints, and the environment physics.
28 - pygame.sprite.Group, supertype for extending containment of groups of objects in the environment."""
29 - def __new__(this, size=(640,480), color=Color('white'), image=None, song=None):
30 """Polymorphs the __new__ method in the base class ode.World so that other supertypes can be fully
31 initialized. Because ode.World is already a defined type (not a class), and therefore is
32 immutable, defining this function ensures that upon constructing objects of this subtype, the
33 __new__ method invokation will be done at the subtype level without conflicting the __init__
34 requirements for initializing the other derived classes in its multi-inherited hierarchy. This is
35 similar to the way in which a string, int, and other immutable types can be extended even though
36 they are not directly treated as classes. Also, importantly, the parameters passed to this method
37 must match exactly with the parameters passed to the __init__ method.
38 Parameter:
39 - size, the canvas' size.
40 - color, the canvas' background color.
41 - image, the canvas' background picture."""
42 return ode.World.__new__(this)
43
44
45 - def __init__(this, size=(640,480), color=Color('white'), image=None, song=None):
46 """Initialization procedure for defaulting the canvas' environment.
47 Parameter:
48 - size, the canvas' size.
49 - color, the canvas' background color.
50 - image, the canvas' background picture (passed as a string path to its location)."""
51 ode.World.__init__(this)
52 super(pygame.sprite.Group, this).__init__()
53 this.size = size
54 this.background = pygame.Surface(size)
55 this.set_bgcolor(color)
56 if image is not None: this.set_bgimage(image, size)
57 this.bgcopy = this.background.copy()
58 this.set_bgmusic(song)
59 this.space = ode.Space()
60 this.setERP(100)
61 this.setCFM(1E-5)
62 this.play()
63
65 """Sets the canvas' background color.
66 Parameter:
67 - color, the color to set the canvas' background to."""
68 this.background.fill(color)
69
71 """Sets the canvas' background image.
72 Parameter:
73 - image, a path to the picture to set the canvas' background to."""
74 this.background.blit(load_image(image, size), (0,0))
75
77 """Sets the background music.
78 Parameter:
79 - song, the path of the background music to be played."""
80 try:
81 load_song(song)
82 pygame.mixer.music.play(-1)
83 except:
84 pass
85
87 """Returns a 2-tuple value of the canvas' screen size, as a (width,height) pair."""
88 return this.size
89
91 """Tests whether an object is located at the current 'pos' location.
92 Parameter:
93 - pos, the (x,y) position where the object is located."""
94 point = this.sprite(pygame.Surface((1,1)), pos, this)
95 return pygame.sprite.spritecollideany(point, this) is not None
96
98 """Returns the sprite that is located at the 'pos' location. Normally used when user selects and
99 object and wants to drag it from one area to another.
100 Parameter:
101 - pos, the (x,y) position where the object is located."""
102 point = this.sprite(pygame.Surface((1,1)), pos, this)
103 return pygame.sprite.spritecollideany(point, this)
104
106 """Invoked whenever two objectas are about to collide, then creates collision points where
107 collision may occur.
108 Parameter:
109 - contactgroup, group for holding contact joints
110 - geom1, one of the geometry objects involved in the collision.
111 - geom2, one of the geometry objects involved in the collision."""
112 for contact in ode.collide(geom1, geom2):
113 contact.setBounce(const.BOUNCE)
114 contact.setMu(const.FRICTION)
115 contact.setSoftCFM(1E-5)
116 contact.setSoftERP(1)
117 joint = ode.ContactJoint(this, contactgroup, contact)
118 joint.attach(geom1.getBody(), geom2.getBody())
119
120 - def step(this, framerate):
121 """Increments the world timestep, and detects near collisions.
122 Parameter:
123 - framerate, the refresh rate for updating the application window."""
124 if this.gravity_on:
125 contactgroup = ode.JointGroup()
126 for i in range(const.CSIMULATE):
127 this.space.collide(contactgroup, this.near_collision)
128 ode.World.step(this, const.TIMESTEP/const.CSIMULATE)
129 contactgroup.empty()
130
132 """Pauses the simulation."""
133 this.setGravity((0,0,0))
134 this.gravity_on = False
135
137 """Plays the simulation"""
138 this.setGravity((0,const.GRAVITY,0))
139 this.gravity_on = True
140
141 - def add(this, image, topleft, vertices=None):
142 """Adds a new object to the canvas' environment. Once an object is added to the environment, it is
143 capable of interacting with other objects within the environment, and with user-driven inputs.
144 Parameter:
145 - object, the object to be registered to the canvas' environment. The object should contain
146 the basic configurations for representing its visual state as a surface; for the
147 area and position it occupies; and for the vertices it is composed from."""
148 super(pygame.sprite.RenderUpdates, this).add(this.sketch(image, topleft, vertices, this))
149
150 - def sketch(this, image, topleft, vertices, world):
151 """Creates a new object given it's image copy, vertices, position, and world.
152 Parameter:
153 - image, a copy of the object.
154 - topleft, position of the object.
155 - vertices, points composing the object.
156 - world, the canvas the object belongs to."""
157 body = this.sprite(image, topleft, this)
158 mass = ode.Mass()
159 mass.setBox(const.MDENSITY, const.MASS, const.MASS, const.MASS)
160 body.setMass(mass)
161 body.setPosition((topleft[0], topleft[1], 0))
162 geom = ode.GeomBox(this.space, (image.get_size()[0], image.get_size()[1], 1))
163 geom.setBody(body)
164 return body
165
166
167 - class sprite(ode.Body, pygame.sprite.Sprite):
168 """Abstract object represented in an area of the canvas. Objects can either be drawn manually by the
169 user, or be imported from an image file. The resulting objects are also represented as surfaces,
170 occupying a given region of the canvas.
171 Parameter:
172 - ode.Body, supertype for extending containment of physical properties of a body/object.
173 - pygame.sprite.Sprite, supertype for extending containment of sprites/objects in the environment."""
174 - def __new__(this, image, rect, world):
175 """Polymorphs the __new__ method in the base class ode.Body so that other supertypes can be fully
176 initialized. Because ode.Body is already a defined type (not a class), and therefore is
177 immutable, defining this function ensures that upon constructing objects of this subtype, the
178 __new__ method invokation will be done at the subtype level without conflicting the __init__
179 requirements for initializing the other derived classes in its multi-inherited hierarchy. This
180 is similar to the way in which a string, int, and other immutable types can be extended even
181 though they are not directly treated as classes. Also, importantly, the parameters passed to
182 this method must match exactly with the parameters passed to the __init__ method.
183 Parameter:
184 - _object, the object to be represented in a canvas world; this contains the initial
185 configuration of an object belonging to a canvas' environment.
186 - world, the canvas the object belongs to (membership constraint)."""
187 return ode.Body.__new__(this, world)
188
189
190 - def __init__(this, image, topleft, world):
191 """Initialization procedure for defaulting the object's general configuration.
192 Parameter:
193 - _object, the object to be represented in a canvas world; this contains the configuration
194 for object's initial state in the world.
195 - world, the canvas the object belongs to (membership constraint)."""
196 ode.Body.__init__(this, world)
197 pygame.sprite.Sprite.__init__(this)
198 this.mass = None
199 this.image = image
200 this.rect = image.get_rect();
201 if topleft is not None: this.rect.topleft = topleft
202
204 """Updates the object's current position.
205 Parameter:
206 - x, the x-position of the object.
207 - y, the y-position of the object."""
208 this.rect.move_ip(x, y)
209 this.setPosition((this.rect.left+x, this.rect.top+y, 0))
210
211
213 """This is the container class for holding and loading the tools, and general GUI layers, of the application.
214 Some of the tools that may be included are functions for adjusting the input pen style and size, the canvas
215 repository, etc., along with their associated GUI icons and properties."""
216
217 - def __init__(this, canvas=None, image=None, rect=(0,0,128,256), alpha=255):
218 """Initializes the workspace.
219 Parameter:
220 - canvas, the canvas the workspace belongs to.
221 - the background image of the workspace.
222 - the position and demensions (x,y,width,height) of the workspace.
223 - alpha, applies transparency to the workspace background."""
224 pygame.sprite.Sprite.__init__(this)
225 this.canvas = canvas
226 if image is None:
227 this.image = pygame.Surface((rect[2], rect[3]))
228 this.image.fill(Color('white'))
229 else:
230 this.image = load_image(image, (rect[2], rect[3]))
231 this.image.set_alpha(alpha)
232 this.rect = rect
233
235 """Shows the workspace on top of canvas."""
236 if this.canvas is not None:
237 this.canvas.background.blit(this.image, (0,0))
238 else:
239 pass
240
241
243 """This is the class for controlling the network communication between multiple online canvases inbounded on
244 the current machine, or outbounded to multiple remote machines. The primary goal of this class is to
245 establish and maintain multiple connections by spawning a single server process thread/daemon which waits
246 for incoming/inbounding connections and acknowledging them upon the user's grant; and to make
247 outgoing/outbounding requests to a remote system based on the user's request. The application may be in
248 several states at a time: it may act as a server without having a session with another machine; it may
249 may be a server without an inbound connection, but an established outbounded connection with one or more
250 remote machines; it may be a server with inbounded connections, but no outbounded connections; or it may
251 be both connected inboundly and outboundly."""
252
254 """Initializes the network, by creating a new server daemon process."""
255 this.ip = gethostbyaddr(gethostname())[-1][0]
256 this.clients = []
257 this.s = socket(AF_INET, SOCK_STREAM)
258 this.server(const.PORT)
259
260 - def client(this, host, port=const.PORT):
261 """Connects to a remote host machine, given it's ip and, optionally, it's port number."""
262 try:
263 this.s.connect((host, port))
264 except:
265
266 error('Unable to connect to \'%s\'' % host)
267
269 """Starts-up the server on the current machine, by binding a sever daeom process to the a ip address, and
270 port number.
271 Parameter:
272 - port, the port number to bound the server process."""
273 try:
274
275 this.s.bind((this.ip, port))
276 this.s.listen(5)
277 thread.start_new(this.serverthrd, ())
278 except:
279
280 error('Unable to start server')
281
283 """Server thread used to act independently of the current user processes. Each thread is dedicated to a single
284 connecting remote client machine - the events passed from that machine are pushed to the current hosting
285 machine."""
286 c = this.s.accept()
287 this.clients.append(c)
288 thread.start_new(this.serverthrd, ())
289 this.receive(c)
290
291 - def send(this, event):
292 """Sends an event to a remote machine. The respective machines will then push this event to their event stack to
293 to be handled has if the current sending machine is directly interacting with the remote machine.
294 Parameter:
295 - event, the event object to be transmitted to a remote client."""
296 for c in this.clients:
297 c[0].send(event + '\n')
298
300 """A server thread immediately jumps into receving mode, where in the lifetime of its connection with the remote
301 client machine, it receives and queues each event that is transmitted.
302 Parameter:
303 - c, the (socket, address) pair of the client machine to receive events from."""
304 data = []
305 while True:
306 try:
307 data.append(c[0].recv(1))
308 if(data[-1] == '\n'):
309 event_regexp = re.match("^<Event\((\d*).*({\'ip\':.*: )<Event\((\d*).*({.*})\)\>(}).*$", str(data))
310 event = pygame.event.Event(int(y.group(1)), eval(y.group(2) + "\'\'" + y.group(5)))
311 sub_event = str(pygame.event.Event(int(y.group(3)), eval(y.group(4))))
312 event.update({'val': sub_event})
313 pygame.event.post(event)
314 data = []
315 except:
316
317 c.close()
318 this.clients.remove(c)
319 break
320
322 """Updates the application window with the changes made in the canvas world."""
323 window.blit(sketchpad.background, (0,0))
324
325 pygame.display.update()
326
328 """Handles command-line line requests."""
329 while True:
330 try:
331 cmd = re.match("^\s*(\w*)\s*(.*)$", raw_input(">> "))
332 if cmd.group(1) == "connect":
333 ip = re.match("^(\d+\.\d+\.\d+\.\d+)\s*(:\s*(\d+))?\s*$", cmd.group(2))
334 if ip == None:
335 ip = re.match("^(\d+\.\d+\.\d+\.\d+)\s*(:\s*(\d+))?\s*$", raw_input("host ip: "))
336 while ip == None:
337 print 'Error: Invalid host IPv4 ip address format.'
338 ip = re.match("^(\d+\.\d+\.\d+\.\d+)\s*(:\s*(\d+))?\s*$", raw_input("host ip: "))
339 if ip.groups()[2] != None:
340 _network.client(ip.group(1), ip.group(3))
341 else:
342 _network.client(ip.group(1))
343 elif cmd.group(1) == "load":
344 path = re.match("^\s*(\S*)\s*$", cmd.group(2))
345 if path.group(1) == '':
346 path = re.match("^\s*(\S*)\s*$", raw_input("image path: "))
347 while path == None:
348 print 'Error: image path must be specified.'
349 path = re.match("^\s*(\S*)\s*$", raw_input("image path: "))
350 path = load_image(path.group(1))
351 sketchpad.add(path, (0,0))
352 elif cmd.group(1) == "shape":
353 shape = cmd.group(2).strip()
354 if shape in const.SHAPES:
355 shapetype = shape
356 else:
357 try:
358 load_image(shape)
359 shapetype = shape
360 except:
361 pass
362 setshape(shapetype)
363 elif cmd.group(1) == "play":
364 sketchpad.play()
365 print 'gravity on'
366 elif cmd.group(1) == "pause":
367 sketchpad.pause()
368 print 'gravity off'
369 elif cmd.group(1) in ("quit", "exit"):
370 pygame.event.post(pygame.event.Event(pygame.QUIT, []))
371 else:
372 print 'Invalid command \'%s\'' % cmd.group(1)
373 except:
374 pass
375
376
378 """Returns a shape given a set of vertices. Currently, shapes are predefined, where only circles, boxes, and
379 custom user images can be returned.
380 Parameter:
381 - vertices, the vertices to use to construct the new shape."""
382 color = const.POINT_RCOLOR[random.randint(0, len(const.POINT_RCOLOR)-1)]
383 rect = pygame.draw.lines(sketchpad.background.copy(), const.POINT_COLOR, False, vertices, const.POINT_SIZE)
384 screen = pygame.Surface((rect.width+1, rect.height+1))
385 screen.blit(sketchpad.bgcopy, (0,0), rect)
386 if shapetype in const.SHAPES:
387 if shapetype == 'box':
388 pygame.draw.rect(screen, Color(color[0]), ((0,0), rect.size), 0)
389 pygame.draw.rect(screen, Color(color[1]), ((0,0), rect.size), const.POINT_SIZE)
390 elif shapetype == 'circle':
391 pygame.draw.ellipse(screen, Color(color[0]), ((0,0), rect.size), 0)
392 pygame.draw.ellipse(screen, Color(color[1]), ((0,0), rect.size), const.POINT_SIZE)
393 else:
394 load_image(shapetype, rect.size)
395 return (rect, screen)
396
397
399 """The application's main event loop for controlling general user and application-driven events.
400 The events that are handled are as follow:
401 - Closing application window:
402 - by clicking the application window's close button
403 - by pressing ALT + F4
404 - Resizing application window:
405 - by clicking the application window's maximize and normalize buttons
406 - by pressing ALT + ENTER
407 - by pressing ESC (to revert from fullscreen mode to normal mode)
408 - by pressing F (to switch between fullscreen and normal modes)
409 - Registering canvas inputs:
410 - by clicking the left mouse-click button, and then moving the mouse at least two pixel points over
411 an area of the canvas, and then finally releasing the left mouse-click button. The registered inputs
412 are stored as a 2-tuple list of (x,y) vertices for each pixel points/path tranversed during the left
413 mouse-click hold and drag events.
414 Parameter:
415 - framerate, sets the frame rate at which the application window refreshes, and at which, for each refresh
416 interval, all updates are carried out. Also, the canvas environment's time step increases by a fraction
417 of 1/framerate. The higher the frame rate, the faster the animation of objects in the canvas environment."""
418 clock = pygame.time.Clock()
419 getinput = {_network.ip: {'record': False, 'drag': False, 'id': None, 'screen': None, 'vertices': []}}
420 done = False
421
422 while not done:
423 for event in pygame.event.get():
424 if event.type == USEREVENT:
425 getinput.update({ip: {'record': False, 'drag': False, 'id': None, 'screen': None, 'vertices': []}})
426 ip = event.ip
427 event = event.val
428 else:
429 _network.send(str(pygame.event.Event(pygame.USEREVENT, ip=_network.ip, val=event)))
430 ip = _network.ip
431
432 if event.type == KEYDOWN:
433
434 if event.key == K_f or ((pygame.key.get_mods() & KMOD_ALT) and pygame.key.get_pressed()[pygame.K_RETURN]):
435 if(window.get_flags() & FULLSCREEN):
436 pygame.display.set_mode(const.WINDOW_SIZE, RESIZABLE)
437 else:
438 pygame.display.set_mode(sketchpad.size, FULLSCREEN)
439 elif event.key == K_ESCAPE and window.get_flags() & FULLSCREEN:
440 pygame.display.set_mode(const.WINDOW_SIZE, RESIZABLE)
441 elif (pygame.key.get_mods() & KMOD_ALT) and pygame.key.get_pressed()[pygame.K_F4]:
442 done = True
443 break
444 elif event.type == QUIT:
445 done = True
446 break
447 elif event.type == VIDEOEXPOSE:
448 pass
449 elif event.type == VIDEORESIZE:
450 pygame.display.set_mode(event.size, RESIZABLE)
451 elif event.type == MOUSEBUTTONDOWN:
452 if event.button == 1:
453 if sketchpad.is_sprite(event.pos):
454 getinput[ip].update({'drag': True, 'id': sketchpad.get_sprite(event.pos), 'vertices': [event.pos]})
455 getinput[ip]['id'].disable()
456 else:
457
458
459 getinput[ip].update({'record': True, 'vertices': [event.pos]})
460 elif event.button == 3:
461 if sketchpad.is_sprite(event.pos):
462 sketchpad.get_sprite(event.pos).kill()
463 sketchpad.clear(sketchpad.background, sketchpad.bgcopy)
464 sketchpad.draw(sketchpad.background)
465 elif event.type == MOUSEBUTTONUP:
466
467 if getinput[ip]['record'] is True:
468
469
470 try:
471 rect, getinput[ip]['screen'] = getshape(getinput[ip]['vertices'])
472 sketchpad.background.blit(sketchpad.bgcopy, (0,0))
473 sketchpad.add(getinput[ip]['screen'], rect.topleft, getinput[ip]['vertices'])
474 sketchpad.draw(sketchpad.background)
475 getinput[ip]['screen'] = None
476 except Exception, err:
477
478 pass
479 getinput[ip]['record'] = False
480 elif getinput[ip]['drag'] is True:
481 getinput[ip]['id'].setLinearVel((0,0,0))
482 getinput[ip]['id'].enable()
483 getinput[ip]['drag'] = False
484 elif event.type == MOUSEMOTION:
485
486
487 if getinput[ip]['record'] is True:
488
489
490
491 getinput[ip]['vertices'].append(event.pos)
492 pygame.draw.lines(sketchpad.background, const.POINT_COLOR, True, [getinput[ip]['vertices'][-2], getinput[ip]['vertices'][-1]], const.POINT_SIZE)
493
494 if len(getinput[ip]['vertices']) > 2:
495 getinput[ip]['vertices'][-3:] = optimize(getinput[ip]['vertices'][-3:])
496 if getinput[ip]['drag'] is True:
497 try:
498 getinput[ip]['vertices'].append(event.pos)
499 x,y = getinput[ip]['vertices'][-1][0] - getinput[ip]['vertices'][-2][0], getinput[ip]['vertices'][-1][1] - getinput[ip]['vertices'][-2][1]
500 sketchpad.clear(sketchpad.background, sketchpad.bgcopy)
501 getinput[ip]['id'].update(x,y)
502 sketchpad.draw(sketchpad.background)
503 except:
504
505 pass
506
507 for body in sketchpad:
508 if getinput[ip]['drag'] == True and getinput[ip]['id'] == body:
509 pass
510 else:
511 if sketchpad.background.get_rect().colliderect(body.rect):
512 sketchpad.clear(sketchpad.background, sketchpad.bgcopy)
513 body.rect.left, body.rect.top = body.getPosition()[:2]
514 sketchpad.draw(sketchpad.background)
515 else:
516 body.kill()
517
518 update()
519 sketchpad.step(framerate)
520 clock.tick(framerate)
521
522
524 """This is the main application method for initializing the application window upon program startup, and
525 for configuring a sketchpad/canvas for use within the window."""
526 psyco.full()
527 pygame.init()
528 pygame.display.set_caption(const.WINDOW_TITLE)
529
530 global window, sketchpad, _workspace, _network, shapetype
531 window = pygame.display.set_mode(const.WINDOW_SIZE, RESIZABLE)
532 sketchpad = canvas(max(pygame.display.list_modes()), const.CANVAS_COLOR, const.CANVAS_BGIMG, const.CANVAS_BGSONG)
533
534
535
536
537 _network = network()
538
539 shapetype = 'box'
540
541 thread.start_new(cmdline, ())
542
543 event_loop(const.FRAMERATE)
544 pygame.quit()
545
546
547
548 if __name__ == '__main__':
549 main()
550