| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660 |
- # #ifdef PRIMER_BOOK
- ===== Example of classes for geometric objects =====
- # #else
- ======= Inner workings of the Pysketcher tool =======
- # #endif
- 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.
- # #ifndef PRIMER_BOOK
- idx{tree data structure}
- 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 =====
- # #endif
- 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`, specified by the coordinates of
- the lower left corner and its width and height:
- !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 that 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`. For class `Rectangle` the `x`
- and `y` arrays contain the corner points of the rectangle in
- counterclockwise direction, starting and ending with in the lower left
- corner.
- Class `Line` is also a simple class:
- !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 want to 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 (infinite `self.a`). The true source code of `Line` therefore
- provides a more general solution at the cost of significantly longer
- code with more tests.
- A circle implies a 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 $(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 as the number
- of $t$ values. 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_s, \theta_s + \theta_a]$. Giving
- $\theta_s$ and $\theta_a$ 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.start_angle = radians(start_angle)
- self.arc_angle = radians(arc_angle)
- 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 be defined 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
- === Class curve ===
- Class `Curve` sits on the coordinates to be drawn, but how is that
- done? The constructor of class `Curve` 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 in class `Curve` 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 figure),
- * `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 in the `self.shapes` dictionary. 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)
- def draw(self):
- drawing_tool.plot_curve(
- self.x, self.y,
- self.linestyle, self.linewidth, self.linecolor, ...)
- def set_linewidth(self, width):
- self.linewidth = width
- det set_linestyle(self, style):
- self.linestyle = style
- ...
- !ec
- === Compound geometric objects ===
- The simple classes `Line`, `Arc`, and `Circle` could can the geometric
- shape through just one `Curve` object. More complicated shapes 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{sketcher:fig:vehicle0}. That is, instead of composing the drawing
- in a Python program as shown above, we make a subclass `Vehicle0` in
- the `Shape` hierarchy for doing the same thing.
- 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{sketcher:fig:vehicle0}.
- !bc pycod
- from pysketcher import *
- 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}
- !ec
- 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, as shown in the right part of
- Figure ref{sketcher:fig:vehicle0:v2}, could in class `Vehicle0`
- be offered by a method:
- !bc pycod
- 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)
- !ec
- The usage of the class is simple: after having set up an appropriate
- coordinate system as previously shown, we can do
- !bc pycod
- vehicle = Vehicle0(w_1, R, L, H)
- vehicle.draw()
- drawing_tool.display()
- !ec
- and go on the make a painted version by
- !bc pycod
- drawing_tool.erase()
- vehicle.colorful()
- vehicle.draw()
- drawing_tool.display()
- !ec
- A complete code defining and using class `Vehicle0` is found in the file
- "`vehicle2.py`": "${src_path_pysketcher}/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 =====
- idx{recursive function calls}
- The really powerful feature of our class hierarchy is that we can add
- much functionality to the superclass `Shape` and to the ``bottom'' class
- `Curve`, and then all other classes for various types of geometrical shapes
- immediately get the new functionality. To explain the idea we may
- 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 ===
- Note that we work with two types of hierarchies in the
- present documentation: 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:
- !bc pycod
- def draw(self):
- for shape in self.shapes:
- self.shapes[shape].draw()
- !ec
- 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
- !bc pycod
- v.shapes['vehicle'].draw()
- v.shapes['ground'].draw()
- !ec
- 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 `self.shapes`, now containing
- the `wheel1` 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 at the ``bottom'' 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.
- === 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`
- that 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`:
- !bc pycod
- 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)
- !ec
- 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, i.e., every row of objects in Figure
- ref{sketcher:fig:Vehicle0:hier2}. This indentation 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 has the keys `'over'` and `'under'`,
- will be
- !bc dat
- Composition: body.shapes has entries 'over', 'under'
- call body.shapes["over"].recurse("over", 6)
- !ec
- The number of leading blanks on each line corresponds to the value of
- `indent`. The code printing out such messages looks like
- !bc pycod
- 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)
- !ec
- 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`)
- !bc dat
- ground
- wall
- vehicle
- body
- over
- rectangle
- under
- rectangle
- wheels
- wheel1
- arc
- wheel2
- arc
- !ec
- 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*.
- FIGURE: [fig-tut/Vehicle0_hier2, width=600 frac=0.8] Hierarchy of figure elements in an instance of class `Vehicle0`. label{sketcher:fig:Vehicle0:hier2}
- 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.
- A part of the printout of `v.recurse('vehicle')` looks like
- !bc dat
- 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
- ...
- !ec
- 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.
- ===== Scaling, translating, and rotating a figure =====
- label{sketcher:scaling}
- 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 $n$
- coordinates $(x_i,y_i)$ that make up a curve, scaling by a factor $a$
- means that we multiply all the $x$ and $y$ coordinates by $a$:
- !bt
- \[
- x_i \leftarrow ax_i,\quad y_i\leftarrow ay_i,
- \quad i=0,\ldots,n-1\thinspace .
- \]
- !et
- Here we apply the arrow as an assignment operator.
- The corresponding Python implementation in
- class `Curve` reads
- !bc pycod
- class Curve:
- ...
- def scale(self, factor):
- self.x = factor*self.x
- self.y = factor*self.y
- !ec
- 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,
- !bc pycod
- class Curve:
- ...
- def scale(self, factor):
- self.x *= factor
- self.y *= factor
- !ec
- as this saves the creation of temporary arrays like `factor*self.x`.
- 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 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:
- !bc pycod
- class Shape:
- ...
- def scale(self, factor):
- for shape in self.shapes:
- self.shapes[shape].scale(factor)
- !ec
- 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 $(x_i, y_i)$ can be translated $v_0$ units in
- the $x$ direction and $v_1$ units in the $y$ direction using the formulas
- !bt
- \begin{equation*}
- x_i\leftarrow x_i+v_0,\quad y_i\leftarrow y_i+v_1,
- \quad i=0,\ldots,n-1\thinspace .
- \end{equation*}
- !et
- The natural specification of the translation is in terms of the
- vector $v=(v_0,v_1)$.
- The corresponding Python implementation in class `Curve` becomes
- !bc pycod
- class Curve:
- ...
- def translate(self, v):
- self.x += v[0]
- self.y += v[1]
- !ec
- 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:
- !bc pycod
- class Shape:
- ....
- def translate(self, v):
- for shape in self.shapes:
- self.shapes[shape].translate(v)
- !ec
- === Rotation ===
- Rotating a figure is more complicated than scaling and translating.
- A counter clockwise rotation of $\theta$ degrees for a set of
- coordinates $(x_i,y_i)$ is given by
- !bt
- \begin{align*}
- \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 .
- \end{align*}
- !et
- This rotation is performed around the origin. If we want the figure
- to be rotated with respect to a general point $(x,y)$, we need to
- extend the formulas above:
- !bt
- \begin{align*}
- \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 .
- \end{align*}
- !et
- The Python implementation in class `Curve`, assuming that $\theta$
- is given in degrees and not in radians, becomes
- !bc pycod
- 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
- !ec
- The `rotate` method in class `Shape` follows the principle of the
- `draw`, `scale`, and `translate` methods.
- We have already seen the `rotate` method in action when animating the
- rolling wheel at the end of Section ref{sketcher:vehicle1:anim}.
|