Hans Petter Langtangen 13 年之前
父节点
当前提交
b9f0304209
共有 67 个文件被更改,包括 455 次插入145 次删除
  1. 7 0
      doc/src/sketcher/Shape1a.dot
  2. 10 0
      doc/src/sketcher/Shape1b.dot
  3. 13 0
      doc/src/sketcher/Shape2.py
  4. 二进制
      doc/src/sketcher/figs-sketcher/Shape1a.png
  5. 二进制
      doc/src/sketcher/figs-sketcher/Shape1b.png
  6. 二进制
      doc/src/sketcher/figs-sketcher/Shape2.png
  7. 二进制
      doc/src/sketcher/figs-sketcher/vehicle0_hier1.png
  8. 二进制
      doc/src/sketcher/figs-sketcher/vehicle0_hier2.png
  9. 二进制
      doc/src/sketcher/figs-sketcher/wheel_on_inclined_plane.png
  10. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle0/anim.html
  11. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0000.png
  12. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0001.png
  13. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0002.png
  14. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0003.png
  15. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0004.png
  16. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0005.png
  17. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0006.png
  18. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0007.png
  19. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0008.png
  20. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0009.png
  21. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0010.png
  22. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0011.png
  23. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0012.png
  24. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0013.png
  25. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0014.png
  26. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0015.png
  27. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0016.png
  28. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0017.png
  29. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0018.png
  30. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0019.png
  31. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0020.png
  32. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0021.png
  33. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0022.png
  34. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0023.png
  35. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0024.png
  36. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle1/anim.html
  37. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0000.png
  38. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0001.png
  39. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0002.png
  40. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0003.png
  41. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0004.png
  42. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0005.png
  43. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0006.png
  44. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0007.png
  45. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0008.png
  46. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0009.png
  47. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0010.png
  48. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0011.png
  49. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0012.png
  50. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0013.png
  51. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0014.png
  52. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0015.png
  53. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0016.png
  54. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0017.png
  55. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0018.png
  56. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0019.png
  57. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0020.png
  58. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0021.png
  59. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0022.png
  60. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0023.png
  61. 0 0
      doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0024.png
  62. 138 51
      doc/src/sketcher/sketcher.do.txt
  63. 2 2
      doc/src/sketcher/src-sketcher/osc1.py
  64. 25 5
      doc/src/sketcher/src-sketcher/vehicle0.py
  65. 58 0
      doc/src/sketcher/src-sketcher/vehicle0_dim.py
  66. 35 28
      examples/wheel_on_inclined_plane.py
  67. 167 59
      pysketcher/shapes.py

+ 7 - 0
doc/src/sketcher/Shape1a.dot

@@ -0,0 +1,7 @@
+digraph {
+ "Shape" -> "Line" [dir=none];
+ "Shape" -> "Arc" [dir=none];
+ "Shape" -> "Rectangle" [dir=none];
+ "Shape" -> "Curve" [dir=none];
+ "Arc" -> "Circle" [dir=none];
+}

+ 10 - 0
doc/src/sketcher/Shape1b.dot

@@ -0,0 +1,10 @@
+digraph {
+ "Shape" -> "Line" [dir=none];
+ "Shape" -> "Arc" [dir=none];
+ "Shape" -> "Rectangle" [dir=none];
+ "Shape" -> "Curve" [dir=none];
+ "Shape" -> "Point" [dir=none];
+ "Point" -> "Text" [dir=none];
+ "Text" -> "Text_wArrow" [dir=none];
+ "Arc" -> "Circle" [dir=none];
+}

+ 13 - 0
doc/src/sketcher/Shape2.py

@@ -0,0 +1,13 @@
+import commands, os
+shapes = os.path.join(os.pardir, os.pardir, os.pardir, 'pysketcher', 'shapes.py')
+cmd = 'egrep "^class\s+[A-Za-z_0-9]+\([A-Za-z_0-9]+\):" %s' % shapes
+failure, outtext = commands.getstatusoutput(cmd)
+f = open('Shape2.dot', 'w')
+f.write('digraph G {\n')
+for line in outtext.splitlines():
+    child, parent = line[6:-2].split('(')
+    f.write('  "%s" -> "%s" [dir=none];\n' % (parent, child))
+f.write('}\n')
+f.close()
+
+

二进制
doc/src/sketcher/figs-sketcher/Shape1a.png


二进制
doc/src/sketcher/figs-sketcher/Shape1b.png


二进制
doc/src/sketcher/figs-sketcher/Shape2.png


二进制
doc/src/sketcher/figs-sketcher/vehicle0_hier1.png


二进制
doc/src/sketcher/figs-sketcher/vehicle0_hier2.png


二进制
doc/src/sketcher/figs-sketcher/wheel_on_inclined_plane.png


doc/src/sketcher/src-sketcher/animation_vehicle0/anim.html → doc/src/sketcher/movies-sketcher/anim.html_vehicle0/anim.html


doc/src/sketcher/src-sketcher/animation_vehicle0/frame_0000.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0000.png


doc/src/sketcher/src-sketcher/animation_vehicle0/frame_0001.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0001.png


doc/src/sketcher/src-sketcher/animation_vehicle0/frame_0002.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0002.png


doc/src/sketcher/src-sketcher/animation_vehicle0/frame_0003.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0003.png


doc/src/sketcher/src-sketcher/animation_vehicle0/frame_0004.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0004.png


doc/src/sketcher/src-sketcher/animation_vehicle0/frame_0005.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0005.png


doc/src/sketcher/src-sketcher/animation_vehicle0/frame_0006.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0006.png


doc/src/sketcher/src-sketcher/animation_vehicle0/frame_0007.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0007.png


doc/src/sketcher/src-sketcher/animation_vehicle0/frame_0008.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0008.png


doc/src/sketcher/src-sketcher/animation_vehicle0/frame_0009.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0009.png


doc/src/sketcher/src-sketcher/animation_vehicle0/frame_0010.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0010.png


doc/src/sketcher/src-sketcher/animation_vehicle0/frame_0011.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0011.png


doc/src/sketcher/src-sketcher/animation_vehicle0/frame_0012.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0012.png


doc/src/sketcher/src-sketcher/animation_vehicle0/frame_0013.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0013.png


doc/src/sketcher/src-sketcher/animation_vehicle0/frame_0014.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0014.png


doc/src/sketcher/src-sketcher/animation_vehicle0/frame_0015.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0015.png


doc/src/sketcher/src-sketcher/animation_vehicle0/frame_0016.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0016.png


doc/src/sketcher/src-sketcher/animation_vehicle0/frame_0017.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0017.png


