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
 Classes are very suitable for implementing the various components that
 build up a sketch and their functionality. In particular, we shall
 build up a sketch and their functionality. In particular, we shall
 demonstrate that as soon some classes are established, more are easily
 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 =====
 ===== 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}
 FIGURE: [figs-sketcher/vehicle0_dim.png, width=400] Sketch of a simple figure. label{sketcher:fig:vehicle0}
 
 
 === Basic Drawing ===
 === Basic Drawing ===
 
 
 A typical program creating these five elements is shown next.
 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
 define a coordinate system. Some graphics operations are done with
 a helper object called `drawing_tool` (imported from `pysketcher`).
 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
 !bc pycod
 from pysketcher import *
 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)
 wheel1 = Circle(center=(w_1, R), radius=R)
 !ec
 !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
 To translate the geometric information about the `wheel1` object to
 instructions for the plotting engine (in this case Matplotlib), one calls the
 instructions for the plotting engine (in this case Matplotlib), one calls the
 `wheel1.draw()`. To display all drawn objects, one issues
 `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
 !bc pycod
 wheel1 = Circle(center=(w_1, R), radius=R)
 wheel1 = Circle(center=(w_1, R), radius=R)
 wheel1.draw()
 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
 group objects and call `draw`, or perform other operations, for
 the whole group. For example, we may collect the two wheels
 the whole group. For example, we may collect the two wheels
 in a `wheels` group and the `over` and `under` rectangles
 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
 !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
 !ec
 
 
 The ground is illustrated by an object of type `Wall`,
 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
 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
 !bc pycod
 ground = Wall(x=[w_1 - L, w_1 + 3*L], y=[0, 0], thickness=-0.3*R)
 ground = Wall(x=[w_1 - L, w_1 + 3*L], y=[0, 0], thickness=-0.3*R)
 !ec
 !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:
 the whole figure:
 !bc pycod
 !bc pycod
-fig = Compose({'vehicle': vehicle, 'ground': ground})
+fig = Composition({'vehicle': vehicle, 'ground': ground})
 fig.draw()  # send all figures to plotting backend
 fig.draw()  # send all figures to plotting backend
 drawing_tool.display()
 drawing_tool.display()
 drawing_tool.savefig('tmp.png')
 drawing_tool.savefig('tmp.png')
@@ -130,13 +149,15 @@ wheel1.set_linecolor('red')
 !ec
 !ec
 and so on.
 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:
 special visual pattern:
 !bc
 !bc
 # Set filling of all curves
 # 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
 # Turn off filling of all curves
 drawing_tool.set_filled_curves(False)
 drawing_tool.set_filled_curves(False)
+
 # Fill the wheel with red color
 # Fill the wheel with red color
 wheel1.set_filled_curves('red')
 wheel1.set_filled_curves('red')
 !ec
 !ec
@@ -180,21 +201,21 @@ print fig.show_hierarchy('std')
 yielding the output
 yielding the output
 !bc dat
 !bc dat
 ground (Wall):
 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):
         over (Rectangle):
             rectangle (Curve): 5 coords
             rectangle (Curve): 5 coords
         under (Rectangle):
         under (Rectangle):
             rectangle (Curve): 5 coords
             rectangle (Curve): 5 coords
-    wheels (Compose):
+    wheels (Composition):
         wheel1 (Circle):
         wheel1 (Circle):
             arc (Curve): 181 coords
             arc (Curve): 181 coords
         wheel2 (Circle):
         wheel2 (Circle):
             arc (Curve): 181 coords
             arc (Curve): 181 coords
 !ec
 !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.).
 special settings of the basic figure (fillcolor, line types, etc.).
 For example, `wheel2` is a `Circle` object consisting of an `arc`,
 For example, `wheel2` is a `Circle` object consisting of an `arc`,
 which is a `Curve` object consisting of 181 coordinates (the
 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
 The other object types are just compositions used to group
 parts of the complete figure.
 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.,
 Any of the objects can in the program be reached through their names, e.g.,
 !bc pycodc
 !bc pycodc
 fig['vehicle']
 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_filled_curves('blue')
 fig['vehicle']['wheels'].set_linewidth(6)
 fig['vehicle']['wheels'].set_linewidth(6)
 fig['vehicle']['wheels'].set_linecolor('black')
 fig['vehicle']['wheels'].set_linecolor('black')
+
 fig['vehicle']['body']['under'].set_filled_curves('red')
 fig['vehicle']['body']['under'].set_filled_curves('red')
+
 fig['vehicle']['body']['over'].set_filled_curves(pattern='/')
 fig['vehicle']['body']['over'].set_filled_curves(pattern='/')
 fig['vehicle']['body']['over'].set_linewidth(14)
 fig['vehicle']['body']['over'].set_linewidth(14)
+fig['vehicle']['body']['over']['rectangle'].linewidth = 4
 !ec
 !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}
 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.
 The relevant parts constitute the `fig['vehicle']` object.
 This part of the figure can be translated, rotated, and scaled.
 This part of the figure can be translated, rotated, and scaled.
 A translation along the ground means a translation in $x$ direction,
 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
 !bc pycod
 fig['vehicle'].translate((L,0))
 fig['vehicle'].translate((L,0))
 !ec
 !ec
@@ -248,7 +298,7 @@ drawing_tool.erase()
 fig.draw()
 fig.draw()
 drawing_tool.display()
 drawing_tool.display()
 !ec
 !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
 the figure so you get two vehicles. Without `fig.draw()` the
 new coordinates of the vehicle will not be communicated to
 new coordinates of the vehicle will not be communicated to
 the drawing tool, and without calling dislay the updated
 the drawing tool, and without calling dislay the updated
@@ -260,7 +310,7 @@ to that velocity in small steps of time:
 def v(t):
 def v(t):
     return -8*R*t*(1 - t/(2*R))
     return -8*R*t*(1 - t/(2*R))
 
 
-animate(fig, tp, user_action)
+animate(fig, tp, action)
 !ec
 !ec
 For small time steps `dt` the corresponding displacement is
 For small time steps `dt` the corresponding displacement is
 well approximated by `dt*v(t)` (we could integrate the velocity
 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`,
 time points `tp`, and a user function `action`,
 and then a new figure is drawn for each time point and the user
 and then a new figure is drawn for each time point and the user
 can through the provided `action` function modify desired parts
 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
 !bc pycod
 def move_vehicle(t, fig):
 def move_vehicle(t, fig):
     x_displacement = dt*v(t)
     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
 The `files` variable holds a string with the family of
 files constituting the frames in the movie, here
 files constituting the frames in the movie, here
 `'tmp_frame*.png'`. Making a movie out of the individual
 `'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
 !bc pycod
 from scitools.std import movie
 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
 !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
 # #ifdef PRIMER_BOOK
 `vehicle0.py`.
 `vehicle0.py`.
 # #else
 # #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
 # #endif
 
 
 === Animation: Rolling the Wheels ===
 === 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
 The construction of the wheels will now involve a circle
 and two lines:
 and two lines:
 !bc pycod
 !bc pycod
-wheel1 = Compose({'wheel':
+wheel1 = Composition({'wheel':
                   Circle(center=(w_1, R), radius=R),
                   Circle(center=(w_1, R), radius=R),
                   'cross':
                   '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))})})
                            'cross2': Line((w_1-R,R), (w_1+R,R))})})
 wheel2 = wheel1.copy()
 wheel2 = wheel1.copy()
 wheel2.translate((L,0))
 wheel2.translate((L,0))
@@ -378,7 +465,7 @@ The complete example is found in the file
 # #ifdef PRIMER_BOOK
 # #ifdef PRIMER_BOOK
 `vehicle1.py`.
 `vehicle1.py`.
 # #else
 # #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
 # #endif
 
 
 The advantages with making figures this way through programming,
 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,
     'dashpot': d, 'spring': s, 'mass': M, 'left wall': left_wall,
     'ground': ground, 'wheel1': wheel1, 'wheel2': wheel2})
     'ground': ground, 'wheel1': wheel1, 'wheel2': wheel2})
 
 
-#fig.draw()
-s.draw()
+fig.draw()
+#s.draw()
 print s
 print s
 print s.shapes['bar1']['line'].x, s.shapes['bar1']['line'].y
 print s.shapes['bar1']['line'].x, s.shapes['bar1']['line'].y
 print s.shapes['bar2']['line'].x, s.shapes['bar2']['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),
 over  = Rectangle(lower_left_corner=(w_1, 2*R + H),
                   width=2.5*R, height=1.25*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)
 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
 fig.draw()  # send all figures to plotting backend
 
 
 drawing_tool.display()
 drawing_tool.display()
@@ -44,6 +44,8 @@ drawing_tool.display()
 drawing_tool.savefig('tmp2.png')
 drawing_tool.savefig('tmp2.png')
 
 
 print fig
 print fig
+fig.recurse('fig')
+fig.graphviz_dot('fig', False)
 
 
 import time
 import time
 time.sleep(1)
 time.sleep(1)
@@ -65,7 +67,25 @@ def move_vehicle(t, fig):
 files = animate(fig, tp, move_vehicle, moviefiles=True,
 files = animate(fig, tp, move_vehicle, moviefiles=True,
                 pause_per_frame=0)
                 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
 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()
 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()
 print 'drawin_tool' in dir()
 
 
 def inclined_plane():
 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,
                                        #axis=True,
                                        )
                                        )
     #drawing_tool.set_grid(True)
     #drawing_tool.set_grid(True)
     fontsize = 18
     fontsize = 18
     from math import tan, radians
     from math import tan, radians
 
 
-    theta = 30.
-    L = 10.
-    a = 1.
     B = point(a+L, 0)
     B = point(a+L, 0)
     A = point(a, tan(radians(theta))*L)
     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,
                       start_angle=180-theta, arc_angle=theta,
                       fontsize=fontsize)
                       fontsize=fontsize)
     angle.set_linecolor('black')
     angle.set_linecolor('black')
@@ -49,44 +52,48 @@ def inclined_plane():
     wheel = Compose({'outer': outer_wheel, 'inner': hole})
     wheel = Compose({'outer': outer_wheel, 'inner': hole})
 
 
     drawing_tool.set_linecolor('black')
     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 = Line(contact, contact + point(0,4))
     x_const.set_linestyle('dotted')
     x_const.set_linestyle('dotted')
     x_const.rotate(-theta, contact)
     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,
     fixed = Compose({'angle': angle, 'inclined wall': wall,
                      'wheel': wheel, 'ground': ground,
                      'wheel': wheel, 'ground': ground,
                      'x start': x_const, 'x axis': x_axis})
                      'x start': x_const, 'x axis': x_axis})
 
 
     fig = Compose({'body': body, 'fixed elements': fixed})
     fig = Compose({'body': body, 'fixed elements': fixed})
 
 
-    #import copy
-    #body2 = copy.deepcopy(body)
-    #body2.translate(3, 0)
-    #body2.draw()
-
     fig.draw()
     fig.draw()
     drawing_tool.savefig('tmp.png')
     drawing_tool.savefig('tmp.png')
     drawing_tool.display()
     drawing_tool.display()
     import time
     import time
     time.sleep(1)
     time.sleep(1)
     tangent_vec = point(normal_vec[1], -normal_vec[0])
     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 str(fig)
     print repr(fig)
     print repr(fig)
 
 

+ 167 - 59
pysketcher/shapes.py

@@ -85,8 +85,8 @@ def is_sequence(*sequences, **kwargs):
                 print msg
                 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:
     if moviefiles:
         # Clean up old frame files
         # Clean up old frame files
         framefilestem = 'tmp_frame_'
         framefilestem = 'tmp_frame_'
@@ -97,13 +97,13 @@ def animate(fig, time_points, user_action, moviefiles=False,
     for n, t in enumerate(time_points):
     for n, t in enumerate(time_points):
         drawing_tool.erase()
         drawing_tool.erase()
 
 
-        user_action(t, fig)
+        action(t, fig, **action_kwargs)
         #could demand returning fig, but in-place modifications
         #could demand returning fig, but in-place modifications
         #are done anyway
         #are done anyway
-        #fig = user_action(t, fig)
+        #fig = action(t, fig)
         #if fig is None:
         #if fig is None:
         #    raise TypeError(
         #    raise TypeError(
-        #        'animate: user_action returns None, not fig\n'
+        #        'animate: action returns None, not fig\n'
         #        '(a Shape object with the whole figure)')
         #        '(a Shape object with the whole figure)')
 
 
         fig.draw()
         fig.draw()
@@ -193,14 +193,14 @@ class Shape:
                     raise TypeError(
                     raise TypeError(
                         'class %s has a self.shapes member "%s" that is just\n'
                         'class %s has a self.shapes member "%s" that is just\n'
                         'a plain dictionary,\n%s\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,
                         'object?' % (self.__class__.__name__, shape_name,
                         str(shape)))
                         str(shape)))
                 elif isinstance(shape, (list,tuple)):
                 elif isinstance(shape, (list,tuple)):
                     raise TypeError(
                     raise TypeError(
                         'class %s has self.shapes member "%s" containing\n'
                         'class %s has self.shapes member "%s" containing\n'
                         'a %s object %s,\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,
                         'object?' % (self.__class__.__name__, shape_name,
                         type(shape), str(shape)))
                         type(shape), str(shape)))
                 elif shape is None:
                 elif shape is None:
@@ -256,19 +256,72 @@ class Shape:
         self._for_all_shapes('minmax_coordinates', minmax)
         self._for_all_shapes('minmax_coordinates', minmax)
         return minmax
         return minmax
 
 
-    def traverse_hierarchy(self, indent=0):
+    def recurse(self, name, indent=0):
         if not isinstance(self.shapes, dict):
         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
         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]
               str(list(self.shapes.keys()))[1:-1]
         for shape in self.shapes:
         for shape in self.shapes:
             print space,
             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):
     def set_linestyle(self, style):
         styles = ('solid', 'dashed', 'dashdot', 'dotted')
         styles = ('solid', 'dashed', 'dashdot', 'dotted')
@@ -457,11 +510,13 @@ class Curve(Shape):
         minmax['ymax'] = max(self.y.max(), minmax['ymax'])
         minmax['ymax'] = max(self.y.max(), minmax['ymax'])
         return minmax
         return minmax
 
 
-    def traverse_hierarchy(self, indent=0):
+    def recurse(self, name, indent=0):
         space = ' '*indent
         space = ' '*indent
         print space, 'reached "bottom" object %s' % \
         print space, 'reached "bottom" object %s' % \
               self.__class__.__name__
               self.__class__.__name__
-        return len(space)/4  # level
+
+    def _object_couplings(self, parent, couplings=[], classname=True):
+        return
 
 
     def set_linecolor(self, color):
     def set_linecolor(self, color):
         self.linecolor = color
         self.linecolor = color
@@ -569,11 +624,13 @@ class Point(Shape):
         minmax['ymax'] = max(self.y, minmax['ymax'])
         minmax['ymax'] = max(self.y, minmax['ymax'])
         return minmax
         return minmax
 
 
-    def traverse_hierarchy(self, indent=0):
+    def recurse(self, name, indent=0):
         space = ' '*indent
         space = ' '*indent
         print space, 'reached "bottom" object %s' % \
         print space, 'reached "bottom" object %s' % \
               self.__class__.__name__
               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'):
     def show_hierarchy(self, indent=0, format='std'):
         s = '%s at (%g,%g)' % (self.__class__.__name__, self.x, self.y)
         s = '%s at (%g,%g)' % (self.__class__.__name__, self.x, self.y)
@@ -870,7 +927,7 @@ class Wall(Shape):
             x1 = asarray(x, float)
             x1 = asarray(x, float)
         if isinstance(y[0], (tuple,list,ndarray)):
         if isinstance(y[0], (tuple,list,ndarray)):
             # x is list of curves
             # x is list of curves
-            y = concatenate(y)
+            y1 = concatenate(y)
         else:
         else:
             y1 = asarray(y, float)
             y1 = asarray(y, float)
 
 
@@ -904,18 +961,31 @@ class Wall2(Shape):
             x1 = asarray(x, float)
             x1 = asarray(x, float)
         if isinstance(y[0], (tuple,list,ndarray)):
         if isinstance(y[0], (tuple,list,ndarray)):
             # x is list of curves
             # x is list of curves
-            y = concatenate(y)
+            y1 = concatenate(y)
         else:
         else:
             y1 = asarray(y, float)
             y1 = asarray(y, float)
 
 
         # Displaced curve (according to thickness)
         # 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
             # 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
         # Combine x1,y1 with x2,y2 reversed
         from numpy import concatenate
         from numpy import concatenate
         x = concatenate((x1, x2[-1::-1]))
         x = concatenate((x1, x2[-1::-1]))
@@ -1061,39 +1131,33 @@ class Text_wArrow(Text):
 
 
 
 
 class Axis(Shape):
 class Axis(Shape):
-    def __init__(self, start, length, label, below=True,
+    def __init__(self, start, length, label,
                  rotation_angle=0, fontsize=0,
                  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
         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).
         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
         The `label_spacing` denotes the space between the label
         and the arrow tip as a fraction of the length of the plot
         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 is vertical arrow, make it horizontal
         arrow = Arrow3(start, length, rotation_angle=-90)
         arrow = Arrow3(start, length, rotation_angle=-90)
         arrow.rotate(rotation_angle, start)
         arrow.rotate(rotation_angle, start)
         spacing = drawing_tool.xrange*label_spacing
         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 = Text(label, position=label_pos, fontsize=fontsize)
         label.rotate(rotation_angle, start)
         label.rotate(rotation_angle, start)
         self.shapes = {'arrow': arrow, 'label': label}
         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):
 class Force(Arrow1):
     """
     """
@@ -1105,28 +1169,67 @@ class Force(Arrow1):
     area away from the specified point.
     area away from the specified point.
     """
     """
     def __init__(self, start, end, text, text_spacing=1./60,
     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='->')
         Arrow1.__init__(self, start, end, style='->')
         spacing = drawing_tool.xrange*text_spacing
         spacing = drawing_tool.xrange*text_spacing
         start, end = arr2D(start), arr2D(end)
         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 isinstance(text_pos, str):
             if text_pos == 'start':
             if text_pos == 'start':
                 spacing_dir = unit_vec(start - end)
                 spacing_dir = unit_vec(start - end)
+                if upward:
+                    spacing *= 1.7
                 text_pos = start + spacing*spacing_dir
                 text_pos = start + spacing*spacing_dir
             elif text_pos == 'end':
             elif text_pos == 'end':
                 spacing_dir = unit_vec(end - start)
                 spacing_dir = unit_vec(end - start)
+                if downward:
+                    spacing *= 1.7
                 text_pos = end + spacing*spacing_dir
                 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
         # Stored geometric features
         self.start = start
         self.start = start
         self.end = end
         self.end = end
         self.symbol_location = text_pos
         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):
 class Distance_wText(Shape):
     """
     """
@@ -1190,7 +1293,7 @@ class Arc_wText(Shape):
         self.shapes = {'arc': arc,
         self.shapes = {'arc': arc,
                        'text': Text(text, text_pos, fontsize=fontsize)}
                        'text': Text(text, text_pos, fontsize=fontsize)}
 
 
-class Compose(Shape):
+class Composition(Shape):
     def __init__(self, shapes):
     def __init__(self, shapes):
         """shapes: list or dict of Shape objects."""
         """shapes: list or dict of Shape objects."""
         self.shapes = shapes
         self.shapes = shapes
@@ -1290,7 +1393,7 @@ class Wheel(Shape):
         lines = [Line((xi,yi),(xo,yo)) for xi, yi, xo, yo in \
         lines = [Line((xi,yi),(xo,yo)) for xi, yi, xo, yo in \
                  zip(xinner, yinner, xouter, youter)]
                  zip(xinner, yinner, xouter, youter)]
         self.shapes = {'inner': inner, 'outer': outer,
         self.shapes = {'inner': inner, 'outer': outer,
-                       'spokes': Compose(
+                       'spokes': Composition(
                            {'spoke%d' % i: lines[i]
                            {'spoke%d' % i: lines[i]
                             for i in range(len(lines))})}
                             for i in range(len(lines))})}
 
 
@@ -1510,7 +1613,7 @@ class Dashpot(Shape):
         abs_piston_pos = P0[1] + piston_pos
         abs_piston_pos = P0[1] + piston_pos
 
 
         gap = w*Dashpot.piston_gap_fraction
         gap = w*Dashpot.piston_gap_fraction
-        shapes['piston'] = Compose(
+        shapes['piston'] = Composition(
             {'line': Line(P2, (B[0], abs_piston_pos + piston_thickness)),
             {'line': Line(P2, (B[0], abs_piston_pos + piston_thickness)),
              'rectangle': Rectangle((B[0] - w+gap, abs_piston_pos),
              'rectangle': Rectangle((B[0] - w+gap, abs_piston_pos),
                                     2*w-2*gap, piston_thickness),
                                     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')
         pp = Text('abs_piston_pos', (B[0]+7*w, abs_piston_pos), alignment='left')
         dims = {'start': start, 'width': width, 'dashpot_length': dplength,
         dims = {'start': start, 'width': width, 'dashpot_length': dplength,
                 'bar_length': blength, 'total_length': tlength,
                 '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
         self.dimensions = dims
 
 
         # Stored geometric features
         # Stored geometric features
@@ -1555,14 +1658,19 @@ def test_Axis():
                           instruction_file='tmp_Axis.py')
                           instruction_file='tmp_Axis.py')
     x_axis = Axis((7.5,2), 5, 'x', rotation_angle=0)
     x_axis = Axis((7.5,2), 5, 'x', rotation_angle=0)
     y_axis = Axis((7.5,2), 5, 'y', below=False, rotation_angle=90)
     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()
     system.draw()
     drawing_tool.display()
     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.rotate(40, (7.5,2))
     system.draw()
     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.display('Axis')
     drawing_tool.savefig('tmp_Axis.png')
     drawing_tool.savefig('tmp_Axis.png')
     print repr(system)
     print repr(system)
@@ -1575,7 +1683,7 @@ def test_Distance_wText():
     #drawing_tool.arrow_head_width = 0.1
     #drawing_tool.arrow_head_width = 0.1
     fontsize=14
     fontsize=14
     t = r'$ 2\pi R^2 $'
     t = r'$ 2\pi R^2 $'
-    dims2 = Compose({
+    dims2 = Composition({
         'a0': Distance_wText((4,5), (8, 5), t, fontsize),
         'a0': Distance_wText((4,5), (8, 5), t, fontsize),
         'a6': Distance_wText((4,5), (4, 4), t, fontsize),
         'a6': Distance_wText((4,5), (4, 4), t, fontsize),
         'a1': Distance_wText((0,2), (2, 4.5), t, fontsize),
         'a1': Distance_wText((0,2), (2, 4.5), t, fontsize),