From 445dc0cc5770f9aa62264efb37c35d5964e41c0f Mon Sep 17 00:00:00 2001 From: Walter Bender Date: Wed, 11 Dec 2013 18:24:21 +0000 Subject: unpacking elements so as to be able to remove cjson dependency --- diff --git a/.gitignore b/.gitignore index b6cb09a..37c737b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ *.pyc +*~ locale -.DS_Store dist diff --git a/DEVELOPING b/DEVELOPING deleted file mode 100644 index 3369c8a..0000000 --- a/DEVELOPING +++ /dev/null @@ -1,9 +0,0 @@ -activity/ -activity.py -elements/ - (upstream, but branched here) Simplification wrapper around pyBox2D (in a subdirectory here) -helpers.py - mathematical helper functions -icons/ - all graphics used in Physics (mostly svg menu icons) -olpcgames/ - (upstream) The Pygame wrapper for the OLPC Sugar platform -physics.py - contains screen setup, main loop, tool list -setup.py - just runs the Sugar bundlebuilder -tools.py - defines Tool class and all available tools (contexts for input/creation) diff --git a/MANIFEST b/MANIFEST deleted file mode 100644 index fdc2528..0000000 --- a/MANIFEST +++ /dev/null @@ -1,127 +0,0 @@ -activity.py -COPYING -DEVELOPING -helpers.py -physics.py -setup.py -standardcursor.png -tools.py -activity/activity-physics.svg -activity/activity.info -activity/application-x-physics-project.svg -activity/mimetypes.xml -icons/box.svg -icons/circle.svg -icons/destroy.svg -icons/grab.svg -icons/joint.svg -icons/magicpen.svg -icons/motor.svg -icons/pin.svg -icons/polygon.svg -icons/roll.svg -icons/triangle.svg -lib/Box2D-2.0.2b1-py2.5-linux-i686.egg -lib/Elements-0.13-py2.5.egg -lib/pkg_resources.py -olpcgames/__init__.py -olpcgames/_cairoimage.py -olpcgames/_gtkmain.py -olpcgames/_version.py -olpcgames/activity.py -olpcgames/buildmanifest.py -olpcgames/camera.py -olpcgames/canvas.py -olpcgames/copying -olpcgames/dbusproxy.py -olpcgames/eventwrap.py -olpcgames/gtkEvent.py -olpcgames/mesh.py -olpcgames/pangofont.py -olpcgames/pausescreen.py -olpcgames/svg.py -olpcgames/svgsprite.py -olpcgames/textsprite.py -olpcgames/util.py -olpcgames/video.py -olpcgames/data/__init__.py -olpcgames/data/sleeping.svg -olpcgames/data/sleeping_svg.py -po/af.po -po/am.po -po/ar.po -po/ay.po -po/bg.po -po/bi.po -po/bn.po -po/bn_IN.po -po/ca.po -po/cpp.po -po/cs.po -po/de.po -po/dz.po -po/el.po -po/en.po -po/es.po -po/fa.po -po/fa_AF.po -po/ff.po -po/fil.po -po/fr.po -po/gu.po -po/ha.po -po/he.po -po/hi.po -po/ht.po -po/hu.po -po/ig.po -po/is.po -po/it.po -po/ja.po -po/km.po -po/ko.po -po/kos.po -po/mg.po -po/mk.po -po/ml.po -po/mn.po -po/mr.po -po/ms.po -po/mvo.po -po/na.po -po/nb.po -po/ne.po -po/nl.po -po/pa.po -po/pap.po -po/Physics.pot -po/pis.po -po/pl.po -po/ps.po -po/pt.po -po/pt_BR.po -po/qu.po -po/ro.po -po/ru.po -po/rw.po -po/sd.po -po/si.po -po/sk.po -po/sl.po -po/sq.po -po/sv.po -po/sw.po -po/ta.po -po/te.po -po/th.po -po/tpi.po -po/tr.po -po/tvl.po -po/tzo.po -po/ug.po -po/ur.po -po/vi.po -po/wa.po -po/yo.po -po/zh_CN.po -po/zh_TW.po diff --git a/elements/__init__.py b/elements/__init__.py new file mode 100644 index 0000000..723c2cc --- /dev/null +++ b/elements/__init__.py @@ -0,0 +1,2 @@ +__all__ = ['locals', 'menu'] +from elements import Elements diff --git a/elements/add_objects.py b/elements/add_objects.py new file mode 100644 index 0000000..49bf538 --- /dev/null +++ b/elements/add_objects.py @@ -0,0 +1,538 @@ +""" +This file is part of the 'Elements' Project +Elements is a 2D Physics API for Python (supporting Box2D2) + +Copyright (C) 2008, The Elements Team, + +Home: http://elements.linuxuser.at +IRC: #elements on irc.freenode.org + +Code: http://www.assembla.com/wiki/show/elements + svn co http://svn2.assembla.com/svn/elements + +License: GPLv3 | See LICENSE for the full text +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" +from locals import * +from elements import box2d + +# Imports +from math import pi +from math import sqrt +from math import asin + +import tools_poly + +class Add: + element_count = 0 + + def __init__(self, parent): + self.parent = parent + + def ground(self): + """ Add a static ground to the scene + + Return: box2d.b2Body + """ + return self._rect((-10.0, 0.0), 50.0, 0.1, dynamic=False) + + def triangle(self, pos, sidelength, dynamic=True, density=1.0, restitution=0.16, friction=0.5, screenCoord=True): + """ Add a triangle | pos & a in the current input unit system (meters or pixels) + + Parameters: + pos .... position (x,y) + sidelength ...... sidelength + other .. see [physics parameters] + + Return: box2d.b2Body + """ + vertices = [(-sidelength, 0.0), (sidelength, 0.0), (0.0, 2*sidelength)] + return self.poly(pos, vertices, dynamic, density, restitution, friction, screenCoord) + + def ball(self, pos, radius, dynamic=True, density=1.0, restitution=0.16, friction=0.5, screenCoord=True): + """ Add a dynamic ball at pos after correcting the positions and legths to the internal + meter system if neccessary (if INPUT_PIXELS), then call self._add_ball(...) + + Parameters: + pos ..... position (x,y) + radius .. circle radius + other ... see [physics parameters] + + Return: box2d.b2Body + """ + # Bring coordinates into the world coordinate system (flip, camera offset, ...) + if screenCoord: x, y = self.parent.to_world(pos) + else: x, y = pos + + + if self.parent.input == INPUT_PIXELS: + x /= self.parent.ppm + y /= self.parent.ppm + radius /= self.parent.ppm + + return self._ball((x,y), radius, dynamic, density, restitution, friction) + + def _ball(self, pos, radius, dynamic=True, density=1.0, restitution=0.16, friction=0.5): + # Add a ball without correcting any settings + # meaning, pos and vertices are in meters + # Define the body + x, y = pos + bodyDef = box2d.b2BodyDef() + bodyDef.position=(x, y) + + userData = { 'color' : self.parent.get_color() } + bodyDef.userData = userData + + # Create the Body + if not dynamic: + density = 0 + + body = self.parent.world.CreateBody(bodyDef) + + self.parent.element_count += 1 + + # Add a shape to the Body + circleDef = box2d.b2CircleDef() + circleDef.density = density + circleDef.radius = radius + circleDef.restitution = restitution + circleDef.friction = friction + + body.CreateShape(circleDef) + body.SetMassFromShapes() + + return body + + def rect(self, pos, width, height, angle=0, dynamic=True, density=1.0, restitution=0.16, friction=0.5, screenCoord=True): + """ Add a dynamic rectangle with input unit according to self.input (INPUT_PIXELS or INPUT_METERS) + Correcting the positions to meters and calling self._add_rect() + + Parameters: + pos ..... position (x,y) + width ....... horizontal line + height ....... vertical line + angle ........ in degrees (0 .. 360) + other ... see [physics parameters] + + Return: box2d.b2Body + """ + # Bring coordinates into the world coordinate system (flip, camera offset, ...) + if screenCoord: x, y = self.parent.to_world(pos) + else: x, y = pos + + # If required, translate pixel -> meters + if self.parent.input == INPUT_PIXELS: + x /= self.parent.ppm + y /= self.parent.ppm + width /= self.parent.ppm + height /= self.parent.ppm + + # grad -> radians + angle = (angle * pi) / 180 + + return self._rect((x,y), width, height, angle, dynamic, density, restitution, friction) + + + def wall(self, pos1, pos2, width=5, density=1.0, restitution=0.16, friction=0.5, screenCoord=True): + """ Add a static rectangle between two arbitrary points with input unit according to self.input + (INPUT_PIXELS or INPUT_METERS) Correcting the positions to meters and calling self._add_rect() + + Return: box2d.b2Body + """ + if width < 5: width = 5 + + if (pos1[0] < pos2[0]): + x1, y1 = pos1 + x2, y2 = pos2 + else: + x1, y1 = pos2 + x2, y2 = pos1 + + # Bring coordinates into the world coordinate system (flip, camera offset, ...) + if screenCoord: + x1, y1 = self.parent.to_world((x1, y1)) + x2, y2 = self.parent.to_world((x2, y2)) + + # If required, translate pixel -> meters + if self.parent.input == INPUT_PIXELS: + x1 /= self.parent.ppm + y1 /= self.parent.ppm + x2 /= self.parent.ppm + y2 /= self.parent.ppm + width /= self.parent.ppm + + length = sqrt( (x1-x2)*(x1-x2) + (y1-y2)*(y1-y2) )*0.5 + + if width > 0: + halfX = x1 + (x2-x1)*0.5 + halfY = y1 + (y2-y1)*0.5 + + angle = asin( (y2-halfY)/length ) + return self._rect((halfX, halfY), length, width, angle, False, density, restitution, friction) + + def _rect(self, pos, width, height, angle=0, dynamic=True, density=1.0, restitution=0.16, friction=0.5): + # Add a rect without correcting any settings + # meaning, pos and vertices are in meters + # angle is now in radians ((degrees * pi) / 180)) + x, y = pos + bodyDef = box2d.b2BodyDef() + bodyDef.position=(x, y) + + userData = { 'color' : self.parent.get_color() } + bodyDef.userData = userData + + # Create the Body + if not dynamic: + density = 0 + + body = self.parent.world.CreateBody(bodyDef) + + self.parent.element_count += 1 + + # Add a shape to the Body + boxDef = box2d.b2PolygonDef() + + boxDef.SetAsBox(width, height, (0,0), angle) + boxDef.density = density + boxDef.restitution = restitution + boxDef.friction = friction + body.CreateShape(boxDef) + + body.SetMassFromShapes() + + return body + + def poly(self, pos, vertices, dynamic=True, density=1.0, restitution=0.16, friction=0.5, screenCoord=True): + """ Add a dynamic polygon, which has the vertices arranged around the poly's center at pos + Correcting the positions to meters if INPUT_PIXELS, and calling self._add_poly() + + Parameters: + pos ....... position (x,y) + vertices .. vertices arranged around the center + other ... see [physics parameters] + + Return: box2d.b2Body + """ + # Bring coordinates into the world coordinate system (flip, camera offset, ...) + if screenCoord: x, y = self.parent.to_world(pos) + else: x, y = pos + + # If required, translate pixel -> meters + if self.parent.input == INPUT_PIXELS: + # translate pixel -> meters + x /= self.parent.ppm + y /= self.parent.ppm + + # Translate vertices from pixels to meters + v_new = [] + for v in vertices: + vx, vy = v + v_new.append((vx/self.parent.ppm, vy/self.parent.ppm)) + vertices = v_new + + return self._poly((x,y), vertices, dynamic, density, restitution, friction) + + def _poly(self, pos, vertices, dynamic=True, density=1.0, restitution=0.16, friction=0.5): + # add a centered poly at pos without correcting any settings + # meaning, pos and vertices are in meters + x, y = pos + bodyDef = box2d.b2BodyDef() + bodyDef.position=(x, y) + + userData = { 'color' : self.parent.get_color() } + bodyDef.userData = userData + + # Create the Body + if not dynamic: + density = 0 + + body = self.parent.world.CreateBody(bodyDef) + + self.parent.element_count += 1 + + # Add a shape to the Body + polyDef = box2d.b2PolygonDef() + + polyDef.setVertices(vertices) + polyDef.density = density + polyDef.restitution = restitution + polyDef.friction = friction + + body.CreateShape(polyDef) + body.SetMassFromShapes() + + return body + + def concavePoly(self, vertices, dynamic=True, density=1.0, restitution=0.16, friction=0.5, screenCoord=True): + # 1. Step: Reduce + # Detect if the polygon is closed or open + if vertices[0] != vertices[-1]: + is_closed = False + else: + is_closed = True + + # Continue reducing the vertecs + x, y = c = tools_poly.calc_center(vertices) + vertices = tools_poly.poly_center_vertices(vertices) + + # Bring coordinates into the world coordinate system (flip, camera offset, ...) + if screenCoord: x, y = self.parent.to_world(c) + else: x, y = c + + # If required, translate pixel -> meters + if self.parent.input == INPUT_PIXELS: + # translate pixel -> meters + x /= self.parent.ppm + y /= self.parent.ppm + + # Let's add the body + bodyDef = box2d.b2BodyDef() + bodyDef.position=(x, y) + + userData = { 'color' : self.parent.get_color() } + bodyDef.userData = userData + + # Create the Body + if not dynamic: + density = 0 + + body = self.parent.world.CreateBody(bodyDef) + + self.parent.element_count += 1 + + # Create the reusable Box2D polygon and circle definitions + polyDef = box2d.b2PolygonDef() + polyDef.vertexCount = 4 # rectangle + polyDef.density = density + polyDef.restitution = restitution + polyDef.friction = friction + + circleDef = box2d.b2CircleDef() + circleDef.density = density + circleDef.radius = 0.086 + circleDef.restitution = restitution + circleDef.friction = friction + + # Set the scale factor + factor = 8.0 + + v2 = box2d.b2Vec2(*vertices[0]) + for v in vertices[1:]: + v1 = v2.copy() + v2 = box2d.b2Vec2(*v) + + vdir = v2-v1 # (v2x-v1x, v2y-v1y) + vdir.Normalize() + + # we need a little size for the end part + vn = box2d.b2Vec2(-vdir.y*factor, vdir.x*factor) + + v = [ v1+vn, v1-vn, v2-vn, v2+vn ] + + # Create a line (rect) for each part of the polygon, + # and attach it to the body + polyDef.setVertices( [vi / self.parent.ppm for vi in v] ) + + try: + polyDef.checkValues() + except ValueError: + print "concavePoly: Created an invalid polygon!" + return None + + body.CreateShape(polyDef) + + # Now add a circle to the points between the rects + # to avoid sharp edges and gaps + if not is_closed and v2.tuple() == vertices[-1]: + # Don't add a circle at the end + break + + circleDef.localPosition = v2 / self.parent.ppm + body.CreateShape(circleDef) + + # Now, all shapes have been attached + body.SetMassFromShapes() + + # Return hard and soft reduced vertices + return body + + def complexPoly(self, vertices, dynamic=True, density=1.0, restitution=0.16, friction=0.5): + # 1. Step: Reduce + # 2. Step: See if start and end are close, if so then close the polygon + # 3. Step: Detect if convex or concave + # 4. Step: Start self.convexPoly or self.concavePoly + vertices, is_convex = tools_poly.reduce_poly_by_angle(vertices) + #print "->", is_convex + + # If start and endpoints are close to each other, close polygon + x1, y1 = vertices[0] + x2, y2 = vertices[-1] + dx = x2 - x1 + dy = y2 - y1 + l = sqrt((dx*dx)+(dy*dy)) + + if l < 50: + vertices[-1] = vertices[0] + else: + # Never convex if open (we decide so :) + is_convex = False + + if tools_poly.is_line(vertices): + # Lines shall be drawn by self.concavePoly(...) + print "is line" + is_convex = False + + if is_convex: + print "convex" + return self.convexPoly(vertices, dynamic, density, restitution, friction), vertices + else: + print "concave" + return self.concavePoly(vertices, dynamic, density, restitution, friction), vertices + + + def convexPoly(self, vertices, dynamic=True, density=1.0, restitution=0.16, friction=0.5): + """ Add a complex polygon with vertices in absolute positions (meters or pixels, according + to INPUT_PIXELS or INPUT_METERS). This function does the reduction and convec hulling + of the poly, and calls add_poly(...) + + Parameters: + vertices .. absolute vertices positions + other ..... see [physics parameters] + + Return: box2d.b2Body + """ + # NOTE: Box2D has a maximum poly vertex count, defined in Common/box2d.b2Settings.h (box2d.b2_maxPolygonVertices) + # We need to make sure, that we reach that by reducing the poly with increased tolerance + # Reduce Polygon + tolerance = 10 #5 + v_new = vertices + while len(v_new) > box2d.b2_maxPolygonVertices: + tolerance += 1 + v_new = tools_poly.reduce_poly(vertices, tolerance) + + print "convexPoly: Polygon reduced from %i to %i vertices | tolerance: %i" % (len(vertices), len(v_new), tolerance) + vertices = v_new + + # So poly should be alright now + # Continue reducing the vertecs + vertices_orig_reduced = vertices + vertices = tools_poly.poly_center_vertices(vertices) + + vertices = tools_poly.convex_hull(vertices) + + if len(vertices) < 3: + return + + # Define the body + x, y = c = tools_poly.calc_center(vertices_orig_reduced) + return self.poly((x,y), vertices, dynamic, density, restitution, friction) + + def to_b2vec(self, pt): + # Convert vector to a b2vect + pt = self.parent.to_world(pt) + ptx, pty = pt + ptx /= self.parent.ppm + pty /= self.parent.ppm + pt = box2d.b2Vec2(ptx, pty) + return pt + + def joint(self, *args): + print "* Add Joint:", args + + if len(args) == 5: + # Tracking Joint + b1, b2, p1, p2, flag = args + + p1 = self.to_b2vec(p1) + p2 = self.to_b2vec(p2) + + jointDef = box2d.b2DistanceJointDef() + jointDef.Initialize(b1, b2, p1, p2) + jointDef.collideConnected = flag + + self.parent.world.CreateJoint(jointDef) + + elif len(args) == 4: + # Distance Joint + b1, b2, p1, p2 = args + + p1 = self.to_b2vec(p1) + p2 = self.to_b2vec(p2) + + jointDef = box2d.b2DistanceJointDef() + jointDef.Initialize(b1, b2, p1, p2) + jointDef.collideConnected = True + + self.parent.world.CreateJoint(jointDef) + + elif len(args) == 3: + # Revolute Joint between two bodies (unimplemented) + pass + + elif len(args) == 2: + # Revolute Joint to the Background, at point + b1 = self.parent.world.GetGroundBody() + b2 = args[0] + p1 = self.to_b2vec(args[1]) + + jointDef = box2d.b2RevoluteJointDef() + jointDef.Initialize(b1, b2, p1) + self.parent.world.CreateJoint(jointDef) + + elif len(args) == 1: + # Revolute Joint to the Background, body center + b1 = self.parent.world.GetGroundBody() + b2 = args[0] + p1 = b2.GetWorldCenter() + + jointDef = box2d.b2RevoluteJointDef() + jointDef.Initialize(b1, b2, p1) + + self.parent.world.CreateJoint(jointDef) + + def motor(self, body, pt, torque=900, speed=-10): + # Revolute joint to the background with motor torque applied + b1 = self.parent.world.GetGroundBody() + pt = self.to_b2vec(pt) + + jointDef = box2d.b2RevoluteJointDef() + jointDef.Initialize(b1, body, pt) + jointDef.maxMotorTorque = torque + jointDef.motorSpeed = speed + jointDef.enableMotor = True + + self.parent.world.CreateJoint(jointDef) + + def mouseJoint(self, body, pos, jointForce=100.0): + pos = self.parent.to_world(pos) + x, y = pos + x /= self.parent.ppm + y /= self.parent.ppm + + mj = box2d.b2MouseJointDef() + mj.body1 = self.parent.world.GetGroundBody() + mj.body2 = body + mj.target = (x, y) + mj.maxForce = jointForce * body.GetMass() + if 'getAsType' in dir(box2d.b2Joint): + self.parent.mouseJoint = self.parent.world.CreateJoint(mj).getAsType() + else: + self.parent.mouseJoint = self.parent.world.CreateJoint(mj) + body.WakeUp() + + def remove_mouseJoint(self): + if self.parent.mouseJoint: + self.parent.world.DestroyJoint(self.parent.mouseJoint) + self.parent.mouseJoint = None + diff --git a/elements/callbacks.py b/elements/callbacks.py new file mode 100644 index 0000000..01e9545 --- /dev/null +++ b/elements/callbacks.py @@ -0,0 +1,122 @@ +""" +This file is part of the 'Elements' Project +Elements is a 2D Physics API for Python (supporting Box2D2) + +Copyright (C) 2008, The Elements Team, + +Home: http://elements.linuxuser.at +IRC: #elements on irc.freenode.org + +Code: http://www.assembla.com/wiki/show/elements + svn co http://svn2.assembla.com/svn/elements + +License: GPLv3 | See LICENSE for the full text +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" +from locals import * +from elements import box2d + +class CallbackHandler: + # List of contact callbacks and shapes to start them - sorted by type for quicker access + # Callbacks are saved as callbacks[callback_type][[function, parameters], ...] + callbacks = {} + + def __init__(self, parent): + self.parent = parent + + # init callback dict to avoid those slow try + # (especially for self.get, as it is called *often*) + for i in xrange(10): + self.callbacks[i] = [] + + def add(self, callback_type, callback_handler, *args): + """ Users can add callbacks for certain (or all) collisions + + Parameters: + callback_type ......... CALLBACK_CONTACT (nothing else for now) + callback_handler ...... a callback function + args (optional) ....... a list of parameters which can be used with callbacks.get + + Return: + callback_id ... used to remove a callback later (int) + """ + # Create contact listener if required + if callback_type in [CALLBACK_CONTACT_ADD, CALLBACK_CONTACT_PERSIST, CALLBACK_CONTACT_REMOVE]: + if self.parent.listener == None: + self.parent.listener = kContactListener(self.get) + self.parent.world.SetContactListener( self.parent.listener ) + print "* ContactListener added" + + # Get callback dict for this callback_type + c = self.callbacks[callback_type] + + # Append to the Callback Dictionary + c.append([callback_handler, args]) + self.callbacks[callback_type] = c + + # Return Callback ID + # ID = callback_type.callback_index (1...n) + return "%i.%i" % (callback_type, len(c)) + + def get(self, callback_type): + return self.callbacks[callback_type] + + def start(self, callback_type, *args): + callbacks = self.get(callback_type) + for c in callbacks: + callback, params = c + callback() + +class kContactListener(box2d.b2ContactListener): + def __init__(self, get_callbacks): + # Init the Box2D b2ContactListener + box2d.b2ContactListener.__init__(self) + + # Function to get the current callbacks + self.get_callbacks = get_callbacks + + def check_contact(self, contact_type, point): + # Checks if a callback should be started with this contact point + contacts = self.get_callbacks(contact_type) + + # Step through all callbacks for this type (eg ADD, PERSIST, REMOVE) + for c in contacts: + callback, bodylist = c + if len(bodylist) == 0: + # Without bodylist it's a universal callback (for all bodies) + callback(point) + + else: + # This is a callback with specified bodies + # See if this contact involves one of the specified + b1 = str(point.shape1.GetBody()) + b2 = str(point.shape2.GetBody()) + for s in bodylist: + s = str(s) + if b1 == s or b2 == s: + # Yes, that's the one :) + callback(point) + + def Add(self, point): + """Called when a contact point is created""" + self.check_contact(CALLBACK_CONTACT_ADD, point) + + def Persist(self, point): + """Called when a contact point persists for more than a time step""" + self.check_contact(CALLBACK_CONTACT_PERSIST, point) + + def Remove(self, point): + """Called when a contact point is removed""" + self.check_contact(CALLBACK_CONTACT_REMOVE, point) + diff --git a/elements/camera.py b/elements/camera.py new file mode 100644 index 0000000..c45b27d --- /dev/null +++ b/elements/camera.py @@ -0,0 +1,124 @@ +""" +This file is part of the 'Elements' Project +Elements is a 2D Physics API for Python (supporting Box2D2) + +Copyright (C) 2008, The Elements Team, + +Home: http://elements.linuxuser.at +IRC: #elements on irc.freenode.org + +Code: http://www.assembla.com/wiki/show/elements + svn co http://svn2.assembla.com/svn/elements + +License: GPLv3 | See LICENSE for the full text +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" +from locals import * + +class Camera: + """ The Camera class. We will see :) + Please also see: http://www.assembla.com/spaces/elements/tickets/31 + + This class currently handles: + - Scaling factor + - Screen Offset from the World Coordinate System + + Inputs from the user have to be checked for them. + - Places to check for it: elements.py, drawing.py, add_objects.py + + """ + scale_factor = 1.0 # All coords to the renderer are multiplied with the scale factor in elements.draw() + track_body = None # Body which means to be tracked. Offset is set at each elements.draw() + + def __init__(self, parent): + self.parent = parent + + def track(self, body): + """ Start tracking a specific body + """ + self.track_body = body + + def track_stop(self): + """ Stop tracking a body + """ + self.track_body = None + + def center(self, pos, screenCoord=True, stopTrack=True): + """ Centers the camera at given screen coordinates -- in pixel + Typical call: world.camera.center(event.pos) + + Problem: Works ONLY WITH screenCoord now! + """ + x, y = pos + + x -= self.parent.display_width / 2 + y -= self.parent.display_height / 2 + + if screenCoord: + x /= self.scale_factor + y /= self.scale_factor + + # Set the offset + self.inc_offset((x, y), screenCoord, stopTrack) + + def set_offset(self, offset, screenCoord=True, stopTrack=True): + """ Set an offset from the screen to the world cs + -- in screen (or world) coordinates and in pixel + """ + # Stop tracking of an object + if stopTrack: self.track_stop() + + # If screenCoords, we have to bring them to the world cs + if screenCoord: x, y = self.parent.to_world(offset) + else: x, y = offset + + self._set_offset((x/self.parent.ppm, y/self.parent.ppm)) + + def inc_offset(self, offset, screenCoord=True, stopTrack=True): + """ Increment an offset from the screen to the world cs -- in world coordinates and in pixel + """ + # Stop tracking of an object + if stopTrack: self.track_stop() + + # Get Current Offset + x, y = self.parent.screen_offset_pixel + dx, dy = offset + + # Bring the directions into the world coordinate system + if screenCoord: + if self.parent.inputAxis_x_left: dx *= -1 + if self.parent.inputAxis_y_down: dy *= -1 + + # Set New Offset + self._set_offset(((x+dx)/self.parent.ppm, (y+dy)/self.parent.ppm)) + + def _set_offset(self, offset): + """ Set the screen offset to the world coordinate system + (using meters and the world coordinate system's orientation) + """ + x, y = offset + self.parent.screen_offset = (x, y) + self.parent.screen_offset_pixel = (x*self.parent.ppm, y*self.parent.ppm) + + def set_scale_factor(self, factor=1.0): + """ Zoom factor for the renderer 1.0 = 1:1 (original) + """ + self.scale_factor = factor + + def inc_scale_factor(self, factor=0.0): + """ Increases the zooms for the renderer a given factor + """ + self.scale_factor += factor + + \ No newline at end of file diff --git a/elements/drawing.py b/elements/drawing.py new file mode 100644 index 0000000..b4722fb --- /dev/null +++ b/elements/drawing.py @@ -0,0 +1,376 @@ +""" +This file is part of the 'Elements' Project +Elements is a 2D Physics API for Python (supporting Box2D2) + +Copyright (C) 2008, The Elements Team, + +Home: http://elements.linuxuser.at +IRC: #elements on irc.freenode.org + +Code: http://www.assembla.com/wiki/show/elements + svn co http://svn2.assembla.com/svn/elements + +License: GPLv3 | See LICENSE for the full text +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" +from math import pi +from math import cos +from math import sin +from math import sqrt + +import tools + +# Functions of a rendering class +# mandatory: +# __init__ +# start_drawing +# after_drawing +# draw_circle +# draw_polygon +# draw_lines +# set_lineWidth +# +# renderer-specific mandatory functions: +# for pygame: +# set_surface +# for cairo: +# draw_text +# for opengl: +# + +# IMPORTANT +# The drawing functions get the coordinates in their screen coordinate system +# no need for translations anymore :) + +class draw_pygame(object): + """ This class handles the drawing with pygame, which is really + simple since we only need draw_ellipse and draw_polygon. + """ + lineWidth = 0 + + def __init__(self): + """ Load pygame.draw and pygame.Rect, and reference it for + the drawing methods + + Parameters: + surface .... pygame surface (default: None) + lineWidth .. + + Return: Class draw_pygame() + """ + print "* Pygame selected as renderer" + from pygame import draw + from pygame import Rect + + self.draw = draw + self.Rect = Rect + + def set_lineWidth(self, lw): + """ + """ + self.lineWidth = lw + + def set_surface(self, surface): + """ + """ + self.surface = surface + + def get_surface(self): + """ + """ + return self.surface + + def start_drawing(self): + pass + + def after_drawing(self): + pass + + def draw_circle(self, clr, pt, radius, angle): + """ Draw a circle + + Parameters: + pt ........ (x, y) + clr ....... color in rgb ((r), (g), (b)) + radius .... circle radius + angle ..... rotation in radians + + Return: - + """ + x, y = pt + + x1 = x - radius + y1 = y - radius + + rect = self.Rect( [x1, y1, 2*radius, 2*radius] ) + self.draw.ellipse(self.surface, clr, rect, self.lineWidth) + + # draw the orientation vector + if radius > 10: + rx = cos(angle) * radius + ry = -sin(angle) * radius + + self.draw.line(self.surface, (255,255,255), pt, (x+rx, y+ry)) + + def draw_polygon(self, clr, points): + """ Draw a polygon + + Parameters: + clr ....... color in rgb ((r), (g), (b)) + points .... polygon points in normal (x,y) positions + + Return: - + """ + self.draw.polygon(self.surface, clr, points, self.lineWidth) + #self.draw.lines(self.surface, clr, True, points) + + def draw_lines(self, clr, closed, points, width=None): + """ Draw a polygon + + Parameters: + clr ....... color in rgb ((r), (g), (b)) + points .... polygon points in normal (x,y) positions + + Return: - + """ + if width == None: + lw = self.lineWidth + else: + lw = width + + self.draw.lines(self.surface, clr, closed, points, lw) + +class draw_cairo(object): + """ This class handles the drawing with cairo, which is really + simple since we only need draw_ellipse and draw_polygon. + """ + window = None + da = None + circle_surface = None + box_surface = None + + def __init__(self, drawMethod="filled"): + """ Load cairo.draw and cairo.Rect, and reference it for + the drawing methods + + Return: Class draw_cairo() + """ + print "* Cairo selected as renderer" + import cairo + self.cairo = cairo + self.set_drawing_method(drawMethod) + #self.draw_box = self.draw_box_image + + def set_lineWidth(self, lw): # unused + self.lineWidth = lw + + def set_drawing_area(self, da): + """ Set the area for Cairo to draw to + + da ...... drawing area (gtk.DrawingArea) + + Return: - + """ + self.da = da + self.window = da.window + print "* Cairo renderer drawing area set" + + def set_drawing_method(self, type): + """ type = filled, image """ + self.draw_circle = getattr(self, "draw_circle_%s" % type) + #self.draw_box = getattr(self, "draw_box_%s" % type) + + def start_drawing(self): + self.width, self.height = self.window.get_size() + self.imagesurface = self.cairo.ImageSurface(self.cairo.FORMAT_ARGB32, self.width, self.height); + self.ctx = ctx = self.cairo.Context(self.imagesurface) + + ctx.set_source_rgb(1, 1, 1) # background color + ctx.paint() + + ctx.move_to(0, 0) + ctx.set_source_rgb(0, 0, 0) # defaults for the rest of the drawing + ctx.set_line_width(1) + ctx.set_tolerance(0.1) + + ctx.set_line_join(self.cairo.LINE_CAP_BUTT) + # LINE_CAP_BUTT, LINE_CAP_ROUND, LINE_CAP_SQUARE, LINE_JOIN_BEVEL, LINE_JOIN_MITER, LINE_JOIN_ROUND + + #ctx.set_dash([20/4.0, 20/4.0], 0) + + def after_drawing(self): + dest_ctx = self.window.cairo_create() + dest_ctx.set_source_surface(self.imagesurface) + dest_ctx.paint() + + def set_circle_image(self, filename): + self.circle_surface = self.cairo.ImageSurface.create_from_png(filename) + self.draw_circle = self.draw_circle_image + +# def set_box_image(self, filename): +# self.box_surface = self.cairo.ImageSurface.create_from_png(filename) +# self.draw_box = self.draw_box_image + + def draw_circle_filled(self, clr, pt, radius, angle=0): + x, y = pt + + clr = tools.rgb2floats(clr) + self.ctx.set_source_rgb(*clr) + self.ctx.move_to(x, y) + self.ctx.arc(x, y, radius, 0, 2*3.1415) + self.ctx.fill() + + def draw_circle(): + pass + + def draw_circle_image(self, clr, pt, radius, angle=0, sf=None): + if sf == None: + sf = self.circle_surface + x, y = pt + self.ctx.save() + self.ctx.translate(x, y) + self.ctx.rotate(-angle) + image_r = sf.get_width() / 2 + scale = float(radius) / image_r + self.ctx.scale(scale, scale) + self.ctx.translate(-0.5*sf.get_width(), -0.5*sf.get_height()) + self.ctx.set_source_surface(sf) + self.ctx.paint() + self.ctx.restore() + + def draw_image(self, source, pt, scale=1.0, rot=0, sourcepos=(0,0)): + self.ctx.save() + self.ctx.rotate(rot) + self.ctx.scale(scale, scale) + destx, desty = self.ctx.device_to_user_distance(pt[0], pt[1]) + self.ctx.set_source_surface(source, destx-sourcepos[0], desty-sourcepos[1]) + self.ctx.rectangle(destx, desty, source.get_width(), source.get_height()) + self.ctx.fill() + self.ctx.restore() + + def draw_polygon(self, clr, points): + """ Draw a polygon + + Parameters: + clr ....... color in rgb ((r), (g), (b)) + points .... polygon points in normal (x,y) positions + + Return: - + """ + clr = tools.rgb2floats(clr) + self.ctx.set_source_rgb(clr[0], clr[1], clr[2]) + + pt = points[0] + self.ctx.move_to(pt[0], pt[1]) + for pt in points[1:]: + self.ctx.line_to(pt[0], pt[1]) + + self.ctx.fill() + + def draw_text(self, text, center, clr=(0,0,0), size=12, fontname="Georgia"): + clr = tools.rgb2floats(clr) + self.ctx.set_source_rgb(clr[0], clr[1], clr[2]) + + self.ctx.select_font_face(fontname, self.cairo.FONT_SLANT_NORMAL, self.cairo.FONT_WEIGHT_NORMAL) + self.ctx.set_font_size(size) + x_bearing, y_bearing, width, height = self.ctx.text_extents(text)[:4] + self.ctx.move_to(center[0] + 0.5 - width / 2 - x_bearing, center[1] + 0.5 - height / 2 - y_bearing) + self.ctx.show_text(text) + + def draw_lines(self, clr, closed, points): + """ Draw a polygon + + Parameters: + clr ....... color in rgb ((r), (g), (b)) + closed .... whether or not to close the lines (as a polygon) + points .... polygon points in normal (x,y) positions + Return: - + """ + clr = tools.rgb2floats(clr) + self.ctx.set_source_rgb(clr[0], clr[1], clr[2]) + + pt = points[0] + self.ctx.move_to(pt[0], pt[1]) + for pt in points[1:]: + self.ctx.line_to(pt[0], pt[1]) + + if closed: + pt = points[0] + self.ctx.line_to(pt[0], pt[1]) + + self.ctx.stroke() + +class draw_opengl_pyglet(object): + """ This class handles the drawing with pyglet + """ + lineWidth = 0 + def __init__(self): + """ Load pyglet.gl, and reference it for the drawing methods + + Parameters: + surface .... not used with pyglet + lineWidth .. + """ + print "* OpenGL_Pyglet selected as renderer" + + from pyglet import gl + self.gl = gl + + def set_lineWidth(self, lw): + self.lineWidth = lw + + def draw_circle(self, clr, pt, radius, a=0): + clr = tools.rgb2floats(clr) + self.gl.glColor3f(clr[0], clr[1], clr[2]) + + x, y = pt + segs = 15 + coef = 2.0*pi/segs; + + self.gl.glBegin(self.gl.GL_LINE_LOOP) + for n in range(segs): + rads = n*coef + self.gl.glVertex2f(radius*cos(rads + a) + x, radius*sin(rads + a) + y) + self.gl.glVertex2f(x,y) + self.gl.glEnd() + + def draw_polygon(self, clr, points): + clr = tools.rgb2floats(clr) + self.gl.glColor3f(clr[0], clr[1], clr[2]) + + self.gl.glBegin(self.gl.GL_LINES) + + p1 = points[0] + for p in points[1:]: + x1, y1 = p1 + x2, y2 = p1 = p + + self.gl.glVertex2f(x1, y1) + self.gl.glVertex2f(x2, y2) + + x1, y1 = points[0] + + self.gl.glVertex2f(x2, y2) + self.gl.glVertex2f(x1, y1) + + self.gl.glEnd() + + def draw_lines(self, clr, closed, points): + pass + + def start_drawing(self): + pass + + def after_drawing(self): + pass diff --git a/elements/elements.py b/elements/elements.py new file mode 100644 index 0000000..2b6fb71 --- /dev/null +++ b/elements/elements.py @@ -0,0 +1,589 @@ +#!/usr/bin/python +""" +This file is part of the 'Elements' Project +Elements is a 2D Physics API for Python (supporting pybox2d) + +Copyright (C) 2008, The Elements Team, + +Home: http://elements.linuxuser.at +IRC: #elements on irc.freenode.org + +Code: http://www.assembla.com/wiki/show/elements + svn co http://svn2.assembla.com/svn/elements + +License: GPLv3 | See LICENSE for the full text +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" +__version__= '0.11' +__contact__ = '' + +# Load Box2D +try: + import Box2D as box2d +except: + print 'Could not load the pybox2d library (Box2D).' + print 'Please run "setup.py install" to install the dependencies.' + print + print 'Alternatively, recompile pybox2d for your system and python version.' + print "See http://code.google.com/p/pybox2d" + exit() + +# Standard Imports +from random import shuffle + +# Load Elements Definitions +from locals import * + +# Load Elements Modules +import tools +import drawing +import add_objects +import callbacks +import camera + +# Main Class +class Elements: + """The class which handles all interaction with the box2d engine + """ + # Settings + run_physics =True # Can pause the simulation + element_count =0 # Element Count + renderer =None # Drawing class (from drawing.py) + input =INPUT_PIXELS # Default Input in Pixels! (can change to INPUT_METERS) + line_width =0 # Line Width in Pixels (0 for fill) + listener =None + + screen_offset = (0, 0) # Offset screen from world coordinate system (x, y) [meter5] + screen_offset_pixel = (0, 0) # Offset screen from world coordinate system (x, y) [pixel] + + # The internal coordination system is y+=up, x+=right + # But it's possible to change the input coords to something else, + # they will then be translated on input + inputAxis_x_left = False # positive to the right by default + inputAxis_y_down = True # positive to up by default + + mouseJoint = None + + def __init__(self, screen_size, gravity=(0.0,-9.0), ppm=100.0, renderer='pygame'): + """ Init the world with boundaries and gravity, and init colors. + + Parameters: + screen_size .. (w, h) -- screen size in pixels [int] + gravity ...... (x, y) in m/s^2 [float] default: (0.0, -9.0) + ppm .......... pixels per meter [float] default: 100.0 + renderer ..... which drawing method to use (str) default: 'pygame' + + Return: class Elements() + """ + self.set_screenSize(screen_size) + self.set_drawingMethod(renderer) + + # Create Subclasses + self.add = add_objects.Add(self) + self.callbacks = callbacks.CallbackHandler(self) + self.camera = camera.Camera(self) + + # Set Boundaries + self.worldAABB=box2d.b2AABB() + self.worldAABB.lowerBound = (-100.0, -100.0) + self.worldAABB.upperBound = (100.0, 100.0) + + # Gravity + Bodies will sleep on outside + self.gravity = gravity + self.doSleep = True + + # Create the World + self.world = box2d.b2World(self.worldAABB, self.gravity, self.doSleep) + + # Init Colors + self.init_colors() + + # Set Pixels per Meter + self.ppm = ppm + + def set_inputUnit(self, input): + """ Change the input unit to either meter or pixels + + Parameters: + input ... INPUT_METERS or INPUT_PIXELS + + Return: - + """ + self.input = input + + def set_inputAxisOrigin(self, left=True, top=False): + """ Change the origin of the input coordinate system axis + + Parameters: + left ... True or False -- x = 0 is at the left? + top .... True or False -- y = 0 is at the top? + + Return: - + """ + self.inputAxis_x_left = not left + self.inputAxis_y_down = top + + def set_drawingMethod(self, m, *kw): + """ Set a drawing method (from drawing.py) + + Parameters: + m .... 'pygame' or 'cairo' + *kw .. keywords to pass to the initializer of the drawing method + + Return: True if ok, False if no method identifier m found + """ + try: + self.renderer = getattr(drawing, "draw_%s" % m) (*kw) + return True + except AttributeError: + return False + + def set_screenSize(self, size): + """ Set the current screen size + + Parameters: + size ... (int(width), int(height)) in pixels + + Return: - + """ + self.display_width, self.display_height = size + + def init_colors(self): + """ Init self.colors with a fix set of hex colors + + Return: - + """ + self.fixed_color = None + self.cur_color = 0 + self.colors = [ + "#737934", "#729a55", "#040404", "#1d4e29", "#ae5004", "#615c57", + "#6795ce", "#203d61", "#8f932b" + ] + shuffle(self.colors) + + def set_color(self, clr): + """ Set a fixed color for all future Elements (until reset_color() is called) + + Parameters: + clr ... Hex '#123123' or RGB ((r), (g), (b)) + + Return: - + """ + self.fixed_color = clr + + def reset_color(self): + """ All Elements from now on will be drawn in random colors + + Return: - + """ + self.fixed_color = None + + def get_color(self): + """ Get a color - either the fixed one or the next from self.colors + + Return: clr = ((R), (G), (B)) + """ + if self.fixed_color != None: + return self.fixed_color + + if self.cur_color == len(self.colors): + self.cur_color = 0 + shuffle(self.colors) + + clr = self.colors[self.cur_color] + if clr[0] == "#": + clr = tools.hex2rgb(clr) + + self.cur_color += 1 + return clr + + def update(self, fps=50.0, vel_iterations=10, pos_iterations=8): + """ Update the physics, if not paused (self.run_physics) + + Parameters: + fps ............. fps with which the physics engine shall work + vel_iterations .. velocity substeps per step for smoother simulation + pos_iterations .. position substeps per step for smoother simulation + + Return: - + """ + if self.run_physics: + self.world.Step(1.0 / fps, vel_iterations, pos_iterations) + + def translate_coord(self, point): + """ Flips the coordinates in another coordinate system orientation, if necessary + (screen <> world coordinate system) + """ + x, y = point + + if self.inputAxis_x_left: + x = self.display_width - x + + if self.inputAxis_y_down: + y = self.display_height - y + + return (x, y) + + def translate_coords(self, pointlist): + """ Flips the coordinates in another coordinate system orientation, if necessary + (screen <> world coordinate system) + """ + p_out = [] + for p in pointlist: + p_out.append(self.translate_coord(p)) + return p_out + + def to_world(self, pos): + """ Transfers a coordinate from the screen to the world coordinate system (pixels) + - Change to the right axis orientation + - Include the offset: screen -- world coordinate system + - Include the scale factor (Screen coordinate system might have a scale factor) + """ + dx, dy = self.screen_offset_pixel + + x = pos[0] / self.camera.scale_factor + y = pos[1] / self.camera.scale_factor + + x, y = self.translate_coord((round(x), round(y))) + return (x+dx, y+dy) + + def to_screen(self, pos): + """ Transfers a coordinate from the world to the screen coordinate system (pixels) + and by the screen offset + """ + dx, dy = self.screen_offset_pixel + x = pos[0] - dx + y = pos[1] - dy + + sx, sy = self.translate_coord((x, y)) + return (sx * self.camera.scale_factor, sy * self.camera.scale_factor) + + def meter_to_screen(self, i): + return i * self.ppm * self.camera.scale_factor + + def get_bodies_at_pos(self, search_point, include_static=False, area=0.01): + """ Check if given point (screen coordinates) is inside any body. + If yes, return all found bodies, if not found return False + """ + sx, sy = self.to_world(search_point) + sx /= self.ppm + sy /= self.ppm + + f = area/self.camera.scale_factor + + AABB=box2d.b2AABB() + AABB.lowerBound = (sx-f, sy-f) + AABB.upperBound = (sx+f, sy+f) + + amount, shapes = self.world.Query(AABB, 2) + + if amount == 0: + return False + else: + bodylist = [] + for s in shapes: + body = s.GetBody() + if not include_static: + if body.IsStatic() or body.GetMass() == 0.0: + continue + + if s.TestPoint(body.GetXForm(), (sx, sy)): + bodylist.append(body) + + return bodylist + + def draw(self): + """ If a drawing method is specified, this function passes the objects + to the module in pixels. + + Return: True if the objects were successfully drawn + False if the renderer was not set or another error occurred + """ + self.callbacks.start(CALLBACK_DRAWING_START) + + # No need to run through the loop if there's no way to draw + if not self.renderer: + return False + + if self.camera.track_body: + # Get Body Center + p1 = self.camera.track_body.GetWorldCenter() + + # Center the Camera There, False = Don't stop the tracking + self.camera.center(self.to_screen((p1.x*self.ppm, p1.y*self.ppm)), stopTrack=False) + + # Walk through all known elements + self.renderer.start_drawing() + + for body in self.world.bodyList: + xform = body.GetXForm() + shape = body.GetShapeList() + angle = body.GetAngle() + + if shape: + userdata = body.GetUserData() + clr = userdata['color'] + + for shape in body.shapeList: + type = shape.GetType() + + if type == box2d.e_circleShape: + position = box2d.b2Mul(xform, shape.GetLocalPosition()) + + pos = self.to_screen((position.x*self.ppm, position.y*self.ppm)) + self.renderer.draw_circle(clr, pos, self.meter_to_screen(shape.radius), angle) + + elif type == box2d.e_polygonShape: + points = [] + for v in shape.vertices: + pt = box2d.b2Mul(xform, v) + x, y = self.to_screen((pt.x*self.ppm, pt.y*self.ppm)) + points.append([x, y]) + + self.renderer.draw_polygon(clr, points) + + else: + print " unknown shape type:%d" % shape.GetType() + + + for joint in self.world.jointList: + p2 = joint.GetAnchor1() + p2 = self.to_screen((p2.x*self.ppm, p2.y*self.ppm)) + + p1 = joint.GetAnchor2() + p1 = self.to_screen((p1.x*self.ppm, p1.y*self.ppm)) + + if p1 == p2: + self.renderer.draw_circle((255,255,255), p1, 2, 0) + else: + self.renderer.draw_lines((0,0,0), False, [p1, p2], 3) + + self.callbacks.start(CALLBACK_DRAWING_END) + self.renderer.after_drawing() + + return True + + + def mouse_move(self, pos): + pos = self.to_world(pos) + x, y = pos + x /= self.ppm + y /= self.ppm + + if self.mouseJoint: + self.mouseJoint.SetTarget((x,y)) + + def pickle_save(self, fn, additional_vars={}): + import cPickle as pickle + self.add.remove_mouseJoint() + + if not additional_vars and hasattr(self, '_pickle_vars'): + additional_vars=dict((var, getattr(self, var)) for var in self._pickle_vars) + + save_values = [self.world, box2d.pickle_fix(self.world, additional_vars, 'save')] + + try: + pickle.dump(save_values, open(fn, 'wb')) + except Exception, s: + print 'Pickling failed: ', s + return + + print 'Saved to %s' % fn + + def pickle_load(self, fn, set_vars=True, additional_vars=[]): + """ + Load the pickled world in file fn. + additional_vars is a dictionary to be populated with the + loaded variables. + """ + import cPickle as pickle + try: + world, variables = pickle.load(open(fn, 'rb')) + world = world._pickle_finalize() + variables = box2d.pickle_fix(world, variables, 'load') + except Exception, s: + print 'Error while loading world: ', s + return + + self.world = world + + if set_vars: + # reset the additional saved variables: + for var, value in variables.items(): + if hasattr(self, var): + setattr(self, var, value) + else: + print 'Unknown property %s=%s' % (var, value) + + print 'Loaded from %s' % fn + + return variables + + def json_save(self, path, additional_vars = {}): + import json + worldmodel = {} + + save_id_index = 1 + self.world.GetGroundBody().userData = {"saveid" : 0} + + bodylist = [] + for body in self.world.GetBodyList(): + if not body == self.world.GetGroundBody(): + body.userData["saveid"] = save_id_index #set temporary data + save_id_index+=1 + shapelist = body.GetShapeList() + modelbody = {} + modelbody['position'] = body.position.tuple() + modelbody['dynamic'] = body.IsDynamic() + modelbody['userData'] = body.userData + modelbody['angle'] = body.angle + modelbody['angularVelocity'] = body.angularVelocity + modelbody['linearVelocity'] = body.linearVelocity.tuple() + if shapelist and len(shapelist) > 0: + shapes = [] + for shape in shapelist: + modelshape = {} + modelshape['density'] = shape.density + modelshape['restitution'] = shape.restitution + modelshape['friction'] = shape.friction + shapename = shape.__class__.__name__ + if shapename == "b2CircleShape": + modelshape['type'] = 'circle' + modelshape['radius'] = shape.radius + modelshape['localPosition'] = shape.localPosition.tuple() + if shapename == "b2PolygonShape": + modelshape['type'] = 'polygon' + modelshape['vertices'] = shape.vertices + shapes.append(modelshape) + modelbody['shapes'] = shapes + + bodylist.append(modelbody) + + worldmodel['bodylist'] = bodylist + + jointlist = [] + + for joint in self.world.GetJointList(): + modeljoint = {} + + if joint.__class__.__name__ == "b2RevoluteJoint": + modeljoint['type'] = 'revolute' + modeljoint['anchor'] = joint.GetAnchor1().tuple() + modeljoint['enableMotor'] = joint.enableMotor + modeljoint['motorSpeed'] = joint.motorSpeed + modeljoint['maxMotorTorque'] = joint.maxMotorTorque + elif joint.__class__.__name__ == "b2DistanceJoint": + modeljoint['type'] = 'distance' + modeljoint['anchor1'] = joint.GetAnchor1().tuple() + modeljoint['anchor2'] = joint.GetAnchor2().tuple() + + modeljoint['body1'] = joint.body1.userData['saveid'] + modeljoint['body2'] = joint.body2.userData['saveid'] + modeljoint['collideConnected'] = joint.collideConnected + modeljoint['userData'] = joint.userData + + + jointlist.append(modeljoint) + + worldmodel['jointlist'] = jointlist + + controllerlist = [] + worldmodel['controllerlist'] = controllerlist + + worldmodel['additional_vars'] = additional_vars + + f = open(path,'w') + f.write(json.dumps(worldmodel)) + f.close() + + for body in self.world.GetBodyList(): + del body.userData['saveid'] #remove temporary data + + def json_load(self, path, additional_vars = {}): + import json + + self.world.GetGroundBody().userData = {"saveid" : 0} + + f = open(path, 'r') + worldmodel = json.loads(f.read()) + f.close() + #clean world + for joint in self.world.GetJointList(): + self.world.DestroyJoint(joint) + for body in self.world.GetBodyList(): + if body != self.world.GetGroundBody(): + self.world.DestroyBody(body) + + #load bodys + for body in worldmodel['bodylist']: + bodyDef = box2d.b2BodyDef() + bodyDef.position = body['position'] + bodyDef.userData = body['userData'] + bodyDef.angle = body['angle'] + newBody = self.world.CreateBody(bodyDef) + #_logger.debug(newBody) + newBody.angularVelocity = body['angularVelocity'] + newBody.linearVelocity = body['linearVelocity'] + if body.has_key('shapes'): + for shape in body['shapes']: + if shape['type'] == 'polygon': + polyDef = box2d.b2PolygonDef() + polyDef.setVertices(shape['vertices']) + polyDef.density = shape['density'] + polyDef.restitution = shape['restitution'] + polyDef.friction = shape['friction'] + newBody.CreateShape(polyDef) + if shape['type'] == 'circle': + circleDef = box2d.b2CircleDef() + circleDef.radius = shape['radius'] + circleDef.density = shape['density'] + circleDef.restitution = shape['restitution'] + circleDef.friction = shape['friction'] + circleDef.localPosition = shape['localPosition'] + newBody.CreateShape(circleDef) + newBody.SetMassFromShapes() + + for joint in worldmodel['jointlist']: + if joint['type'] == 'distance': + jointDef = box2d.b2DistanceJointDef() + body1 = self.getBodyWithSaveId(joint['body1']) + anch1 = joint['anchor1'] + body2 = self.getBodyWithSaveId(joint['body2']) + anch2 = joint['anchor2'] + jointDef.collideConnected = joint['collideConnected'] + jointDef.Initialize(body1,body2,anch1,anch2) + jointDef.SetUserData(joint['userData']) + self.world.CreateJoint(jointDef) + if joint['type'] == 'revolute': + jointDef = box2d.b2RevoluteJointDef() + body1 = self.getBodyWithSaveId(joint['body1']) + body2 = self.getBodyWithSaveId(joint['body2']) + anchor = joint['anchor'] + jointDef.Initialize(body1,body2,anchor) + jointDef.SetUserData(joint['userData']) + jointDef.enableMotor = joint['enableMotor'] + jointDef.motorSpeed = joint['motorSpeed'] + jointDef.maxMotorTorque = joint['maxMotorTorque'] + self.world.CreateJoint(jointDef) + + for (k,v) in worldmodel['additional_vars'].items(): + additional_vars[k] = v + + for body in self.world.GetBodyList(): + del body.userData['saveid'] #remove temporary data + + def getBodyWithSaveId(self,saveid): + for body in self.world.GetBodyList(): + if body.userData['saveid'] == saveid: + return body diff --git a/elements/locals.py b/elements/locals.py new file mode 100644 index 0000000..85528f7 --- /dev/null +++ b/elements/locals.py @@ -0,0 +1,37 @@ +""" +This file is part of the 'Elements' Project +Elements is a 2D Physics API for Python (supporting Box2D2) + +Copyright (C) 2008, The Elements Team, + +Home: http://elements.linuxuser.at +IRC: #elements on irc.freenode.org + +Code: http://www.assembla.com/wiki/show/elements + svn co http://svn2.assembla.com/svn/elements + +License: GPLv3 | See LICENSE for the full text +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" +INPUT_METERS = 0 +INPUT_PIXELS = 1 + +CALLBACK_CONTACT_ADD = 0 +CALLBACK_CONTACT_PERSIST = 1 +CALLBACK_CONTACT_REMOVE = 2 + +CALLBACK_DRAWING_START = 3 +CALLBACK_DRAWING_END = 4 + +FLT_EPSILON = 1.192092896e-07 diff --git a/elements/menu.py b/elements/menu.py new file mode 100644 index 0000000..1c8a417 --- /dev/null +++ b/elements/menu.py @@ -0,0 +1,237 @@ +""" +This file is part of the 'Elements' Project +Elements is a 2D Physics API for Python (supporting Box2D2) + +Copyright (C) 2008, The Elements Team, + +Home: http://elements.linuxuser.at +IRC: #elements on irc.freenode.org + +Code: http://www.assembla.com/wiki/show/elements + svn co http://svn2.assembla.com/svn/elements + +License: GPLv3 | See LICENSE for the full text +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" +import pygame +from pygame.locals import * + +import tools + +COLOR_HEX_BLUE1 = "6491a4" +COLOR_HEX_BLUE2 = "9ec9ff" + +class MenuItem: + # padding [px]: left, top, right, bottom + padding = (5, 2, 5, 2) + + def empty(self, *args): + pass + + def __init__(self, title, pos, userData, parent=None, callback=None): + self.title = title + self.userData = userData + self.parent = parent + self.childs = [] + + if self.parent: + self.visible = False + else: + self.visible = True + + if callback: + self.callback = callback + else: + self.callback = self.empty + + # Create Surface and Stuff :) + self.font = pygame.font.Font(None, 32) + text = self.font.render(title, 1, (255,255,255)) + + rx, ry, rw, rh = rect = text.get_rect() + pl, pt, pr, pb = self.padding + + s1 = pygame.Surface((rw+pl+pr, rh+pt+pb)) + s1.fill(tools.hex2rgb(COLOR_HEX_BLUE1)) + s1.blit(text, (pl, pt)) + + s2 = pygame.Surface((rw+pl+pr, rh+pt+pb)) + s2.fill(tools.hex2rgb(COLOR_HEX_BLUE2)) + s2.blit(text, (pl, pt)) + + self.rect = s1.get_rect().move(pos) + + self.surface_inactive = s1 + self.surface_active = s2 + + def pos_inside(self, pos): + if not self.visible: + return False + + x,y,w,h = self.rect + px, py = pos + + if px > x and px < x+w and py > y and py < y+h: + return True + else: + return False + +class MenuClass: + """ Important: Never delete an Item, just overwrite it if deleting, + else the menuitem id's get messed up + """ + # current active menu point it + focus = False + + # each item is stored as MenuItem + items = [] + + # where to start drawing + start_at = (0, 0) + + # menubar properties + height = 0 # px + width = 0 # px (set in set_width) + setWidth = False # if width was set by hand (if not, increase width by adding stuff) + + def __init__(self): + self.draw_at = self.start_at + + def set_width(self, width): + self.setWidth = True + self.width = width + + def addItem(self, title, callback=None, userData='', parent=None): + # Get position for the Item + if parent: draw_at = (0,0) + else: draw_at = self.draw_at + + # Create Items + M = MenuItem(title=title, pos=draw_at, userData=userData, parent=parent, callback=callback) + self.items.append(M) + + # Set a new position + x,y,w,h = M.rect + x, y = self.draw_at + + if parent: + # Set the info that the item has a child to the parent item + self.items[parent-1].childs.append(len(self.items)-1) + + else: + # New next drawing position + self.draw_at = (x+w, y) + + # Adjust the width of the menu bar + if not self.setWidth: + self.width = x+w + + # Adjust the height of the menu bar + if h > self.height: self.height = h + 2 + + # Return array id of this item + return len(self.items) + + def click(self, pos): + """ Checks a click for menuitems and starts the callback if found + + Return: True if a menu item was found or hit the MenuBar, and False if not + """ + focus_in = self.focus + + found = False + for i in xrange(len(self.items)): + item = self.items[i] + if item.pos_inside(pos): + found = True + item.callback(item.title, item.userData) + + # Expand the menu if necessary + if len(item.childs) > 0: + self.focus = i+1 + + # Close any opened menu windows if clicked somewhere else + if self.focus == focus_in: + self.focus = False + self.subwin_rect = (0,0,0,0) + for item in self.items: + if item.parent: + item.visible = False + + # Check if click is inside menubar + x,y = pos + mx, my = self.start_at + + if found: + return True + else: + return False + + def draw(self, surface): + """ Draw the menu with pygame on a given surface + """ + s = pygame.Surface((self.width, self.height)) + s.fill(tools.hex2rgb(COLOR_HEX_BLUE1)) + + surface.blit(s, (0,0)) + + for i in xrange(len(self.items)): + item = self.items[i] + if not item.parent: + x,y,w,h = item.rect + if self.focus == i+1: + surface.blit(item.surface_active, (x,y)) + else: + surface.blit(item.surface_inactive, (x,y)) + + # If a menu item is open, draw that + if self.focus: + width = 0 + height = 0 + + i = [] + for j in self.items: + if j.parent == self.focus: + i.append(j) + x, y, w, h = j.rect + if w > width: width = w + height += h + + if len(i) > 0: + s = pygame.Surface((width, height)) + s.fill(tools.hex2rgb(COLOR_HEX_BLUE1)) + + # Parent Coordinates + px, py, pw, ph = self.items[self.focus-1].rect + + # y Counter + y = 0 + + for item in i: + item.visible = True + s.blit(item.surface_inactive, (0, y)) + + ix, iy, iw, ih = item.rect + if (ix, iy) == (0, 0): + item.rect = item.rect.move((px, y+ph)) + ix, iy, iw, ih = item.rect + + if iw < width: + item.rect = (ix,iy,width,ih) + + y += ih + + surface.blit(s, (px,py+ph)) + self.subwin_rect = s.get_rect().move(px, py+ph) + diff --git a/elements/tools.py b/elements/tools.py new file mode 100644 index 0000000..9851ff8 --- /dev/null +++ b/elements/tools.py @@ -0,0 +1,65 @@ +""" +This file is part of the 'Elements' Project +Elements is a 2D Physics API for Python (supporting Box2D2) + +Copyright (C) 2008, The Elements Team, + +Home: http://elements.linuxuser.at +IRC: #elements on irc.freenode.org + +Code: http://www.assembla.com/wiki/show/elements + svn co http://svn2.assembla.com/svn/elements + +License: GPLv3 | See LICENSE for the full text +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" +# Some Hex Tools +def hex2dec(hex): + """ Convert and hex value in a decimal number + """ + return int(hex, 16) + +def hex2rgb(hex): + """ Convert a hex color (#123abc) in RGB ((r), (g), (b)) + """ + if hex[0:1] == '#': hex = hex[1:]; + return (hex2dec(hex[:2]), hex2dec(hex[2:4]), hex2dec(hex[4:6])) + +def rgb2floats(rgb): + """Convert a color in the RGB (0..255,0..255,0..255) format to the + (0..1, 0..1, 0..1) float format + """ + ret = [] + for c in rgb: + ret.append(float(c) / 255) + return ret + +def point_in_poly(point, poly): + #print ">", point, poly + x, y = point + n = len(poly) + inside = False + p1x,p1y = poly[0] + for i in range(n+1): + p2x,p2y = poly[i % n] + if y > min(p1y,p2y): + if y <= max(p1y,p2y): + if x <= max(p1x,p2x): + if p1y != p2y: + xinters = (y-p1y)*(p2x-p1x)/(p2y-p1y)+p1x + if p1x == p2x or x <= xinters: + inside = not inside + p1x,p1y = p2x,p2y + return inside + \ No newline at end of file diff --git a/elements/tools_poly.py b/elements/tools_poly.py new file mode 100644 index 0000000..5ca3986 --- /dev/null +++ b/elements/tools_poly.py @@ -0,0 +1,347 @@ +""" +This file is part of the 'Elements' Project +Elements is a 2D Physics API for Python (supporting Box2D2) + +Copyright (C) 2008, The Elements Team, + +Home: http://elements.linuxuser.at +IRC: #elements on irc.freenode.org + +Code: http://www.assembla.com/wiki/show/elements + svn co http://svn2.assembla.com/svn/elements + +License: GPLv3 | See LICENSE for the full text +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" +from functools import partial + +from math import fabs +from math import sqrt +from math import atan2 +from math import degrees +from math import acos + +from locals import * +from elements import box2d + +def calc_center(points): + """ Calculate the center of a polygon + + Return: The center (x,y) + """ + tot_x, tot_y = 0,0 + for p in points: + tot_x += p[0] + tot_y += p[1] + n = len(points) + return (tot_x/n, tot_y/n) + +def poly_center_vertices(pointlist): + """ Rearranges vectors around the center + + Return: pointlist ([(x, y), ...]) + """ + poly_points_center = [] + center = cx, cy = calc_center(pointlist) + + for p in pointlist: + x = p[0] - cx + y = cy - p[1] + poly_points_center.append((x, y)) + + return poly_points_center + +def is_line(vertices, tolerance=25.0): + """ Check if passed vertices are a line. Done by comparing + the angles of all vectors and check tolerance. + + Parameters: + vertices ... a list of vertices (x, y) + tolerance .. how many degrees should be allowed max to be a line + + Returns: True if line, False if no line + """ + if len(vertices) <= 2: + return True + + # Step 1: Points -> Vectors + p_old = vertices[0] + alphas = [] + + + for p in vertices[1:]: + x1, y1 = p_old + x2, y2 = p + p_old = p + + # Create Vector + vx, vy = (x2-x1, y2-y1) + + # Check Length + l = sqrt((vx*vx) + (vy*vy)) + if l == 0.0: continue + + # Normalize vector + vx /= l + vy /= l + + # Append angle + if fabs(vx) < 0.2: alpha = 90.0 + else: alpha = degrees(atan2(vy,vx)) + + alphas.append(fabs(alpha)) + + # Sort angles + alphas.sort() + + # Get maximum difference + alpha_diff = fabs(alphas[-1] - alphas[0]) + print "alpha difference:", alpha_diff + + if alpha_diff < tolerance: + return True + else: + return False + +def reduce_poly_by_angle(vertices, tolerance=10.0, minlen=20): + """ This function reduces a poly by the angles of the vectors (detect lines) + If the angle difference from one vector to the last > tolerance: use last point + If the angle is quite the same, it's on the line + + Parameters: + vertices ... a list of vertices (x, y) + tolerance .. how many degrees should be allowed max + + Returns: (1) New Pointlist, (2) Soft reduced pointlist (reduce_poly()) + """ + v_last = vertices[-1] + vertices = vxx = reduce_poly(vertices, minlen) + + p_new = [] + p_new.append(vertices[0]) + + dir = None + is_convex = True + + for i in xrange(len(vertices)-1): + if i == 0: + p_old = vertices[i] + continue + + x1, y1 = p_old + x2, y2 = vertices[i] + x3, y3 = vertices[i+1] + p_old = vertices[i] + + # Create Vectors + v1x = (x2 - x1) * 1.0 + v1y = (y2 - y1) * 1.0 + + v2x = (x3 - x2) * 1.0 + v2y = (y3 - y2) * 1.0 + + # Calculate angle + a = ((v1x * v2x) + (v1y * v2y)) + b = sqrt((v1x*v1x) + (v1y*v1y)) + c = sqrt((v2x*v2x) + (v2y*v2y)) + + # No Division by 0 :) + if (b*c) == 0.0: continue + + # Get the current degrees + # We have a bug here sometimes... + try: + angle = degrees(acos(a / (b*c))) + except: + # cos=1.0 + print "cos=", a/(b*c) + continue + + # Check if inside tolerance + if fabs(angle) > tolerance: + p_new.append(vertices[i]) + # print "x", 180-angle, is_left(vertices[i-1], vertices[i], vertices[i+1]) + + # Check if convex: + if dir == None: + dir = is_left(vertices[i-1], vertices[i], vertices[i+1]) + else: + if dir != is_left(vertices[i-1], vertices[i], vertices[i+1]): + is_convex = False + + # We also want to append the last point :) + p_new.append(v_last) + + # Returns: (1) New Pointlist, (2) Soft reduced pointlist (reduce_poly()) + return p_new, is_convex + + """ OLD FUNCTION: """ + # Wipe all points too close to each other + vxx = vertices = reduce_poly(vertices, minlen) + + # Create Output List + p_new = [] + p_new.append(vertices[0]) + + # Set the starting vertice + p_old = vertices[0] + alpha_old = None + + # For each vector, compare the angle difference to the last one + for i in range(1, len(vertices)): + x1, y1 = p_old + x2, y2 = vertices[i] + p_old = (x2, y2) + + # Make Vector + vx, vy = (x2-x1, y2-y1) + + # Vector length + l = sqrt((vx*vx) + (vy*vy)) + + # normalize + vx /= l + vy /= l + + # Get Angle + if fabs(vx) < 0.2: + alpha = 90 + else: + alpha = degrees(atan2(vy,vx)) + + if alpha_old == None: + alpha_old = alpha + continue + + # Get difference to previous angle + alpha_diff = fabs(alpha - alpha_old) + alpha_old = alpha + + # If the new vector differs from the old one, we add the old point + # to the output list, as the line changed it's way :) + if alpha_diff > tolerance: + #print ">",alpha_diff, "\t", vx, vy, l + p_new.append(vertices[i-1]) + + # We also want to append the last point :) + p_new.append(vertices[-1]) + + # Returns: (1) New Pointlist, (2) Soft reduced pointlist (reduce_poly()) + return p_new, vxx + + +# The following functions is_left, reduce_poly and convex_hull are +# from the pymunk project (http://code.google.com/p/pymunk/) +def is_left(p0, p1, p2): + """Test if p2 is left, on or right of the (infinite) line (p0,p1). + + :return: > 0 for p2 left of the line through p0 and p1 + = 0 for p2 on the line + < 0 for p2 right of the line + """ + sorting = (p1[0] - p0[0])*(p2[1]-p0[1]) - (p2[0]-p0[0])*(p1[1]-p0[1]) + if sorting > 0: return 1 + elif sorting < 0: return -1 + else: return 0 + +def is_convex(points): + """Test if a polygon (list of (x,y)) is strictly convex or not. + + :return: True if the polygon is convex, False otherwise + """ + #assert len(points) > 2, "not enough points to form a polygon" + + p0 = points[0] + p1 = points[1] + p2 = points[2] + + xc, yc = 0, 0 + is_same_winding = is_left(p0, p1, p2) + for p2 in points[2:] + [p0] + [p1]: + if is_same_winding != is_left(p0, p1, p2): + return False + a = p1[0] - p0[0], p1[1] - p0[1] # p1-p0 + b = p2[0] - p1[0], p2[1] - p1[1] # p2-p1 + if sign(a[0]) != sign(b[0]): xc +=1 + if sign(a[1]) != sign(b[1]): yc +=1 + p0, p1 = p1, p2 + + return xc <= 2 and yc <= 2 + +def sign(x): + if x < 0: return -1 + else: return 1 + + +def reduce_poly(points, tolerance=50): + """Remove close points to simplify a polyline + tolerance is the min distance between two points squared. + + :return: The reduced polygon as a list of (x,y) + """ + curr_p = points[0] + reduced_ps = [points[0]] + + for p in points[1:]: + x1, y1 = curr_p + x2, y2 = p + dx = fabs(x2 - x1) + dy = fabs(y2 - y1) + l = sqrt((dx*dx) + (dy*dy)) + if l > tolerance: + curr_p = p + reduced_ps.append(p) + + return reduced_ps + +def convex_hull(points): + """Create a convex hull from a list of points. + This function uses the Graham Scan Algorithm. + + :return: Convex hull as a list of (x,y) + """ + ### Find lowest rightmost point + p0 = points[0] + for p in points[1:]: + if p[1] < p0[1]: + p0 = p + elif p[1] == p0[1] and p[0] > p0[0]: + p0 = p + points.remove(p0) + + ### Sort the points angularly about p0 as center + f = partial(is_left, p0) + points.sort(cmp = f) + points.reverse() + points.insert(0, p0) + + ### Find the hull points + hull = [p0, points[1]] + + for p in points[2:]: + + pt1 = hull[-1] + pt2 = hull[-2] + l = is_left(pt2, pt1, p) + if l > 0: + hull.append(p) + else: + while l <= 0 and len(hull) > 2: + hull.pop() + pt1 = hull[-1] + pt2 = hull[-2] + l = is_left(pt2, pt1, p) + hull.append(p) + return hull + diff --git a/lib/Elements-0.13-py2.5.egg b/lib/Elements-0.13-py2.5.egg deleted file mode 100644 index 93869b9..0000000 --- a/lib/Elements-0.13-py2.5.egg +++ /dev/null Binary files differ -- cgit v0.9.1