소스 검색

updates, debugging vehicle

Hans Petter Langtangen 13 년 전
부모
커밋
67b41aa3ad
85개의 변경된 파일643개의 추가작업 그리고 247개의 파일을 삭제
  1. 1 0
      README
  2. BIN
      doc/src/sketcher/figs-sketcher/vehicle0.png
  3. BIN
      doc/src/sketcher/figs-sketcher/vehicle0b.png
  4. BIN
      doc/src/sketcher/figs-sketcher/vehicle0v2.png
  5. BIN
      doc/src/sketcher/figs-sketcher/vehicle1.png
  6. 291 57
      doc/src/sketcher/sketcher.do.txt
  7. 0 0
      doc/src/sketcher/src-sketcher/animation_vehicle0/anim.html
  8. 0 0
      doc/src/sketcher/src-sketcher/animation_vehicle1/anim.html
  9. 22 18
      doc/src/sketcher/src-sketcher/vehicle0.py
  10. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0000.png
  11. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0001.png
  12. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0002.png
  13. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0003.png
  14. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0004.png
  15. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0005.png
  16. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0006.png
  17. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0007.png
  18. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0008.png
  19. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0009.png
  20. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0010.png
  21. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0011.png
  22. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0012.png
  23. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0013.png
  24. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0014.png
  25. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0015.png
  26. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0016.png
  27. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0017.png
  28. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0018.png
  29. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0019.png
  30. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0020.png
  31. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0021.png
  32. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0022.png
  33. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0023.png
  34. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0024.png
  35. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0025.png
  36. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0026.png
  37. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0027.png
  38. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0028.png
  39. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0029.png
  40. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0030.png
  41. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0031.png
  42. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0032.png
  43. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0033.png
  44. BIN
      doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0034.png
  45. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0000.png
  46. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0001.png
  47. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0002.png
  48. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0003.png
  49. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0004.png
  50. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0005.png
  51. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0006.png
  52. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0007.png
  53. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0008.png
  54. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0009.png
  55. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0010.png
  56. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0011.png
  57. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0012.png
  58. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0013.png
  59. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0014.png
  60. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0015.png
  61. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0016.png
  62. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0017.png
  63. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0018.png
  64. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0019.png
  65. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0020.png
  66. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0021.png
  67. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0022.png
  68. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0023.png
  69. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0024.png
  70. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0025.png
  71. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0026.png
  72. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0027.png
  73. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0028.png
  74. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0029.png
  75. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0030.png
  76. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0031.png
  77. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0032.png
  78. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0033.png
  79. BIN
      doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0034.png
  80. 1 1
      examples/wheel_on_inclined_plane.py
  81. 9 9
      index.html
  82. 86 46
      pysketcher/MatplotlibDraw.py
  83. 7 0
      pysketcher/__init__.py
  84. 213 116
      pysketcher/shapes.py
  85. 13 0
      setup.py

+ 1 - 0
README

@@ -0,0 +1 @@
+Python-based drawing tool for making sketches of mechanics problems.

BIN
doc/src/sketcher/figs-sketcher/vehicle0.png


BIN
doc/src/sketcher/figs-sketcher/vehicle0b.png


BIN
doc/src/sketcher/figs-sketcher/vehicle0v2.png


BIN
doc/src/sketcher/figs-sketcher/vehicle1.png


+ 291 - 57
doc/src/sketcher/sketcher.do.txt

@@ -35,18 +35,21 @@ With the drawing area in place we can make the first `Circle` object:
 !bc pycod
 from pysketcher import *
 
-drawing_tool.set_coordinate_system(xmin=0, xmax=15,
-                                   ymin=-1, ymax=10)
-R = 1  # radius of wheel
-wheel1 = Circle(center=(4, R), radius=R)
+R = 1    # radius of wheel
+L = 4    # distance between wheels
+H = 2    # height of vehicle body
+w_1 = 5  # position of front wheel
+drawing_tool.set_coordinate_system(xmin=0, xmax=w_1 + 2*L + 3*R,
+                                   ymin=-1, ymax=2*R + 3*H)
+
+wheel1 = Circle(center=(w_1, R), radius=R)
 !ec
 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:
 !bc pycod
-r = 1  # radius of wheel
-wheel1 = Circle(center=(4, r), radius=r)
+wheel1 = Circle(center=(w_1, R), radius=R)
 wheel1.draw()
 
 # Define other objects and call their draw() methods
@@ -59,16 +62,14 @@ translating the object a distance (to the right) described by the
 vector $(4,0)$:
 !bc pycod
 wheel2 = wheel1.copy()
-wheel2.translate((4,0))
+wheel2.translate((L,0))
 !ec
 The two rectangles are made in an intuitive way:
 !bc pycod
-height1 = 2
-under = Rectangle(lower_left_corner=(2, 2*R),
-                  width=8, height=height1)
-height2 = 2.5
-over = Rectangle(lower_left_corner=(4, 2*R+height1),
-                 width=2.5, height=height2)
+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)
 !ec
 
 === Groups of Objects ===
@@ -92,7 +93,8 @@ 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:
 !bc pycod
-ground = CurveWall(x=[0, 12], y=[0, 0], thickness=-0.3)
+ground = CurveWall(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 whole figure:
@@ -224,7 +226,7 @@ fig['vehicle']['body']['over'].set_filled_curves(pattern='/')
 fig['vehicle']['body']['over'].set_linewidth(10)
 !ec
 
-FIGURE: [figs-sketcher/vehicle0v2.png, width=400] Changed properties of parts of the figure. label{sketcher:fig:vehicle0:v2}
+FIGURE: [figs-sketcher/vehicle0b.png, width=400] Changed properties of parts of the figure. label{sketcher:fig:vehicle0:v2}
 
 We can also change position of parts of the figure and thereby make
 animations, as shown next.
@@ -238,7 +240,7 @@ 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:
 !bc pycod
-fig['vehicle'].translate((3,0))
+fig['vehicle'].translate((L,0))
 !ec
 You need to erase, draw, and display to see the movement:
 !bc pycod
@@ -256,7 +258,7 @@ Let us make a velocity function and move the object according
 to that velocity in small steps of time:
 !bc pydoc
 def v(t):
-    return point(-t*(1-t/5.), 0)
+    return -8*R*t*(1 - t/(2*R))
 
 animate(fig, tp, user_action)
 !ec
@@ -278,7 +280,7 @@ Defining a set of time points for the frames in the animation
 and performing the animation is done by
 !bc pycod
 import numpy
-tp = numpy.linspace(0, 5, 35)
+tp = numpy.linspace(0, 2*R, 25)
 dt = tp[1] - tp[0]  # time step
 
 animate(fig, tp, move_vehicle, pause_per_frame=0.2)
@@ -316,14 +318,13 @@ two crossing lines, see Figure ref{sketcher:fig:vehicle1}.
 The construction of the wheels will now involve a circle
 and two lines:
 !bc pycod
-R = 1.  # radius of wheel
 wheel1 = Compose({'wheel':
-                  Circle(center=(4, R), radius=R),
+                  Circle(center=(w_1, R), radius=R),
                   'cross':
-                  Compose({'cross1': Line((4,0), (4,2*R)),
-                          'cross2': Line((4-R,R), (4+R,R))})})
+                  Compose({'cross1': Line((w_1,0),   (w_1,2*R)),
+                           'cross2': Line((w_1-R,R), (w_1+R,R))})})
 wheel2 = wheel1.copy()
-wheel2.translate((4,0))
+wheel2.translate((L,0))
 !ec
 Observe that `wheel1.copy()` copies all the objects that make
 up the first wheel, and `wheel2.translate` translates all
