| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383 |
- .. Automatically generated reST file from Doconce source
- (http://code.google.com/p/doconce/)
- Using Pysketcher to Create Principal Sketches of Physics Problems
- =================================================================
- :Author: Hans Petter Langtangen
- :Date: Apr 1, 2012
- *Abstract.* Pysketcher is a Python package which allows principal sketches of
- physics and mechanics problems to be realized through short programs
- instead of interactive (and potentially tedious and inaccurate)
- drawing. Elements of the sketch, such as lines, circles, angles,
- forces, coordinate systems, etc., are realized as objects and
- collected in hierarchical structures. Parts of the hierarchical
- structures can easily change line styles and colors, or be copied,
- scaled, translated, and rotated. These features make it
- straightforward to move parts of the sketch to create animation,
- usually in accordance with the physics of the underlying problem.
- Exact dimensioning of the elements in the sketch is trivial to obtain
- since distances are specified in computer code.
- Pysketcher is easy to learn from a number of examples. Beyond
- essential Python programming and a knowledge about mechanics problems,
- no further background is required.
- .. Task (can be questions): make sketches of physical problems, see fig
- .. through user-friendly composition of basic shapes
- .. Desired knowledge: plotting curves, basic OO (ch. X.Y, ...)
- .. Required knowledge?
- .. Learning Goals: these targets the inner workings of pysketcher,
- .. which is just a part of this document...
- A First Glimpse of Pysketcher
- =============================
- Formulation of physical problems makes heavy use of *principal sketches*
- such as the one in Figure :ref:`sketcher:fig:inclinedplane`.
- This particular sketch illustrates the classical mechanics problem
- of a rolling wheel on an inclined plane.
- The figure
- 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 :math:`N` and :math:`Mg` forces, and the :math:`x`
- axis), an angle with symbol :math:`\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. And as soon we start
- *programming* to construct the figure we get a range of other
- powerful tools at disposal. For example, we can easily translate and
- rotate parts of the figure and make an animation that illustrates the
- physics of the problem.
- .. _sketcher:fig:inclinedplane:
- .. figure:: figs-sketcher/wheel_on_inclined_plane.png
- :width: 600
- *Sketch of a physics problem*
- Basic Construction of Sketches
- ------------------------------
- Before attacking real-life sketches as in Figure :ref:`sketcher:fig1`
- we focus on the significantly simpler drawing shown in
- in Figure :ref:`sketcher:fig:vehicle0`. This toy sketch consists of
- several elements: two circles, two rectangles, and a "ground" element.
- .. _sketcher:fig:vehicle0:
- .. figure:: figs-sketcher/vehicle0_dim.png
- :width: 600
- *Sketch of a simple figure*
- Basic Drawing
- ~~~~~~~~~~~~~
- A typical program creating these five elements is shown next.
- After importing the ``pysketcher`` package, the first task is always to
- define a coordinate system. Some graphics operations are done with
- a helper object called ``drawing_tool`` (imported from ``pysketcher``).
- With the drawing area in place we can make the first ``Circle`` object
- in an intuitive fashion:
- .. code-block:: python
- 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
- 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)
- By using symbols for characteristic lengths in the drawing, rather than
- absolute lengths, it is easier
- to change dimensions later.
- To translate the geometric information about the ``wheel1`` object to
- instructions for the plotting engine (in this case Matplotlib), one calls the
- ``wheel1.draw()``. To display all drawn objects, one issues
- ``drawing_tool.display()``. The typical steps are hence:
- .. code-block:: python
- wheel1 = Circle(center=(w_1, R), radius=R)
- wheel1.draw()
-
- # Define other objects and call their draw() methods
- drawing_tool.display()
- drawing_tool.savefig('tmp.png') # store picture
- The next wheel can be made by taking a copy of ``wheel1`` and
- translating the object a distance (to the right) described by the
- vector :math:`(4,0)`:
- .. code-block:: python
- wheel2 = wheel1.copy()
- wheel2.translate((L,0))
- The two rectangles are made in an intuitive way:
- .. code-block:: python
- 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)
- Groups of Objects
- ~~~~~~~~~~~~~~~~~
- Instead of calling the ``draw`` method of every object, we can
- group objects and call ``draw``, or perform other operations, for
- the whole group. For example, we may collect the two wheels
- in a ``wheels`` group and the ``over`` and ``under`` rectangles
- in a ``body`` group. The whole vehicle is a composition
- of its ``wheels`` and ``body`` groups. The codes goes like
- .. code-block:: python
- wheels = Composition({'wheel1': wheel1, 'wheel2': wheel2})
- body = Composition({'under': under, 'over': over})
-
- vehicle = Composition({'wheels': wheels, 'body': body})
- The ground is illustrated by an object of type ``Wall``,
- mostly used to indicate walls in sketches of mechanical systems.
- A ``Wall`` takes the ``x`` and ``y`` coordinates of some curve,
- and a ``thickness`` parameter, and creates a "thick" curve filled
- with a simple pattern. In this case the curve is just a flat
- line so the construction is made of two points on the
- ground line (:math:`(w_1-L,0)` and :math:`(w_1+3L,0)`):
- .. code-block:: python
- ground = Wall(x=[w_1 - L, w_1 + 3*L], y=[0, 0], thickness=-0.3*R)
- 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:
- .. code-block:: python
- fig = Composition({'vehicle': vehicle, 'ground': ground})
- fig.draw() # send all figures to plotting backend
- drawing_tool.display()
- drawing_tool.savefig('tmp.png')
- The ``fig.draw()`` call will visit
- all subgroups, their subgroups,
- and so in the hierarchical tree structure that we have collected,
- and call ``draw`` for every object.
- Changing Line Styles and Colors
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Controlling the line style, line color, and line width is
- fundamental when designing figures. The ``pysketcher``
- package allows the user to control such properties in
- single objects, but also set global properties that are
- used if the object has no particular specification of
- the properties. Setting the global properties are done like
- .. code-block:: python
- drawing_tool.set_linestyle('dashed')
- drawing_tool.set_linecolor('black')
- drawing_tool.set_linewidth(4)
- At the object level the properties are specified in a similar
- way:
- .. code-block:: python
- wheel1.set_linestyle('solid')
- wheel1.set_linecolor('red')
- and so on.
- Geometric figures can be specified as *filled*, either with a color or with a
- special visual pattern:
- .. code-block:: py
- # Set filling of all curves
- drawing_tool.set_filled_curves(color='blue', pattern='/')
-
- # Turn off filling of all curves
- drawing_tool.set_filled_curves(False)
-
- # Fill the wheel with red color
- wheel1.set_filled_curves('red')
- .. http://packages.python.org/ete2/ for visualizing tree structures!
- The Figure Composition as an Object Hierarchy
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- The composition of objects is hierarchical, as in a family, where
- each object has a parent and a number of children. Do a
- ``print fig`` to display the relations:
- .. code-block:: python
- ground
- wall
- vehicle
- body
- over
- rectangle
- under
- rectangle
- wheels
- wheel1
- arc
- wheel2
- arc
- The indentation reflects how deep down in the hierarchy (family)
- we are.
- This output is to be interpreted as follows:
- * ``fig`` contains two objects, ``ground`` and ``vehicle``
- * ``ground`` contains an object ``wall``
- * ``vehicle`` contains two objects, ``body`` and ``wheels``
- * ``body`` contains two objects, ``over`` and ``under``
- * ``wheels`` contains two objects, ``wheel1`` and ``wheel2``
- More detailed information can be printed by
- .. code-block:: python
- print fig.show_hierarchy('std')
- yielding the output
- .. code-block:: python
- ground (Wall):
- wall (Curve): 4 coords fillcolor='white' fillpattern='/'
- vehicle (Composition):
- body (Composition):
- over (Rectangle):
- rectangle (Curve): 5 coords
- under (Rectangle):
- rectangle (Curve): 5 coords
- wheels (Composition):
- wheel1 (Circle):
- arc (Curve): 181 coords
- wheel2 (Circle):
- arc (Curve): 181 coords
- Here we can see the class type for each figure object, how many
- coordinates that are involved in basic figures (``Curve`` objects), and
- special settings of the basic figure (fillcolor, line types, etc.).
- For example, ``wheel2`` is a ``Circle`` object consisting of an ``arc``,
- which is a ``Curve`` object consisting of 181 coordinates (the
- points needed to draw a smooth circle). The ``Curve`` objects are the
- only objects that really holds specific coordinates to be drawn.
- The other object types are just compositions used to group
- parts of the complete figure.
- One can also get a graphical overview of the hierarchy 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:
- .. code-block:: console
- Terminal> dot -Tpng -o fig.png fig.dot
- .. _sketcher:fig:vehicle0:hier1:
- .. figure:: figs-sketcher/vehicle0_hier1.png
- :width: 500
- *Hierarchical relation between figure objects*
- 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.
- .. _sketcher:fig:vehicle0:hier2:
- .. figure:: figs-sketcher/vehicle0_hier2.png
- :width: 500
- *Hierarchical relation between figure objects, including their class names*
- Any of the objects can in the program be reached through their names, e.g.,
- .. code-block:: python
- fig['vehicle']
- fig['vehicle']['wheels']
- fig['vehicle']['wheels']['wheel2']
- fig['vehicle']['wheels']['wheel2']['arc']
- fig['vehicle']['wheels']['wheel2']['arc'].x # x coords
- fig['vehicle']['wheels']['wheel2']['arc'].y # y coords
- fig['vehicle']['wheels']['wheel2']['arc'].linestyle
- fig['vehicle']['wheels']['wheel2']['arc'].linetype
- Grabbing a part of the figure this way is very handy for
- changing properties of that part, for example, colors, line styles
- (see Figure :ref:`sketcher:fig:vehicle0:v2`):
- .. code-block:: python
- fig['vehicle']['wheels'].set_filled_curves('blue')
- fig['vehicle']['wheels'].set_linewidth(6)
- fig['vehicle']['wheels'].set_linecolor('black')
-
- fig['vehicle']['body']['under'].set_filled_curves('red')
-
- fig['vehicle']['body']['over'].set_filled_curves(pattern='/')
- fig['vehicle']['body']['over'].set_linewidth(14)
- fig['vehicle']['body']['over']['rectangle'].linewidth = 4
- 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`.
- .. _sketcher:fig:vehicle0:v2:
- .. figure:: figs-sketcher/vehicle0.png
- :width: 700
- *Left: Basic line-based drawing. Right: Thicker lines and filled parts*
- We can also change position of parts of the figure and thereby make
- animations, as shown next.
- Animation: Translating the Vehicle
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Can we make our little vehicle roll? A first attempt will be to
- fake rolling by just displacing all parts of the vehicle.
- The relevant parts constitute the ``fig['vehicle']`` object.
- This part of the figure can be translated, rotated, and scaled.
- A translation along the ground means a translation in :math:`x` direction,
- say a length :math:`L` to the right:
- .. code-block:: python
- fig['vehicle'].translate((L,0))
- You need to erase, draw, and display to see the movement:
- .. code-block:: python
- drawing_tool.erase()
- fig.draw()
- drawing_tool.display()
- Without erasing the old position of the vehicle will remain in
- the figure so you get two vehicles. Without ``fig.draw()`` the
- new coordinates of the vehicle will not be communicated to
- the drawing tool, and without calling display the updated
- drawing will not be visible.
- Let us make a velocity function and move the object according
- to that velocity in small steps of time:
- .. code-block:: python
- def v(t):
- return -8*R*t*(1 - t/(2*R))
-
- animate(fig, tp, action)
- For small time steps ``dt`` the corresponding displacement is
- well approximated by ``dt*v(t)`` (we could integrate the velocity
- to obtain the exact position, but we would anyway need to
- calculate the displacement from time step to time step).
- The ``animate`` function takes as arguments some figure ``fig``, a set of
- time points ``tp``, and a user function ``action``,
- and then a new figure is drawn for each time point and the user
- can through the provided ``action`` function modify desired parts
- of the figure. Here the ``action`` function will move the vehicle:
- .. code-block:: python
- def move_vehicle(t, fig):
- x_displacement = dt*v(t)
- fig['vehicle'].translate((x_displacement, 0))
- Defining a set of time points for the frames in the animation
- and performing the animation is done by
- .. code-block:: python
- import numpy
- tp = numpy.linspace(0, 2*R, 25)
- dt = tp[1] - tp[0] # time step
-
- animate(fig, tp, move_vehicle, pause_per_frame=0.2)
- The ``pause_per_frame`` adds a pause, here 0.2 seconds, between
- each frame.
- We can also make a movie file of the animation:
- .. code-block:: python
- files = animate(fig, tp, move_vehicle, moviefiles=True,
- pause_per_frame=0.2)
- The ``files`` variable holds a string with the family of
- files constituting the frames in the movie, here
- ``'tmp_frame*.png'``. Making a movie out of the individual
- frames can be done in many ways.
- A simple approach is to make an animated GIF file with help of
- ``convert``, a program in the ImageMagick software suite:
- .. code-block:: console
- Terminal> convert -delay 20 tmp_frame*.png anim.gif
- Terminal> animate anim.gif # play movie
- 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.,
- .. code-block:: console
- Terminal> ffmpeg -i "tmp_frame_%04d.png" -b 800k -r 25 \
- -vcodec mpeg4 -y -qmin 2 -qmax 31 anim.mpeg
- An easy-to-use interface to movie-making tools is provided by the
- SciTools package:
- .. code-block:: python
- from scitools.std import movie
-
- # 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
- 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
- ``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>`_.
- .. _sketcher:vehicle1:anim:
- Animation: Rolling the Wheels
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- It is time to show rolling wheels. To this end, we make somewhat
- more complicated wheels with spokes as on a bicyle, formed by
- two crossing lines, see Figure :ref:`sketcher:fig:vehicle1`.
- The construction of the wheels will now involve a circle
- and two lines:
- .. code-block:: python
- wheel1 = Composition({
- 'wheel': Circle(center=(w_1, R), radius=R),
- 'cross': Composition({'cross1': Line((w_1,0), (w_1,2*R)),
- 'cross2': Line((w_1-R,R), (w_1+R,R))})})
- wheel2 = wheel1.copy()
- wheel2.translate((L,0))
- Observe that ``wheel1.copy()`` copies all the objects that make
- up the first wheel, and ``wheel2.translate`` translates all
- the copied objects.
- .. _sketcher:fig:vehicle1:
- .. figure:: figs-sketcher/vehicle1.png
- :width: 400
- *Wheels with spokes to show rotation*
- The ``move_vehicle`` function now needs to displace all the objects in the
- entire vehicle and also rotate the ``cross1`` and ``cross2``
- objects in both wheels.
- The rotation angle follows from the fact that the arc length
- of a rolling wheel equals the displacement of the center of
- the wheel, leading to a rotation angle
- .. code-block:: python
- angle = - x_displacement/R
- With ``w_1`` tracking the :math:`x` coordinate of the center
- of the front wheel, we can rotate that wheel by
- .. code-block:: python
- w1 = fig['vehicle']['wheels']['wheel1']
- from math import degrees
- w1.rotate(degrees(angle), center=(w_1, R))
- 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
- .. code-block:: python
- w2 = fig['vehicle']['wheels']['wheel2']
- w2.rotate(degrees(angle), center=(w_1 + L, R))
- That is, the angle is the same, but the rotation point is different.
- The update of the center point is done by ``w_1 += displacement[0]``.
- The complete ``move_vehicle`` function then becomes
- .. code-block:: python
- w_1 = w_1 + L # start position
-
- def move_vehicle(t, fig):
- x_displacement = dt*v(t)
- fig['vehicle'].translate((x_displacement, 0))
-
- # Rotate wheels
- global w_1
- w_1 += x_displacement
- # R*angle = -x_displacement
- angle = - x_displacement/R
- w1 = fig['vehicle']['wheels']['wheel1']
- w1.rotate(degrees(angle), center=(w_1, R))
- w2 = fig['vehicle']['wheels']['wheel2']
- w2.rotate(degrees(angle), center=(w_1 + L, R))
- The complete example is found in the file
- ``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>`_.
- The advantages with making figures this way through programming,
- rather than using interactive drawing programs, are numerous. For
- example, the objects are parameterized by variables so that various
- dimensions can easily be changed. Subparts of the figure, possible
- involving a lot of figure objects, can change
- color, linetype, filling or other properties through a *single* function
- call. Subparts of the figure can be rotated, translated, or scaled.
- Subparts of the figure can also be copied and moved to other parts of the
- drawing area. However, the single most important feature is probably
- the ability to make animations governed by mathematical formulas or
- data coming from physics simulations of the problem sketched in
- the drawing, as very simplistically shown in the example above.
- Inner Workings of the Pysketcher Tool
- =====================================
- We shall now explain how we can, quite easily, realize software with
- the capabilities demonstrated in the previous examples. 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.
- Class programming is a key technology for realizing Pysketcher.
- As soon as some classes are established, more are easily
- 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. The fundamental data structure
- involved in the ``pysketcher`` package is a hierarchical tree, and much
- of the material on implementation issues targets how to traverse tree
- structures with recursive function calls in object hierarchies. This
- topic is of key relevance in a wide range of other applications as
- well. In total, the inner workings of Pysketcher constitute an
- excellent example on the power of class programming.
- Example of Classes for Geometric Objects
- ----------------------------------------
- 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.
- Simple Geometric Objects
- ~~~~~~~~~~~~~~~~~~~~~~~~
- One simple subclass is ``Rectangle``:
- .. code-block:: python
- 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)}
- 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 :math:`(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 :math:`x` and
- :math:`y` axis. However, we may easily rotate the rectangle about
- any point once the object is constructed.
- Class ``Line`` constitutes a similar example:
- .. code-block:: python
- class Line(Shape):
- def __init__(self, start, end):
- x = [start[0], end[0]]
- y = [start[1], end[1]]
- self.shapes = {'line': Curve(x, y)}
- 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 :math:`x` coordinate and have the class calculate the
- corresponding :math:`y` coordinate:
- .. code-block:: python
- 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
- Unfortunately, this is too simplistic because vertical lines cannot
- be handled (infinite ``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 geometric 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 :math:`(x,y)` on a curve with radius
- :math:`R` and center at :math:`(x_0, y_0)` are given by
- .. math::
-
- x &= x_0 + R\cos (t),\\
- y &= y_0 + R\sin (t),
-
- where :math:`t\in [0, 2\pi]`. A discrete set of :math:`t` values in this
- interval gives the corresponding set of :math:`(x,y)` coordinates on
- the circle. The user must specify the resolution, i.e., the number
- of :math:`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
- .. code-block:: python
- 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)}
- As in class ``Line`` we can offer the possibility to give an angle
- :math:`\theta` (equivalent to :math:`t` in the formulas above)
- and then get the corresponding :math:`x` and :math:`y` coordinates:
- .. code-block:: python
- 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)
- 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 :math:`t` runs in :math:`[\theta_0, \theta_1]`. Giving
- :math:`\theta_1` and :math:`\theta_2` the slightly more descriptive names
- ``start_angle`` and ``arc_angle``, the code looks like this:
- .. code-block:: python
- 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)}
- Having the ``Arc`` class, a ``Circle`` can alternatively befined as
- a subclass specializing the arc to a circle:
- .. code-block:: python
- class Circle(Arc):
- def __init__(self, center, radius, resolution=180):
- Arc.__init__(self, center, radius, 0, 360, resolution)
- 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
- .. code-block:: python
- 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}
- 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 :math:`x` and :math:`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
- .. code-block:: python
- 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
- ...
- Compound Geometric Objects
- ~~~~~~~~~~~~~~~~~~~~~~~~~~
- The simple classes ``Line``, ``Arc``, and ``Circle`` could define the geometric
- shape through just one ``Curve`` object. More complicated figure elements
- are built from instances of various subclasses of ``Shape``. Classes used
- for professional drawings soon get quite complex in composition and
- have a lot of geometric details, so here we prefer to make a very simple
- composition: the already drawn vehicle from
- Figure ref:ref:`sketcher:fig:vehicle0`.
- That is, instead of composing the drawing in a Python code we make a class
- ``Vehicle0`` for doing the same thing, and derive it from ``Shape``.
- The ``Shape`` hierarchy is found in the ``pysketcher`` package, so to use these
- classes or derive a new one, we need to import ``pysketcher``. The constructor
- of class ``Vehicle0`` performs approximately the same statements as
- in the example program we developed for making the drawing in
- Figure ref:ref:`sketcher:fig:vehicle0`.
- .. code-block:: python
- class Vehicle0(Shape):
- def __init__(self, w_1, R, L, H):
- 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})
- xmax = w_1 + 2*L + 3*R
- ground = Wall(x=[R, xmax], y=[0, 0], thickness=-0.3*R)
-
- self.shapes = {'vehicle': vehicle, 'ground': ground}
- Any subclass of ``Shape`` *must* define the ``shapes`` attribute, otherwise
- the inherited ``draw`` method (and a lot of other methods too) will
- not work.
- The painting of the vehicle could be offered by a method:
- .. code-block:: python
- def colorful(self):
- wheels = self.shapes['vehicle']['wheels']
- wheels.set_filled_curves('blue')
- wheels.set_linewidth(6)
- wheels.set_linecolor('black')
- under = self.shapes['vehicle']['body']['under']
- under.set_filled_curves('red')
- over = self.shapes['vehicle']['body']['over']
- over.set_filled_curves(pattern='/')
- over.set_linewidth(14)
- The usage of the class is simple: after having set up an appropriate
- coordinate system a s previously shown, we can do
- .. code-block:: python
- vehicle = Vehicle0(w_1, R, L, H)
- vehicle.draw()
- drawing_tool.display()
- The color from Figure :ref:`sketcher:fig:vehicle0:v2` is realized by
- .. code-block:: python
- drawing_tool.erase()
- vehicle.colorful()
- vehicle.draw()
- drawing_tool.display()
- A complete code defining and using class ``Vehicle0`` is found in the file
- ``vehicle2.py` <http://hplgit.github.com/pysketcher/doc/src/sketcher/src-sketcher/vehicle2.py>`_.
- The ``pysketcher`` package contains a wide range of classes for various
- geometrical objects, particularly those that are frequently used in
- drawings of mechanical systems.
- Adding Functionality via Recursion
- ==================================
- The really powerful feature of our class hierarchy is that we can add
- much functionality to the superclass ``Shape`` and to the "bottom" classes
- ``Curve``, and all other classes for all types of geometrical shapes
- immediately get the new functionality. To explain the idea we first have
- to look at the ``draw`` method, which all classes in the ``Shape``
- hierarchy must have. The inner workings of the ``draw`` method explain
- the secrets of how a series of other useful operations on figures
- can be implemented.
- Basic Principles of Recursion
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- We work with two types of class hierarchies: one Python class hierarchy,
- with ``Shape`` as superclass, and one *object hierarchy* of figure elements
- in a specific figure. A subclass of ``Shape`` stores its figure in the
- ``self.shapes`` dictionary. This dictionary represents the object hierarchy
- of figure elements for that class. We want to make one ``draw`` call
- for an instance, say our class ``Vehicle0``, and then we want this call
- to be propagated to *all* objects that are contained in
- ``self.shapes`` and all is nested subdictionaries. How is this done?
- The natural starting point is to call ``draw`` for each ``Shape`` object
- in the ``self.shapes`` dictionary:
- .. code-block:: python
- def draw(self):
- for shape in self.shapes:
- self.shapes[shape].draw()
- This general method can be provided by class ``Shape`` and inherited in
- subclasses like ``Vehicle0``. Let ``v`` be a ``Vehicle0`` instance.
- Seemingly, a call ``v.draw()`` just calls
- .. code-block:: python
- v.shapes['vehicle'].draw()
- v.shapes['ground'].draw()
- However, in the former call we call the ``draw`` method of a ``Composition`` object
- whose ``self.shapes`` attributed has two elements: ``wheels`` and ``body``.
- Since class ``Composition`` inherits the same ``draw`` method, this method will
- run through ``self.shapes`` and call ``wheels.draw()`` and ``body.draw()``.
- Now, the ``wheels`` object is also a ``Composition`` with the same ``draw``
- method, which will run through the ``shapes`` dictionary, now containing
- the ``wheel1`` and and ``wheel2`` objects. The ``wheel1`` object is a ``Circle``,
- so calling ``wheel1.draw()`` calls the ``draw`` method in class ``Circle``,
- but this is the same ``draw`` method as shown above. This method will
- therefore traverse the circle's ``shapes`` dictionary, which we have seen
- consists of one ``Curve`` element.
- The ``Curve`` object holds the coordinates to be plotted so here ``draw``
- really needs to do something "physical", namely send the coordinates to
- the plotting program. The ``draw`` method is outlined in the short listing
- of class ``Curve`` shown previously.
- We can go to any of the other shape objects that appear in the figure
- hierarchy and follow their ``draw`` calls in the similar way. Every time,
- a ``draw`` call will invoke a new ``draw`` call, until we eventually hit
- a ``Curve`` object in the "botton" of the figure hierarchy, and then that part
- of the figure is really plotted (or more precisely, the coordinates
- are sent to a plotting program).
- When a method calls itself, such as ``draw`` does, the calls are known as
- *recursive* and the programming principle is referred to as
- *recursion*. This technique is very often used to traverse hierarchical
- structures like the figure structures we work with here. Even though the
- hierarchy of objects building up a figure are of different types, they
- all inherit the same ``draw`` method and therefore exhibit the same
- behavior with respect to drawing. Only the ``Curve`` object has a different
- ``draw`` method, which does not lead to more recursion. Without this
- different ``draw`` method in class ``Curve``, the repeated ``draw`` calls would
- go on forever.
- Explaining Recursion
- ~~~~~~~~~~~~~~~~~~~~
- Understanding recursion is usually a challenge. To get a better idea of
- how recursion works, we have equipped class ``Shape`` with a method ``recurse``
- which just visits all the objects in the ``shapes`` dictionary and prints
- out a message for each object.
- This feature allows us to trace the execution and see exactly where
- we are in the hierarchy and which objects that are visited.
- The ``recurse`` method is very similar to ``draw``:
- .. code-block:: python
- def recurse(self, name, indent=0):
- # print message where we are (name is where we come from)
- for shape in self.shapes:
- # print message about which object to visit
- self.shapes[shape].recurse(indent+2, shape)
- The ``indent`` parameter governs how much the message from this
- ``recurse`` method is intended. We increase ``indent`` by 2 for every level
- in the hierarchy. This makes it easy to see on the printout how far
- down in the hierarchy we are.
- A typical message written by ``recurse`` when ``name`` is ``body`` and
- the ``shapes`` dictionary contains two entries, ``over`` and ``under``,
- will be
- .. code-block:: python
- Composition: body.shapes has entries 'over', 'under'
- call body.shapes["over"].recurse("over", 6)
- The number of leading blanks on each line corresponds to the value of
- ``indent``. The code printing out such messages looks like
- .. code-block:: python
- def recurse(self, name, indent=0):
- space = ' '*indent
- print space, '%s: %s.shapes has entries' % \
- (self.__class__.__name__, name), \
- str(list(self.shapes.keys()))[1:-1]
-
- for shape in self.shapes:
- print space,
- print 'call %s.shapes["%s"].recurse("%s", %d)' % \
- (name, shape, shape, indent+2)
- self.shapes[shape].recurse(shape, indent+2)
- Let us follow a ``v.recurse('vehicle')`` call in detail, ``v`` being
- a ``Vehicle0`` instance. Before looking into the output from ``recurse``,
- let us get an overview of the figure hierarchy in the ``v`` object
- (as produced by ``print v``)
- .. code-block:: python
- ground
- wall
- vehicle
- body
- over
- rectangle
- under
- rectangle
- wheels
- wheel1
- arc
- wheel2
- arc
- The ``recurse`` method performs the same kind of traversal of the
- hierarchy, but writes out and explains a lot more.
- The data structure represented by ``v.shapes`` is known as a *tree*.
- As in physical trees, there is a *root*, here the ``v.shapes``
- dictionary. A graphical illustration of the tree (upside down) is
- shown in Figure :ref:`sketcher:fig:Vehicle0:hier2`.
- From the root there are one or more branches, here two:
- ``ground`` and ``vehicle``. Following the ``vehicle`` branch, it has two new
- branches, ``body`` and ``wheels``. Relationships as in family trees
- are often used to describe the relations in object trees too: we say
- that ``vehicle`` is the parent of ``body`` and that ``body`` is a child of
- ``vehicle``. The term *node* is also often used to describe an element
- in a tree. A node may have several other nodes as *descendants*.
- .. _sketcher:fig:Vehicle0:hier2:
- .. figure:: figs-sketcher/Vehicle0_hier2.png
- :width: 600
- *Hierarchy of figure elements in an instance of class `Vehicle0`*
- Recursion is the principal programming technique to traverse tree structures.
- Any object in the tree can be viewed as a root of a subtree. For
- example, ``wheels`` is the root of a subtree that branches into
- ``wheel1`` and ``wheel2``. So when processing an object in the tree,
- we imagine we process the root and then recurse into a subtree, but the
- first object we recurse into can be viewed as the root of the subtree, so the
- processing procedure of the parent object can be repeated.
- A recommended next step is to simulate the ``recurse`` method by hand and
- carefully check that what happens in the visits to ``recurse`` is
- consistent with the output listed below. Although tedious, this is
- a major exercise that guaranteed will help to demystify recursion.
- Also remember that it requires some efforts to understand recursion.
- A part of the printout of ``v.recurse('vehicle')`` looks like
- .. code-block:: python
- Vehicle0: vehicle.shapes has entries 'ground', 'vehicle'
- call vehicle.shapes["ground"].recurse("ground", 2)
- Wall: ground.shapes has entries 'wall'
- call ground.shapes["wall"].recurse("wall", 4)
- reached "bottom" object Curve
- call vehicle.shapes["vehicle"].recurse("vehicle", 2)
- Composition: vehicle.shapes has entries 'body', 'wheels'
- call vehicle.shapes["body"].recurse("body", 4)
- Composition: body.shapes has entries 'over', 'under'
- call body.shapes["over"].recurse("over", 6)
- Rectangle: over.shapes has entries 'rectangle'
- call over.shapes["rectangle"].recurse("rectangle", 8)
- reached "bottom" object Curve
- call body.shapes["under"].recurse("under", 6)
- Rectangle: under.shapes has entries 'rectangle'
- call under.shapes["rectangle"].recurse("rectangle", 8)
- reached "bottom" object Curve
- ...
- This example should clearly demonstrate the principle that we
- can start at any object in the tree and do a recursive set
- of calls with that object as root.
- .. _sketcher:scaling:
- Scaling, Translating, and Rotating a Figure
- -------------------------------------------
- With recursion, as explained in the previous section, we can within
- minutes equip *all* classes in the ``Shape`` hierarchy, both present and
- future ones, with the ability to scale the figure, translate it,
- or rotate it. This added functionality requires only a few lines
- of code.
- Scaling
- ~~~~~~~
- We start with the simplest of the three geometric transformations,
- namely scaling.
- For a ``Curve`` instance containing a set of :math:`n` coordinates
- :math:`(x_i,y_i)` that make up a curve, scaling by
- a factor :math:`a` means that we multiply all the :math:`x` and :math:`y` coordinates
- by :math:`a`:
- .. math::
-
- x_i \leftarrow ax_i,\quad y_i\leftarrow ay_i,
- \quad i=0,\ldots,n-1\thinspace .
-
- Here we apply the arrow as an assignment operator.
- The corresponding Python implementation in
- class ``Curve`` reads
- .. code-block:: python
- class Curve:
- ...
- def scale(self, factor):
- self.x = factor*self.x
- self.y = factor*self.y
- Note here that ``self.x`` and ``self.y`` are Numerical Python arrays,
- so that multiplication by a scalar number ``factor`` is
- a vectorized operation.
- An even more efficient implementation is
- to make use of in-place multiplication in the arrays, as this saves the creation
- of temporary arrays like ``factor*self.x``, which is then assigned to
- ``self.x``:
- .. code-block:: python
- class Curve:
- ...
- def scale(self, factor):
- self.x *= factor
- self.y *= factor
- In an instance of a subclass of ``Shape``,
- the meaning of a method ``scale`` is
- to run through all objects in the dictionary ``shapes`` and ask
- each object to scale itself. This is the same delegation of actions
- to subclass instances as we do in the ``draw`` (or ``recurse``) method. All
- all objects, except ``Curve`` instances, can share the same
- implementation of the ``scale`` method. Therefore, we place
- the ``scale`` method in the superclass ``Shape`` such that all
- subclasses inherit the method.
- Since ``scale`` and ``draw`` are so similar,
- we can easily implement the ``scale`` method in class ``Shape`` by
- copying and editing the ``draw`` method:
- .. code-block:: python
- class Shape:
- ...
- def scale(self, factor):
- for shape in self.shapes:
- self.shapes[shape].scale(factor)
- This is all we have to do in order to equip all subclasses of
- ``Shape`` with scaling functionality!
- Any piece of the figure will scale itself, in the same manner
- as it can draw itself.
- Translation
- ~~~~~~~~~~~
- A set of coordinates :math:`(x_i, y_i)` can be translated :math:`v_0` units in
- the :math:`x` direction and :math:`v_1` units in the :math:`y` direction using the formulas
- .. math::
-
- x_i\leftarrow x_i+v_0,\quad y_i\leftarrow y_i+v_1,\quad i=0,\ldots,n-1\thinspace .
- The natural specification of the translation is in terms of a
- vector :math:`v=(v_0,v_1)`.
- The corresponding Python implementation in class ``Curve`` becomes
- .. code-block:: python
- class Curve:
- ...
- def translate(self, v):
- self.x += v[0]
- self.y += v[1]
- The translation operation for a shape object is very similar to the
- scaling and drawing operations. This means that we can implement a
- common method ``translate`` in the superclass ``Shape``. The code
- is parallel to the ``scale`` method:
- .. code-block:: python
- class Shape:
- ....
- def translate(self, v):
- for shape in self.shapes:
- self.shapes[shape].translate(v)
- Rotation
- ~~~~~~~~
- Rotating a figure is more complicated than scaling and translating.
- A counter clockwise rotation of :math:`\theta` degrees for a set of
- coordinates :math:`(x_i,y_i)` is given by
- .. math::
-
- \bar x_i &\leftarrow x_i\cos\theta - y_i\sin\theta,\\
- \bar y_i &\leftarrow x_i\sin\theta + y_i\cos\theta\thinspace .
-
- This rotation is performed around the origin. If we want the figure
- to be rotated with respect to a general point :math:`(x,y)`, we need to
- extend the formulas above:
- .. math::
-
- \bar x_i &\leftarrow x + (x_i -x)\cos\theta - (y_i -y)\sin\theta,\\
- \bar y_i &\leftarrow y + (x_i -x)\sin\theta + (y_i -y)\cos\theta\thinspace .
-
- The Python implementation in class ``Curve``, assuming that :math:`\theta`
- is given in degrees and not in radians, becomes
- .. code-block:: python
- def rotate(self, angle, center):
- angle = radians(angle)
- x, y = center
- c = cos(angle); s = sin(angle)
- xnew = x + (self.x - x)*c - (self.y - y)*s
- ynew = y + (self.x - x)*s + (self.y - y)*c
- self.x = xnew
- self.y = ynew
- The ``rotate`` method in class ``Shape`` is identical to the
- ``draw``, ``scale``, and ``translate`` methods except that we
- recurse into ``self.rotate(angle, center)``.
- We have already seen the ``rotate`` method in action when animating the
- rolling wheel at the end of the section :ref:`sketcher:vehicle1:anim`.
|