doc/src/sketcher/src-sketcher/animation_vehicle0/frame_0018.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0018.png


doc/src/sketcher/src-sketcher/animation_vehicle0/frame_0019.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0019.png


doc/src/sketcher/src-sketcher/animation_vehicle0/frame_0020.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0020.png


doc/src/sketcher/src-sketcher/animation_vehicle0/frame_0021.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0021.png


doc/src/sketcher/src-sketcher/animation_vehicle0/frame_0022.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0022.png


doc/src/sketcher/src-sketcher/animation_vehicle0/frame_0023.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0023.png


doc/src/sketcher/src-sketcher/animation_vehicle0/frame_0024.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle0/frame_0024.png


doc/src/sketcher/src-sketcher/animation_vehicle1/anim.html → doc/src/sketcher/movies-sketcher/anim.html_vehicle1/anim.html


doc/src/sketcher/src-sketcher/animation_vehicle1/frame_0000.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0000.png


doc/src/sketcher/src-sketcher/animation_vehicle1/frame_0001.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0001.png


doc/src/sketcher/src-sketcher/animation_vehicle1/frame_0002.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0002.png


doc/src/sketcher/src-sketcher/animation_vehicle1/frame_0003.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0003.png


doc/src/sketcher/src-sketcher/animation_vehicle1/frame_0004.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0004.png


doc/src/sketcher/src-sketcher/animation_vehicle1/frame_0005.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0005.png


doc/src/sketcher/src-sketcher/animation_vehicle1/frame_0006.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0006.png


doc/src/sketcher/src-sketcher/animation_vehicle1/frame_0007.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0007.png


doc/src/sketcher/src-sketcher/animation_vehicle1/frame_0008.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0008.png


doc/src/sketcher/src-sketcher/animation_vehicle1/frame_0009.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0009.png


doc/src/sketcher/src-sketcher/animation_vehicle1/frame_0010.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0010.png


doc/src/sketcher/src-sketcher/animation_vehicle1/frame_0011.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0011.png


doc/src/sketcher/src-sketcher/animation_vehicle1/frame_0012.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0012.png


doc/src/sketcher/src-sketcher/animation_vehicle1/frame_0013.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0013.png


doc/src/sketcher/src-sketcher/animation_vehicle1/frame_0014.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0014.png


doc/src/sketcher/src-sketcher/animation_vehicle1/frame_0015.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0015.png


doc/src/sketcher/src-sketcher/animation_vehicle1/frame_0016.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0016.png


doc/src/sketcher/src-sketcher/animation_vehicle1/frame_0017.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0017.png


doc/src/sketcher/src-sketcher/animation_vehicle1/frame_0018.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0018.png


doc/src/sketcher/src-sketcher/animation_vehicle1/frame_0019.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0019.png


doc/src/sketcher/src-sketcher/animation_vehicle1/frame_0020.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0020.png


doc/src/sketcher/src-sketcher/animation_vehicle1/frame_0021.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0021.png


doc/src/sketcher/src-sketcher/animation_vehicle1/frame_0022.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0022.png


doc/src/sketcher/src-sketcher/animation_vehicle1/frame_0023.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0023.png


doc/src/sketcher/src-sketcher/animation_vehicle1/frame_0024.png → doc/src/sketcher/movies-sketcher/anim.html_vehicle1/frame_0024.png


+ 138 - 51
doc/src/sketcher/sketcher.do.txt

@@ -1,37 +1,48 @@
 
-Implementing a drawing program provides a very good example on the usefulness
-of object-oriented programming. In the following we shall develop
-the simpler parts of a relatively small and compact drawing program
-for making sketches of the type shown in Figure ref{sketcher:fig1}.
-This sketch is made up many individual elements....
-
-FIGURE: [figs-sketcher/...png, width=500]
+Implementing a drawing program provides a very good example on the
+usefulness of object-oriented programming. In the following we shall
+develop the simpler parts of a relatively small and compact drawing
+program for making sketches of the type shown in
+Figure ref{sketcher:fig:inclinedplane}. This is a typical principal sketch of
+a physics problem, here involving a rolling wheel on an inclined
+plane.  This sketch is made up many individual elements: a rectangle
+filled with a pattern (the inclined plane), a hollow circle with color
+(the wheel), arrows with label (the $N$ and $Mg$ forces, and the $x$
+axis), an angle with symbol $\theta$, and a dashed line indicating the
+starting location of the wheel.  Drawing software and plotting
+programs can produce such figures quite easily in principle, but the
+amount of details the user needs to control with the mouse can be
+substantial. Software more tailored to producing sketches of this type
+would work with more convenient abstractions, such as circle, wall,
+angle, force arrow, axis, and so forth.
+
+FIGURE: [figs-sketcher/wheel_on_inclined_plane.png, width=500] Sketch of a physics problem. label{sketcher:fig:inclinedplane}
 
 Classes are very suitable for implementing the various components that
 build up a sketch and their functionality. In particular, we shall
 demonstrate that as soon some classes are established, more are easily
-added, and enhanced functionality for all the classes is also easy to
-implement in common, generic code that can be shared by all classes.
+added. Enhanced functionality for all the classes is also easy to
+implement in common, generic code that can immediately be shared by all
+present and future classes.
 
 ===== Using the Object Collection =====
 
-Before we dive into implementation details, let us first decide upon
-the interface we want to take advantage of to make sketches of the type in
-Figure ref{sketcher:fig1}. We start with a significantly simpler
-example as depicted in Figure ref{sketcher:fig:vehicle0}.
-This toy sketch consists of several elements: two circles, two
-rectangles, and a "ground" element.
+We start by demonstrating a convenient user interface for making
+sketches of the type in Figure ref{sketcher:fig1}. However, it is more
+appropriate to start with a significantly simpler example as depicted
+in Figure ref{sketcher:fig:vehicle0}.  This toy sketch consists of
+several elements: two circles, two rectangles, and a "ground" element.
 
 FIGURE: [figs-sketcher/vehicle0_dim.png, width=400] Sketch of a simple figure. label{sketcher:fig:vehicle0}
 
 === Basic Drawing ===
 
 A typical program creating these five elements is shown next.
-The drawing package is named `pysketcher` so it is natural that we
-must import tools from `pysketcher`. The first task is always to
+After importing the `pysketcher` package, the first task is always to
 define a coordinate system. Some graphics operations are done with
 a helper object called `drawing_tool` (imported from `pysketcher`).
-With the drawing area in place we can make the first `Circle` object:
+With the drawing area in place we can make the first `Circle` object
+in an intuitive fashion:
 !bc pycod
 from pysketcher import *
 