@@ -339,36 +340,39 @@ the wheel, leading to a rotation angle
 !bc pycod
 angle = - displacement[0]/R
 !ec
-With `wheel1_center` tracking the $x$ coordinate of the center
+With `w_1` tracking the $x$ coordinate of the center
 of the front wheel, we can rotate that wheel by
 !bc pycod
 w1 = fig['vehicle']['wheels']['wheel1']
 from math import degrees
-w1.rotate(degrees(angle), center=(wheel1_center, R))
+w1.rotate(degrees(angle), center=(w_1, R))
 !ec
 The `rotate` function takes two parameters: the rotation angle
 (in degrees) and the center point of the rotation, which is the
 center of the wheel in this case. The other wheel is rotated by
 !bc pycod
 w2 = fig['vehicle']['wheels']['wheel2']
-w2.rotate(degrees(angle), center=(wheel1_center+4, R))
+w2.rotate(degrees(angle), center=(w_1 + L, R))
 !ec
 That is, the angle is the same, but the rotation point is different.
-The update of the center point is done by `wheel1_center += displacement[0]`.
+The update of the center point is done by `w_1 += displacement[0]`.
 The complete `move_vehicle` function then becomes
 !bc pycod
-wheel1_center = 7   # start position
+w_1 = w_1 + L   # start position
 
 def move_vehicle(t, fig):
     displacement = dt*v(t)
     fig['vehicle'].translate(displacement)
-    global wheel1_center
-    wheel1_center += displacement[0]
+
+    # Rotate wheels
+    global w_1
+    w_1 += displacement[0]
+    # R*angle = -x_displacement
     angle = - displacement[0]/R
     w1 = fig['vehicle']['wheels']['wheel1']
-    w1.rotate(degrees(angle), center=(wheel1_center, R))
+    w1.rotate(degrees(angle), center=(w_1, R))
     w2 = fig['vehicle']['wheels']['wheel2']
-    w2.rotate(degrees(angle), center=(wheel1_center+4, R))
+    w2.rotate(degrees(angle), center=(w_1 + L, R))
 !ec
 The complete example is found in the file
 # #ifdef PRIMER_BOOK
@@ -391,38 +395,268 @@ based on solving differential equations reflecting the physics
 of the problem.
 
 
+===== Example of Classes for Geometric Objects =====
 
+We shall now explain how we can, quite easily, realize software
+with the capabilities demonstrated above. Each object in the
+figure is represented as a class in a class hierarchy. Using
+inheritance, classes can inherit properties from parent classes
+and add new geometric features.
 
+We introduce class `Shape` as superclass for all specialized objects
+in a figure. This class does not store any data, but provides a
+series of functions that add functionality to all the subclasses.
+This will be shown later.
 
-We can change the color and thickness of the lines and also fill
-circles, rectangles, etc.~with a color. Figure ref{fig:shapes2}
-shows the result of the following example, where
-we first define elements in the figure and then adjust the line
-color and other properties prior to calling the `draw` methods:
-!bc cod
-r1 = Rectangle(lower_left_corner=(0,1), width=3, height=5)
-c1 = Circle(center=(5,7), radius=1)
-w1 = Wheel(center=(6,2), radius=2, inner_radius=0.5, nlines=7)
-c2 = Circle(center=(7,7), radius=1)
-filled_curves(True)
-c1.draw()               # filled red circle
-set_linecolor('blue')
-r1.draw()               # filled blue rectangle
-set_linecolor('aqua')
-c2.draw()               # filled aqua/cyan circle
-# Add thick aqua line around rectangle
-filled_curves(False)
-set_linewidth(4)
-r1.draw()
-set_linecolor('red')
-w1.draw()
-display()
+=== Simple Geometric Objects ===
+
+One simple subclass is `Rectangle`:
+!bc pycod
+class Rectangle(Shape):
+    def __init__(self, lower_left_corner, width, height):
+        p = lower_left_corner  # short form
+        x = [p[0], p[0] + width,
+             p[0] + width, p[0], p[0]]
+        y = [p[1], p[1], p[1] + height,
+             p[1] + height, p[1]]
+        self.shapes = {'rectangle': Curve(x,y)}
+!ec
+Any subclass of `Shape` will have a constructor which takes
+geometric information about the shape of the object and
+creates a dictionary `self.shapes` with the shape built of
+simpler shapes. The most fundamental shape is `Curve`, which is
+just a collection of $(x,y)$ coordinates in two arrays `x` and `y`.
+Drawing the `Curve` object is a matter of plotting `y` versus `x`.
+
+The `Rectangle` class illustrates how the constructor takes information
+about the lower left corner, the width and the height, and
+creates coordinate arrays `x` and `y` consisting of the four corners,
+plus the first one repeated such that plotting `x` and `y` will
+form a closed four-sided rectangle. This construction procedure
+demands that the rectangle will always be aligned with the $x$ and
+$y$ axis. However, we may easily rotate the rectangle about
+any point once the object is constructed.
+
+Class `Line` constitutes a similar example:
+!bc pycod
+class Line(Shape):
+    def __init__(self, start, end):
+        x = [start[0], end[0]]
+        y = [start[1], end[1]]
+        self.shapes = {'line': Curve(x, y)}
+!ec
+Here we only need two points, the start and end point on the line.
+However, we may add some useful functionality, e.g., the ability
+to give an $x$ coordinate and have the class calculate the
+corresponding $y$ coordinate:
+!bc pycod
+    def __call__(self, x):
+        """Given x, return y on the line."""
+        x, y = self.shapes['line'].x, self.shapes['line'].y
+        self.a = (y[1] - y[0])/(x[1] - x[0])
+        self.b = y[0] - self.a*x[0]
+        return self.a*x + self.b
+!ec
+Unfortunately, this is too simplistic because vertical lines cannot
+be handled (infinte `self.a`). The source code of `Line` therefore
+provides a more general solution at the cost of significantly
+longer code with more tests.
+
+A circle gives us somewhat increased complexity. Again we represent
+the geometic object by a `Curve` object, but this time the `Curve`
+object needs to store a large number of points on the curve such
+that a plotting program produces a visually smooth curve.
+The points on the circle must be calculated manually in the constructor
+of class `Circle`. The formulas for points $(x,y)$ on a curve with radius
+$R$ and center at $(x_0, y_0)$ are given by
+!bt
+\begin{align*}
+x &= x_0 + R\cos (t),\\
+y &= y_0 + R\sin (t),
+\end{align*}
+!et
+where $t\in [0, 2\pi]$. A discrete set of $t$ values in this
+interval gives the corresponding set of $(x,y)$ coordinates on
+the circle. The user must specify the resolution, i.e., the number
+of $t$ values, or equivalently, points on the circle. The circle's
+radius and center must of course also be specified.
+
+We can write the `Circle` class as
+!bc pycod
+class Circle(Shape):
+    def __init__(self, center, radius, resolution=180):
+        self.center, self.radius = center, radius
+        self.resolution = resolution
+
+        t = linspace(0, 2*pi, resolution+1)
+        x0 = center[0];  y0 = center[1]
+        R = radius
+        x = x0 + R*cos(t)
+        y = y0 + R*sin(t)
+        self.shapes = {'circle': Curve(x, y)}
+!ec
+As in class `Line` we can offer the possibility to give an angle
+$\theta$ (equivalent to $t$ in the formulas above)
+and then get the corresponding $x$ and $y$ coordinates:
+!bc pycod
+    def __call__(self, theta):
+        """Return (x, y) point corresponding to angle theta."""
+        return self.center[0] + self.radius*cos(theta), \
+               self.center[1] + self.radius*sin(theta)
+!ec
+There is one flaw with this method: it yields illegal values after
+a translation, scaling, or rotation of the circle.
+
+A part of a circle, an arc, is a frequent geometric object when
+drawing mechanical systems. The arc is constructed much like
+a circle, but $t$ runs in $[\theta_0, \theta_1]$. Giving
+$\theta_1$ and $\theta_2$ the slightly more descriptive names
+`start_angle` and `arc_angle`, the code looks like this:
+!bc pycod
+class Arc(Shape):
+    def __init__(self, center, radius,
+                 start_angle, arc_angle,
+                 resolution=180):
+        self.center = center
+        self.radius = radius
+        self.start_angle = start_angle*pi/180  # radians
+        self.arc_angle = arc_angle*pi/180
+        self.resolution = resolution
+
+        t = linspace(self.start_angle,
+                     self.start_angle + self.arc_angle,
+                     resolution+1)
+        x0 = center[0];  y0 = center[1]
+        R = radius
+        x = x0 + R*cos(t)
+        y = y0 + R*sin(t)
+        self.shapes = {'arc': Curve(x, y)}
+!ec
+
+Having the `Arc` class, a `Circle` can alternatively befined as
+a subclass specializing the arc to a circle:
+!bc pycod
+class Circle(Arc):
+    def __init__(self, center, radius, resolution=180):
+        Arc.__init__(self, center, radius, 0, 360, resolution)
 !ec
 
