Hans Petter Langtangen 13 years ago
parent
commit
efb19681ca

doc/src/figs-pysketcher/wheel_on_inclined_plane.png → doc/src/sketcher/figs-sketcher/wheel_on_inclined_plane.png


File diff suppressed because it is too large
+ 1000 - 0
doc/src/sketcher/sketcher.do.txt


+ 67 - 0
doc/src/sketcher/src-sketcher/vehicle0.py

@@ -0,0 +1,67 @@
+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)
+wheel2 = wheel1.copy()
+wheel2.translate((4,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)
+
+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)
+
+fig = Compose({'vehicle': vehicle, 'ground': ground})
+fig.draw()  # send all figures to plotting backend
+
+drawing_tool.display()
+drawing_tool.savefig('tmp1.png')
+
+fig['vehicle']['wheels'].set_filled_curves('blue')
+fig['vehicle']['wheels'].set_linewidth(6)
+fig['vehicle']['wheels'].set_linecolor('black')
+fig['vehicle']['body']['under'].set_filled_curves('red')
+fig['vehicle']['body']['over'].set_filled_curves(pattern='/')
+fig['vehicle']['body']['over'].set_linewidth(10)
+
+drawing_tool.erase()  # avoid drawing old and new fig on top of each other
+fig.draw()
+drawing_tool.display()
+drawing_tool.savefig('tmp2.png')
+
+print fig
+
+time.sleep(1)
+
+# Animate motion
+fig['vehicle'].translate((3,0))  # move to start point for "driving"
+
+def v(t):
+    return point(-t*(1-t/5.), 0)
+
+import numpy
+tp = numpy.linspace(0, 5, 35)
+dt = tp[1] - tp[0]  # time step
+
+def move_vehicle(t, fig):
+    displacement = dt*v(t)
+    fig['vehicle'].translate(displacement)
+
+files = animate(fig, tp, move_vehicle, moviefiles=True,
+                pause_per_frame=0)
+
+from scitools.std import movie
+movie(files, encoder='html', output_file='anim')
+
+raw_input()

+ 2 - 0
examples/wheel_on_inclined_plane.py

@@ -1,3 +1,5 @@
+import sys, os
+sys.path.insert(0, os.path.join(os.pardir, 'pysketcher'))
 from shapes import *
 
 print dir()

+ 17 - 14
pysketcher/MatplotlibDraw.py

@@ -16,7 +16,10 @@ class MatplotlibDraw:
         instruction_file: name of file where all the instructions
         are recorded.
         """
-        self.instruction_file = filename
+        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):
         """
@@ -30,10 +33,6 @@ class MatplotlibDraw:
         self.xrange = self.xmax - self.xmin
         self.yrange = self.ymax - self.ymin
         self.axis = axis
-        if self.instruction_file:
-            self.instruction_file = open(self.instruction_file, 'w')
-        else:
-            self.instruction_file = None
 
         # Compute the right X11 geometry on the screen based on the
         # x-y ratio of axis ranges
@@ -101,15 +100,15 @@ ax.set_aspect('equal')
         """Change the line width (int, starts at 1)."""
         self.linewidth = width
 
-    def set_filled_curves(self, color='', hatch=''):
+    def set_filled_curves(self, color='', pattern=''):
         """Fill area inside curves with current line color."""
         if color is False:
             self.fillcolor = ''
-            self.fillhatch = ''
+            self.fillpattern = ''
         else:
             self.fillcolor = color if len(color) == 1 else \
                          MatplotlibDraw.line_colors[color]
-            self.fillhatch = hatch
+            self.fillpattern = pattern
 
     def set_grid(self, on=False):
         self.mpl.grid(on)
@@ -127,7 +126,7 @@ ax.set_aspect('equal')
     def define_curve(self, x, y,
                      linestyle=None, linewidth=None,
                      linecolor=None, arrow=None,
-                     fillcolor=None, fillhatch=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)
@@ -141,8 +140,8 @@ ax.set_aspect('equal')
             linewidth = self.linewidth
         if fillcolor is None:
             fillcolor = self.fillcolor
-        if fillhatch is None:
-            fillhatch = self.fillhatch
+        if fillpattern is None:
+            fillpattern = self.fillpattern
 
         if self.instruction_file is not None:
             import pprint
