implementation.do.txt 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  1. # #ifdef PRIMER_BOOK
  2. ===== Example of Classes for Geometric Objects =====
  3. # #else
  4. ======= Inner Workings of the Pysketcher Tool =======
  5. # #endif
  6. We shall now explain how we can, quite easily, realize software with
  7. the capabilities demonstrated in the previous examples. Each object in
  8. the figure is represented as a class in a class hierarchy. Using
  9. inheritance, classes can inherit properties from parent classes and
  10. add new geometric features.
  11. # #ifndef PRIMER_BOOK
  12. Class programming is a key technology for realizing Pysketcher.
  13. As soon as some classes are established, more are easily
  14. added. Enhanced functionality for all the classes is also easy to
  15. implement in common, generic code that can immediately be shared by
  16. all present and future classes. The fundamental data structure
  17. involved in the `pysketcher` package is a hierarchical tree, and much
  18. of the material on implementation issues targets how to traverse tree
  19. structures with recursive function calls in object hierarchies. This
  20. topic is of key relevance in a wide range of other applications as
  21. well. In total, the inner workings of Pysketcher constitute an
  22. excellent example on the power of class programming.
  23. ===== Example of Classes for Geometric Objects =====
  24. # #endif
  25. We introduce class `Shape` as superclass for all specialized objects
  26. in a figure. This class does not store any data, but provides a
  27. series of functions that add functionality to all the subclasses.
  28. This will be shown later.
  29. === Simple Geometric Objects ===
  30. One simple subclass is `Rectangle`, specified by the coordinates of
  31. the lower left corner and its width and height:
  32. !bc pycod
  33. class Rectangle(Shape):
  34. def __init__(self, lower_left_corner, width, height):
  35. p = lower_left_corner # short form
  36. x = [p[0], p[0] + width,
  37. p[0] + width, p[0], p[0]]
  38. y = [p[1], p[1], p[1] + height,
  39. p[1] + height, p[1]]
  40. self.shapes = {'rectangle': Curve(x,y)}
  41. !ec
  42. Any subclass of `Shape` will have a constructor which takes
  43. geometric information about the shape of the object and
  44. creates a dictionary `self.shapes` with the shape built of
  45. simpler shapes. The most fundamental shape is `Curve`, which is
  46. just a collection of $(x,y)$ coordinates in two arrays `x` and `y`.
  47. Drawing the `Curve` object is a matter of plotting `y` versus `x`.
  48. For class `Rectangle` the `x` and `y` arrays contain the corner points
  49. of the rectangle in counterclockwise direction, starting and ending
  50. with in the lower left corner.
  51. Class `Line` is also a simple class:
  52. !bc pycod
  53. class Line(Shape):
  54. def __init__(self, start, end):
  55. x = [start[0], end[0]]
  56. y = [start[1], end[1]]
  57. self.shapes = {'line': Curve(x, y)}
  58. !ec
  59. Here we only need two points, the start and end point on the line.
  60. However, we may want to add some useful functionality, e.g., the ability
  61. to give an $x$ coordinate and have the class calculate the
  62. corresponding $y$ coordinate:
  63. !bc pycod
  64. def __call__(self, x):
  65. """Given x, return y on the line."""
  66. x, y = self.shapes['line'].x, self.shapes['line'].y
  67. self.a = (y[1] - y[0])/(x[1] - x[0])
  68. self.b = y[0] - self.a*x[0]
  69. return self.a*x + self.b
  70. !ec
  71. Unfortunately, this is too simplistic because vertical lines cannot be
  72. handled (infinite `self.a`). The true source code of `Line` therefore
  73. provides a more general solution at the cost of significantly longer
  74. code with more tests.
  75. A circle implies a somewhat increased complexity. Again we represent
  76. the geometric object by a `Curve` object, but this time the `Curve`
  77. object needs to store a large number of points on the curve such
  78. that a plotting program produces a visually smooth curve.
  79. The points on the circle must be calculated manually in the constructor
  80. of class `Circle`. The formulas for points $(x,y)$ on a curve with radius
  81. $R$ and center at $(x_0, y_0)$ are given by
  82. !bt
  83. \begin{align*}
  84. x &= x_0 + R\cos (t),\\
  85. y &= y_0 + R\sin (t),
  86. \end{align*}
  87. !et
  88. where $t\in [0, 2\pi]$. A discrete set of $t$ values in this
  89. interval gives the corresponding set of $(x,y)$ coordinates on
  90. the circle. The user must specify the resolution as the number
  91. of $t$ values. The circle's radius and center must of course
  92. also be specified.
  93. We can write the `Circle` class as
  94. !bc pycod
  95. class Circle(Shape):
  96. def __init__(self, center, radius, resolution=180):
  97. self.center, self.radius = center, radius
  98. self.resolution = resolution
  99. t = linspace(0, 2*pi, resolution+1)
  100. x0 = center[0]; y0 = center[1]
  101. R = radius
  102. x = x0 + R*cos(t)
  103. y = y0 + R*sin(t)
  104. self.shapes = {'circle': Curve(x, y)}
  105. !ec
  106. As in class `Line` we can offer the possibility to give an angle
  107. $\theta$ (equivalent to $t$ in the formulas above)
  108. and then get the corresponding $x$ and $y$ coordinates:
  109. !bc pycod
  110. def __call__(self, theta):
  111. """Return (x, y) point corresponding to angle theta."""
  112. return self.center[0] + self.radius*cos(theta), \
  113. self.center[1] + self.radius*sin(theta)
  114. !ec
  115. There is one flaw with this method: it yields illegal values after
  116. a translation, scaling, or rotation of the circle.
  117. A part of a circle, an arc, is a frequent geometric object when
  118. drawing mechanical systems. The arc is constructed much like
  119. a circle, but $t$ runs in $[\theta_s, \theta_s + \theta_a]$. Giving
  120. $\theta_s$ and $\theta_a$ the slightly more descriptive names
  121. `start_angle` and `arc_angle`, the code looks like this:
  122. !bc pycod
  123. class Arc(Shape):
  124. def __init__(self, center, radius,
  125. start_angle, arc_angle,
  126. resolution=180):
  127. self.start_angle = radians(start_angle)
  128. self.arc_angle = radians(arc_angle)
  129. t = linspace(self.start_angle,
  130. self.start_angle + self.arc_angle,
  131. resolution+1)
  132. x0 = center[0]; y0 = center[1]
  133. R = radius
  134. x = x0 + R*cos(t)
  135. y = y0 + R*sin(t)
  136. self.shapes = {'arc': Curve(x, y)}
  137. !ec
  138. Having the `Arc` class, a `Circle` can alternatively be defined as
  139. a subclass specializing the arc to a circle:
  140. !bc pycod
  141. class Circle(Arc):
  142. def __init__(self, center, radius, resolution=180):
  143. Arc.__init__(self, center, radius, 0, 360, resolution)
  144. !ec
  145. === Class Curve ===
  146. Class `Curve` sits on the coordinates to be drawn, but how is that
  147. done? The constructor of class `Curve` just stores the coordinates,
  148. while a method `draw` sends the coordinates to the plotting program to
  149. make a graph. Or more precisely, to avoid a lot of (e.g.)
  150. Matplotlib-specific plotting commands in class `Curve` we have created
  151. a small layer with a simple programming interface to plotting
  152. programs. This makes it straightforward to change from Matplotlib to
  153. another plotting program. The programming interface is represented by
  154. the `drawing_tool` object and has a few functions:
  155. * `plot_curve` for sending a curve in terms of $x$ and $y$ coordinates
  156. to the plotting program,
  157. * `set_coordinate_system` for specifying the graphics area,
  158. * `erase` for deleting all elements of the graph,
  159. * `set_grid` for turning on a grid (convenient while constructing the figure),
  160. * `set_instruction_file` for creating a separate file with all
  161. plotting commands (Matplotlib commands in our case),
  162. * a series of `set_X` functions where `X` is some property like
  163. `linecolor`, `linestyle`, `linewidth`, `filled_curves`.
  164. This is basically all we need to communicate to a plotting program.
  165. Any class in the `Shape` hierarchy inherits `set_X` functions for
  166. setting properties of curves. This information is propagated to
  167. all other shape objects in the `self.shapes` dictionary. Class
  168. `Curve` stores the line properties together with the coordinates
  169. of its curve and propagates this information to the plotting program.
  170. When saying `vehicle.set_linewidth(10)`, all objects that make
  171. up the `vehicle` object will get a `set_linewidth(10)` call,
  172. but only the `Curve` object at the end of the chain will actually
  173. store the information and send it to the plotting program.
  174. A rough sketch of class `Curve` reads
  175. !bc pycod
  176. class Curve(Shape):
  177. """General curve as a sequence of (x,y) coordintes."""
  178. def __init__(self, x, y):
  179. self.x = asarray(x, dtype=float)
  180. self.y = asarray(y, dtype=float)
  181. def draw(self):
  182. drawing_tool.plot_curve(
  183. self.x, self.y,
  184. self.linestyle, self.linewidth, self.linecolor, ...)
  185. def set_linewidth(self, width):
  186. self.linewidth = width
  187. det set_linestyle(self, style):
  188. self.linestyle = style
  189. ...
  190. !ec
  191. === Compound Geometric Objects ===
  192. The simple classes `Line`, `Arc`, and `Circle` could can the geometric
  193. shape through just one `Curve` object. More complicated shapes are
  194. built from instances of various subclasses of `Shape`. Classes used
  195. for professional drawings soon get quite complex in composition and
  196. have a lot of geometric details, so here we prefer to make a very
  197. simple composition: the already drawn vehicle from Figure
  198. ref{sketcher:fig:vehicle0}. That is, instead of composing the drawing
  199. in a Python program as shown above, we make a subclass `Vehicle0` in
  200. the `Shape` hierarchy for doing the same thing.
  201. The `Shape` hierarchy is found in the `pysketcher` package, so to use these
  202. classes or derive a new one, we need to import `pysketcher`. The constructor
  203. of class `Vehicle0` performs approximately the same statements as
  204. in the example program we developed for making the drawing in
  205. Figure ref{sketcher:fig:vehicle0}.
  206. !bc pycod
  207. from pysketcher import *
  208. class Vehicle0(Shape):
  209. def __init__(self, w_1, R, L, H):
  210. wheel1 = Circle(center=(w_1, R), radius=R)
  211. wheel2 = wheel1.copy()
  212. wheel2.translate((L,0))
  213. under = Rectangle(lower_left_corner=(w_1-2*R, 2*R),
  214. width=2*R + L + 2*R, height=H)
  215. over = Rectangle(lower_left_corner=(w_1, 2*R + H),
  216. width=2.5*R, height=1.25*H)
  217. wheels = Composition(
  218. {'wheel1': wheel1, 'wheel2': wheel2})
  219. body = Composition(
  220. {'under': under, 'over': over})
  221. vehicle = Composition({'wheels': wheels, 'body': body})
  222. xmax = w_1 + 2*L + 3*R
  223. ground = Wall(x=[R, xmax], y=[0, 0], thickness=-0.3*R)
  224. self.shapes = {'vehicle': vehicle, 'ground': ground}
  225. !ec
  226. Any subclass of `Shape` *must* define the `shapes` attribute, otherwise
  227. the inherited `draw` method (and a lot of other methods too) will
  228. not work.
  229. The painting of the vehicle, as shown in the right part of
  230. Figure ref{sketcher:fig:vehicle0:v2}, could in class `Vehicle0`
  231. be offered by a method:
  232. !bc pycod
  233. def colorful(self):
  234. wheels = self.shapes['vehicle']['wheels']
  235. wheels.set_filled_curves('blue')
  236. wheels.set_linewidth(6)
  237. wheels.set_linecolor('black')
  238. under = self.shapes['vehicle']['body']['under']
  239. under.set_filled_curves('red')
  240. over = self.shapes['vehicle']['body']['over']
  241. over.set_filled_curves(pattern='/')
  242. over.set_linewidth(14)
  243. !ec
  244. The usage of the class is simple: after having set up an appropriate
  245. coordinate system as previously shown, we can do
  246. !bc pycod
  247. vehicle = Vehicle0(w_1, R, L, H)
  248. vehicle.draw()
  249. drawing_tool.display()
  250. !ec
  251. and go on the make a painted version by
  252. !bc pycod
  253. drawing_tool.erase()
  254. vehicle.colorful()
  255. vehicle.draw()
  256. drawing_tool.display()
  257. !ec
  258. A complete code defining and using class `Vehicle0` is found in the file
  259. "`vehicle2.py`": "${src_path_pysketcher}/vehicle2.py".
  260. The `pysketcher` package contains a wide range of classes for various
  261. geometrical objects, particularly those that are frequently used in
  262. drawings of mechanical systems.
  263. ===== Adding Functionality via Recursion =====
  264. The really powerful feature of our class hierarchy is that we can add
  265. much functionality to the superclass `Shape` and to the "bottom" class
  266. `Curve`, and then all other classes for various types of geometrical shapes
  267. immediately get the new functionality. To explain the idea we may
  268. look at the `draw` method, which all classes in the `Shape`
  269. hierarchy must have. The inner workings of the `draw` method explain
  270. the secrets of how a series of other useful operations on figures
  271. can be implemented.
  272. === Basic Principles of Recursion ===
  273. Note that we work with two types of hierarchies in the
  274. present documentation: one Python *class hierarchy*,
  275. with `Shape` as superclass, and one *object hierarchy* of figure elements
  276. in a specific figure. A subclass of `Shape` stores its figure in the
  277. `self.shapes` dictionary. This dictionary represents the object hierarchy
  278. of figure elements for that class. We want to make one `draw` call
  279. for an instance, say our class `Vehicle0`, and then we want this call
  280. to be propagated to *all* objects that are contained in
  281. `self.shapes` and all is nested subdictionaries. How is this done?
  282. The natural starting point is to call `draw` for each `Shape` object
  283. in the `self.shapes` dictionary:
  284. !bc pycod
  285. def draw(self):
  286. for shape in self.shapes:
  287. self.shapes[shape].draw()
  288. !ec
  289. This general method can be provided by class `Shape` and inherited in
  290. subclasses like `Vehicle0`. Let `v` be a `Vehicle0` instance.
  291. Seemingly, a call `v.draw()` just calls
  292. !bc pycod
  293. v.shapes['vehicle'].draw()
  294. v.shapes['ground'].draw()
  295. !ec
  296. However, in the former call we call the `draw` method of a `Composition` object
  297. whose `self.shapes` attributed has two elements: `wheels` and `body`.
  298. Since class `Composition` inherits the same `draw` method, this method will
  299. run through `self.shapes` and call `wheels.draw()` and `body.draw()`.
  300. Now, the `wheels` object is also a `Composition` with the same `draw`
  301. method, which will run through `self.shapes`, now containing
  302. the `wheel1` and and `wheel2` objects. The `wheel1` object is a `Circle`,
  303. so calling `wheel1.draw()` calls the `draw` method in class `Circle`,
  304. but this is the same `draw` method as shown above. This method will
  305. therefore traverse the circle's `shapes` dictionary, which we have seen
  306. consists of one `Curve` element.
  307. The `Curve` object holds the coordinates to be plotted so here `draw`
  308. really needs to do something "physical", namely send the coordinates to
  309. the plotting program. The `draw` method is outlined in the short listing
  310. of class `Curve` shown previously.
  311. We can go to any of the other shape objects that appear in the figure
  312. hierarchy and follow their `draw` calls in the similar way. Every time,
  313. a `draw` call will invoke a new `draw` call, until we eventually hit
  314. a `Curve` object in the "botton" of the figure hierarchy, and then that part
  315. of the figure is really plotted (or more precisely, the coordinates
  316. are sent to a plotting program).
  317. When a method calls itself, such as `draw` does, the calls are known as
  318. *recursive* and the programming principle is referred to as
  319. *recursion*. This technique is very often used to traverse hierarchical
  320. structures like the figure structures we work with here. Even though the
  321. hierarchy of objects building up a figure are of different types, they
  322. all inherit the same `draw` method and therefore exhibit the same
  323. behavior with respect to drawing. Only the `Curve` object has a different
  324. `draw` method, which does not lead to more recursion.
  325. === Explaining Recursion ===
  326. Understanding recursion is usually a challenge. To get a better idea of
  327. how recursion works, we have equipped class `Shape` with a method `recurse`
  328. which just visits all the objects in the `shapes` dictionary and prints
  329. out a message for each object.
  330. This feature allows us to trace the execution and see exactly where
  331. we are in the hierarchy and which objects that are visited.
  332. The `recurse` method is very similar to `draw`:
  333. !bc pycod
  334. def recurse(self, name, indent=0):
  335. # print message where we are (name is where we come from)
  336. for shape in self.shapes:
  337. # print message about which object to visit
  338. self.shapes[shape].recurse(indent+2, shape)
  339. !ec
  340. The `indent` parameter governs how much the message from this
  341. `recurse` method is intended. We increase `indent` by 2 for every
  342. level in the hierarchy, i.e., every row of objects in Figure
  343. ref{sketcher:fig:Vehicle0:hier2}. This indentation makes it easy to
  344. see on the printout how far down in the hierarchy we are.
  345. A typical message written by `recurse` when `name` is `'body'` and
  346. the `shapes` dictionary has the keys `'over'` and `'under'`,
  347. will be
  348. !bc dat
  349. Composition: body.shapes has entries 'over', 'under'
  350. call body.shapes["over"].recurse("over", 6)
  351. !ec
  352. The number of leading blanks on each line corresponds to the value of
  353. `indent`. The code printing out such messages looks like
  354. !bc pycod
  355. def recurse(self, name, indent=0):
  356. space = ' '*indent
  357. print space, '%s: %s.shapes has entries' % \
  358. (self.__class__.__name__, name), \
  359. str(list(self.shapes.keys()))[1:-1]
  360. for shape in self.shapes:
  361. print space,
  362. print 'call %s.shapes["%s"].recurse("%s", %d)' % \
  363. (name, shape, shape, indent+2)
  364. self.shapes[shape].recurse(shape, indent+2)
  365. !ec
  366. Let us follow a `v.recurse('vehicle')` call in detail, `v` being
  367. a `Vehicle0` instance. Before looking into the output from `recurse`,
  368. let us get an overview of the figure hierarchy in the `v` object
  369. (as produced by `print v`)
  370. !bc dat
  371. ground
  372. wall
  373. vehicle
  374. body
  375. over
  376. rectangle
  377. under
  378. rectangle
  379. wheels
  380. wheel1
  381. arc
  382. wheel2
  383. arc
  384. !ec
  385. The `recurse` method performs the same kind of traversal of the
  386. hierarchy, but writes out and explains a lot more.
  387. The data structure represented by `v.shapes` is known as a *tree*.
  388. As in physical trees, there is a *root*, here the `v.shapes`
  389. dictionary. A graphical illustration of the tree (upside down) is
  390. shown in Figure ref{sketcher:fig:Vehicle0:hier2}.
  391. From the root there are one or more branches, here two:
  392. `ground` and `vehicle`. Following the `vehicle` branch, it has two new
  393. branches, `body` and `wheels`. Relationships as in family trees
  394. are often used to describe the relations in object trees too: we say
  395. that `vehicle` is the parent of `body` and that `body` is a child of
  396. `vehicle`. The term *node* is also often used to describe an element
  397. in a tree. A node may have several other nodes as *descendants*.
  398. FIGURE: [fig-tut/Vehicle0_hier2.png, width=600] Hierarchy of figure elements in an instance of class `Vehicle0`. label{sketcher:fig:Vehicle0:hier2}
  399. Recursion is the principal programming technique to traverse tree structures.
  400. Any object in the tree can be viewed as a root of a subtree. For
  401. example, `wheels` is the root of a subtree that branches into
  402. `wheel1` and `wheel2`. So when processing an object in the tree,
  403. we imagine we process the root and then recurse into a subtree, but the
  404. first object we recurse into can be viewed as the root of the subtree, so the
  405. processing procedure of the parent object can be repeated.
  406. A recommended next step is to simulate the `recurse` method by hand and
  407. carefully check that what happens in the visits to `recurse` is
  408. consistent with the output listed below. Although tedious, this is
  409. a major exercise that guaranteed will help to demystify recursion.
  410. A part of the printout of `v.recurse('vehicle')` looks like
  411. !bc dat
  412. Vehicle0: vehicle.shapes has entries 'ground', 'vehicle'
  413. call vehicle.shapes["ground"].recurse("ground", 2)
  414. Wall: ground.shapes has entries 'wall'
  415. call ground.shapes["wall"].recurse("wall", 4)
  416. reached "bottom" object Curve
  417. call vehicle.shapes["vehicle"].recurse("vehicle", 2)
  418. Composition: vehicle.shapes has entries 'body', 'wheels'
  419. call vehicle.shapes["body"].recurse("body", 4)
  420. Composition: body.shapes has entries 'over', 'under'
  421. call body.shapes["over"].recurse("over", 6)
  422. Rectangle: over.shapes has entries 'rectangle'
  423. call over.shapes["rectangle"].recurse("rectangle", 8)
  424. reached "bottom" object Curve
  425. call body.shapes["under"].recurse("under", 6)
  426. Rectangle: under.shapes has entries 'rectangle'
  427. call under.shapes["rectangle"].recurse("rectangle", 8)
  428. reached "bottom" object Curve
  429. ...
  430. !ec
  431. This example should clearly demonstrate the principle that we
  432. can start at any object in the tree and do a recursive set
  433. of calls with that object as root.
  434. ===== Scaling, Translating, and Rotating a Figure =====
  435. label{sketcher:scaling}
  436. With recursion, as explained in the previous section, we can within
  437. minutes equip *all* classes in the `Shape` hierarchy, both present and
  438. future ones, with the ability to scale the figure, translate it,
  439. or rotate it. This added functionality requires only a few lines
  440. of code.
  441. === Scaling ===
  442. We start with the simplest of the three geometric transformations,
  443. namely scaling.
  444. For a `Curve` instance containing a set of $n$ coordinates
  445. $(x_i,y_i)$ that make up a curve, scaling by
  446. a factor $a$ means that we multiply all the $x$ and $y$ coordinates
  447. by $a$:
  448. !bt
  449. \[
  450. x_i \leftarrow ax_i,\quad y_i\leftarrow ay_i,
  451. \quad i=0,\ldots,n-1\thinspace .
  452. \]
  453. !et
  454. Here we apply the arrow as an assignment operator.
  455. The corresponding Python implementation in
  456. class `Curve` reads
  457. !bc cod
  458. class Curve:
  459. ...
  460. def scale(self, factor):
  461. self.x = factor*self.x
  462. self.y = factor*self.y
  463. !ec
  464. Note here that `self.x` and `self.y` are Numerical Python arrays,
  465. so that multiplication by a scalar number `factor` is
  466. a vectorized operation.
  467. An even more efficient implementation is
  468. to make use of in-place multiplication in the arrays,
  469. !bc cod
  470. class Curve:
  471. ...
  472. def scale(self, factor):
  473. self.x *= factor
  474. self.y *= factor
  475. !ec
  476. as this saves the creation of temporary arrays like `factor*self.x`.
  477. In an instance of a subclass of `Shape`, the meaning of a method
  478. `scale` is to run through all objects in the dictionary `shapes` and
  479. ask each object to scale itself. This is the same delegation of
  480. actions to subclass instances as we do in the `draw` (or `recurse`)
  481. method. All objects, except `Curve` instances, can share the same
  482. implementation of the `scale` method. Therefore, we place the `scale`
  483. method in the superclass `Shape` such that all subclasses inherit the
  484. method. Since `scale` and `draw` are so similar, we can easily
  485. implement the `scale` method in class `Shape` by copying and editing
  486. the `draw` method:
  487. !bc cod
  488. class Shape:
  489. ...
  490. def scale(self, factor):
  491. for shape in self.shapes:
  492. self.shapes[shape].scale(factor)
  493. !ec
  494. This is all we have to do in order to equip all subclasses of
  495. `Shape` with scaling functionality!
  496. Any piece of the figure will scale itself, in the same manner
  497. as it can draw itself.
  498. === Translation ===
  499. A set of coordinates $(x_i, y_i)$ can be translated $v_0$ units in
  500. the $x$ direction and $v_1$ units in the $y$ direction using the formulas
  501. !bt
  502. \begin{equation*}
  503. x_i\leftarrow x_i+v_0,\quad y_i\leftarrow y_i+v_1,\quad i=0,\ldots,n-1\thinspace . \end{equation*}
  504. !et
  505. The natural specification of the translation is in terms of the
  506. vector $v=(v_0,v_1)$.
  507. The corresponding Python implementation in class `Curve` becomes
  508. !bc cod
  509. class Curve:
  510. ...
  511. def translate(self, v):
  512. self.x += v[0]
  513. self.y += v[1]
  514. !ec
  515. The translation operation for a shape object is very similar to the
  516. scaling and drawing operations. This means that we can implement a
  517. common method `translate` in the superclass `Shape`. The code
  518. is parallel to the `scale` method:
  519. !bc cod
  520. class Shape:
  521. ....
  522. def translate(self, v):
  523. for shape in self.shapes:
  524. self.shapes[shape].translate(v)
  525. !ec
  526. === Rotation ===
  527. Rotating a figure is more complicated than scaling and translating.
  528. A counter clockwise rotation of $\theta$ degrees for a set of
  529. coordinates $(x_i,y_i)$ is given by
  530. !bt
  531. \begin{align*}
  532. \bar x_i &\leftarrow x_i\cos\theta - y_i\sin\theta,\\
  533. \bar y_i &\leftarrow x_i\sin\theta + y_i\cos\theta\thinspace .
  534. \end{align*}
  535. !et
  536. This rotation is performed around the origin. If we want the figure
  537. to be rotated with respect to a general point $(x,y)$, we need to
  538. extend the formulas above:
  539. !bt
  540. \begin{align*}
  541. \bar x_i &\leftarrow x + (x_i -x)\cos\theta - (y_i -y)\sin\theta,\\
  542. \bar y_i &\leftarrow y + (x_i -x)\sin\theta + (y_i -y)\cos\theta\thinspace .
  543. \end{align*}
  544. !et
  545. The Python implementation in class `Curve`, assuming that $\theta$
  546. is given in degrees and not in radians, becomes
  547. !bc cod
  548. def rotate(self, angle, center):
  549. angle = radians(angle)
  550. x, y = center
  551. c = cos(angle); s = sin(angle)
  552. xnew = x + (self.x - x)*c - (self.y - y)*s
  553. ynew = y + (self.x - x)*s + (self.y - y)*c
  554. self.x = xnew
  555. self.y = ynew
  556. !ec
  557. The `rotate` method in class `Shape` follows the principle of the
  558. `draw`, `scale`, and `translate` methods.
  559. We have already seen the `rotate` method in action when animating the
  560. rolling wheel at the end of Section ref{sketcher:vehicle1:anim}.