Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSai Vineet <saivineet89@gmail.com>2013-12-11 19:36:41 (GMT)
committer Sai Vineet <saivineet89@gmail.com>2013-12-11 19:36:41 (GMT)
commitd577decc9f49bac76c410a12b62e26ec582c5c81 (patch)
treee2f7d81ee6967e9563bfff4008a0467775691fda
parent0d17137aaff76290b0f497bf10885620454f0a5a (diff)
Add a Pen tool to PhysicsHEADmaster
Also took out the Elements package from the .egg and brought it out so that it can be edited. The Export to csv functionality has also been changed to reflect pygame coords only. Previously the coords were Box2d meters.
-rw-r--r--activity.py2
-rw-r--r--elements/__init__.py2
-rw-r--r--elements/add_objects.py537
-rw-r--r--elements/callbacks.py122
-rw-r--r--elements/camera.py124
-rw-r--r--elements/drawing.py376
-rw-r--r--elements/elements.py589
-rw-r--r--elements/locals.py37
-rw-r--r--elements/menu.py237
-rw-r--r--elements/tools.py65
-rw-r--r--elements/tools_poly.py347
-rw-r--r--physics.py25
-rw-r--r--tools.py36
13 files changed, 2483 insertions, 16 deletions
diff --git a/activity.py b/activity.py
index eef0e36..c42f2ec 100644
--- a/activity.py
+++ b/activity.py
@@ -214,7 +214,7 @@ class PhysicsActivity(activity.Activity):
clear_all_alert = ConfirmationAlert()
clear_all_alert.props.title = _('Are You Sure?')
clear_all_alert.props.msg = \
- _('All you work will be discarded. This cannot be undone!')
+ _('All your work will be discarded. This cannot be undone!')
clear_all_alert.connect('response', self.clear_all_alert_cb)
self.add_alert(clear_all_alert)
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..05e62b6
--- /dev/null
+++ b/elements/add_objects.py
@@ -0,0 +1,537 @@
+"""
+This file is part of the 'Elements' Project
+Elements is a 2D Physics API for Python (supporting Box2D2)
+
+Copyright (C) 2008, The Elements Team, <elements@linuxuser.at>
+
+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 <http://www.gnu.org/licenses/>.
+"""
+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) == 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) == 5:
+ b1, b2, p1, p2, collideConnected = args
+
+ p1 = self.to_b2vec(p1)
+ p2 = self.to_b2vec(p2)
+
+ jointDef = box2d.b2DistanceJointDef()
+ jointDef.Initialize(b1, b2, p1, p2)
+ jointDef.collideConnected = collideConnected
+
+ 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, <elements@linuxuser.at>
+
+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 <http://www.gnu.org/licenses/>.
+"""
+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, <elements@linuxuser.at>
+
+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 <http://www.gnu.org/licenses/>.
+"""
+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, <elements@linuxuser.at>
+
+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 <http://www.gnu.org/licenses/>.
+"""
+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..1003df9
--- /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, <elements@linuxuser.at>
+
+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 <http://www.gnu.org/licenses/>.
+"""
+__version__= '0.11'
+__contact__ = '<elements@linuxuser.at>'
+
+# 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 cjson
+ 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(cjson.encode(worldmodel))
+ f.close()
+
+ for body in self.world.GetBodyList():
+ del body.userData['saveid'] #remove temporary data
+
+ def json_load(self, path, additional_vars = {}):
+ import cjson
+
+ self.world.GetGroundBody().userData = {"saveid" : 0}
+
+ f = open(path, 'r')
+ worldmodel = cjson.decode(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, <elements@linuxuser.at>
+
+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 <http://www.gnu.org/licenses/>.
+"""
+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, <elements@linuxuser.at>
+
+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 <http://www.gnu.org/licenses/>.
+"""
+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, <elements@linuxuser.at>
+
+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 <http://www.gnu.org/licenses/>.
+"""
+# 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, <elements@linuxuser.at>
+
+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 <http://www.gnu.org/licenses/>.
+"""
+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/physics.py b/physics.py
index 64e4ae1..f7ef867 100644
--- a/physics.py
+++ b/physics.py
@@ -40,7 +40,8 @@ sys.path.append("lib/")
import pkg_resources
# If your architecture is different, comment these lines and install
# the modules in your system.
-sys.path.append("lib/Elements-0.13-py2.5.egg")
+# Elements is now inside the source itself. No need for the egg.
+# sys.path.append("lib/Elements-0.13-py2.5.egg")
sys.path.append("lib/Box2D-2.0.2b1-py2.5-linux-i686.egg")
import Box2D as box2d
import elements
@@ -136,19 +137,25 @@ class PhysicsGame:
if type(body.userData) == type({}):
if body.userData.has_key('track_index'):
trackdex = body.userData['track_index']
-
- def round_to_3(n):
- return math.ceil(n * 1000) / 1000.0
-
- roundx = round_to_3(body.position.x)
- roundy = round_to_3(body.position.y)
- tupled_pos = (roundx, roundy)
+
+ def to_screen(pos):
+ px = self.world.meter_to_screen(
+ pos[0])
+ py = self.world.meter_to_screen(
+ pos[1])
+ py = self.world.renderer.get_surface() \
+ .get_height() - py
+ return (px, py)
+
+ x = body.position.x
+ y = body.position.y
+ tupled_pos = to_screen((x, y))
posx = tupled_pos[0]
posy = tupled_pos[1]
try:
self.full_pos_list[trackdex].append(posx)
self.full_pos_list[trackdex].append(posy)
- except:
+ except IndexError:
self.full_pos_list.append([posx, posy])
if type(body.userData) == type({}):
diff --git a/tools.py b/tools.py
index d1af693..63923bb 100644
--- a/tools.py
+++ b/tools.py
@@ -57,6 +57,9 @@ class Tool(object):
# Add ground, because we destroyed it before
self.game.world.add.ground()
+
+ # Also clear the points recorded in pens.
+ self.game.full_pos_list = [[] for _ in self.game.full_pos_list]
elif event.action == "focus_in":
self.game.in_focus = True
elif event.action == "focus_out":
@@ -78,8 +81,17 @@ class Tool(object):
pass
def draw(self):
- # Default drawing method is don't draw anything
- pass
+ # Default drawing method is draw the pen points.
+ full_pos_list = self.game.full_pos_list
+ surface = self.game.world.renderer.get_surface()
+ for i, pos_list in enumerate(full_pos_list):
+ color = self.game.tracked_bodies[i]
+ for i in range(0, len(pos_list), 2):
+ posx = int(pos_list[i])
+ posy = int(pos_list[i+1])
+ pygame.draw.circle(surface, color,
+ (posx, posy),
+ 2)
def cancel(self):
# Default cancel doesn't do anything
@@ -111,6 +123,7 @@ class CircleTool(Tool):
self.pt1 = None
def draw(self):
+ Tool.draw(self)
# Draw a circle from pt1 to mouse
if self.pt1 != None:
delta = distance(self.pt1,
@@ -162,6 +175,7 @@ class BoxTool(Tool):
self.pt1 = None
def draw(self):
+ Tool.draw(self)
# Draw a box from pt1 to mouse
if self.pt1 != None:
mouse_x_y = tuple_to_int(pygame.mouse.get_pos())
@@ -230,6 +244,7 @@ class TriangleTool(Tool):
self.vertices = None
def draw(self):
+ Tool.draw(self)
# Draw a triangle from pt1 to mouse
if self.pt1 != None:
mouse_x_y = tuple_to_int(pygame.mouse.get_pos())
@@ -307,6 +322,7 @@ class PolygonTool(Tool):
self.safe = True
def draw(self):
+ Tool.draw(self)
# Draw the poly being created
if self.vertices:
for i in range(len(self.vertices) - 1):
@@ -362,6 +378,7 @@ class MagicPenTool(Tool):
self.safe = True
def draw(self):
+ Tool.draw(self)
# Draw the poly being created
if self.vertices:
if len(self.vertices) > 1:
@@ -467,6 +484,7 @@ class JointTool(Tool):
self.jb1 = self.jb2 = self.jb1pos = self.jb2pos = None
def draw(self):
+ Tool.draw(self)
if self.jb1:
pygame.draw.line(self.game.screen, (100, 180, 255), self.jb1pos,
tuple_to_int(pygame.mouse.get_pos()), 3)
@@ -585,6 +603,7 @@ class DestroyTool(Tool):
self.cancel()
def draw(self):
+ Tool.draw(self)
# Draw the trail
if self.vertices:
if len(self.vertices) > 1:
@@ -611,18 +630,23 @@ class TrackTool(Tool):
if pygame.mouse.get_pressed()[0]:
current_body = self.game.world.get_bodies_at_pos(
- tuple_to_int(event.pos))[0]
+ tuple_to_int(event.pos))
+
if current_body:
+ current_body = current_body[0]
+ color = current_body.userData['color']
+
point_pos = tuple_to_int(event.pos)
track_circle = self.game.world.add.ball(point_pos, self.radius,
dynamic=True, density=0.001,
restitution=0.16, friction=0.1)
track_circle.userData['track_index'] = len(
self.game.tracked_bodies)
-
self.game.world.add.joint(
- track_circle, current_body, point_pos, point_pos)
- self.game.tracked_bodies.append(track_circle)
+ track_circle, current_body, point_pos, point_pos, False)
+ self.game.tracked_bodies.append(color)
+ # Note: tracked_bodies list stores colors of bodies for color
+ # pen rendering.
def getAllTools():