@@ -151,11 +150,15 @@ ax.set_aspect('equal')
             self.instruction_file.write('y = %s\n' % \
                                         pprint.pformat(self.ydata.tolist()))
 
-        if fillcolor or fillhatch:
+
+        if fillcolor or fillpattern:
+            if fillpattern != '':
+                fillcolor = 'white'
+            #print '%d coords, fillcolor="%s" linecolor="%s" fillpattern="%s"' % (x.size, fillcolor, linecolor, fillpattern)
             self.ax.fill(x, y, fillcolor, edgecolor=linecolor,
-                         hatch=fillhatch)
+                         linewidth=linewidth, hatch=fillpattern)
             if self.instruction_file is not None:
-                self.instruction_file.write("ax.fill(x, y, '%s', edgecolor='%s', hatch='%s')\n" % (linecolor, fillcolor, fillhatch))
+                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)

+ 1 - 0
pysketcher/__init__.py

@@ -0,0 +1 @@
+from shapes import *

+ 131 - 18
pysketcher/shapes.py

@@ -1,5 +1,5 @@
 from numpy import linspace, sin, cos, pi, array, asarray, ndarray, sqrt, abs
-import pprint
+import pprint, copy, glob, os
 
 from MatplotlibDraw import MatplotlibDraw
 drawing_tool = MatplotlibDraw()
@@ -7,6 +7,36 @@ drawing_tool = MatplotlibDraw()
 def point(x, y):
     return array((x, y), dtype=float)
 
+def animate(fig, time_points, user_action, moviefiles=False,
+            pause_per_frame=0.5):
+    if moviefiles:
+        # Clean up old frame files
+        framefilestem = 'tmp_frame_'
+        framefiles = glob.glob('%s*.png' % framefilestem)
+        for framefile in framefiles:
+            os.remove(framefile)
+
+    for n, t in enumerate(time_points):
+        drawing_tool.erase()
+
+        user_action(t, fig)
+        #could demand returning fig, but in-place modifications
+        #are done anyway
+        #fig = user_action(t, fig)
+        #if fig is None:
+        #    raise TypeError(
+        #        'animate: user_action returns None, not fig\n'
+        #        '(a Shape object with the whole figure)')
+
+        fig.draw()
+        drawing_tool.display()
+
+        if moviefiles:
+            drawing_tool.savefig('%s%04d.png' % (framefilestem, n))
+
+    if moviefiles:
+        return '%s*.png' % framefilestem
+
 
 class Shape:
     """
@@ -34,6 +64,27 @@ class Shape:
               'as a *list* of Shape objects'
         return [self]  # Make the iteration work
 
+    def copy(self):
+        return copy.deepcopy(self)
+
+    def __getitem__(self, name):
+        """
+        Allow indexing like::
+
+           obj1['name1']['name2']
+
+        all the way down to ``Curve`` or ``Point`` (``Text``)
+        objects.
+        """
+        if hasattr(self, 'shapes'):
+            if name in self.shapes:
+                return self.shapes[name]
+            else:
+                for shape in self.shapes:
+                    return self.shapes[shape][name]
+        else:
+            return self
+
     def for_all_shapes(self, func, *args, **kwargs):
         if not hasattr(self, 'shapes'):
             # When self.shapes is lacking, we either come to
@@ -50,6 +101,20 @@ class Shape:
         for shape in self.shapes:
             if is_dict:
                 shape = self.shapes[shape]
+            if not isinstance(shape, Shape):
+                if isinstance(shape, (dict,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__)
+                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)))
+
             getattr(shape, func)(*args, **kwargs)
 
     def draw(self):
@@ -76,18 +141,44 @@ class Shape:
     def set_arrow(self, style):
         self.for_all_shapes('set_arrow', style)
 
-    def set_name(self, name):
-        self.for_all_shapes('set_name', name)
+    def set_filled_curves(self, color='', pattern=''):
+        self.for_all_shapes('set_filled_curves', color, pattern)
 
-    def set_filled_curves(self, fillcolor='', fillhatch=''):
-        self.for_all_shapes('set_filled_curves', fillcolor, fillhatch)
+    def show_hierarchy(self, indent=0, format='std'):
+        """Recursive pretty print of hierarchy of objects."""
+        s = ''
+        if format == 'dict':
+            s += '{'
+        for shape in self.shapes:
+            if format == 'dict':
+                shape_str = repr(shape) + ':'
+            elif format == 'plain':
+                shape_str = shape
+            else:
+                shape_str = shape + ':'
+            if format == 'dict' or format == 'plain':
+                class_str = ''
+            else:
+                class_str = ' (%s)' % \
+                            self.shapes[shape].__class__.__name__
+            s += '\n%s%s%s %s' % (
+                ' '*indent,
+                shape_str,
+                class_str,
+                self.shapes[shape].show_hierarchy(indent+4, format))
+
+        if format == 'dict':
+            s += '}'
+        return s
 
     def __str__(self):
-        return self.__class__.__name__
+        """Display hierarchy with minimum information (just object names)."""
+        return self.show_hierarchy(format='plain')
 
     def __repr__(self):
-        #print 'repr in class', self.__class__.__name__
-        return pprint.pformat(self.shapes)
+        """Display hierarchy as a dictionary."""
+        return self.show_hierarchy(format='dict')
+        #return pprint.pformat(self.shapes)
 
 
 class Curve(Shape):
@@ -108,9 +199,8 @@ class Curve(Shape):
         self.linewidth = None
         self.linecolor = None
         self.fillcolor = None
-        self.fillhatch = None
+        self.fillpattern = None
         self.arrow = None
-        self.name = None
 
     def inside_plot_area(self, verbose=True):
         """Check that all coordinates are within drawing_tool's area."""