+A wall is about drawing a curve, displacing the curve vertically by
+some thickness, and then filling the space between the curves
+by some pattern. The input is the `x` and `y` coordinate arrays
+of the curve and a thickness parameter. The computed coordinates
+will be a polygon: going along the originally curve and then back again
+along the vertically displaced curve. The relevant code becomes
+!bc pycod
+class CurveWall(Shape):
+    def __init__(self, x, y, thickness):
+        # User's curve
+        x1 = asarray(x, float)
+        y1 = asarray(y, float)
+        # Displaced curve (according to thickness)
+        x2 = x1
+        y2 = y1 + thickness
+        # Combine x1,y1 with x2,y2 reversed
+        from numpy import concatenate
+        x = concatenate((x1, x2[-1::-1]))
+        y = concatenate((y1, y2[-1::-1]))
+        wall = Curve(x, y)
+        wall.set_filled_curves(color='white', pattern='/')
+        self.shapes = {'wall': wall}
+!ec
+
+=== Class Curve ===
+
+Class `Curve` sits on the coordinates to be drawn, but how is
+that done? The constructor just stores the coordinates, while
+a method `draw` sends the coordinates to the plotting program
+to make a graph.
+Or more precisely, to avoid a lot of (e.g.) Matplotlib-specific
+plotting commands we have created a small layer with a
+simple programming interface to plotting programs. This makes it
+straightforward to change from Matplotlib to another plotting
+program. The programming interface is represented by the `drawing_tool`
+object and has a few functions:
+
+  * `plot_curve` for sending a curve in terms of $x$ and $y$ coordinates
+    to the plotting program,
+  * `set_coordinate_system` for specifying the graphics area,
+  * `erase` for deleting all elements of the graph,
+  * `set_grid` for turning on a grid (convenient while constructing the plot),
+  * `set_instruction_file` for creating a separate file with all
+    plotting commands (Matplotlib commands in our case),
+  * a series of `set_X` functions where `X` is some property like
+    `linecolor`, `linestyle`, `linewidth`, `filled_curves`.
+
+This is basically all we need to communicate to a plotting program.
+
+Any class in the `Shape` hierarchy inherits `set_X` functions for
+setting properties of curves. This information is propagated to
+all other shape objects that make up the figure. Class
+`Curve` stores the line properties together with the coordinates
+of its curve and propagates this information to the plotting program.
+When saying `vehicle.set_linewidth(10)`, all objects that make
+up the `vehicle` object will get a `set_linewidth(10)` call,
+but only the `Curve` object at the end of the chain will actually
+store the information and send it to the plotting program.
+
+A rough sketch of class `Curve` reads
+!bc pycod
+class Curve(Shape):
+    """General curve as a sequence of (x,y) coordintes."""
+    def __init__(self, x, y):
+        self.x = asarray(x, dtype=float)
+        self.y = asarray(y, dtype=float)
+
+        self.linestyle = None
+        self.linewidth = None
+        self.linecolor = None
+        self.fillcolor = None
+        self.fillpattern = None
+        self.arrow = None
+
+    def draw(self):
+        drawing_tool.plot_curve(
+            self.x, self.y,
+            self.linestyle, self.linewidth, self.linecolor,
+            self.arrow, self.fillcolor, self.fillpattern)
+
+    def set_linewidth(self, width):
+        self.linewidth = width
+
+    det set_linestyle(self, style):
+        self.linestyle = style
+    ...
+!ec
+
+=== Compound Geometric Objects ===
+
+The sample classes so far has managed to define the geometric shape
+through just one `Curve` object.[[[
+
+
+
+Some objects in a figure will be associated with a point and not
+a curve. Therefore, it is natural to introduce a `Point` class
+as superclass for such objects:
+!bc pycod
+class Point(Shape):
+    def __init__(self, x, y):
+        self.x, self.y = x, y
+!ec
+A candidate for subclass is a text located at a given point:
+!bc pycod
+class Text(Point):
+    def __init__(self, text, position, alignment='center', fontsize=18):
+        self.text = text
+        self.alignment, self.fontsize = alignment, fontsize
+        is_sequence(position, length=2, can_be_None=True)
+        Point.__init__(self, position[0], position[1])
+        #no need for self.shapes here
+!ec
 
-===== Overall Design of the Class Hierarchy =====
+[[[[[[[[[[[
 
-Let us have a class `Shape` as superclass for all specialized shapes.
 Class `Line` is a subclass of `Shape` and
 represents the simplest shape: a stright line between two points.
 Class `Rectangle` is another subclass of `Shape`, implementing the

doc/src/sketcher/src-sketcher/vehicle0_animation/anim.html → doc/src/sketcher/src-sketcher/animation_vehicle0/anim.html


doc/src/sketcher/src-sketcher/vehicle1_animation/anim.html → doc/src/sketcher/src-sketcher/animation_vehicle1/anim.html


+ 22 - 18
doc/src/sketcher/src-sketcher/vehicle0.py

@@ -1,26 +1,29 @@
-import sys, os, time
-sys.path.insert(0, os.path.join(os.pardir, os.pardir, os.pardir, os.pardir))
 from pysketcher import *
 
-drawing_tool.set_coordinate_system(xmin=0, xmax=15,
-                                   ymin=-1, ymax=10)
-R = 1  # radius of wheel
-wheel1 = Circle(center=(4, R), radius=R)
+R = 1    # radius of wheel
+L = 4    # distance between wheels
+H = 2    # height of vehicle body
+w_1 = 5  # position of front wheel
+
+drawing_tool.set_coordinate_system(xmin=0, xmax=w_1 + 2*L + 3*R,
+                                   ymin=-1, ymax=2*R + 3*H,
+                                   axis=True)
+
+wheel1 = Circle(center=(w_1, R), radius=R)
 wheel2 = wheel1.copy()
-wheel2.translate((4,0))
+wheel2.translate((L,0))
 
-height1 = 2
-under = Rectangle(lower_left_corner=(2, 2*R),
-                  width=8, height=height1)
-height2 = 2.5
-over = Rectangle(lower_left_corner=(4, 2*R+height1),
-                 width=2.5, height=height2)
+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 = Compose({'wheel1': wheel1, 'wheel2': wheel2})
 body = Compose({'under': under, 'over': over})
 
 vehicle = Compose({'wheels': wheels, 'body': body})
-ground = CurveWall(x=[0, 12], y=[0, 0], thickness=-0.3)
+ground = CurveWall(x=[w_1 - L, w_1 + 3*L], y=[0, 0],
+                   thickness=-0.3*R)
 
 fig = Compose({'vehicle': vehicle, 'ground': ground})
 fig.draw()  # send all figures to plotting backend
@@ -42,21 +45,22 @@ drawing_tool.savefig('tmp2.png')
 
 print fig
 
+import time
 time.sleep(1)
 
 # Animate motion
-fig['vehicle'].translate((3,0))  # move to start point for "driving"
+fig['vehicle'].translate((L,0))  # move to start point for "driving"
 
 def v(t):
-    return point(-t*(1-t/5.), 0)
+    return -8*R*t*(1 - t/(2*R))
 
 import numpy
-tp = numpy.linspace(0, 5, 35)
+tp = numpy.linspace(0, 2*R, 25)
 dt = tp[1] - tp[0]  # time step
 
 def move_vehicle(t, fig):
     displacement = dt*v(t)
-    fig['vehicle'].translate(displacement)
+    fig['vehicle'].translate((displacement, 0))
 
 files = animate(fig, tp, move_vehicle, moviefiles=True,
                 pause_per_frame=0)

BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0000.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0001.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0002.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0003.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0004.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0005.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0006.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0007.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0008.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0009.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0010.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0011.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0012.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0013.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0014.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0015.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0016.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0017.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0018.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0019.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0020.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0021.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0022.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0023.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0024.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0025.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0026.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0027.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0028.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0029.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0030.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0031.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0032.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0033.png


BIN
doc/src/sketcher/src-sketcher/vehicle0_animation/tmp_frame_0034.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0000.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0001.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0002.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0003.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0004.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0005.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0006.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0007.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0008.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0009.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0010.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0011.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0012.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0013.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0014.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0015.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0016.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0017.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0018.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0019.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0020.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0021.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0022.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0023.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0024.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0025.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0026.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0027.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0028.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0029.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0030.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0031.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0032.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0033.png


BIN
doc/src/sketcher/src-sketcher/vehicle1_animation/tmp_frame_0034.png


+ 1 - 1
examples/wheel_on_inclined_plane.py

@@ -23,7 +23,7 @@ def inclined_plane():
     wall = CurveWall(x=[A[0], B[0]], y=[A[1], B[1]], thickness=-0.25)
 
     angle = ArcSymbol(r'$\theta$', center=B, radius=3,
-                      start_degrees=180-theta, opening_degrees=theta,
+                      start_angle=180-theta, arc_angle=theta,
                       fontsize=fontsize)
     angle.set_linecolor('black')
     angle.set_linewidth(1)

+ 9 - 9
index.html

@@ -46,26 +46,26 @@
       <span class="small">by <a href="https://github.com/hplgit">hplgit</a></span></h1>
 
     <div class="description">
-      Python-based drawing tool for making sketches of physical systems
+      Python-based drawing tool for making sketches of mechanics problems
     </div>
 
-    
 
-    
 
-    
 
-    
 
-    
+
+
+
+
+
       <h2>Authors</h2>
       <p>Hans Petter Langtangen (hpl@simula.no)
<br/>      </p>
-    
 
-    
+
+
       <h2>Contact</h2>
       <p>Hans Petter Langtangen (hpl@simula.no)
<br/>      </p>
-    
+
 
     <h2>Download</h2>
     <p>

+ 86 - 46
pysketcher/MatplotlibDraw.py

@@ -11,21 +11,15 @@ class MatplotlibDraw:
     def __init__(self):
         self.instruction_file = None
 
-    def set_instruction_file(self, filename='tmp_mpl.py'):
-        """
-        instruction_file: name of file where all the instructions
-        are recorded.
-        """
-        if filename:
-            self.instruction_file = open(self.instruction_file, 'w')
-        else:
-            self.instruction_file = None
-
-    def set_coordinate_system(self, xmin, xmax, ymin, ymax, axis=False):
+    def set_coordinate_system(self, xmin, xmax, ymin, ymax, axis=False,
+                              instruction_file=None):
         """
         Define the drawing area [xmin,xmax]x[ymin,ymax].
         axis: None or False means that axes with tickmarks
         are not drawn.
+        instruction_file: name of file where all the instructions
+        for the plotting program are stored (useful for debugging
+        a figure or tailoring plots).
         """
         self.mpl = mpl
         self.xmin, self.xmax, self.ymin, self.ymax = \
@@ -42,8 +36,13 @@ class MatplotlibDraw:
         geometry = '%dx%d' % (self.xsize, self.ysize)
         # See http://stackoverflow.com/questions/7449585/how-do-you-set-the-absolute-position-of-figure-windows-with-matplotlib
 
+        if isinstance(instruction_file, str):
+            self.instruction_file = open(instruction_file, 'w')
+        else:
+            self.instruction_file = None
+
         self.mpl.ion()  # important for interactive drawing and animation
-        if self.instruction_file is not None:
+        if self.instruction_file:
             self.instruction_file.write("""\
 import matplotlib.pyplot as mpl
 
@@ -54,10 +53,13 @@ mpl.ion()  # for interactive drawing
         manager = self.mpl.get_current_fig_manager()
         manager.window.wm_geometry(geometry)
 
+
+        # Default properties
         self.set_linecolor('red')
         self.set_linewidth(2)
         self.set_linestyle('solid')
-        self.set_filled_curves()
+        self.set_filled_curves()  # no filling
+        self.arrow_head_width = 0.2
 
     def _make_axes(self, new_figure=False):
         if new_figure:
@@ -73,7 +75,7 @@ mpl.ion()  # for interactive drawing
         else:
             axis_cmd = ''
 
-        if self.instruction_file is not None:
+        if self.instruction_file:
             fig = 'fig = mpl.figure()\n' if new_figure else ''
             self.instruction_file.write("""\
 %s
@@ -86,8 +88,32 @@ ax.set_aspect('equal')
 
 """ % (fig, self.xmin, self.xmax, self.ymin, self.ymax, axis_cmd))
 
+    def inside(self, pt):
+        """Is point pt inside the defined plotting area?"""
+        area = '[%s,%s]x[%s,%s]' % \
+               (self.xmin, self.xmax, self.ymin, self.ymax)
+        pt_inside = True
+        if self.xmin <= pt[0] <= self.xmax:
+            pass
+        else:
+            pt_inside = False
+        if self.ymin <= pt[1] <= self.ymax:
+            pass
+        else:
+            pt_inside = False
+        if pt_inside:
+            return pt_inside, 'point=%s is inside plotting area %s' % \
+                   (pt, area)
+        else:
+            return pt_inside, 'point=%s is outside plotting area %s' % \
+                   (pt, area)
+
     def set_linecolor(self, color):
-        """Change the color of lines."""
+        """
+        Change the color of lines. Available colors are
+        'black', 'white', 'red', 'blue', 'green', 'yellow',
+        'magenta', 'cyan'.
+        """
         self.linecolor = MatplotlibDraw.line_colors[color]
 
     def set_linestyle(self, style):
@@ -101,7 +127,11 @@ ax.set_aspect('equal')
         self.linewidth = width
 
     def set_filled_curves(self, color='', pattern=''):
-        """Fill area inside curves with current line color."""
+        """
+        Fill area inside curves with specified color and/or pattern.
+        A common pattern is '/' (45 degree lines). Other patterns
+        include....
+        """
         if color is False:
             self.fillcolor = ''
             self.fillpattern = ''
@@ -112,21 +142,21 @@ ax.set_aspect('equal')
 
     def set_grid(self, on=False):
         self.mpl.grid(on)
-        if self.instruction_file is not None:
+        if self.instruction_file:
             self.instruction_file.write("\nmpl.grid(%s)\n" % str(on))
 
     def erase(self):
         """Erase the current figure."""
         self.mpl.delaxes()
-        if self.instruction_file is not None:
+        if self.instruction_file:
             self.instruction_file.write("\nmpl.delaxes()  # erase\n")
 
         self._make_axes(new_figure=False)
 
-    def define_curve(self, x, y,
-                     linestyle=None, linewidth=None,
-                     linecolor=None, arrow=None,
-                     fillcolor=None, fillpattern=None):
+    def plot_curve(self, x, y,
+                   linestyle=None, linewidth=None,
+                   linecolor=None, arrow=None,
+                   fillcolor=None, fillpattern=None):
         """Define a curve with coordinates x and y (arrays)."""
         self.xdata = np.asarray(x, dtype=np.float)
         self.ydata = np.asarray(y, dtype=np.float)
@@ -143,7 +173,7 @@ ax.set_aspect('equal')
         if fillpattern is None:
             fillpattern = self.fillpattern
 
-        if self.instruction_file is not None:
+        if self.instruction_file:
             import pprint
             self.instruction_file.write('x = %s\n' % \
                                         pprint.pformat(self.xdata.tolist()))
@@ -157,39 +187,41 @@ ax.set_aspect('equal')
             #print '%d coords, fillcolor="%s" linecolor="%s" fillpattern="%s"' % (x.size, fillcolor, linecolor, fillpattern)
             self.ax.fill(x, y, fillcolor, edgecolor=linecolor,
                          linewidth=linewidth, hatch=fillpattern)
-            if self.instruction_file is not None:
+            if self.instruction_file:
                 self.instruction_file.write("ax.fill(x, y, '%s', edgecolor='%s', linewidth=%d, hatch='%s')\n" % (fillcolor, linecolor, linewidth, fillpattern))
         else:
             self.ax.plot(x, y, linecolor, linewidth=linewidth,
                          linestyle=linestyle)
-            if self.instruction_file is not None:
+            if self.instruction_file:
                 self.instruction_file.write("ax.plot(x, y, '%s', linewidth=%d, linestyle='%s')\n" % (linecolor, linewidth, linestyle))
         if arrow:
-            if not arrow in ('start', 'end', 'both'):
-                raise ValueError("arrow argument must be 'start', 'end', or 'both', not %s" % repr(arrow))
+            if not arrow in ('->', '<-', '<->'):
+                raise ValueError("arrow argument must be '->', '<-', or '<->', not %s" % repr(arrow))
 
             # Add arrow to first and/or last segment
-            start = arrow == 'start' or arrow == 'both'
-            end = arrow == 'end' or arrow == 'both'
+            start = arrow == '<-' or arrow == '<->'
+            end = arrow == '->' or arrow == '<->'
             if start:
                 x_s, y_s = x[1], y[1]
                 dx_s, dy_s = x[0]-x[1], y[0]-y[1]
-                self.arrow(x_s, y_s, dx_s, dy_s)
+                self.plot_arrow(x_s, y_s, dx_s, dy_s, '->',
+                                linestyle, linewidth, linecolor)
             if end:
                 x_e, y_e = x[-2], y[-2]
                 dx_e, dy_e = x[-1]-x[-2], y[-1]-y[-2]
-                self.arrow(x_e, y_e, dx_e, dy_e)
+                self.plot_arrow(x_e, y_e, dx_e, dy_e, '->',
+                                linestyle, linewidth, linecolor)
 
     def display(self):
         """Display the figure. Last possible command."""
         self.mpl.draw()
-        if self.instruction_file is not None:
+        if self.instruction_file:
             self.instruction_file.write('mpl.draw()\n')
 
     def savefig(self, filename):
         """Save figure in file."""
         self.mpl.savefig(filename)
-        if self.instruction_file is not None:
+        if self.instruction_file:
             self.instruction_file.write('mpl.savefig(%s)\n' % filename)
 
     def text(self, text, position, alignment='center', fontsize=18,
@@ -205,7 +237,7 @@ ax.set_aspect('equal')
         if arrow_tip is None:
             self.ax.text(x, y, text, horizontalalignment=alignment,
                          fontsize=fontsize)
-            if self.instruction_file is not None:
+            if self.instruction_file:
                 self.instruction_file.write("""\
 ax.text(%g, %g, %s,
         horizontalalignment=%s, fontsize=%d)
@@ -225,7 +257,7 @@ ax.text(%g, %g, %s,
                                              linewidth=1,
                                              shrinkA=5,
                                              shrinkB=5))
-            if self.instruction_file is not None:
+            if self.instruction_file:
                 self.instruction_file.write("""\
 ax.annotate('%s', xy=%s, xycoords='data',
             textcoords='data', xytext=%s,
@@ -245,7 +277,7 @@ ax.annotate('%s', xy=%s, xycoords='data',
 #http://matplotlib.sourceforge.net/users/annotations_intro.html
 #http://matplotlib.sourceforge.net/users/annotations_guide.html#plotting-guide-annotation
 
-    def arrow(self, x, y, dx, dy, style='->',
+    def plot_arrow(self, x, y, dx, dy, style='->',
               linestyle=None, linewidth=None, linecolor=None):
         """Draw arrow (dx,dy) at (x,y). `style` is '->', '<-' or '<->'."""
         if linestyle is None:
@@ -261,11 +293,12 @@ ax.annotate('%s', xy=%s, xycoords='data',
                            facecolor=linecolor,
                            edgecolor=linecolor,
                            linewidth=linewidth,
-                           head_width=0.1,
-                           #width=1,
+                           head_width=self.arrow_head_width,
+                           #head_width=0.1,
+                           #width=1,  # width of arrow body in coordinate scale
                            length_includes_head=True,
                            shape='full')
-            if self.instruction_file is not None:
+            if self.instruction_file:
                 self.instruction_file.write("""\
 mpl.arrow(x=%g, y=%g, dx=%g, dy=%g,
           facecolor='%s', edgecolor='%s',
@@ -282,7 +315,7 @@ mpl.arrow(x=%g, y=%g, dx=%g, dy=%g,
                            #width=1,
                            length_includes_head=True,
                            shape='full')
-            if self.instruction_file is not None:
+            if self.instruction_file:
                 self.instruction_file.write("""\
 mpl.arrow(x=%g, y=%g, dx=%g, dy=%g,
           facecolor='%s', edgecolor='%s',
@@ -293,14 +326,21 @@ mpl.arrow(x=%g, y=%g, dx=%g, dy=%g,
 
     def arrow2(self, x, y, dx, dy, style='->'):
         """Draw arrow (dx,dy) at (x,y). `style` is '->', '<-' or '<->'."""
-        self.ax.annotate('', xy=(x+dx,y+dy) , xytext=(x,y),
+        self.ax.annotate('', xy=(x+dx,y+dy), xytext=(x,y),
                          arrowprops=dict(arrowstyle=style,
                                          facecolor='black',
                                          linewidth=1,
                                          shrinkA=0,
                                          shrinkB=0))
-        if self.instruction_file is not None:
-            self.instruction_file.write("")
+        if self.instruction_file:
+            self.instruction_file.write("""
+ax.annotate('', xy=(%s,%s), xytext=(%s,%s),
+                         arrowprops=dict(arrowstyle=%s,
+                                         facecolor='black',
+                                         linewidth=1,
+                                         shrinkA=0,
+                                         shrinkB=0))
+""" % (x+dx, y+dy, x, y, style))
 
 
 
@@ -311,9 +351,9 @@ def _test():
     # triangle
     x = np.array([1, 4, 1, 1]);  y = np.array([1, 1, 4, 1])
     d.set_filled_curves('magenta')
-    d.define_curve(x, y)
+    d.plot_curve(x, y)
     d.set_filled_curves(False)
-    d.define_curve(x+4, y)
+    d.plot_curve(x+4, y)
     d.text('some text1', position=(8,4), arrow_tip=(6, 1), alignment='left',
            fontsize=18)
     pos = np.array((7,4.5))  # numpy points work fine
@@ -325,7 +365,7 @@ def _test():
     d.arrow2(4.5, 0, 0, 3, style='<->')
     x = np.linspace(0, 9, 201)
     y = 4.5 + 0.45*np.cos(0.5*np.pi*x)
-    d.define_curve(x, y, arrow='end')
+    d.plot_curve(x, y, arrow='end')
     d.display()
     raw_input()
 

+ 7 - 0
pysketcher/__init__.py

@@ -1 +1,8 @@
+"""
+Pysketcher is a simple tool which allows you to create
+sketches of, e.g., mechanical systems in Python.
+"""
+__version__ = '0.1'
+__author__ = 'Hans Petter Langtangen <hpl@simula.no>'
+
 from shapes import *

+ 213 - 116
pysketcher/shapes.py

@@ -5,8 +5,68 @@ from MatplotlibDraw import MatplotlibDraw
 drawing_tool = MatplotlibDraw()
 
 def point(x, y):
+    if isinstance(x, (float,int)) and isinstance(y, (float,int)):
+        pass
+    else:
+        raise TypeError('x=%s,y=%s must be float,float, not %s,%s' %
+                        (x, y, type(x), type(y)))
+    ok, msg = drawing_tool.inside((x,y))
+    if not ok: print msg
+
     return array((x, y), dtype=float)
 
+def arr2D(x):
+    if isinstance(x, (tuple,list,ndarray)):
+        if len(x) == 2:
+            pass
+        else:
+            raise ValueError('x=%s has length %d, not 2' % (x, len(x)))
+    else:
+        raise TypeError('x=%s must be list/tuple/ndarray, not %s' %
+                        (x, type(x)))
+    ok, msg = drawing_tool.inside(x)
+    if not ok: print msg
+
+    return asarray(x, dtype=float)
+
+def _is_sequence(seq, length=None,
+                 can_be_None=False, error_message=True):
+    if can_be_None:
+        legal_types = (list,tuple,ndarray,None)
+    else:
+        legal_types = (list,tuple,ndarray)
+
+    if isinstance(seq, legal_types):
+        if length is not None:
+            if length == len(seq):
+                return True
+            elif error_message:
+                raise TypeError('%s is %s; must be %s of length %d' %
+                            (str(seq), type(seq),
+                            ', '.join([str(t) for t in legal_types]),
+                             len(seq)))
+            else:
+                return False
+        else:
+            return True
+    elif error_message:
+        raise TypeError('%s is %s; must be %s' %
+                        (str(seq), type(seq),
+                        ','.join([str(t)[5:-1] for t in legal_types])))
+    else:
+        return False
+
+def is_sequence(*sequences, **kwargs):
+    length = kwargs.get('length', 2)
+    can_be_None = kwargs.get('can_be_None', False)
+    error_message = kwargs.get('error_message', True)
+    for x in sequences:
+        _is_sequence(x, length=length, can_be_None=can_be_None,
+                     error_message=error_message)
+        ok, msg = drawing_tool.inside(x)
+        if not ok: print msg
+
+
 def animate(fig, time_points, user_action, moviefiles=False,
             pause_per_frame=0.5):
     if moviefiles:
@@ -107,46 +167,86 @@ class Shape:
             if is_dict:
                 shape = self.shapes[shape]
             if not isinstance(shape, Shape):
-                if isinstance(shape, (dict,list,tuple)):
+                if isinstance(shape, dict):
+                    raise TypeError(
+                        'class %s has a shapes attribute that is just\n'
+                        'a plain dictionary,\n%s\n'
+                        'Did you mean to embed this dict in a Compose\n'
+                        'object?' % (self.__class__.__name__,
+                        str(shape)))
+                elif isinstance(shape, (list,tuple)):
                     raise TypeError(
-                        'class %s has a shapes attribute containing '
-                        'dict/list/tuple objects (nested shapes),\n'
-                        'which is not allowed - all object must be '
-                        'derived from Shape and the shapes dict/list\n'
-                        'cannot be nested.' % self.__class__.__name__)
+                        'class %s has a shapes attribute containing\n'
+                        'a %s object %s,\n'
+                        'Did you mean to embed this list in a Compose\n'
+                        'object?' % (self.__class__.__name__,
+                        type(shape), str(shape)))
                 else:
                     raise TypeError(
-                        'class %s has a shapes attribute where not all '
-                        'values are Shape objects:\n%s' %
-                        (self.__class__.__name__, pprint.pformat(self.shapes)))
+                        'class %s has a shapes attribute %s which is not'
+                        'a Shape objects\n%s' %
+                        (self.__class__.__name__, type(shape),
+                         pprint.pformat(self.shapes)))
 
             getattr(shape, func)(*args, **kwargs)
 
     def draw(self):
         self.for_all_shapes('draw')
 
-    def rotate(self, angle, center=point(0,0)):
+    def rotate(self, angle, center):
+        is_sequence(center, length=2)
         self.for_all_shapes('rotate', angle, center)
 
     def translate(self, vec):
+        is_sequence(vec, length=2)
         self.for_all_shapes('translate', vec)
 
     def scale(self, factor):
         self.for_all_shapes('scale', factor)
 
     def set_linestyle(self, style):
+        styles = ('solid', 'dashed', 'dashdot', 'dotted')
+        if style not in styles:
+            raise ValueError('%s: style=%s must be in %s' %
+                             (self.__class__.__name__ + '.set_linestyle:',
+                              style, str(styles)))
         self.for_all_shapes('set_linestyle', style)
 
     def set_linewidth(self, width):
+        if not isinstance(width, int) and width >= 0:
+            raise ValueError('%s: width=%s must be positive integer' %
+                             (self.__class__.__name__ + '.set_linewidth:',
+                              width))
         self.for_all_shapes('set_linewidth', width)
 
     def set_linecolor(self, color):
+        if color in drawing_tool.line_colors:
+            color = drawing_tool.line_colors[color]
+        elif color in drawing_tool.line_colors.values():
+            pass # color is ok
+        else:
+            raise ValueError('%s: invalid color "%s", must be in %s' %
+                             (self.__class__.__name__ + '.set_linecolor:',
+                                 color, list(drawing_tool.line_colors.keys())))
         self.for_all_shapes('set_linecolor', color)
 
     def set_arrow(self, style):
+        styles = ('->', '<-', '<->')
+        if not style in styles:
+            raise ValueError('%s: style=%s must be in %s' %
+                             (self.__class__.__name__ + '.set_arrow:',
+                              style, styles))
         self.for_all_shapes('set_arrow', style)
 
     def set_filled_curves(self, color='', pattern=''):
+        if color in drawing_tool.line_colors:
+            color = drawing_tool.line_colors[color]
+        elif color in drawing_tool.line_colors.values():
+            pass # color is ok
+        else:
+            raise ValueError('%s: invalid color "%s", must be in %s' %
+                             (self.__class__.__name__ + '.set_filled_curves:',
+                              color, list(drawing_tool.line_colors.keys())))
         self.for_all_shapes('set_filled_curves', color, pattern)
 
     def show_hierarchy(self, indent=0, format='std'):
@@ -195,10 +295,8 @@ class Curve(Shape):
         """
         `x`, `y`: arrays holding the coordinates of the curve.
         """
-        self.x, self.y = x, y
-        # Turn to numpy arrays
-        self.x = asarray(self.x, dtype=float)
-        self.y = asarray(self.y, dtype=float)
+        self.x = asarray(x, dtype=float)
+        self.y = asarray(y, dtype=float)
         #self.shapes must not be defined in this class
         #as self.shapes holds children objects:
         #Curve has no children (end leaf of self.shapes tree)
@@ -242,12 +340,12 @@ class Curve(Shape):
         plotting commands for the chosen engine.
         """
         self.inside_plot_area()
-        drawing_tool.define_curve(
+        drawing_tool.plot_curve(
             self.x, self.y,
             self.linestyle, self.linewidth, self.linecolor,
             self.arrow, self.fillcolor, self.fillpattern)
 
-    def rotate(self, angle, center=point(0,0)):
+    def rotate(self, angle, center):
         """
         Rotate all coordinates: `angle` is measured in degrees and
         (`x`,`y`) is the "origin" of the rotation.
@@ -280,9 +378,6 @@ class Curve(Shape):
         self.linestyle = style
 
     def set_arrow(self, style=None):
-        styles = ('->', '<-', '<->')
-        if not style in styles:
-            raise ValueError('style=%s must be in %s' % (style, styles))
         self.arrow = style
 
     def set_name(self, name):
@@ -335,7 +430,7 @@ class Point(Shape):
             'class %s must implement the draw method' %
             self.__class__.__name__)
 
-    def rotate(self, angle, center=point(0,0)):
+    def rotate(self, angle):
         """Rotate point an `angle` (in degrees) around (`x`,`y`)."""
         angle = angle*pi/180
         x, y = center
@@ -367,27 +462,29 @@ class Point(Shape):
 # no need to store input data as they are invalid after rotations etc.
 class Rectangle(Shape):
     def __init__(self, lower_left_corner, width, height):
-        ll = lower_left_corner  # short form
-        x = [ll[0], ll[0] + width,
-             ll[0] + width, ll[0], ll[0]]
-        y = [ll[1], ll[1], ll[1] + height,
-             ll[1] + height, ll[1]]
+        is_sequence(lower_left_corner)
+        p = lower_left_corner  # short form
+        x = [p[0], p[0] + width,
+             p[0] + width, p[0], p[0]]
+        y = [p[1], p[1], p[1] + height,
+             p[1] + height, p[1]]
         self.shapes = {'rectangle': Curve(x,y)}
 
 class Triangle(Shape):
     """Triangle defined by its three vertices p1, p2, and p3."""
     def __init__(self, p1, p2, p3):
+        is_sequence(p1, p2, p3)
         x = [p1[0], p2[0], p3[0], p1[0]]
         y = [p1[1], p2[1], p3[1], p1[1]]
         self.shapes = {'triangle': Curve(x,y)}
 
 
 class Line(Shape):
-    def __init__(self, start, stop):
-        x = [start[0], stop[0]]
-        y = [start[1], stop[1]]
+    def __init__(self, start, end):
+        is_sequence(start, end)
+        x = [start[0], end[0]]
+        y = [start[1], end[1]]
         self.shapes = {'line': Curve(x, y)}
-        self.compute_formulas()
 
     def compute_formulas(self):
         x, y = self.shapes['line'].x, self.shapes['line'].y
@@ -462,23 +559,27 @@ class Circle(Shape):
         self.shapes = {'circle': Curve(x, y)}
 
     def __call__(self, theta):
-        """Return (x, y) point corresponding to theta."""
+        """
+        Return (x, y) point corresponding to angle theta.
+        Not valid after a translation, rotation, or scaling.
+        """
         return self.center[0] + self.radius*cos(theta), \
                self.center[1] + self.radius*sin(theta)
 
 
 class Arc(Shape):
     def __init__(self, center, radius,
-                 start_degrees, opening_degrees,
+                 start_angle, arc_angle,
                  resolution=180):
+        is_sequence(center)
         self.center = center
         self.radius = radius
-        self.start_degrees = start_degrees*pi/180  # radians
-        self.opening_degrees = opening_degrees*pi/180
+        self.start_angle = start_angle*pi/180  # radians
+        self.arc_angle = arc_angle*pi/180
         self.resolution = resolution
 
-        t = linspace(self.start_degrees,
-                     self.start_degrees + self.opening_degrees,
+        t = linspace(self.start_angle,
+                     self.start_angle + self.arc_angle,
                      resolution+1)
         x0 = center[0];  y0 = center[1]
         R = radius
@@ -487,9 +588,12 @@ class Arc(Shape):
         self.shapes = {'arc': Curve(x, y)}
 
     def __call__(self, theta):
-        """Return (x,y) point at start_degrees + theta."""
+        """
+        Return (x,y) point at start_angle + theta.
+        Not valid after translation, rotation, or scaling.
+        """
         theta = theta*pi/180
-        t = self.start_degrees + theta
+        t = self.start_angle + theta
         x0 = self.center[0]
         y0 = self.center[1]
         R = self.radius
@@ -570,58 +674,32 @@ class XWall(Shape):
             taps = [Line((xi,y), (xi+dx, y+dy)) for xi in x[:-1]]
         self.shapes = [Line(start, (start[0]+length, start[1]))] + taps
 
-class Wall(Shape):
-    def __init__(self, start, length, thickness, rotation_angle=0):
-        p1 = asarray(start)
-        p2 = p1 + asarray([length, 0])
-        p3 = p2 + asarray([0, thickness])
-        p4 = p1 + asarray([0, thickness])
-        p5 = p1
-        x = [p[0] for p in p1, p2, p3, p4, p5]
-        y = [p[1] for p in p1, p2, p3, p4, p5]
-        wall = Curve(x, y)
-        wall.set_filled_curves('white', '/')
-        wall.rotate(rotation_angle, start)
-        self.shapes = {'wall': wall}
-
-"""
-    def draw(self):
-        x = self.shapes['wall'].x
-        y = self.shapes['wall'].y
-        drawing_tool.ax.fill(x, y, 'w',
-                             edgecolor=drawing_tool.linecolor,
-                             hatch='/')
-"""
 
 class CurveWall(Shape):
     def __init__(self, x, y, thickness):
+        # User's curve
         x1 = asarray(x, float)
         y1 = asarray(y, float)
+        # Displaced curve (according to thickness)
         x2 = x1
         y2 = y1 + thickness
+        # Combine x1,y1 with x2,y2 reversed
         from numpy import concatenate
-        # x1/y1 + reversed x2/y2
         x = concatenate((x1, x2[-1::-1]))
         y = concatenate((y1, y2[-1::-1]))
         wall = Curve(x, y)
-        wall.set_filled_curves('white', '/')
+        wall.set_filled_curves(color='white', pattern='/')
         self.shapes = {'wall': wall}
 
-"""
-    def draw(self):
-        x = self.shapes['wall'].x
-        y = self.shapes['wall'].y
-        drawing_tool.ax.fill(x, y, 'w',
-                             edgecolor=drawing_tool.linecolor,
-                             hatch='/')
-"""
-
 
 class Text(Point):
-    def __init__(self, text, position, alignment='center', fontsize=18):
-        self.text = text
-        self.alignment, self.fontsize = alignment, fontsize
+    def __init__(self, text, position, alignment='center', fontsize=14):
+        is_sequence(position)
         is_sequence(position, length=2, can_be_None=True)
+        self.text = text
+        self.position = position
+        self.alignment = alignment
+        self.fontsize = fontsize
         Point.__init__(self, position[0], position[1])
         #no need for self.shapes here
 
@@ -638,8 +716,9 @@ class Text(Point):
 
 class Text_wArrow(Text):
     def __init__(self, text, position, arrow_tip,
-                 alignment='center', fontsize=18):
+                 alignment='center', fontsize=14):
         is_sequence(arrow_tip, length=2, can_be_None=True)
+        is_sequence(position)
         self.arrow_tip = arrow_tip
         Text.__init__(self, text, position, alignment, fontsize)
 
@@ -709,25 +788,29 @@ class DistanceSymbol(Shape):
     for identifying a distance with a symbol.
     """
     def __init__(self, start, end, symbol, fontsize=14):
-        start = asarray(start, float)
-        end = asarray(end, float)
+        start = arr2D(start)
+        end   = arr2D(end)
         mid = 0.5*(start + end)  # midpoint of start-end line
         tangent = end - start
-        normal = asarray([-tangent[1], tangent[0]])/\
-                 sqrt(tangent[0]**2 + tangent[1]**2)
+        normal = arr2D([-tangent[1], tangent[0]])/\
+                       sqrt(tangent[0]**2 + tangent[1]**2)
         symbol_pos = mid + normal*drawing_tool.xrange/60.
-        self.shapes = {'arrow': Arrow1(start, end, style='<->'),
+        arrow = Arrow1(start, end, style='<->')
+        arrow.set_linecolor('black')
+        arrow.set_linewidth(1)
+        self.shapes = {'arrow': arrow,
                        'symbol': Text(symbol, symbol_pos, fontsize=fontsize)}
+        print 'Line in Arrow1:', arrow.shapes['arrow']['line'].linecolor, arrow.shapes['arrow']['line'].linewidth #[[[
 
 
 class ArcSymbol(Shape):
     def __init__(self, symbol, center, radius,
-                 start_degrees, opening_degrees,
+                 start_angle, arc_angle,
                  resolution=180, fontsize=14):
-        arc = Arc(center, radius, start_degrees, opening_degrees,
+        arc = Arc(center, radius, start_angle, arc_angle,
                   resolution)
-        mid = asarray(arc(opening_degrees/2.))
-        normal = mid - asarray(center, float)
+        mid = arr2D(arc(arc_angle/2.))
+        normal = mid - arr2D(center)
         normal = normal/sqrt(normal[0]**2 + normal[1]**2)
         symbol_pos = mid + normal*drawing_tool.xrange/60.
         self.shapes = {'arc': arc,
@@ -752,8 +835,9 @@ class Compose(Shape):
 class Arrow1(Shape):
     """Draw an arrow as Line with arrow."""
     def __init__(self, start, end, style='->'):
-        self.start, self.end, self.style = start, end, style
-        self.shapes = {'arrow': Line(start, end, arrow=style)}
+        arrow = Line(start, end)
+        arrow.set_arrow(style)
+        self.shapes = {'arrow': arrow}
 
 class Arrow3(Shape):
     """Draw a vertical line and arrow head. Then rotate `rotation_angle`."""
@@ -811,7 +895,7 @@ class Wheel(Shape):
                  zip(xinner, yinner, xouter, youter)]
         self.shapes = [outer, inner] + lines
 
-class Wave(Shape):
+class SineWave(Shape):
     def __init__(self, xstart, xstop,
                  wavelength, amplitude, mean_level):
         self.xstart = xstart
@@ -832,19 +916,58 @@ class Wave(Shape):
 
 
 class Spring(Shape):
-    def __init__(self, bottom_point, length, tagwidth, ntags=4):
+    def __init__(self, bottom_point, length, tooth_spacing, ntooths=4):
         """
         Specify a vertical spring, starting at bottom_point and
         having a specified lengths. In the middle third of the
-        spring there are ntags tags.
+        spring there are ntooths saw thooth tips.
         """
         self.B = bottom_point
-        self.n = ntags - 1  # n counts tag intervals
+        self.n = ntooths - 1  # n counts tag intervals
         # n must be odd:
         if self.n % 2 == 0:
             self.n = self.n+1
         self.L = length
-        self.w = tagwidth
+        self.w = tooth_spacing
+
+        B, L, n, w = self.B, self.L, self.n, self.w  # short forms
+        t = L/(3.0*n)  # must be better worked out
+        P0 = (B[0], B[1]+L/3.0)
+        P1 = (B[0], B[1]+L/3.0+t/2.0)
+        P2 = (B[0], B[1]+L*2/3.0)
+        P3 = (B[0], B[1]+L)
+        line1 = Line(B, P1)
+        lines = [line1]
+        #line2 = Line(P2, P3)
+        T1 = P1
+        T2 = (T1[0] + w, T1[1] + t/2.0)
+        lines.append(Line(T1,T2))
+        T1 = (T2[0], T2[1])
+        for i in range(n):
+            T2 = (T1[0] + (-1)**(i+1)*2*w, T1[1] + t/2.0)
+            lines.append(Line(T1, T2))
+            T1 = (T2[0], T2[1])
+        T2 = (T1[0] + w, T1[1] + t/2.0)
+        lines.append(Line(T1,T2))
+
+        #print P2, T2
+        lines.append(Line(T2, P3))
+        self.shapes = lines
+
+class Spring(Shape):
+    def __init__(self, bottom_point, length, tooth_spacing, ntooths=4):
+        """
+        Specify a vertical spring, starting at bottom_point and
+        having a specified lengths. In the middle third of the
+        spring there are ntooths tags.
+        """
+        self.B = bottom_point
+        self.n = ntooths - 1  # n counts tag intervals
+        # n must be odd:
+        if self.n % 2 == 0:
+            self.n = self.n+1
+        self.L = length
+        self.w = tooth_spacing
 
         B, L, n, w = self.B, self.L, self.n, self.w  # short forms
         t = L/(3.0*n)  # must be better worked out
@@ -995,32 +1118,6 @@ def rolling_wheel(total_rotation_angle):
     failure, output = commands.getstatusoutput(cmd)
     if failure:  print 'Could not run', cmd
 
-def is_sequence(seq, length=None,
-                can_be_None=False, error_message=True):
-    if can_be_None:
-        legal_types = (list,tuple,ndarray,None)
-    else:
-        legal_types = (list,tuple,ndarray)
-
-    if isinstance(seq, legal_types):
-        if length is not None:
-            if length == len(seq):
-                return True
-            elif error_message:
-                raise TypeError('%s is %s; must be %s of length %d' %
-                            (str(point), type(point),
-                            ', '.join([str(t) for t in legal_types]),
-                             len(seq)))
-            else:
-                return False
-        else:
-            return True
-    elif error_message:
-        raise TypeError('%s is %s; must be %s' %
-                        str(point), type(point),
-                        ', '.join([str(t) for t in legal_types]))
-    else:
-        return False
 
 if __name__ == '__main__':
     #rolling_wheel(40)

+ 13 - 0
setup.py

@@ -0,0 +1,13 @@
+from distutils.core import setup
+import pysketcher  # much easier when no lib dir
+setup(name='pysketcher',
+      version=pysketcher.__version__,
+      url='',
+      author=pysketcher.__author__,
+      description='',
+      license='BSD',
+      long_description=pysketcher.__doc__,
+      platforms='any',
+      #package_data={'name': ['pysketcher/*.dat'],},
+      packages=['pysketcher'])
+