Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/plugins/physics
diff options
context:
space:
mode:
authorWalter Bender <walter@sugarlabs.org>2013-06-14 11:56:14 (GMT)
committer Walter Bender <walter@sugarlabs.org>2013-06-14 11:56:14 (GMT)
commitd220434cac3f7edc0ebd3dab0fea99d9fe31f052 (patch)
tree4e77280885edfccce426d27f63e96558f1d3c56a /plugins/physics
parent296195b45afb72383b53cc56238106456df15dcc (diff)
add physics plugin
Diffstat (limited to 'plugins/physics')
-rw-r--r--plugins/physics/__init__.py0
-rw-r--r--plugins/physics/icons/physicsoff.svg50
-rw-r--r--plugins/physics/icons/physicson.svg49
-rw-r--r--plugins/physics/physics.py969
4 files changed, 1068 insertions, 0 deletions
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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="55"
+ height="55"
+ viewBox="0 0 55 55"
+ id="svg2"
+ xml:space="preserve"><metadata
+ id="metadata15"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs13" />
+<rect
+ width="42.763924"
+ height="42.763924"
+ x="6.1180382"
+ y="6.1180382"
+ id="rect2986"
+ style="fill:#282828;fill-opacity:1;stroke:#282828;stroke-width:2.23607516;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /><g
+ transform="matrix(0.92179243,0,0,0.92179243,4.0949018,3.6500136)"
+ id="g3085"
+ style="fill:none;stroke:#ffffff;stroke-opacity:1"><circle
+ cx="12"
+ cy="17"
+ r="9"
+ id="circle3080"
+ style="fill:none;stroke:#ffffff;stroke-width:3;stroke-opacity:1" /><path
+ d="M 35.185185,29.174601 16.664804,18.174228 35.451598,7.6352942 z"
+ transform="matrix(0.8539616,-0.5203361,0.5203361,0.8539616,-7.6318759,40.439437)"
+ id="path2210"
+ style="fill:none;stroke:#ffffff;stroke-width:3;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /><path
+ d="M 28.835979,40.608468 10.978836,46.296298 5.2910049,28.439155 23.148148,22.751324 z"
+ transform="matrix(0.8992892,-0.4373546,-0.4373546,-0.8992892,36.526622,53.589052)"
+ id="path2212"
+ style="fill:none;stroke:#ffffff;stroke-width:3;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /><rect
+ width="45.899471"
+ height="4.7037034"
+ ry="1.0486668"
+ x="-1.8733307"
+ y="29.09301"
+ transform="matrix(0.9895466,-0.1442134,0.1442134,0.9895466,0,0)"
+ id="rect2214"
+ style="fill:none;stroke:#ffffff;stroke-width:3;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g></svg> \ 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="55"
+ height="55"
+ viewBox="0 0 55 55"
+ id="svg2"
+ xml:space="preserve"><metadata
+ id="metadata15"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs13" /><rect
+ width="55"
+ height="55"
+ x="0"
+ y="0"
+ id="rect3269"
+ style="fill:#ffd200;fill-opacity:1;fill-rule:nonzero;stroke:none" /><g
+ transform="matrix(0.92179243,0,0,0.92179243,4.0949018,3.6500136)"
+ id="g3085"
+ style="fill:none;stroke:#50a000;stroke-opacity:1"><circle
+ cx="12"
+ cy="17"
+ r="9"
+ id="circle3080"
+ style="fill:none;stroke:#50a000;stroke-width:3;stroke-opacity:1" /><path
+ d="M 35.185185,29.174601 16.664804,18.174228 35.451598,7.6352942 z"
+ transform="matrix(0.8539616,-0.5203361,0.5203361,0.8539616,-7.6318759,40.439437)"
+ id="path2210"
+ style="fill:none;stroke:#50a000;stroke-width:3;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /><path
+ d="M 28.835979,40.608468 10.978836,46.296298 5.2910049,28.439155 23.148148,22.751324 z"
+ transform="matrix(0.8992892,-0.4373546,-0.4373546,-0.8992892,36.526622,53.589052)"
+ id="path2212"
+ style="fill:none;stroke:#50a000;stroke-width:3;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /><rect
+ width="45.899471"
+ height="4.7037034"
+ ry="1.0486668"
+ x="-1.8733307"
+ y="29.09301"
+ transform="matrix(0.9895466,-0.1442134,0.1442134,0.9895466,0,0)"
+ id="rect2214"
+ style="fill:none;stroke:#50a000;stroke-width:3;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g></svg> \ 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