From d220434cac3f7edc0ebd3dab0fea99d9fe31f052 Mon Sep 17 00:00:00 2001 From: Walter Bender Date: Fri, 14 Jun 2013 11:56:14 +0000 Subject: add physics plugin --- diff --git a/plugins/physics/__init__.py b/plugins/physics/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/physics/__init__.py diff --git a/plugins/physics/icons/physicsoff.svg b/plugins/physics/icons/physicsoff.svg new file mode 100644 index 0000000..aa75606 --- /dev/null +++ b/plugins/physics/icons/physicsoff.svg @@ -0,0 +1,50 @@ + + + +image/svg+xml + \ No newline at end of file diff --git a/plugins/physics/icons/physicson.svg b/plugins/physics/icons/physicson.svg new file mode 100644 index 0000000..883be8e --- /dev/null +++ b/plugins/physics/icons/physicson.svg @@ -0,0 +1,49 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/plugins/physics/physics.py b/plugins/physics/physics.py new file mode 100644 index 0000000..14d8d45 --- /dev/null +++ b/plugins/physics/physics.py @@ -0,0 +1,969 @@ +#!/usr/bin/env python +#Copyright (c) 2011 Walter Bender +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +from math import pi, sin, cos, sqrt, atan2 +from random import uniform + +from gettext import gettext as _ + +from sugar.datastore import datastore +from sugar import profile + +from plugins.plugin import Plugin +from TurtleArt.tapalette import make_palette +from TurtleArt.talogo import primitive_dictionary +from TurtleArt.tautils import debug_output, json_dump, get_path, round_int + +import logging +_logger = logging.getLogger('turtleart-activity physics plugin') + + +THRESHOLD = 0.1 # default distance metric for hits + + +class Physics(Plugin): + + SCALE_FACTOR = 10. + LINE_SCALE = 24. + TOOTH_SCALE = 10 + TOOTH_ANGLE = 75 * pi / 180. + + def __init__(self, parent): + self._tw = parent + self._status = True + self._scale = self.SCALE_FACTOR / self._tw.canvas.width + self._id = 1 + self._density = 1. + self._friction = 0.5 + self._bounce = 0.15 + self._dynamic = True + self._polygon = [] + self._dict = {'bodylist': [], + 'jointlist': [], + 'controllerlist': [], + 'additional_vars': {}} + self._prim_box2d_reset() + + def setup(self): + # set up physics specific blocks + palette = make_palette('physics', + colors=['#50A000', '#60C020'], + help_string=_('Palette of physics blocks')) + + primitive_dictionary['box2dcircle'] = self._prim_box2d_circle + primitive_dictionary['box2dtriangle'] = self._prim_box2d_triangle + primitive_dictionary['box2drectangle'] = self._prim_box2d_rectangle + primitive_dictionary['box2dgear'] = self._prim_box2d_gear + primitive_dictionary['savebox2d'] = self._prim_save_box2d + primitive_dictionary['box2ddensity'] = self._prim_box2d_density + primitive_dictionary['box2dfriction'] = self._prim_box2d_friction + primitive_dictionary['box2dbounce'] = self._prim_box2d_bounce + primitive_dictionary['box2ddynamic'] = self._prim_box2d_dynamic + primitive_dictionary['box2dmotor'] = self._prim_box2d_motor + primitive_dictionary['box2dpin'] = self._prim_box2d_motor + primitive_dictionary['box2dreset'] = self._prim_box2d_reset + primitive_dictionary['box2djoint'] = self._prim_box2d_joint + primitive_dictionary['box2dstartpolygon'] = \ + self._prim_box2d_start_polygon + primitive_dictionary['box2daddpoint'] = self._prim_box2d_add_point + primitive_dictionary['box2dendpolygon'] = self._prim_box2d_end_polygon + primitive_dictionary['box2dendfilledpolygon'] = \ + self._prim_box2d_end_filled_polygon + + palette.add_block('density', + style='basic-style-1arg', + label=_('density'), + default=100, + help_string=_('Set the density property for objects \ +(density can be any positive number).'), + prim_name='box2ddensity') + self._tw.lc.def_prim( + 'box2ddensity', 1, + lambda self, x: primitive_dictionary['box2ddensity'](x)) + palette.add_block('friction', + style='basic-style-1arg', + label=_('friction'), + default=50, + help_string=_('Set the friction property for \ +objects (value from 0 to 100, where 0 turns friction off and 100 is strong \ +friction).'), + prim_name='box2dfriction') + self._tw.lc.def_prim( + 'box2dfriction', 1, + lambda self, x: primitive_dictionary['box2dfriction'](x)) + palette.add_block('bounce', + style='basic-style-1arg', + label=_('bounciness'), + default=15, + help_string=_('Set the bounciness property for \ +objects (a value from 0 to 100, where 0 means no bounce and 100 is very \ +bouncy).'), + prim_name='box2dbounce') + self._tw.lc.def_prim( + 'box2dbounce', 1, + lambda self, x: primitive_dictionary['box2dbounce'](x)) + palette.add_block('dynamic', + style='basic-style-1arg', + label=_('dynamic'), + hidden=True, # hide until we debug it + default=1, + help_string=_('If dynamic = 1, the object can move; \ +if dynamic = 0, it is fixed in position.'), + prim_name='box2ddynamic') + self._tw.lc.def_prim( + 'box2ddynamic', 1, + lambda self, x: primitive_dictionary['box2ddynamic'](x)) + palette.add_block('startpolygon', + style='basic-style-extended-vertical', + label=_('start polygon'), + help_string=_('Begin defining a new polygon based \ +on the current Turtle xy position.'), + prim_name='box2dstartpolygon') + self._tw.lc.def_prim( + 'box2dstartpolygon', 0, + lambda self: primitive_dictionary['box2dstartpolygon']()) + palette.add_block('addpoint', + style='basic-style-extended-vertical', + label=_('add point'), + help_string=_('Add a new point to the current \ +polygon based on the current Turtle xy position.'), + prim_name='box2daddpoint') + self._tw.lc.def_prim( + 'box2daddpoint', 0, + lambda self: primitive_dictionary['box2daddpoint']()) + palette.add_block('endpolygon', + style='basic-style-extended-vertical', + label=_('end polygon'), + help_string=_('Define a new polygon.'), + prim_name='box2dendpolygon') + self._tw.lc.def_prim( + 'box2dendpolygon', 0, + lambda self: primitive_dictionary['box2dendpolygon']()) + palette.add_block('endfilledpolygon', + style='basic-style-extended-vertical', + label=_('end filled polygon'), + # hidden=True, # until it is debugged + help_string=_('Define a new flled polygon.'), + prim_name='box2dendfilledpolygon') + self._tw.lc.def_prim( + 'box2dendfilledpolygon', 0, + lambda self: primitive_dictionary['box2dendfilledpolygon']( + triangulate=True)) + palette.add_block('triangle', + style='basic-style-2arg', + label=[_('triangle'), _('base'), _('height')], + # make an equilateral triangle by default + default=[100, round_int(100 * sin(pi / 3))], + help_string=_('Add a triangle object to the \ +project.'), + prim_name='box2dtriangle') + self._tw.lc.def_prim( + 'box2dtriangle', 2, + lambda self, x, y: primitive_dictionary['box2dtriangle'](x, y)) + palette.add_block('circle', + style='basic-style-1arg', + label=_('circle'), + default=100, + help_string=_('Add a circle object to the project.'), + prim_name='box2dcircle') + self._tw.lc.def_prim( + 'box2dcircle', 1, + lambda self, x: primitive_dictionary['box2dcircle'](x)) + palette.add_block('rectangle', + style='basic-style-2arg', + label=[_('rectangle'), _('width'), _('height')], + default=[100, 100], + help_string=_('Add a rectangle object to the \ +project.'), + prim_name='box2drectangle') + self._tw.lc.def_prim( + 'box2drectangle', 2, + lambda self, x, y: primitive_dictionary['box2drectangle'](x, y)) + palette.add_block('gear', + style='basic-style-1arg', + label=_('gear'), + default=12, + help_string=_('Add a gear object to the project.'), + prim_name='box2dgear') + self._tw.lc.def_prim( + 'box2dgear', 1, + lambda self, x: primitive_dictionary['box2dgear'](x)) + palette.add_block('reset', + hidden=True, + style='basic-style-extended-vertical', + label=_('reset'), + help_string=_('Reset the project; clear the object \ +list.'), + prim_name='box2dreset') + self._tw.lc.def_prim( + 'box2dreset', 0, + lambda self: primitive_dictionary['box2dreset']()) + palette.add_block('motor', + style='basic-style-2arg', + label=[_('motor'), _('torque'), _('speed')], + default=[900, -10], + help_string=_('Motor torque and speed range from 0 \ +(off) to positive numbers; motor is placed on the most recent object \ +created.'), + prim_name='box2dmotor') + self._tw.lc.def_prim( + 'box2dmotor', 2, + lambda self, x, y: primitive_dictionary['box2dmotor'](x, y)) + palette.add_block('pin', + style='basic-style-extended-vertical', + label=_('pin'), + help_string=_('Pin an object down so that it cannot \ +fall.'), + prim_name='box2dpin') + self._tw.lc.def_prim( + 'box2dpin', 0, + lambda self: primitive_dictionary['box2dmotor'](0, 0)) + palette.add_block('joint', + style='basic-style-2arg', + label=[_('joint'), _('x'), _('y')], + default=[0, 0], + help_string=_('Join two objects together (the most \ +recent object created and the object at point x, y).'), + prim_name='box2djoint') + self._tw.lc.def_prim( + 'box2djoint', 2, + lambda self, x, y: primitive_dictionary['box2djoint'](x, y)) + palette.add_block('savebox2d', + style='basic-style-1arg', + label=_('save as Physics activity'), + default='physics project', + help_string=_('Save the project to the Journal as \ +a Physics activity.'), + prim_name='savebox2d') + self._tw.lc.def_prim( + 'savebox2d', 1, + lambda self, x: primitive_dictionary['savebox2d'](x)) + + def _status_report(self): + ''' Required method ''' + debug_output('Reporting physics status: %s' % (str(self._status))) + return self._status + + def clear(self): + ''' Erase button pressed or clean block executed ''' + self._prim_box2d_reset() + + # Block primitives used in talogo + + def _prim_box2d_reset(self): + ''' Clear the body list ''' + self._id = 1 + self._density = 1. + self._friction = 0.5 + self._bounce = 0.15 + self._dynamic = True + self._polygon = [] + self._dict['bodylist'] = [] + self._dict['jointlist'] = [] + # Always start with a ground plane + self._dict['bodylist'].append( + {'userData': {'color': [114, 114, 185], 'saveid': 1}, + 'linearVelocity': [0.0, 0.0], + 'dynamic': False, + 'angularVelocity': 0.0, + 'shapes': [{'restitution': 0.15, + 'type': 'polygon', + 'vertices': [[-50.0, -0.1], + [50.0, -0.1], + [50.0, 0.1], + [-50.0, 0.1]], + 'friction': 0.5, + 'density': 0.0}], + 'position': [-10.0, 0.0], + 'angle': 0.0}) + + def _prim_box2d_density(self, density): + ''' set the density to be used when creating box2d objects ''' + try: + self._density = abs(float(density) / 100.) + except ValueError: + debug_output('bad argument to density: must be positive float', + self._tw.running_sugar) + self._density = 1. + + def _prim_box2d_friction(self, friction): + ''' set the friction to be used when creating box2d objects ''' + try: + self._friction = abs(float(friction) / 100.) + if self._friction > 1: + self._friction == 1 + debug_output('max friction value is 100', + self._tw.running_sugar) + except ValueError: + debug_output('bad argument to friction: must be positive float', + self._tw.running_sugar) + self._friction = 0.5 + + def _prim_box2d_bounce(self, bounce): + ''' set the bounce to be used when creating box2d objects ''' + try: + self._bounce = abs(float(bounce) / 100.) + if self._bounce > 1.: + self._bounce == 1. + debug_output('max bounce value is 100', + self._tw.running_sugar) + except ValueError: + debug_output('bad argument to bounce: must be a positive float', + self._tw.running_sugar) + self._bounce = 0.5 + + def _prim_box2d_dynamic(self, value): + ''' set the dynamic flag to be used when creating box2d objects ''' + if str(value).lower() in [_('false'), _('no'), '0']: + self._dynamic = False + else: + self._dynamic = True + + def _prim_box2d_start_polygon(self): + ''' start of a collection of points to create a polygon ''' + self._polygon = [(self._tw.canvas.xcor + self._tw.canvas.width / 2., + self._tw.canvas.ycor + self._tw.canvas.height / 2.)] + + def _prim_box2d_add_point(self): + ''' add an point to a collection of points to create a polygon ''' + x = self._tw.canvas.xcor + self._tw.canvas.width / 2. + y = self._tw.canvas.ycor + self._tw.canvas.height / 2. + if self._polygon == []: + self._polygon.append((x, y)) + elif not (x == self._polygon[-1][0] and y == self._polygon[-1][1]): + self._polygon.append((x, y)) + + def _prim_box2d_end_polygon(self): + ''' add a polygon object to box2d dictionary ''' + if not self._status: + return + if self._polygon == []: + return + else: + x = self._tw.canvas.xcor + self._tw.canvas.width / 2. + y = self._tw.canvas.ycor + self._tw.canvas.height / 2. + + # Only append the last point if it is not redundant + if not self._near((x, y), (self._polygon[-1])): + self._polygon.append((x, y)) + + # Box2d chokes on polygons with just 1 point + if len(self._polygon) == 1: + return + # The overall position will be relative to the first point + xpos = self._polygon[0][0] * self._scale + ypos = self._polygon[0][1] * self._scale + + # Create the Physics object... + self._id += 1 + self._dict['bodylist'].append( + {'userData': {'color': self._tw.canvas.fgrgb, + 'saveid': self._id}, + 'linearVelocity': [0.0, 0.0], + 'dynamic': self._dynamic, + 'angularVelocity': 0.0, + 'shapes': [], + 'position': [xpos, ypos], + 'angle': 0.0}) + + for i, p in enumerate(self._polygon): + if i == 0: + p0 = p[:] + continue + p1 = p[:] + self._dict['bodylist'][-1]['shapes'].append( + {'density': self._density, + 'friction': self._friction, + 'type': 'polygon', + 'vertices': [], + 'restitution': self._bounce}) + + a = atan2(p0[1] - p1[1], p0[0] - p1[0]) + dx = sin(a) / self.LINE_SCALE + dy = -cos(a) / self.LINE_SCALE + poly = [[p0[0] * self._scale + dx - xpos, + p0[1] * self._scale + dy - ypos], + [p1[0] * self._scale + dx - xpos, + p1[1] * self._scale + dy - ypos], + [p1[0] * self._scale - dx - xpos, + p1[1] * self._scale - dy - ypos], + [p0[0] * self._scale - dx - xpos, + p0[1] * self._scale - dy - ypos]] + # Make sure points are counter-clockwise + if self._cross_product_area(poly) < 0: + poly = self._reverse_order(poly)[:] + self._dict['bodylist'][-1]['shapes'][-1][ + 'vertices'].append(poly[0]) + self._dict['bodylist'][-1]['shapes'][-1][ + 'vertices'].append(poly[1]) + self._dict['bodylist'][-1]['shapes'][-1][ + 'vertices'].append(poly[2]) + self._dict['bodylist'][-1]['shapes'][-1][ + 'vertices'].append(poly[3]) + if not (i + 1) == len(self._polygon): + self._dict['bodylist'][-1]['shapes'].append( + {'localPosition': [p1[0] * self._scale - xpos, + p1[1] * self._scale - ypos], + 'density': self._density, + 'friction': self._friction, + 'radius': 1. / self.LINE_SCALE, + 'type': 'circle', + 'restitution': self._bounce}) + p0 = p1[:] + + # ... and draw the polygon on the Turtle canvas + self._tw.canvas.canvas.set_source_rgb( + self._tw.canvas.fgrgb[0] / 255., + self._tw.canvas.fgrgb[1] / 255., + self._tw.canvas.fgrgb[2] / 255.) + self._tw.canvas.canvas.set_line_width(1.) + for s in self._dict['bodylist'][-1]['shapes']: + if s['type'] == 'polygon': + self._tw.canvas.canvas.new_path() + for i, p in enumerate(s['vertices']): + x, y = self._tw.canvas.turtle_to_screen_coordinates( + (p[0] + xpos) / self._scale - \ + self._tw.canvas.width / 2., + (p[1] + ypos) / self._scale - \ + self._tw.canvas.height / 2.) + if i == 0: + self._tw.canvas.canvas.move_to(x, y) + else: + self._tw.canvas.canvas.line_to(x, y) + self._tw.canvas.canvas.close_path() + self._tw.canvas.canvas.fill() + elif s['type'] == 'circle': + x, y = self._tw.canvas.turtle_to_screen_coordinates( + (s['localPosition'][0] + xpos) / self._scale - \ + self._tw.canvas.width / 2., + (s['localPosition'][1] + ypos) / self._scale - \ + self._tw.canvas.height / 2.) + self._tw.canvas.canvas.set_line_width(2. / ( + self.LINE_SCALE * self._scale)) + self._tw.canvas.canvas.move_to(x, y) + self._tw.canvas.canvas.line_to(x + 1, y + 1) + self._tw.canvas.canvas.stroke() + self._tw.canvas.canvas.set_line_width(self._tw.canvas.pensize) + self._tw.canvas.inval() + + self._polygon = [] + + def _prim_box2d_end_filled_polygon(self, triangulate=False): + ''' add a filled-polygon object to box2d dictionary ''' + if not self._status: + return + if self._polygon == []: + return + else: + x = self._tw.canvas.xcor + self._tw.canvas.width / 2. + y = self._tw.canvas.ycor + self._tw.canvas.height / 2. + + # Make sure there are no points too near each other + poly = [] + p1 = self._polygon[-1] + for p2 in self._polygon: + if not self._near(p1, p2): + poly.append(p2) + p1 = p2[:] + self._polygon = poly[:] + + if len(self._polygon) < 3: + return + + # Physics requires polygons to be ordered counter clockwise + if self._cross_product_area(self._polygon) < 0: + self._polygon = self._reverse_order(self._polygon)[:] + + # Divide the polygon into triangles + if triangulate: + triangles = self._triangulate(self._polygon) + if triangles is None: + debug_output(_('Not a simple polygon'), + self._tw.running_sugar) + self._tw.showlabel('syntaxerror', + _('Not a simple polygon')) + return + + # Create the Physics object... + xpos = self._polygon[0][0] * self._scale + ypos = self._polygon[0][1] * self._scale + self._id += 1 + self._dict['bodylist'].append( + {'userData': {'color': self._tw.canvas.fgrgb, + 'saveid': self._id}, + 'linearVelocity': [0.0, 0.0], + 'dynamic': self._dynamic, + 'angularVelocity': 0.0, + 'shapes': [], + 'position': [xpos, ypos], + 'angle': 0.0}) + + if triangulate: + for triangle in triangles: + self._add_shape(triangle, xpos, ypos) + else: + self._add_shape(self._polygon, xpos, ypos) + + # ...and draw it on the Turtle canvas + self._tw.canvas.canvas.set_source_rgb( + self._tw.canvas.fgrgb[0] / 255., + self._tw.canvas.fgrgb[1] / 255., + self._tw.canvas.fgrgb[2] / 255.) + self._tw.canvas.canvas.set_line_width(1.) + if triangulate: + for triangle in triangles: + # Make each triangle distinct in the TA rendering + self._randomize_color() + self._draw_polygon(triangle) + # Restore canvas color + self._tw.canvas.canvas.set_source_rgb( + self._tw.canvas.fgrgb[0] / 255., + self._tw.canvas.fgrgb[1] / 255., + self._tw.canvas.fgrgb[2] / 255.) + else: + self._draw_polygon(self._polygon) + self._tw.canvas.canvas.set_line_width(self._tw.canvas.pensize) + self._tw.canvas.inval() + + self._polygon = [] + + def _bounds_check(self, value): + ''' Make sure value is between 0 and 1 ''' + if value < 0.: + value = 0. + elif value > 1.: + value = 1 + return value + + def _randomize_color(self): + ''' Add a bit of noise to the turtle color ''' + dr = uniform(-10, 10) / 100. + dg = uniform(-10, 10) / 100. + db = uniform(-10, 10) / 100. + self._tw.canvas.canvas.set_source_rgb( + self._bounds_check((self._tw.canvas.fgrgb[0] / 255.) + dr), + self._bounds_check((self._tw.canvas.fgrgb[1] / 255.) + dg), + self._bounds_check((self._tw.canvas.fgrgb[2] / 255.) + db)) + + def _draw_polygon(self, polygon): + ''' Draw a polygon on the turtle canvas ''' + self._tw.canvas.canvas.new_path() + for i, p in enumerate(polygon): + x, y = self._tw.canvas.turtle_to_screen_coordinates( + p[0] - self._tw.canvas.width / 2., + p[1] - self._tw.canvas.height / 2.) + if i == 0: + self._tw.canvas.canvas.move_to(x, y) + else: + self._tw.canvas.canvas.line_to(x, y) + self._tw.canvas.canvas.close_path() + self._tw.canvas.canvas.fill() + + def _add_shape(self, polygon, xpos, ypos): + ''' Add a polygon to the shape list ''' + self._dict['bodylist'][-1]['shapes'].append( + {'density': self._density, + 'friction': self._friction, + 'type': 'polygon', + 'vertices': [], + 'restitution': self._bounce}) + for i, p in enumerate(polygon): + self._dict['bodylist'][-1]['shapes'][-1][ + 'vertices'].append([p[0] * self._scale - xpos, + p[1] * self._scale - ypos]) + + def _prim_box2d_triangle(self, base, height): + ''' add a triangle object to box2d dictionary ''' + try: + float(base) + float(height) + except ValueError: + debug_output( + 'bad argument to triangle: base, height must be float', + self._tw.running_sugar) + self._polygon = [] + x = self._tw.canvas.xcor + self._tw.canvas.width / 2. + y = self._tw.canvas.ycor + self._tw.canvas.height / 2. + self._polygon.append([x - base / 2., y - height / 2.]) + self._polygon.append([x, y + height / 2.]) + self._polygon.append([x + base / 2., y - height / 2.]) + if self._tw.canvas.heading != 0: + self._rotate_polygon(x, y, self._tw.canvas.heading * pi / 180.) + self._prim_box2d_end_filled_polygon() + + def _prim_box2d_rectangle(self, width, height): + ''' add a rectangle object to box2d dictionary ''' + try: + float(width) + float(height) + except ValueError: + debug_output( + 'bad argument to rectangle: width, height must be float', + self._tw.running_sugar) + self._polygon = [] + x = self._tw.canvas.xcor + self._tw.canvas.width / 2. + y = self._tw.canvas.ycor + self._tw.canvas.height / 2. + self._polygon.append([x - width / 2., y - height / 2.]) + self._polygon.append([x + width / 2., y - height / 2.]) + self._polygon.append([x + width / 2., y + height / 2.]) + self._polygon.append([x - width / 2., y + height / 2.]) + if self._tw.canvas.heading != 0: + self._rotate_polygon(x, y, self._tw.canvas.heading * pi / 180.) + self._prim_box2d_end_filled_polygon() + + def _prim_box2d_gear(self, tooth_count): + ''' add a gear object to box2d dictionary ''' + try: + if abs(int(tooth_count)) < 2: + raise ValueError + except ValueError: + debug_output('bad argument to gear: tooth count must be int > 1', + self._tw.running_sugar) + + self._polygon = [] + x = self._tw.canvas.xcor + self._tw.canvas.width / 2. + y = self._tw.canvas.ycor + self._tw.canvas.height / 2. + points = self._gear(int(tooth_count)) + for p in points: + self._polygon.append([x + p[0], y + p[1]]) + if self._tw.canvas.heading != 0: + self._rotate_polygon(x, y, self._tw.canvas.heading * pi / 180.) + self._prim_box2d_end_filled_polygon(triangulate=True) + + def _prim_box2d_circle(self, radius): + ''' add a circle object to box2d dictionary ''' + try: + float(radius) + except ValueError: + debug_output('bad argument to circle: radius must be float', + self._tw.running_sugar) + + # Create the Physics object... + x = self._tw.canvas.xcor + self._tw.canvas.width / 2. + y = self._tw.canvas.ycor + self._tw.canvas.height / 2. + self._id += 1 + self._dict['bodylist'].append( + {'userData': {'color': self._tw.canvas.fgrgb, + 'saveid': self._id}, + 'linearVelocity': [0.0, 0.0], + 'dynamic': self._dynamic, + 'angularVelocity': 0.0, + 'shapes': [{'localPosition': [0, 0], + 'density': self._density, + 'friction': self._friction, + 'radius': radius * self._scale / 2., + 'type': 'circle', + 'restitution': self._bounce}], + 'position': [x * self._scale, y * self._scale], + 'angle': 0.0}) + + # ...and draw it on the Turtle canvas + x, y = self._tw.canvas.turtle_to_screen_coordinates( + self._tw.canvas.xcor, self._tw.canvas.ycor) + self._tw.canvas.canvas.set_source_rgb( + self._tw.canvas.fgrgb[0] / 255., + self._tw.canvas.fgrgb[1] / 255., + self._tw.canvas.fgrgb[2] / 255.) + self._tw.canvas.canvas.set_line_width(radius) + self._tw.canvas.canvas.move_to(x, y) + self._tw.canvas.canvas.line_to(x + 1, y + 1) + self._tw.canvas.canvas.stroke() + self._tw.canvas.canvas.set_line_width(self._tw.canvas.pensize) + self._tw.canvas.inval() + + def _prim_box2d_motor(self, torque, speed): + ''' add a motor to an object to box2d dictionary ''' + try: + float(torque) + float(speed) + except ValueError: + debug_output('bad argument to motor: torque, speed must be float', + self._tw.running_sugar) + + # Create the Physics object... + x = self._tw.canvas.xcor + self._tw.canvas.width / 2. + y = self._tw.canvas.ycor + self._tw.canvas.height / 2. + self._dict['jointlist'].append( + {'userData': None, + 'collideConnected': False, + 'maxMotorTorque': torque, + 'motorSpeed': speed, + 'body1': 0, + # Assume that the motor is attached to the most recent object + 'body2': self._id, + 'type': 'revolute', + 'anchor': [x * self._scale, y * self._scale], + 'enableMotor': False}) + if speed != 0: + self._dict['jointlist'][-1]['enableMotor'] = True + + # To do: search for body to attach to... + id = self._search((x * self._scale, y * self._scale)) + if id is not None: + # debug_output('found a match for motor body2 (%d)' % (id), + # self._tw.running_sugar) + self._dict['jointlist'][-1]['body2'] + + # ...and draw it on the Turtle canvas + x, y = self._tw.canvas.turtle_to_screen_coordinates( + self._tw.canvas.xcor, self._tw.canvas.ycor) + if speed == 0: + self._tw.canvas.canvas.set_source_rgb(0., 0., 0.) + else: + self._tw.canvas.canvas.set_source_rgb(1., 1., 1.) + self._tw.canvas.canvas.set_line_width(3.) + self._tw.canvas.canvas.move_to(x, y) + self._tw.canvas.canvas.line_to(x + 1, y + 1) + self._tw.canvas.canvas.stroke() + self._tw.canvas.canvas.set_line_width(self._tw.canvas.pensize) + self._tw.canvas.inval() + + def _prim_box2d_joint(self, x, y): + ''' add a joint between two objects ''' + try: + float(x) + float(y) + except ValueError: + debug_output('bad argument to joint: x, y must be float', + self._tw.running_sugar) + + # Create the Physics object... + x1 = x + self._tw.canvas.width / 2. + y1 = y + self._tw.canvas.height / 2. + x2 = self._tw.canvas.xcor + self._tw.canvas.width / 2. + y2 = self._tw.canvas.ycor + self._tw.canvas.height / 2. + self._dict['jointlist'].append( + {'userData': None, + 'anchor2': [x1 * self._scale, y1 * self._scale], + 'anchor1': [x2 * self._scale, y2 * self._scale], + 'collideConnected': True, + 'body1': self._id, # A reasonable default? + 'body2': 2, # Assume most recent? + 'type': 'distance'}) + + # Search for the body to attach to... + id = self._search((x1 * self._scale, y1 * self._scale)) + if id is not None: + # debug_output('found a match for joint body2 (%d)' % (id), + # self._tw.running_sugar) + self._dict['jointlist'][-1]['body2'] = id + id = self._search((x2 * self._scale, y2 * self._scale)) + if id is not None: + # debug_output('found a match for joint body1 (%d)' % (id), + # self._tw.running_sugar) + self._dict['jointlist'][-1]['body1'] = id + + # ...and draw it on the Turtle canvas + x1, y1 = self._tw.canvas.turtle_to_screen_coordinates(x, y) + x2, y2 = self._tw.canvas.turtle_to_screen_coordinates( + self._tw.canvas.xcor, self._tw.canvas.ycor) + self._tw.canvas.canvas.set_source_rgb(0., 0., 0.) + self._tw.canvas.canvas.set_line_width(3.) + self._tw.canvas.canvas.move_to(x1, y1) + self._tw.canvas.canvas.line_to(x2, y2) + self._tw.canvas.canvas.stroke() + self._tw.canvas.canvas.set_line_width(self._tw.canvas.pensize) + self._tw.canvas.inval() + + def _prim_save_box2d(self, name): + ''' Save bodylist to a Physics project ''' + data = json_dump(self._dict) + if not self._tw.running_sugar: + print data + else: + data_path = get_path(self._tw.activity, 'instance') + tmp_file = os.path.join(data_path, 'tmpfile') + fd = open(tmp_file, 'w') + fd.write(data) + fd.close() + dsobject = datastore.create() + dsobject.metadata['title'] = name + dsobject.metadata['icon-color'] = profile.get_color().to_string() + dsobject.metadata['mime_type'] = 'application/x-physics-activity' + dsobject.metadata['activity'] = 'org.laptop.physics' + dsobject.set_file_path(tmp_file) + datastore.write(dsobject) + dsobject.destroy() + os.remove(tmp_file) + + def _cross_product_area(self, polygon): + ''' Cross-product area is positive for counter-clockwise polygons ''' + a = 0. + for i in range(len(polygon)): + if (i + 1) < len(polygon): + a += (polygon[i][0] * polygon[i + 1][1]) - \ + (polygon[i + 1][0] * polygon[i][1]) + else: + a += (polygon[i][0] * polygon[0][1]) - \ + (polygon[0][0] * polygon[i][1]) + return a / 2. + + def _reverse_order(self, polygon): + ''' Turn a clockwise polygon into a counter-clockwise polygon ''' + n = len(polygon) + for i in range(n / 2): + tmp = polygon[i][:] + polygon[i] = polygon[n - 1 - i][:] + polygon[n - 1 - i] = tmp[:] + return polygon + + def _rotate_polygon(self, cx, cy, angle): + ''' Rotate the polygon points around cx,cy ''' + for p in self._polygon: + h = sqrt((cx - p[0]) * (cx - p[0]) + (cy - p[1]) * (cy - p[1])) + a = atan2(cx - p[0], cy - p[1]) + p[0] = cx + h * cos(a + angle) + p[1] = cy + h * sin(a + angle) + + def _near(self, p1, p2, threshold=THRESHOLD): + ''' Is point 1 near point 2? ''' + if sqrt((p1[0] - p2[0]) * (p1[0] - p2[0]) + \ + (p1[1] - p2[1]) * (p1[1] - p2[1])) < threshold: + return True + return False + + def _search(self, point): + ''' Return object id of object under point ''' + n = len(self._dict['bodylist']) + for i in range(n): + j = n - i - 1 # search in reverse order + if self._hit(self._dict['bodylist'][j]['shapes'], + self._dict['bodylist'][j]['position'], point): + return self._dict['bodylist'][j]['userData']['saveid'] + return None + + def _hit(self, shapes, position, point): + ''' Is xy in shape? ''' + for s in shapes: + if s['type'] == 'circle': + if self._near((s['localPosition'][0] + position[0], + s['localPosition'][1] + position[1]), + point, threshold=s['radius']): + return True + else: # polygon + if self._point_in_polygon((point[0] - position[0], + point[1] - position[1]), + s['vertices']): + return True + return False + + def _point_in_polygon(self, point, polygon): + '''Ray-casting method of determing if point is in polygon ''' + inside = False + p1x, p1y = polygon[0] + for i in range(len(polygon) + 1): + p2x, p2y = polygon[i % len(polygon)] + if point[1] > min(p1y, p2y): + if point[1] <= max(p1y, p2y): + if point[0] <= max(p1x, p2x): + if p1y != p2y: + xinters = (point[1] - p1y) * (p2x - p1x) \ + / (p2y - p1y) + p1x + if p1x == p2x or point[0] <= xinters: + inside = not inside + p1x, p1y = p2x, p2y + return inside + + def _point_in_triangle(self, triangle, point): + ''' Is the point in the triangle? ''' + return \ + self._cross_product_area( + (triangle[0], triangle[1], point)) >= 0 and \ + self._cross_product_area( + (triangle[1], triangle[2], point)) >= 0 and \ + self._cross_product_area( + (triangle[2], triangle[0], point)) >= 0 + + def _triangulate(self, polygon): + ''' Convert a polygon into triangles ''' + # Variation of an ear-cutting algorithm + # Based on an algorithm by Gregor Lingl on python.org + triangles = [] + while len(polygon) > 2: + found_a_triangle = False + count = 0 + while not found_a_triangle and count < len(polygon): + count += 1 + triangle = polygon[:3] + if self._cross_product_area(triangle) >= 0: + for point in polygon[3:]: + if self._point_in_triangle(triangle, point): + break + else: + triangles.append(triangle) + polygon.remove(triangle[1]) + found_a_triangle = True + polygon.append(polygon.pop(0)) + if count == len(polygon): + return None + return triangles + + def _gear(self, tooth_count): + ''' Draw a gear ''' + points = [] + x = 0 + y = 0 + heading = 0 + for i in range(tooth_count): + tooth, heading = self._gear_tooth(x, y, heading) + for p in tooth: + points.append(p) + x, y = tooth[-1][0], tooth[-1][1] + heading -= 2. * pi / tooth_count + minx = 1000 + miny = 1000 + maxx = -1000 + maxy = -1000 + for p in points: + if p[0] < minx: + minx = p[0] + if p[0] > maxx: + maxx = p[0] + if p[1] < miny: + miny = p[1] + if p[1] > maxy: + maxy = p[1] + # Recenter on 0, 0 + cx = (maxx + minx) / 2. + cy = (maxy + miny) / 2. + for p in points: + p[0] -= cx + p[1] -= cy + return points + + def _gear_tooth(self, x, y, heading): + ''' Draw one tooth of a gear ''' + points = [] + half = self.TOOTH_SCALE / 2. + top = 1.5 * self.TOOTH_SCALE - \ + (cos(self.TOOTH_ANGLE) * self.TOOTH_SCALE * 2.) + points.append([half * cos(heading) + x, + half * sin(heading) + y]) + heading += self.TOOTH_ANGLE + points.append([self.TOOTH_SCALE * cos(heading) + points[0][0], + self.TOOTH_SCALE * sin(heading) + points[0][1]]) + heading -= self.TOOTH_ANGLE + points.append([top * cos(heading) + points[1][0], + top * sin(heading) + points[1][1]]) + heading -= self.TOOTH_ANGLE + points.append([self.TOOTH_SCALE * cos(heading) + points[2][0], + self.TOOTH_SCALE * sin(heading) + points[2][1]]) + heading += self.TOOTH_ANGLE + points.append([half * cos(heading) + points[3][0], + half * sin(heading) + points[3][1]]) + return points, heading -- cgit v0.9.1