@@ -137,11 +227,17 @@ class Curve(Shape):
         return inside
 
     def draw(self):
+        """
+        Send the curve to the plotting engine. That is, convert
+        coordinate information in self.x and self.y, together
+        with optional settings of linestyles, etc., to
+        plotting commands for the chosen engine.
+        """
         self.inside_plot_area()
         drawing_tool.define_curve(
             self.x, self.y,
             self.linestyle, self.linewidth, self.linecolor,
-            self.arrow, self.fillcolor, self.fillhatch)
+            self.arrow, self.fillcolor, self.fillpattern)
 
     def rotate(self, angle, center=point(0,0)):
         """
@@ -184,20 +280,29 @@ class Curve(Shape):
     def set_name(self, name):
         self.name = name
 
-    def set_filled_curves(self, fillcolor='', fillhatch=''):
-        self.fillcolor = fillcolor
-        self.fillhatch = fillhatch
+    def set_filled_curves(self, color='', pattern=''):
+        self.fillcolor = color
+        self.fillpattern = pattern
+
+    def show_hierarchy(self, indent=0, format='std'):
+        if format == 'dict':
+            return '"%s"' % str(self)
+        elif format == 'plain':
+            return ''
+        else:
+            return str(self)
 
     def __str__(self):
-        s = '%d (x,y) coordinates' % self.x.size
+        """Compact pretty print of a Curve object."""
+        s = '%d coords' % self.x.size
         if not self.inside_plot_area(verbose=False):
             s += ', some coordinates are outside plotting area!\n'
-        props = ('linecolor', 'linewidth', 'linestyle', 'arrow', 'name',
-                 'fillcolor', 'fillhatch')
+        props = ('linecolor', 'linewidth', 'linestyle', 'arrow',
+                 'fillcolor', 'fillpattern')
         for prop in props:
             value = getattr(self, prop)
             if value is not None:
-                s += ' %s: "%s"' % (prop, value)
+                s += ' %s=%s' % (prop, repr(value))
         return s
 
     def __repr__(self):
@@ -242,6 +347,14 @@ class Point(Shape):
         self.x += vec[0]
         self.y += vec[1]
 
+    def show_hierarchy(self, indent=0, format='std'):
+        s = '%s at (%g,%g)' % (self.__class__.__name__, self.x, self.y)
+        if format == 'dict':
+            return '"%s"' % s
+        elif format == 'plain':
+            return ''
+        else:
+            return s
 
 # no need to store input data as they are invalid after rotations etc.
 class Rectangle(Shape):