@@ -44,10 +55,14 @@ drawing_tool.set_coordinate_system(xmin=0, xmax=w_1 + 2*L + 3*R,
 
 wheel1 = Circle(center=(w_1, R), radius=R)
 !ec
+By using symbols for characteristic lengths in the drawing, rather than
+absolute lengths, it is easier
+to change dimensions later.
+
 To translate the geometric information about the `wheel1` object to
 instructions for the plotting engine (in this case Matplotlib), one calls the
 `wheel1.draw()`. To display all drawn objects, one issues
-`drawing_tool.display()`. The steps are hence:
+`drawing_tool.display()`. The typical steps are hence:
 !bc pycod
 wheel1 = Circle(center=(w_1, R), radius=R)
 wheel1.draw()
@@ -78,28 +93,32 @@ Instead of calling the `draw` method of every object, we can
 group objects and call `draw`, or perform other operations, for
 the whole group. For example, we may collect the two wheels
 in a `wheels` group and the `over` and `under` rectangles
-in a vehicle `body` group. The whole vehicle is a composition
-of the `wheels` and `body` groups. The codes goes like
+in a `body` group. The whole vehicle is a composition
+of its `wheels` and `body` groups. The codes goes like
 !bc pycod
-wheels = Compose({'wheel1': wheel1, 'wheel2': wheel2})
-body = Compose({'under': under, 'over': over})
+wheels  = Composition({'wheel1': wheel1, 'wheel2': wheel2})
+body    = Composition({'under': under, 'over': over})
 
-vehicle = Compose({'wheels': wheels, 'body': body})
+vehicle = Composition({'wheels': wheels, 'body': body})
 !ec
 
 The ground is illustrated by an object of type `Wall`,
-mostly used to indicate walls in sketches of physical systems.
-A `Wall` takes the `x` and `y` coordinates of some curve
-and a `thickness` parameter and creates a "thick" curve filled
+mostly used to indicate walls in sketches of mechanical systems.
+A `Wall` takes the `x` and `y` coordinates of some curve,
+and a `thickness` parameter, and creates a "thick" curve filled
 with a simple pattern. In this case the curve is just a flat
-line so the construction is simple:
+line so the construction is made of two points on the
+ground line ($(w_1-L,0)$ and $(w_1+3L,0)$):
 !bc pycod
 ground = Wall(x=[w_1 - L, w_1 + 3*L], y=[0, 0], thickness=-0.3*R)
 !ec
-We may collect all the objects in a "top" object that contains
+The negative thickness makes the pattern-filled rectangle appear below
+the defined line, otherwise it appears above.
+
+We may now collect all the objects in a "top" object that contains
 the whole figure:
 !bc pycod
-fig = Compose({'vehicle': vehicle, 'ground': ground})
+fig = Composition({'vehicle': vehicle, 'ground': ground})
 fig.draw()  # send all figures to plotting backend
 drawing_tool.display()
 drawing_tool.savefig('tmp.png')
@@ -130,13 +149,15 @@ wheel1.set_linecolor('red')
 !ec
 and so on.
 
-Geometric figures can be filled, either with a color or with a
+Geometric figures can be specified as *filled*, either with a color or with a
 special visual pattern:
 !bc
 # Set filling of all curves
-drawing_tool.set_filled_curves(color='blue', hatch='/')
+drawing_tool.set_filled_curves(color='blue', pattern='/')
+
 # Turn off filling of all curves
 drawing_tool.set_filled_curves(False)
+
 # Fill the wheel with red color
 wheel1.set_filled_curves('red')
 !ec
@@ -180,21 +201,21 @@ print fig.show_hierarchy('std')
 yielding the output
 !bc dat
 ground (Wall):
-    wall (Curve): 4 coords fillcolor='white' fillhatch='/'
-vehicle (Compose):
-    body (Compose):
+    wall (Curve): 4 coords fillcolor='white' fillpattern='/'
+vehicle (Composition):
+    body (Composition):
         over (Rectangle):
             rectangle (Curve): 5 coords
         under (Rectangle):
             rectangle (Curve): 5 coords
-    wheels (Compose):
+    wheels (Composition):
         wheel1 (Circle):
             arc (Curve): 181 coords
         wheel2 (Circle):
             arc (Curve): 181 coords
 !ec
-Here we can see the class type each object, how many
-coordinates that are involved in basic figures, and
+Here we can see the class type for each figure object, how many
+coordinates that are involved in basic figures (`Curve` objects), and
 special settings of the basic figure (fillcolor, line types, etc.).
 For example, `wheel2` is a `Circle` object consisting of an `arc`,
 which is a `Curve` object consisting of 181 coordinates (the
@@ -203,6 +224,28 @@ only objects that really holds specific coordinates to be drawn.
 The other object types are just compositions used to group
 parts of the complete figure.
 
+One can also get a graphical overview of the hiearchy of figure objects
+that build up a particular figure `fig`.
+Just call `fig.graphviz_dot('fig')` to produce a file `fig.dot` in
+the *dot format*. This file contains relations between parent and
+child objects in the figure and can be turned into an image,
+as in Figure ref{sketcher:fig:vehicle0:hier1}, by
+running the `dot` program:
+!bc sys
+Terminal> dot -Tpng -o fig.png fig.dot
+!ec
+
+FIGURE: [figs-sketcher/vehicle0_hier1.png, width=400] Hierarchical relation between figure objects. label{sketcher:fig:vehicle0:hier1}
+
+
+The call `fig.graphviz_dot('fig', classname=True)` makes a `fig.dot` file
+where the class type of each object is also visible, see
+Figure ref{sketcher:fig:vehicle0:hier2}. The ability to write out the
+object hierarchy or view it graphically can be of great help when
+working with complex figures that involve layers of subfigures.
+
+FIGURE: [figs-sketcher/vehicle0_hier1.png, width=400] Hierarchical relation between figure objects, including their class names. label{sketcher:fig:vehicle0:hier2}
+
 Any of the objects can in the program be reached through their names, e.g.,
 !bc pycodc
 fig['vehicle']
@@ -221,10 +264,17 @@ changing properties of that part, for example, colors, line styles
 fig['vehicle']['wheels'].set_filled_curves('blue')
 fig['vehicle']['wheels'].set_linewidth(6)
 fig['vehicle']['wheels'].set_linecolor('black')
+
 fig['vehicle']['body']['under'].set_filled_curves('red')
+
 fig['vehicle']['body']['over'].set_filled_curves(pattern='/')
 fig['vehicle']['body']['over'].set_linewidth(14)
+fig['vehicle']['body']['over']['rectangle'].linewidth = 4
 !ec
+The last line accesses the `Curve` object directly, while the line above,
+accesses the `Rectangle` object which will then set the linewidth of
+its `Curve` object, and other objects if it had any.
+The result of the actions above is shown in Figure ref{sketcher:fig:vehicle0:v2}.
 
 FIGURE: [figs-sketcher/vehicle0.png, width=700] Left: Basic line-based drawing. Right: Thicker lines and filled parts. label{sketcher:fig:vehicle0:v2}
 
@@ -238,7 +288,7 @@ fake rolling by just displacing all parts of the vehicle.
 The relevant parts constitute the `fig['vehicle']` object.
 This part of the figure can be translated, rotated, and scaled.
 A translation along the ground means a translation in $x$ direction,
-say a length $4$ to the right:
+say a length $L$ to the right:
 !bc pycod
 fig['vehicle'].translate((L,0))
 !ec
@@ -248,7 +298,7 @@ drawing_tool.erase()
 fig.draw()
 drawing_tool.display()
 !ec
-Without erasing, the old position of the vehicle will remain in
+Without erasing the old position of the vehicle will remain in
 the figure so you get two vehicles. Without `fig.draw()` the
 new coordinates of the vehicle will not be communicated to
 the drawing tool, and without calling dislay the updated
@@ -260,7 +310,7 @@ to that velocity in small steps of time:
 def v(t):
     return -8*R*t*(1 - t/(2*R))
 
-animate(fig, tp, user_action)
+animate(fig, tp, action)
 !ec
 For small time steps `dt` the corresponding displacement is
 well approximated by `dt*v(t)` (we could integrate the velocity
@@ -270,7 +320,7 @@ The `animate` function takes as arguments some figure `fig`, a set of
 time points `tp`, and a user function `action`,
 and then a new figure is drawn for each time point and the user
 can through the provided `action` function modify desired parts
-of the figure. Here the `action` function will move the `vehicle`:
+of the figure. Here the `action` function will move the vehicle:
 !bc pycod
 def move_vehicle(t, fig):
     x_displacement = dt*v(t)
@@ -296,18 +346,55 @@ files = animate(fig, tp, move_vehicle, moviefiles=True,
 The `files` variable holds a string with the family of
 files constituting the frames in the movie, here
 `'tmp_frame*.png'`. Making a movie out of the individual
-frames can be done in many ways, e.g.,
+frames can be done in many ways.
+A simple approach is to make an animated GIF file with help of
+`convert`, a program in the ImageMagick software suite:
+!bc sys
+Terminal> convert -delay 20 tmp_frame*.png anim.gif
+Terminal> animate anim.gif  # play movie
+!ec
+The delay between frames governs the speed of the movie.
+The `anim.gif` file can be embedded in a web page and shown as
+a movie the page is loaded into a web browser (just insert
+`<img src="anim.gif">` in the HTML code to play the GIF animation).
+
+The tool `ffmpeg` can alternatively be used, e.g.,
+!bc sys
+Terminal> ffmpeg -i "tmp_frame_%04d.png" -b 800k -r 25 \
+          -vcodec mpeg4 -y -qmin 2 -qmax 31 anim.mpeg
+!ec
+An easy-to-use interface to movie-making tools is provided by the
+SciTools package:
 !bc pycod
 from scitools.std import movie
-movie(files, encoder='html', output_file='anim')
+
+# HTML page showing individual frames
+movie(files, encoder='html', output_file='anim.html')
+
+# Standard GIF file
+movie(files, encoder='convert', output_file='anim.gif')
+
+# AVI format
+movie('tmp_*.png', encoder='ffmpeg', fps=4,
+      output_file='anim.avi') # requires ffmpeg package
+
+# MPEG format
+movie('tmp_*.png', encoder='ffmpeg', fps=4,
+      output_file='anim2.mpeg', vodec='mpeg2video')
+# or
+movie(files, encoder='ppmtompeg', fps=24,
+      output_file='anim.mpeg')  # requires the netpbm package
 !ec
-This command makes a movie that is actually an HTML file `anim.html`,
-which can be loaded into a web browser.
-You can try this by running the present example in the file
+When difficulties with encoders and players arise, the simple
+web page for showing a movie, here `anim.html` (generated by the
+first `movie` command above), is a safe method that you always
+can rely on.
+You can try loading `anim.html` into a web browser, after first
+having run the present example in the file
 # #ifdef PRIMER_BOOK
 `vehicle0.py`.
 # #else
-"`vehicle0.py`": "http://hplgit.github.com/pysketcher/doc/src/sketcher/src-sketcher/vehicle0.py", or view a ready-made "movie": "http://hplgit.github.com/pysketcher/doc/src/sketcher/src-sketcher/animation_vehicle0/anim.html".
+"`vehicle0.py`": "http://hplgit.github.com/pysketcher/doc/src/sketcher/src-sketcher/vehicle0.py". Alternatively, you can view a ready-made "movie": "http://hplgit.github.com/pysketcher/doc/src/sketcher/movies-sketcher/anim.html_vehicle0/anim.html".
 # #endif
 
 === Animation: Rolling the Wheels ===
@@ -318,10 +405,10 @@ two crossing lines, see Figure ref{sketcher:fig:vehicle1}.
 The construction of the wheels will now involve a circle
 and two lines:
 !bc pycod
-wheel1 = Compose({'wheel':
+wheel1 = Composition({'wheel':
                   Circle(center=(w_1, R), radius=R),
                   'cross':
-                  Compose({'cross1': Line((w_1,0),   (w_1,2*R)),
+                  Composition({'cross1': Line((w_1,0),   (w_1,2*R)),
                            'cross2': Line((w_1-R,R), (w_1+R,R))})})
 wheel2 = wheel1.copy()
 wheel2.translate((L,0))
@@ -378,7 +465,7 @@ The complete example is found in the file
 # #ifdef PRIMER_BOOK
 `vehicle1.py`.
 # #else
-"`vehicle1.py`": "http://hplgit.github.com/pysketcher/doc/src/sketcher/src-sketcher/vehicle1.py". You may run this file or watch a "ready-made movie": "http://hplgit.github.com/pysketcher/doc/src/sketcher/src-sketcher/animation_vehicle1/anim.html".
+"`vehicle1.py`": "http://hplgit.github.com/pysketcher/doc/src/sketcher/src-sketcher/vehicle1.py". You may run this file or watch a "ready-made movie": "http://hplgit.github.com/pysketcher/doc/src/sketcher/movies-sketcher/anim.html_vehicle1/anim.html".
 # #endif
 
 The advantages with making figures this way through programming,

+ 2 - 2
doc/src/sketcher/src-sketcher/osc1.py

@@ -37,8 +37,8 @@ fig = Compose({
     'dashpot': d, 'spring': s, 'mass': M, 'left wall': left_wall,
     'ground': ground, 'wheel1': wheel1, 'wheel2': wheel2})
 
-#fig.draw()
-s.draw()
+fig.draw()
+#s.draw()
 print s
 print s.shapes['bar1']['line'].x, s.shapes['bar1']['line'].y
 print s.shapes['bar2']['line'].x, s.shapes['bar2']['line'].y

+ 25 - 5
doc/src/sketcher/src-sketcher/vehicle0.py

@@ -19,13 +19,13 @@ under = Rectangle(lower_left_corner=(w_1-2*R, 2*R),
 over  = Rectangle(lower_left_corner=(w_1, 2*R + H),
                   width=2.5*R, height=1.25*H)
 
-wheels = Compose({'wheel1': wheel1, 'wheel2': wheel2})
-body = Compose({'under': under, 'over': over})
+wheels = Composition({'wheel1': wheel1, 'wheel2': wheel2})
+body = Composition({'under': under, 'over': over})
 
-vehicle = Compose({'wheels': wheels, 'body': body})
+vehicle = Composition({'wheels': wheels, 'body': body})
 ground = Wall(x=[R, xmax], y=[0, 0], thickness=-0.3*R)
 
-fig = Compose({'vehicle': vehicle, 'ground': ground})
+fig = Composition({'vehicle': vehicle, 'ground': ground})
 fig.draw()  # send all figures to plotting backend
 
 drawing_tool.display()
@@ -44,6 +44,8 @@ drawing_tool.display()
 drawing_tool.savefig('tmp2.png')
 
 print fig
+fig.recurse('fig')
+fig.graphviz_dot('fig', False)
 
 import time
 time.sleep(1)
@@ -65,7 +67,25 @@ def move_vehicle(t, fig):
 files = animate(fig, tp, move_vehicle, moviefiles=True,
                 pause_per_frame=0)
 
+os.system('convert -delay 20 %s anim.gif' % files)
+os.system('ffmpeg -i "tmp_frame_%04d.png" -b 800k -r 25 -vcodec mpeg4 -y -qmin 2 -qmax 31 anim.mpeg')
+
 from scitools.std import movie
-movie(files, encoder='html', output_file='anim')
+# HTML page showing individual frames
+movie(files, encoder='html', fps=4, output_file='anim.html')
+
+# Standard GIF file
+movie(files, encoder='convert', fps=4, output_file='anim2.gif')
+
+# AVI format
+movie('tmp_*.png', encoder='ffmpeg', fps=4,
+      output_file='anim.avi') # requires ffmpeg package
+
+# MPEG format
+movie('tmp_*.png', encoder='ffmpeg', fps=4,
+      output_file='anim3.mpeg', vodec='mpeg2video')
+# or
+movie(files, encoder='ppmtompeg', fps=24,
+      output_file='anim2.mpeg')  # requires the netpbm package
 
 raw_input()

+ 58 - 0
doc/src/sketcher/src-sketcher/vehicle0_dim.py

@@ -0,0 +1,58 @@
+"""Add dimensions to vehicle0.py figure."""
+from pysketcher import *
+
+R = 1    # radius of wheel
+L = 4    # distance between wheels
+H = 2    # height of vehicle body
+w_1 = 5  # position of front wheel
+
+xmax = w_1 + 2*L + 3*R
+drawing_tool.set_coordinate_system(xmin=0, xmax=xmax,
+                                   ymin=-1, ymax=2*R + 3*H,
+                                   axis=True)
+
+drawing_tool.set_grid(True)
+
+wheel1 = Circle(center=(w_1, R), radius=R)
+wheel2 = wheel1.copy()
+wheel2.translate((L,0))
+
+under = Rectangle(lower_left_corner=(w_1-2*R, 2*R),
+                  width=2*R + L + 2*R, height=H)
+over  = Rectangle(lower_left_corner=(w_1, 2*R + H),
+                  width=2.5*R, height=1.25*H)
+
+wheels = Composition({'wheel1': wheel1, 'wheel2': wheel2})
+body = Composition({'under': under, 'over': over})
+
+vehicle = Composition({'wheels': wheels, 'body': body})
+ground = Wall(x=[R, xmax], y=[0, 0], thickness=-0.3*R)
+
+x_dim = w_1 + L + 2*R + R  # line for vertical dimensions
+y_dim = 2*R + H + 1.25*H + R/2.
+w_1_dim = Text_wArrow('$w_1$', (w_1+2*R, 1.5*R), (w_1, R))
+wheel_dim = Distance_wText((x_dim, 0), (x_dim, R), '$R$')
+under_dim = Distance_wText((x_dim, 2*R), (x_dim, 2*R+H), '$H$')
+over_dim  = Distance_wText((x_dim, 2*R+H), (x_dim, 2*R+H+1.25*H),
+                           r'$\frac{5}{4}{H}$')
+front_dim = Distance_wText((w_1-2*R, y_dim), (w_1, y_dim), '$2R$')
+L_dim     = Distance_wText((w_1, y_dim), (w_1+L, y_dim), '$L$')
+back_dim  = Distance_wText((w_1+L, y_dim), (w_1+L+2*R, y_dim), '$2R$')
+
+dims = Composition(dict(w_1_dim=w_1_dim,
+                        wheel_dim=wheel_dim,
+                        under_dim=under_dim,
+                        over_dim=over_dim,
+                        front_dim=front_dim,
+                        L_dim=L_dim,
+                        back_dim=back_dim))
+
+fig = Composition({'vehicle': vehicle, 'ground': ground, 'dims': dims})
+fig.draw()  # send all figures to plotting backend
+
+drawing_tool.display()
+drawing_tool.savefig('tmp1.png')
+
+print fig
+
+raw_input()

+ 35 - 28
examples/wheel_on_inclined_plane.py

@@ -6,23 +6,26 @@ print dir()
 print 'drawin_tool' in dir()
 
 def inclined_plane():
-    drawing_tool.set_coordinate_system(xmin=0, xmax=15,
-                                       ymin=-1, ymax=10,
+    theta = 30.
+    L = 10.
+    a = 1.
+    xmin = 0
+    ymin = -3
+
+    drawing_tool.set_coordinate_system(xmin=xmin, xmax=xmin+1.5*L,
+                                       ymin=ymin, ymax=ymin+L,
                                        #axis=True,
                                        )
     #drawing_tool.set_grid(True)
     fontsize = 18
     from math import tan, radians
 
-    theta = 30.
-    L = 10.
-    a = 1.
     B = point(a+L, 0)
     A = point(a, tan(radians(theta))*L)
 
-    wall = CurveWall(x=[A[0], B[0]], y=[A[1], B[1]], thickness=-0.25)
+    wall = Wall(x=[A[0], B[0]], y=[A[1], B[1]], thickness=-0.25)
 
-    angle = ArcSymbol(r'$\theta$', center=B, radius=3,
+    angle = Arc_wText(r'$\theta$', center=B, radius=3,
                       start_angle=180-theta, arc_angle=theta,
                       fontsize=fontsize)
     angle.set_linecolor('black')
@@ -49,44 +52,48 @@ def inclined_plane():
     wheel = Compose({'outer': outer_wheel, 'inner': hole})
 
     drawing_tool.set_linecolor('black')
-    N_arr = Arrow3((4,2), 2)
-    N_arr.rotate(-theta, (4,4))
-    N_text = Text('$N$', (3.7,2.6), fontsize=fontsize)
-    N_force = Compose({'arrow': N_arr, 'symbol': N_text})
-
-    g = Gravity(c, 2.5)
+    N = Force(contact - 2*r*normal_vec, contact, r'$N$', text_pos='start')
+              #text_alignment='left')
+    mg = Gravity(c, 3*r, text='$Mg$')
 
     x_const = Line(contact, contact + point(0,4))
     x_const.set_linestyle('dotted')
     x_const.rotate(-theta, contact)
-    x_axis_start = point(5.5, x_const(x=5.5))
-    x_axis = Axis(x_axis_start, 2*L/5, '$x$', rotation_angle=-theta)
+    # or x_const = Line(contact-2*r*normal_vec, contact+4*r*normal_vec).set_linestyle('dotted')
+    x_axis = Axis(start=contact+ 3*r*normal_vec, length=4*r,
+                  label='$x$', rotation_angle=-theta)
 
-    body  = Compose({'wheel': wheel, 'N force': N_force, 'g': g})
+    body  = Compose({'wheel': wheel, 'N': N, 'mg': mg})
     fixed = Compose({'angle': angle, 'inclined wall': wall,
                      'wheel': wheel, 'ground': ground,
                      'x start': x_const, 'x axis': x_axis})
 
     fig = Compose({'body': body, 'fixed elements': fixed})
 
-    #import copy
-    #body2 = copy.deepcopy(body)
-    #body2.translate(3, 0)
-    #body2.draw()
-
     fig.draw()
     drawing_tool.savefig('tmp.png')
     drawing_tool.display()
     import time
     time.sleep(1)
     tangent_vec = point(normal_vec[1], -normal_vec[0])
-    print 'loop'
-    for t in range(7):
-        drawing_tool.erase()
-        body.translate(0.2*t*tangent_vec)
-        time.sleep(0.5)
-        fig.draw()
-        drawing_tool.display()
+
+    import numpy
+    time_points = numpy.linspace(0, 1, 31)
+
+    def position(t):
+        """Position of center point of wheel."""
+        return c + 7*t**2*tangent_vec
+
+    def move(t, fig, dt=None):
+        x = position(t)
+        x0 = position(t-dt)
+        displacement = x - x0
+        fig['body'].translate(displacement)
+
+
+    animate(fig, time_points, move, pause_per_frame=0,
+            dt=time_points[1]-time_points[0])
+
     print str(fig)
     print repr(fig)
 

+ 167 - 59
pysketcher/shapes.py

@@ -85,8 +85,8 @@ def is_sequence(*sequences, **kwargs):
                 print msg
 
 
-def animate(fig, time_points, user_action, moviefiles=False,
-            pause_per_frame=0.5):
+def animate(fig, time_points, action, moviefiles=False,
+            pause_per_frame=0.5, **action_kwargs):
     if moviefiles:
         # Clean up old frame files
         framefilestem = 'tmp_frame_'
@@ -97,13 +97,13 @@ def animate(fig, time_points, user_action, moviefiles=False,
     for n, t in enumerate(time_points):
         drawing_tool.erase()
 
-        user_action(t, fig)
+        action(t, fig, **action_kwargs)
         #could demand returning fig, but in-place modifications
         #are done anyway
-        #fig = user_action(t, fig)
+        #fig = action(t, fig)
         #if fig is None:
         #    raise TypeError(
-        #        'animate: user_action returns None, not fig\n'
+        #        'animate: action returns None, not fig\n'
         #        '(a Shape object with the whole figure)')
 
         fig.draw()
@@ -193,14 +193,14 @@ class Shape:
                     raise TypeError(
                         'class %s has a self.shapes member "%s" that is just\n'
                         'a plain dictionary,\n%s\n'
-                        'Did you mean to embed this dict in a Compose\n'
+                        'Did you mean to embed this dict in a Composition\n'
                         'object?' % (self.__class__.__name__, shape_name,
                         str(shape)))
                 elif isinstance(shape, (list,tuple)):
                     raise TypeError(
                         'class %s has self.shapes member "%s" containing\n'
                         'a %s object %s,\n'
-                        'Did you mean to embed this list in a Compose\n'
+                        'Did you mean to embed this list in a Composition\n'
                         'object?' % (self.__class__.__name__, shape_name,
                         type(shape), str(shape)))
                 elif shape is None:
@@ -256,19 +256,72 @@ class Shape:
         self._for_all_shapes('minmax_coordinates', minmax)
         return minmax
 
-    def traverse_hierarchy(self, indent=0):
+    def recurse(self, name, indent=0):
         if not isinstance(self.shapes, dict):
-            raise TypeError('traverse_hierarchy works only with dict self.shape')
+            raise TypeError('recurse works only with dict self.shape, not %s' %
+                            type(self.shapes))
         space = ' '*indent
-        print space, '%s.shapes has entries' % \
-              self.__class__.__name__,\
+        print space, '%s: %s.shapes has entries' % \
+              (self.__class__.__name__, name), \
               str(list(self.shapes.keys()))[1:-1]
         for shape in self.shapes:
             print space,
-            print 'call self.shapes["%s"].traverse_hierarchy' % \
-                  shape
-            name = self.shapes[shape].traverse_hierarchy(indent+4)
-        return name
+            print 'call %s.shapes["%s"].recurse("%s", %d)' % \
+                  (name, shape, shape, indent+2)
+            name = self.shapes[shape].recurse(shape, indent+2)
+
+    def graphviz_dot(self, name, classname=True):
+        if not isinstance(self.shapes, dict):
+            raise TypeError('recurse works only with dict self.shape, not %s' %
+                            type(self.shapes))
+        dotfile = name + '.dot'
+        pngfile = name + '.png'
+        if classname:
+            name = r"%s:\n%s" % (self.__class__.__name__, name)
+
+        couplings = self._object_couplings(name, classname=classname)
+        # Insert counter for similar names
+        from collections import defaultdict
+        count = defaultdict(lambda: 0)
+        couplings2 = []
+        for i in range(len(couplings)):
+            parent, child = couplings[i]
+            count[child] += 1
+            parent += ' (%d)' % count[parent]
+            child += ' (%d)' % count[child]
+            couplings2.append((parent, child))
+        print 'graphviz', couplings, count
+        # Remove counter for names there are only one of
+        for i in range(len(couplings)):
+            parent2, child2 = couplings2[i]
+            parent, child = couplings[i]
+            if count[parent] > 1:
+                parent = parent2
+            if count[child] > 1:
+                child = child2
+            couplings[i] = (parent, child)
+        print couplings
+        f = open(dotfile, 'w')
+        f.write('digraph G {\n')
+        for parent, child in couplings:
+            f.write('"%s" -> "%s";\n' % (parent, child))
+        f.write('}\n')
+        f.close()
+        print 'Run dot -Tpng -o %s %s' % (pngfile, dotfile)
+
+    def _object_couplings(self, parent, couplings=[], classname=True):
+        """Find all couplings of parent and child objects in a figure."""
+        for shape in self.shapes:
+            if classname:
+                childname = r"%s:\n%s" % \
+                            (self.shapes[shape].__class__.__name__, shape)
+            else:
+                childname = shape
+            couplings.append((parent, childname))
+            self.shapes[shape]._object_couplings(childname, couplings,
+                                                 classname)
+        return couplings
+
 
     def set_linestyle(self, style):
         styles = ('solid', 'dashed', 'dashdot', 'dotted')
@@ -457,11 +510,13 @@ class Curve(Shape):
         minmax['ymax'] = max(self.y.max(), minmax['ymax'])
         return minmax
 
-    def traverse_hierarchy(self, indent=0):
+    def recurse(self, name, indent=0):
         space = ' '*indent
         print space, 'reached "bottom" object %s' % \
               self.__class__.__name__
-        return len(space)/4  # level
+
+    def _object_couplings(self, parent, couplings=[], classname=True):
+        return
 
     def set_linecolor(self, color):
         self.linecolor = color
@@ -569,11 +624,13 @@ class Point(Shape):
         minmax['ymax'] = max(self.y, minmax['ymax'])
         return minmax
 
-    def traverse_hierarchy(self, indent=0):
+    def recurse(self, name, indent=0):
         space = ' '*indent
         print space, 'reached "bottom" object %s' % \
               self.__class__.__name__
-        return len(space)/4  # level
+
+    def _object_couplings(self, parent, couplings=[], classname=True):
+        return
 
     def show_hierarchy(self, indent=0, format='std'):
         s = '%s at (%g,%g)' % (self.__class__.__name__, self.x, self.y)
@@ -870,7 +927,7 @@ class Wall(Shape):
             x1 = asarray(x, float)
         if isinstance(y[0], (tuple,list,ndarray)):
             # x is list of curves
-            y = concatenate(y)
+            y1 = concatenate(y)
         else:
             y1 = asarray(y, float)
 
@@ -904,18 +961,31 @@ class Wall2(Shape):
             x1 = asarray(x, float)
         if isinstance(y[0], (tuple,list,ndarray)):
             # x is list of curves
-            y = concatenate(y)
+            y1 = concatenate(y)
         else:
             y1 = asarray(y, float)
 
         # Displaced curve (according to thickness)
-        for i in range(1, len(x1)-1):
+        x2 = x1.copy()
+        y2 = y1.copy()
+
+        def displace(idx, idx_m, idx_p):
             # Find tangent and normal
-            # set x2, y2 in distance thickness in normal dir
-            # check sign of thickness
-            pass
-        x2 = x1
-        y2 = y1 + thickness
+            tangent = point(x1[idx_m], y1[idx_m]) - point(x1[idx_p], y1[idx_p])
+            tangent = unit_vec(tangent)
+            normal = point(tangent[1], -tangent[0])
+            # Displace length "thickness" in "positive" normal direction
+            displaced_pt = point(x1[idx], y1[idx]) + thickness*normal
+            x2[idx], y2[idx] = displaced_pt
+
+        for i in range(1, len(x1)-1):
+            displace(i-1, i+1, i)  # centered difference for normal comp.
+        # One-sided differences at the end points
+        i = 0
+        displace(i, i+1, i)
+        i = len(x1)-1
+        displace(i-1, i, i)
+
         # Combine x1,y1 with x2,y2 reversed
         from numpy import concatenate
         x = concatenate((x1, x2[-1::-1]))
@@ -1061,39 +1131,33 @@ class Text_wArrow(Text):
 
 
 class Axis(Shape):
-    def __init__(self, start, length, label, below=True,
+    def __init__(self, start, length, label,
                  rotation_angle=0, fontsize=0,
-                 label_spacing=1./30):
+                 label_spacing=1./45, label_alignment='left'):
         """
         Draw axis from start with `length` to the right
-        (x axis). Place label below (True) or above (False) axis.
+        (x axis). Place label at the end of the arrow tip.
         Then return `rotation_angle` (in degrees).
-        To make a standard x axis, call with ``below=True`` and
-        ``rotation_angle=0``. To make a standard y axis, call with
-        ``below=False`` and ``rotation_angle=90``.
-        A tilted axis can also be drawn.
         The `label_spacing` denotes the space between the label
         and the arrow tip as a fraction of the length of the plot
-        in x direction.
+        in x direction. With `label_alignment` one can place
+        the axis label text such that the arrow tip is to the 'left',
+        'right', or 'center' with respect to the text field.
+        The `label_spacing` and `label_alignment` parameters can
+        be used to fine-tune the location of the label.
         """
         # Arrow is vertical arrow, make it horizontal
         arrow = Arrow3(start, length, rotation_angle=-90)
         arrow.rotate(rotation_angle, start)
         spacing = drawing_tool.xrange*label_spacing
-        if below:
-            spacing = - spacing
-        label_pos = [start[0] + length, start[1] + spacing]
+        # should increase spacing for downward pointing axis
+        label_pos = [start[0] + length + spacing, start[1]]
         label = Text(label, position=label_pos, fontsize=fontsize)
         label.rotate(rotation_angle, start)
         self.shapes = {'arrow': arrow, 'label': label}
 
-class Gravity(Axis):
-    """Downward-pointing gravity arrow with the symbol g."""
-    def __init__(self, start, length, fontsize=0):
-        Axis.__init__(self, start, length, '$g$', below=False,
-                      rotation_angle=-90, label_spacing=1./30,
-                      fontsize=fontsize)
-        self.shapes['arrow'].set_linecolor('black')
+
+# Maybe Axis3 with label below/above?
 
 class Force(Arrow1):
     """
@@ -1105,28 +1169,67 @@ class Force(Arrow1):
     area away from the specified point.
     """
     def __init__(self, start, end, text, text_spacing=1./60,
-                 fontsize=0, text_pos='start'):
+                 fontsize=0, text_pos='start', text_alignment='center'):
         Arrow1.__init__(self, start, end, style='->')
         spacing = drawing_tool.xrange*text_spacing
         start, end = arr2D(start), arr2D(end)
-        downward = (end-start)[1] < 0 # needs more space to text if downward
-        if downward:
-            spacing *= 1.5
+
+        # Two cases: label at bottom of line or top, need more
+        # spacing if bottom
+        downward = (end-start)[1] < 0
+        upward = not downward  # for easy code reading
 
         if isinstance(text_pos, str):
             if text_pos == 'start':
                 spacing_dir = unit_vec(start - end)
+                if upward:
+                    spacing *= 1.7
                 text_pos = start + spacing*spacing_dir
             elif text_pos == 'end':
                 spacing_dir = unit_vec(end - start)
+                if downward:
+                    spacing *= 1.7
                 text_pos = end + spacing*spacing_dir
-        self.shapes['text'] = Text(text, text_pos, fontsize=fontsize)
+        self.shapes['text'] = Text(text, text_pos, fontsize=fontsize,
+                                   alignment=text_alignment)
 
         # Stored geometric features
         self.start = start
         self.end = end
         self.symbol_location = text_pos
 
+class Axis2(Force):
+    def __init__(self, start, length, label,
+                 rotation_angle=0, fontsize=0,
+                 label_spacing=1./45, label_alignment='left'):
+        direction = point(cos(radians(rotation_angle)),
+                          sin(radians(rotation_angle)))
+        Force.__init__(start=start, end=length*direction, text=label,
+                       text_spacing=label_spacing,
+                       fontsize=fontsize, text_pos='end',
+                       text_alignment=label_alignment)
+        # Substitute text by label for axis
+        self.shapes['label'] = self.shapes['text']
+        del self.shapes['text']
+
+
+class Gravity(Axis):
+    """Downward-pointing gravity arrow with the symbol g."""
+    def __init__(self, start, length, fontsize=0):
+        Axis.__init__(self, start, length, '$g$', below=False,
+                      rotation_angle=-90, label_spacing=1./30,
+                      fontsize=fontsize)
+        self.shapes['arrow'].set_linecolor('black')
+
+
+class Gravity(Force):
+    """Downward-pointing gravity arrow with the symbol g."""
+    def __init__(self, start, length, text='$g$', fontsize=0):
+        Force.__init__(self, start, (start[0], start[1]-length),
+                       text, text_spacing=1./60,
+                       fontsize=0, text_pos='end')
+        self.shapes['arrow'].set_linecolor('black')
+
 
 class Distance_wText(Shape):
     """
@@ -1190,7 +1293,7 @@ class Arc_wText(Shape):
         self.shapes = {'arc': arc,
                        'text': Text(text, text_pos, fontsize=fontsize)}
 
-class Compose(Shape):
+class Composition(Shape):
     def __init__(self, shapes):
         """shapes: list or dict of Shape objects."""
         self.shapes = shapes
@@ -1290,7 +1393,7 @@ class Wheel(Shape):
         lines = [Line((xi,yi),(xo,yo)) for xi, yi, xo, yo in \
                  zip(xinner, yinner, xouter, youter)]
         self.shapes = {'inner': inner, 'outer': outer,
-                       'spokes': Compose(
+                       'spokes': Composition(
                            {'spoke%d' % i: lines[i]
                             for i in range(len(lines))})}
 
@@ -1510,7 +1613,7 @@ class Dashpot(Shape):
         abs_piston_pos = P0[1] + piston_pos
 
         gap = w*Dashpot.piston_gap_fraction
-        shapes['piston'] = Compose(
+        shapes['piston'] = Composition(
             {'line': Line(P2, (B[0], abs_piston_pos + piston_thickness)),
              'rectangle': Rectangle((B[0] - w+gap, abs_piston_pos),
                                     2*w-2*gap, piston_thickness),
@@ -1534,7 +1637,7 @@ class Dashpot(Shape):
         pp = Text('abs_piston_pos', (B[0]+7*w, abs_piston_pos), alignment='left')
         dims = {'start': start, 'width': width, 'dashpot_length': dplength,
                 'bar_length': blength, 'total_length': tlength,
-                'abs_piston_pos': Compose({'line': line, 'text': pp})}
+                'abs_piston_pos': Composition({'line': line, 'text': pp})}
         self.dimensions = dims
 
         # Stored geometric features
@@ -1555,14 +1658,19 @@ def test_Axis():
                           instruction_file='tmp_Axis.py')
     x_axis = Axis((7.5,2), 5, 'x', rotation_angle=0)
     y_axis = Axis((7.5,2), 5, 'y', below=False, rotation_angle=90)
-    system = Compose({'x axis': x_axis, 'y axis': y_axis})
+    system = Composition({'x axis': x_axis, 'y axis': y_axis})
     system.draw()
     drawing_tool.display()
-    set_linestyle('dashed')
-    #system.shapes['x axis'].rotate(40, (7.5, 2))
-    #system.shapes['y axis'].rotate(40, (7.5, 2))
+    system.set_linestyle('dashed')
     system.rotate(40, (7.5,2))
     system.draw()
+    drawing_tool.display()
+
+    system.set_linestyle('dotted')
+    system.rotate(220, (7.5,2))
+    system.draw()
+    drawing_tool.display()
+
     drawing_tool.display('Axis')
     drawing_tool.savefig('tmp_Axis.png')
     print repr(system)
@@ -1575,7 +1683,7 @@ def test_Distance_wText():
     #drawing_tool.arrow_head_width = 0.1
     fontsize=14
     t = r'$ 2\pi R^2 $'
-    dims2 = Compose({
+    dims2 = Composition({
         'a0': Distance_wText((4,5), (8, 5), t, fontsize),
         'a6': Distance_wText((4,5), (4, 4), t, fontsize),
         'a1': Distance_wText((0,2), (2, 4.5), t, fontsize),